mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
- i18n scanner: lastError sink, no false sinks on defaults and ternaries - shared src/i18n/marker.js for modules that only mark strings - wind field messages move to wind/fields.js; CITY_POIS imports from cityPois.js - drop review IDs, change history and stale references from comments and docs Co-Authored-By: Claude Opus 5.5 <[email protected]>
130 lines
4.1 KiB
JavaScript
130 lines
4.1 KiB
JavaScript
// Imported by the server: the catalog-free marker, not src/i18n/index.js. The
|
|
// text stays English data; layers show it translated (translateKnown).
|
|
import { msg } from '../i18n/marker.js';
|
|
|
|
const tooLarge = () =>
|
|
Object.assign(new Error(msg('Upstream response too large')), {
|
|
code: 'RESPONSE_TOO_LARGE',
|
|
});
|
|
|
|
/**
|
|
* Read a fetch() Response body as text with a hard byte cap. Rejects early on an
|
|
* oversized Content-Length, then streams with a running cap so a chunked or
|
|
* length-omitted response cannot blow past the limit. Throws { code:'RESPONSE_TOO_LARGE' }.
|
|
*/
|
|
export async function readResponseTextCapped(response, maxBytes, signal) {
|
|
const declared = Number(response.headers.get('content-length'));
|
|
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
void response.body?.cancel().catch(() => {});
|
|
throw tooLarge();
|
|
}
|
|
signal?.throwIfAborted();
|
|
const reader = response.body?.getReader?.();
|
|
if (!reader) {
|
|
const text = await response.text();
|
|
signal?.throwIfAborted();
|
|
if (new TextEncoder().encode(text).byteLength > maxBytes) throw tooLarge();
|
|
return text;
|
|
}
|
|
const cancel = () => {
|
|
void reader.cancel().catch(() => {});
|
|
};
|
|
signal?.addEventListener('abort', cancel, { once: true });
|
|
const decoder = new TextDecoder();
|
|
let out = '';
|
|
let total = 0;
|
|
try {
|
|
for (;;) {
|
|
signal?.throwIfAborted();
|
|
const { done, value } = await reader.read();
|
|
signal?.throwIfAborted();
|
|
if (done) break;
|
|
total += value.byteLength;
|
|
if (total > maxBytes) throw tooLarge();
|
|
out += decoder.decode(value, { stream: true });
|
|
}
|
|
return out + decoder.decode();
|
|
} catch (error) {
|
|
cancel();
|
|
throw error;
|
|
} finally {
|
|
signal?.removeEventListener('abort', cancel);
|
|
reader.releaseLock();
|
|
}
|
|
}
|
|
|
|
/** Parse a fetch() JSON response only after enforcing a hard byte cap. */
|
|
export async function readResponseJsonCapped(response, maxBytes, signal) {
|
|
return JSON.parse(await readResponseTextCapped(response, maxBytes, signal));
|
|
}
|
|
|
|
/**
|
|
* Read a fetch() Response body as bytes with the same hard cap as
|
|
* readResponseTextCapped — for protobuf and tile upstreams (GTFS-Realtime,
|
|
* TomTom flow). Same early Content-Length rejection, running cap while
|
|
* streaming and cancellation on abort, so callers can treat the two alike.
|
|
* Throws { code:'RESPONSE_TOO_LARGE' }.
|
|
*/
|
|
export async function readResponseBytesCapped(response, maxBytes, signal) {
|
|
const declared = Number(response.headers.get('content-length'));
|
|
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
await response.body?.cancel().catch(() => {});
|
|
throw tooLarge();
|
|
}
|
|
signal?.throwIfAborted();
|
|
const reader = response.body?.getReader?.();
|
|
if (!reader) {
|
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
signal?.throwIfAborted();
|
|
if (bytes.byteLength > maxBytes) throw tooLarge();
|
|
return bytes;
|
|
}
|
|
const cancel = () => {
|
|
void reader.cancel().catch(() => {});
|
|
};
|
|
signal?.addEventListener('abort', cancel, { once: true });
|
|
const chunks = [];
|
|
let total = 0;
|
|
try {
|
|
for (;;) {
|
|
signal?.throwIfAborted();
|
|
const { done, value } = await reader.read();
|
|
signal?.throwIfAborted();
|
|
if (done) break;
|
|
total += value.byteLength;
|
|
if (total > maxBytes) throw tooLarge();
|
|
chunks.push(value);
|
|
}
|
|
const out = new Uint8Array(total);
|
|
let offset = 0;
|
|
for (const chunk of chunks) {
|
|
out.set(chunk, offset);
|
|
offset += chunk.byteLength;
|
|
}
|
|
return out;
|
|
} catch (error) {
|
|
await reader.cancel().catch(() => {});
|
|
throw error;
|
|
} finally {
|
|
signal?.removeEventListener('abort', cancel);
|
|
reader.releaseLock();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the existing promise for a cache key, or create one and remove it
|
|
* only when that exact promise settles.
|
|
*/
|
|
export function coalesceProxyRequest(inFlight, key, create) {
|
|
const existing = inFlight.get(key);
|
|
if (existing) return { promise: existing, shared: true };
|
|
let promise;
|
|
promise = Promise.resolve()
|
|
.then(create)
|
|
.finally(() => {
|
|
if (inFlight.get(key) === promise) inFlight.delete(key);
|
|
});
|
|
inFlight.set(key, promise);
|
|
return { promise, shared: false };
|
|
}
|