mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
refactor(app): share application components and request services
This commit is contained in:
1 parent
a0a0128a59
commit
a67bc60a29
84 files changed
+2798
-1731
No files matched your search
@@ -0,0 +1,40 @@
|
||||
import { createSourceSlot } from '../app/sourceSlot.js';
|
||||
import { createApplicationRequestServices } from './requests.js';
|
||||
|
||||
// Compatibility owners are page-scoped, like the viewer and its layer registry.
|
||||
const defaults = createApplicationRequestServices();
|
||||
const slots = Object.fromEntries(
|
||||
Object.entries({
|
||||
boundaries: ['query'],
|
||||
terrain: ['getHeights'],
|
||||
regional: ['getBrief'],
|
||||
weather: ['getConditions'],
|
||||
summary: ['summarize'],
|
||||
}).map(([name, methods]) => [
|
||||
name,
|
||||
createSourceSlot(defaults[name], methods, `${name} service`),
|
||||
]),
|
||||
);
|
||||
export const applicationServices = Object.freeze(
|
||||
Object.fromEntries(
|
||||
Object.entries(slots).map(([name, slot]) => [name, slot.source]),
|
||||
),
|
||||
);
|
||||
|
||||
/** Supply selected services before constructing the application; release after its consumers. */
|
||||
export function configureApplicationServices(services) {
|
||||
const releases = [];
|
||||
try {
|
||||
for (const [name, service] of Object.entries(services)) {
|
||||
if (!slots[name])
|
||||
throw new TypeError(`Unknown application service: ${name}`);
|
||||
releases.push(slots[name].configure(service));
|
||||
}
|
||||
} catch (error) {
|
||||
for (const release of releases.reverse()) release();
|
||||
throw error;
|
||||
}
|
||||
return () => {
|
||||
for (const release of releases.reverse()) release();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/** Parse bounded retry information from a service response. */
|
||||
function retryAfterMs(value) {
|
||||
if (value == null || String(value).trim() === '') return null;
|
||||
const seconds = Number(value);
|
||||
if (Number.isFinite(seconds) && seconds >= 0)
|
||||
return Math.ceil(seconds * 1000);
|
||||
const at = Date.parse(String(value));
|
||||
return Number.isFinite(at) ? Math.max(0, at - Date.now()) : null;
|
||||
}
|
||||
|
||||
/** Construct independent request services for compatible application protocols. */
|
||||
export function createApplicationRequestServices({
|
||||
fetchImpl = (...args) => fetch(...args),
|
||||
signal: lifetime,
|
||||
endpoints = {},
|
||||
} = {}) {
|
||||
const urls = {
|
||||
boundaries: '/api/overpass',
|
||||
terrain: '/api/terrain/heights',
|
||||
regional: '/api/regional-brief',
|
||||
weather: '/api/weather-effects',
|
||||
summary: '/api/openai/hud-summary',
|
||||
...endpoints,
|
||||
};
|
||||
async function request(endpoint, { signal, ...init } = {}) {
|
||||
signal = AbortSignal.any([lifetime, signal].filter(Boolean));
|
||||
signal.throwIfAborted();
|
||||
const response = await fetchImpl(endpoint, {
|
||||
...init,
|
||||
signal,
|
||||
redirect: 'error',
|
||||
});
|
||||
signal.throwIfAborted();
|
||||
let data = null;
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch {
|
||||
/* Status remains authoritative for non-JSON errors. */
|
||||
}
|
||||
signal.throwIfAborted();
|
||||
return {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
headers: response.headers,
|
||||
data,
|
||||
};
|
||||
}
|
||||
function pointUrl(endpoint, latitude, longitude) {
|
||||
if (
|
||||
!Number.isFinite(latitude) ||
|
||||
latitude < -90 ||
|
||||
latitude > 90 ||
|
||||
!Number.isFinite(longitude) ||
|
||||
longitude < -180 ||
|
||||
longitude > 180
|
||||
)
|
||||
throw new TypeError('Valid coordinates are required');
|
||||
return `${endpoint}?${new URLSearchParams({ latitude: latitude.toFixed(5), longitude: longitude.toFixed(5) })}`;
|
||||
}
|
||||
function requireOk(response, label) {
|
||||
if (!response.ok)
|
||||
throw new Error(`${label} unavailable (${response.status})`);
|
||||
return response.data;
|
||||
}
|
||||
return {
|
||||
boundaries: {
|
||||
async query(query, { signal } = {}) {
|
||||
const response = await request(urls.boundaries, {
|
||||
method: 'POST',
|
||||
signal,
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: `data=${encodeURIComponent(query)}`,
|
||||
});
|
||||
const retry = response.headers?.get?.('Retry-After');
|
||||
if (
|
||||
response.status === 429 ||
|
||||
(response.status === 503 && retry != null)
|
||||
)
|
||||
return { rateLimited: true, retryAfterMs: retryAfterMs(retry) };
|
||||
if (!response.ok) return null;
|
||||
const remark = String(response.data?.remark || '').toLowerCase();
|
||||
if (/runtime error|timed out|out of memory/.test(remark)) return null;
|
||||
return Array.isArray(response.data?.elements)
|
||||
? response.data.elements
|
||||
: null;
|
||||
},
|
||||
},
|
||||
terrain: {
|
||||
async getHeights(points, { signal } = {}) {
|
||||
const query = points
|
||||
.map(({ lat, lon }) => `${lon.toFixed(5)},${lat.toFixed(5)}`)
|
||||
.join(';');
|
||||
return requireOk(
|
||||
await request(`${urls.terrain}?points=${encodeURIComponent(query)}`, {
|
||||
signal,
|
||||
}),
|
||||
'Terrain heights',
|
||||
)?.results;
|
||||
},
|
||||
},
|
||||
regional: {
|
||||
async getBrief(latitude, longitude, options) {
|
||||
return requireOk(
|
||||
await request(pointUrl(urls.regional, latitude, longitude), options),
|
||||
'Regional brief',
|
||||
);
|
||||
},
|
||||
},
|
||||
weather: {
|
||||
async getConditions(latitude, longitude, options) {
|
||||
return requireOk(
|
||||
await request(pointUrl(urls.weather, latitude, longitude), options),
|
||||
'Weather',
|
||||
);
|
||||
},
|
||||
},
|
||||
summary: {
|
||||
async summarize(context, { signal } = {}) {
|
||||
return request(urls.summary, {
|
||||
method: 'POST',
|
||||
signal,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(context),
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createApplicationRequestServices } from './requests.js';
|
||||
|
||||
test('independent compatible endpoints receive normalized requests without changing global fetch', async () => {
|
||||
const requests = [];
|
||||
const original = globalThis.fetch;
|
||||
const services = createApplicationRequestServices({
|
||||
endpoints: { regional: '/custom/region', terrain: '/custom/floors', summary: '/custom/summary' },
|
||||
fetchImpl: async (url, init) => {
|
||||
requests.push({ url, init });
|
||||
return Response.json({ results: [{ ellipsoid: 10 }], summary: 'fixture' });
|
||||
},
|
||||
});
|
||||
await services.regional.getBrief(30, -97);
|
||||
assert.match(requests[0].url, /^\/custom\/region\?latitude=30.00000&longitude=-97.00000$/);
|
||||
assert.deepEqual(await services.terrain.getHeights([{ lat: 30, lon: -97 }]), [{ ellipsoid: 10 }]);
|
||||
const result = await services.summary.summarize({ place: 'fixture' });
|
||||
assert.equal(result.data.summary, 'fixture');
|
||||
assert.equal(requests[2].init.body, '{"place":"fixture"}');
|
||||
assert.equal(requests[2].init.redirect, 'error');
|
||||
assert.equal(globalThis.fetch, original);
|
||||
});
|
||||
|
||||
test('lifetime cancellation discards a late response body even from an uncooperative transport', async () => {
|
||||
const lifetime = new AbortController();
|
||||
let finish;
|
||||
const services = createApplicationRequestServices({ signal: lifetime.signal,
|
||||
fetchImpl: async () => ({ ok: true, status: 200, json: () => new Promise(resolve => { finish = resolve; }) }),
|
||||
});
|
||||
const pending = services.weather.getConditions(30, -97);
|
||||
while (!finish) await Promise.resolve();
|
||||
lifetime.abort();
|
||||
finish({ weather: { temperatureC: 10 } });
|
||||
await assert.rejects(pending, { name: 'AbortError' });
|
||||
await assert.rejects(services.regional.getBrief(30, -97), { name: 'AbortError' });
|
||||
});
|
||||
|
||||
test('boundary throttle status survives empty or invalid error bodies', async () => {
|
||||
for (const status of [429, 503]) {
|
||||
const services = createApplicationRequestServices({ fetchImpl: async () => new Response('unavailable', { status, headers: { 'Retry-After': '5' } }) });
|
||||
assert.deepEqual(await services.boundaries.query('fixture'), { rateLimited: true, retryAfterMs: 5000 });
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user