mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
fix(cctv): release Tarktee sibling on fetch rejection
This commit is contained in:
1 parent
59646e52f9
commit
97630a598a
3 files changed
+54
-1
No files matched your search
@@ -4457,6 +4457,11 @@ silently demoting every later lookup for the session.
|
|||||||
empty keeps the old catalog and is retried after another 15 minutes, not on the next
|
empty keeps the old catalog and is retried after another 15 minutes, not on the next
|
||||||
request. Estonia's Tarktee DATEX feeds, which answer in 2 to 16.6 s, get 30 s
|
request. Estonia's Tarktee DATEX feeds, which answer in 2 to 16.6 s, get 30 s
|
||||||
(`TARKTEE_FETCH_TIMEOUT_MS`) where other packs get 15 s.
|
(`TARKTEE_FETCH_TIMEOUT_MS`) where other packs get 15 s.
|
||||||
|
Both Tarktee XML catalogs refuse redirects and have a 4 MiB per-document
|
||||||
|
limit, enforced against declared length and streamed bytes. A rejected fetch,
|
||||||
|
unsuccessful response, or body-read failure releases both available response
|
||||||
|
bodies; read failures also cancel an active sibling reader. Cameras sharing
|
||||||
|
an image number are deduplicated before the Tarktee pack cap is applied.
|
||||||
- **CCTV v3 UX — viewshed + calibration gizmo** (built 2026-07-05 and field
|
- **CCTV v3 UX — viewshed + calibration gizmo** (built 2026-07-05 and field
|
||||||
validated 2026-07-21): the COVERAGE toggle is a
|
validated 2026-07-21): the COVERAGE toggle is a
|
||||||
tri-state cycle `OFF → ON → VIEWSHED`; viewshed mode renders each visible camera's frustum
|
tri-state cycle `OFF → ON → VIEWSHED`; viewshed mode renders each visible camera's frustum
|
||||||
|
|||||||
@@ -1200,7 +1200,7 @@ export function parseTarkteeDatexImages(xml) {
|
|||||||
*/
|
*/
|
||||||
export async function loadTarkteeSourcesFromDatex() {
|
export async function loadTarkteeSourcesFromDatex() {
|
||||||
try {
|
try {
|
||||||
const [locResp, imgResp] = await Promise.all([
|
const responses = await Promise.allSettled([
|
||||||
fetch(TARKTEE_LOCATIONS_URL, {
|
fetch(TARKTEE_LOCATIONS_URL, {
|
||||||
headers: { Accept: 'application/xml,text/xml,*/*' },
|
headers: { Accept: 'application/xml,text/xml,*/*' },
|
||||||
redirect: 'manual',
|
redirect: 'manual',
|
||||||
@@ -1212,6 +1212,9 @@ export async function loadTarkteeSourcesFromDatex() {
|
|||||||
signal: AbortSignal.timeout(TARKTEE_FETCH_TIMEOUT_MS),
|
signal: AbortSignal.timeout(TARKTEE_FETCH_TIMEOUT_MS),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
const [locResp, imgResp] = responses.map((result) =>
|
||||||
|
result.status === 'fulfilled' ? result.value : undefined,
|
||||||
|
);
|
||||||
// Both responses are fetched in parallel; if either is rejected or overflows
|
// Both responses are fetched in parallel; if either is rejected or overflows
|
||||||
// its byte cap, both bodies must be released so neither transport leaks.
|
// its byte cap, both bodies must be released so neither transport leaks.
|
||||||
const discardBoth = async () => {
|
const discardBoth = async () => {
|
||||||
@@ -1221,6 +1224,11 @@ export async function loadTarkteeSourcesFromDatex() {
|
|||||||
]);
|
]);
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
const rejected = responses.find((result) => result.status === 'rejected');
|
||||||
|
if (rejected) {
|
||||||
|
await discardBoth();
|
||||||
|
throw rejected.reason;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
(locResp.status >= 300 && locResp.status < 400) ||
|
(locResp.status >= 300 && locResp.status < 400) ||
|
||||||
(imgResp.status >= 300 && imgResp.status < 400)
|
(imgResp.status >= 300 && imgResp.status < 400)
|
||||||
|
|||||||
@@ -168,6 +168,46 @@ test('Tarktee loader refuses a redirect and cancels both response bodies', async
|
|||||||
assert.equal(cancelled.images, true, 'sibling 200 body is also cancelled');
|
assert.equal(cancelled.images, true, 'sibling 200 body is also cancelled');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Tarktee loader cancels fulfilled bodies when either fetch rejects', async (t) => {
|
||||||
|
for (const failedUrl of [TARKTEE_LOCATIONS_URL, TARKTEE_IMAGES_URL]) {
|
||||||
|
for (const delayedSibling of [false, true]) {
|
||||||
|
await t.test(
|
||||||
|
`${failedUrl}, delayed sibling: ${delayedSibling}`,
|
||||||
|
async (t) => {
|
||||||
|
quiet(t);
|
||||||
|
let cancelled = false;
|
||||||
|
const response = new Response(
|
||||||
|
new ReadableStream({
|
||||||
|
start(controller) {
|
||||||
|
controller.enqueue(new TextEncoder().encode('<xml/>'));
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
cancelled = true;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
t.mock.method(globalThis, 'fetch', async (url) => {
|
||||||
|
if (String(url) === failedUrl) throw new TypeError('fetch failed');
|
||||||
|
if (delayedSibling) await new Promise(setImmediate);
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
assert.deepEqual(await loadTarkteeSourcesFromDatex(), []);
|
||||||
|
assert.equal(
|
||||||
|
cancelled,
|
||||||
|
true,
|
||||||
|
'fulfilled sibling body is cancelled',
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
await response.body.cancel();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('Tarktee loader cancels the sibling body when one feed fails or overflows the byte cap', async (t) => {
|
test('Tarktee loader cancels the sibling body when one feed fails or overflows the byte cap', async (t) => {
|
||||||
quiet(t);
|
quiet(t);
|
||||||
let siblingCancelled = false;
|
let siblingCancelled = false;
|
||||||
|
|||||||
Reference in new issue
Block a user