wip(k1): checkpoint connection recovery rewrite
Capture the current unreleased K1 connection, recovery, lifecycle, viewer, and test work as a single known-bad baseline for subsequent fixes.
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { after, before, test } from "node:test";
|
||||
|
||||
import { createServer } from "vite";
|
||||
|
||||
let createLiveViewerDiagnosticLifecycle;
|
||||
let createAbortFencedBuildVerifier;
|
||||
let createUiBuildStaleCoordinator;
|
||||
let liveViewerDiagnosticBody;
|
||||
let server;
|
||||
let uiBuildIdFromModuleScripts;
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
appType: "custom",
|
||||
logLevel: "silent",
|
||||
server: { middlewareMode: true },
|
||||
});
|
||||
({
|
||||
createAbortFencedBuildVerifier,
|
||||
createLiveViewerDiagnosticLifecycle,
|
||||
createUiBuildStaleCoordinator,
|
||||
liveViewerDiagnosticBody,
|
||||
uiBuildIdFromModuleScripts,
|
||||
} = await server.ssrLoadModule("/src/core/observation/liveViewerDiagnostics.ts"));
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await server?.close();
|
||||
});
|
||||
|
||||
function createFakeScheduler() {
|
||||
let now = 0;
|
||||
let nextHandle = 1;
|
||||
const jobs = new Map();
|
||||
const schedule = (callback, delay, interval) => {
|
||||
const handle = nextHandle;
|
||||
nextHandle += 1;
|
||||
jobs.set(handle, { callback, due: now + delay, interval });
|
||||
return handle;
|
||||
};
|
||||
const clear = (handle) => jobs.delete(handle);
|
||||
return {
|
||||
scheduler: {
|
||||
setTimeout: (callback, delay) => schedule(callback, delay, null),
|
||||
clearTimeout: clear,
|
||||
setInterval: (callback, delay) => schedule(callback, delay, delay),
|
||||
clearInterval: clear,
|
||||
},
|
||||
advance(milliseconds) {
|
||||
const target = now + milliseconds;
|
||||
while (true) {
|
||||
const next = [...jobs.entries()]
|
||||
.filter(([, job]) => job.due <= target)
|
||||
.sort((left, right) => left[1].due - right[1].due)[0];
|
||||
if (!next) break;
|
||||
const [handle, job] = next;
|
||||
now = job.due;
|
||||
if (job.interval === null) jobs.delete(handle);
|
||||
else job.due += job.interval;
|
||||
job.callback();
|
||||
}
|
||||
now = target;
|
||||
},
|
||||
pending: () => jobs.size,
|
||||
};
|
||||
}
|
||||
|
||||
const lineage = (viewerInstanceId, lifecycleGeneration = 1) => ({
|
||||
uiBuildId: "/assets/index-abcdefgh.js",
|
||||
documentInstanceId: "00000000-0000-4000-8000-000000000001",
|
||||
viewerInstanceId,
|
||||
lifecycleGeneration,
|
||||
});
|
||||
|
||||
test("mounted viewer admission terminally fences 60 seconds of stale timers", () => {
|
||||
const clock = createFakeScheduler();
|
||||
const callbacks = [];
|
||||
const posts = [];
|
||||
const lifecycle = createLiveViewerDiagnosticLifecycle({
|
||||
lineage: lineage("00000000-0000-4000-8000-000000000011"),
|
||||
scheduler: clock.scheduler,
|
||||
diagnosticPoster: (event, eventLineage) => posts.push({ event, eventLineage }),
|
||||
buildVerifier: () => undefined,
|
||||
});
|
||||
|
||||
lifecycle.armAdmissionTimeout(() => callbacks.push("timeout"), 12_000);
|
||||
lifecycle.armAdmissionInterval(() => callbacks.push("discovery"), 100);
|
||||
lifecycle.markAdmitted();
|
||||
lifecycle.post({ eventCode: "live_receiver_active_store_admitted" });
|
||||
clock.advance(60_000);
|
||||
|
||||
assert.deepEqual(callbacks, []);
|
||||
assert.equal(clock.pending(), 0);
|
||||
assert.equal(posts.length, 1);
|
||||
assert.equal(posts[0].eventLineage.lifecycleGeneration, 1);
|
||||
});
|
||||
|
||||
test("two mounted viewers keep timer and diagnostic lineage isolated", () => {
|
||||
const clock = createFakeScheduler();
|
||||
const posts = [];
|
||||
const first = createLiveViewerDiagnosticLifecycle({
|
||||
lineage: lineage("00000000-0000-4000-8000-000000000021"),
|
||||
scheduler: clock.scheduler,
|
||||
diagnosticPoster: (event, eventLineage) => posts.push({ event, eventLineage }),
|
||||
buildVerifier: () => undefined,
|
||||
});
|
||||
const second = createLiveViewerDiagnosticLifecycle({
|
||||
lineage: lineage("00000000-0000-4000-8000-000000000022", 7),
|
||||
scheduler: clock.scheduler,
|
||||
diagnosticPoster: (event, eventLineage) => posts.push({ event, eventLineage }),
|
||||
buildVerifier: () => undefined,
|
||||
});
|
||||
|
||||
first.armAdmissionTimeout(() => {
|
||||
first.post({ eventCode: "live_receiver_error" });
|
||||
}, 12_000);
|
||||
second.armAdmissionTimeout(() => {
|
||||
second.post({ eventCode: "live_receiver_error" });
|
||||
}, 12_000);
|
||||
first.markAdmitted();
|
||||
clock.advance(12_000);
|
||||
|
||||
assert.equal(posts.length, 1);
|
||||
assert.equal(
|
||||
posts[0].eventLineage.viewerInstanceId,
|
||||
"00000000-0000-4000-8000-000000000022",
|
||||
);
|
||||
assert.equal(posts[0].eventLineage.lifecycleGeneration, 7);
|
||||
});
|
||||
|
||||
test("stale-build and unmount fence callbacks before one reload", () => {
|
||||
const clock = createFakeScheduler();
|
||||
const order = [];
|
||||
const posts = [];
|
||||
const lifecycle = createLiveViewerDiagnosticLifecycle({
|
||||
lineage: lineage("00000000-0000-4000-8000-000000000031"),
|
||||
scheduler: clock.scheduler,
|
||||
diagnosticPoster: (event) => posts.push(event),
|
||||
buildVerifier: () => undefined,
|
||||
});
|
||||
lifecycle.armAdmissionTimeout(() => {
|
||||
lifecycle.post({ eventCode: "live_receiver_error" });
|
||||
}, 12_000);
|
||||
const coordinator = createUiBuildStaleCoordinator({
|
||||
scheduleReload: (callback, delay) => {
|
||||
order.push(`scheduled:${delay}`);
|
||||
clock.scheduler.setTimeout(callback, delay);
|
||||
},
|
||||
reload: () => order.push("reload"),
|
||||
});
|
||||
coordinator.subscribe(() => {
|
||||
order.push("local-transports-closed");
|
||||
lifecycle.dispose();
|
||||
});
|
||||
|
||||
coordinator.report({
|
||||
loadedUiBuildId: "/assets/index-abcdefgh.js",
|
||||
expectedUiBuildId: "/assets/index-ijklmnop.js",
|
||||
});
|
||||
coordinator.report({
|
||||
loadedUiBuildId: "/assets/index-abcdefgh.js",
|
||||
expectedUiBuildId: "/assets/index-qrstuvwx.js",
|
||||
});
|
||||
clock.advance(60_000);
|
||||
lifecycle.post({ eventCode: "live_receiver_error" });
|
||||
|
||||
assert.deepEqual(order, ["local-transports-closed", "scheduled:50", "reload"]);
|
||||
assert.deepEqual(posts, []);
|
||||
assert.equal(lifecycle.active(), false);
|
||||
});
|
||||
|
||||
test("last unsubscribe fences an already queued build verification callback", () => {
|
||||
const controller = new AbortController();
|
||||
const observedSignals = [];
|
||||
const queuedVerify = createAbortFencedBuildVerifier(
|
||||
controller.signal,
|
||||
(signal) => observedSignals.push(signal),
|
||||
);
|
||||
|
||||
queuedVerify();
|
||||
// stopBuildMonitor aborts the locally captured controller when the last
|
||||
// mounted viewer unsubscribes. A browser callback already queued before the
|
||||
// interval/listener removal can still run once, but cannot start a fetch.
|
||||
controller.abort();
|
||||
queuedVerify();
|
||||
|
||||
assert.deepEqual(observedSignals, [controller.signal]);
|
||||
assert.equal(observedSignals[0].aborted, true);
|
||||
});
|
||||
|
||||
test("diagnostic body and build id retain exact document/viewer/build lineage", () => {
|
||||
const eventLineage = lineage("00000000-0000-4000-8000-000000000041", 9);
|
||||
assert.deepEqual(
|
||||
liveViewerDiagnosticBody(
|
||||
{ eventCode: "live_receiver_recovered", streamId: "acquisition-42" },
|
||||
eventLineage,
|
||||
),
|
||||
{
|
||||
schema_version: "missioncore.live-viewer-diagnostic/v2",
|
||||
event_code: "live_receiver_recovered",
|
||||
ui_build_id: "/assets/index-abcdefgh.js",
|
||||
document_instance_id: "00000000-0000-4000-8000-000000000001",
|
||||
viewer_instance_id: "00000000-0000-4000-8000-000000000041",
|
||||
lifecycle_generation: 9,
|
||||
stream_id: "acquisition-42",
|
||||
},
|
||||
);
|
||||
assert.equal(
|
||||
uiBuildIdFromModuleScripts(
|
||||
["https://mission.local/assets/index-dT7dN-y4.js"],
|
||||
"https://mission.local/park",
|
||||
),
|
||||
"/assets/index-dT7dN-y4.js",
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user