feat(control-station): add atomic recorded-session playback
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { after, before, test } from "node:test";
|
||||
|
||||
import { createServer } from "vite";
|
||||
|
||||
let server;
|
||||
let createLatestAsyncCommitter;
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
appType: "custom",
|
||||
logLevel: "silent",
|
||||
server: { middlewareMode: true },
|
||||
});
|
||||
({ createLatestAsyncCommitter } = await server.ssrLoadModule(
|
||||
"/src/core/runtime/latestAsyncCommitter.ts",
|
||||
));
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await server?.close();
|
||||
});
|
||||
|
||||
async function eventually(predicate, timeoutMs = 1_000) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (!predicate()) {
|
||||
if (Date.now() >= deadline) throw new Error("Timed out waiting for async commit queue");
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
}
|
||||
|
||||
test("viewer settings commits are serialized and intermediate slider values collapse", async () => {
|
||||
const calls = [];
|
||||
const resolvers = [];
|
||||
const settled = [];
|
||||
const committer = createLatestAsyncCommitter({
|
||||
commit(value) {
|
||||
calls.push(value);
|
||||
return new Promise((resolve) => resolvers.push(resolve));
|
||||
},
|
||||
onSettled(result) {
|
||||
settled.push(result);
|
||||
},
|
||||
});
|
||||
|
||||
committer.enqueue({ accumulationSeconds: 1 });
|
||||
committer.enqueue({ accumulationSeconds: 2 });
|
||||
committer.enqueue({ accumulationSeconds: 12 });
|
||||
|
||||
assert.deepEqual(calls, [{ accumulationSeconds: 1 }]);
|
||||
assert.equal(committer.isBusy(), true);
|
||||
resolvers.shift()(true);
|
||||
await eventually(() => calls.length === 2);
|
||||
|
||||
assert.deepEqual(calls[1], { accumulationSeconds: 12 });
|
||||
assert.equal(settled[0].superseded, true);
|
||||
resolvers.shift()(true);
|
||||
await eventually(() => !committer.isBusy());
|
||||
|
||||
assert.deepEqual(settled.map(({ value, applied, superseded }) => ({
|
||||
value,
|
||||
applied,
|
||||
superseded,
|
||||
})), [
|
||||
{ value: { accumulationSeconds: 1 }, applied: true, superseded: true },
|
||||
{ value: { accumulationSeconds: 12 }, applied: true, superseded: false },
|
||||
]);
|
||||
});
|
||||
|
||||
test("viewer settings commit failures settle as rejected without wedging the queue", async () => {
|
||||
const settled = [];
|
||||
const committer = createLatestAsyncCommitter({
|
||||
async commit(value) {
|
||||
if (value === "broken") throw new Error("offline");
|
||||
return true;
|
||||
},
|
||||
onSettled(result) {
|
||||
settled.push(result);
|
||||
},
|
||||
});
|
||||
|
||||
committer.enqueue("broken");
|
||||
await eventually(() => settled.length === 1);
|
||||
committer.enqueue("healthy");
|
||||
await eventually(() => settled.length === 2);
|
||||
|
||||
assert.deepEqual(settled.map(({ value, applied }) => ({ value, applied })), [
|
||||
{ value: "broken", applied: false },
|
||||
{ value: "healthy", applied: true },
|
||||
]);
|
||||
assert.equal(committer.isBusy(), false);
|
||||
});
|
||||
|
||||
test("waitForIdle resolves only after the running commit and latest queued value settle", async () => {
|
||||
const calls = [];
|
||||
const resolvers = [];
|
||||
const committer = createLatestAsyncCommitter({
|
||||
commit(value) {
|
||||
calls.push(value);
|
||||
return new Promise((resolve) => resolvers.push(resolve));
|
||||
},
|
||||
});
|
||||
|
||||
committer.enqueue("initial");
|
||||
committer.enqueue("latest");
|
||||
let idle = false;
|
||||
const idlePromise = committer.waitForIdle().then(() => {
|
||||
idle = true;
|
||||
});
|
||||
|
||||
await Promise.resolve();
|
||||
assert.equal(idle, false);
|
||||
resolvers.shift()(true);
|
||||
await eventually(() => calls.length === 2);
|
||||
assert.deepEqual(calls, ["initial", "latest"]);
|
||||
assert.equal(idle, false);
|
||||
|
||||
resolvers.shift()(true);
|
||||
await idlePromise;
|
||||
assert.equal(idle, true);
|
||||
assert.equal(committer.isBusy(), false);
|
||||
});
|
||||
|
||||
test("waitForIdle observes work enqueued by onSettled and resolves immediately when idle", async () => {
|
||||
const calls = [];
|
||||
let committer;
|
||||
committer = createLatestAsyncCommitter({
|
||||
async commit(value) {
|
||||
calls.push(value);
|
||||
return true;
|
||||
},
|
||||
onSettled({ value }) {
|
||||
if (value === "first") committer.enqueue("follow-up");
|
||||
},
|
||||
});
|
||||
|
||||
await committer.waitForIdle();
|
||||
committer.enqueue("first");
|
||||
await committer.waitForIdle();
|
||||
|
||||
assert.deepEqual(calls, ["first", "follow-up"]);
|
||||
assert.equal(committer.isBusy(), false);
|
||||
});
|
||||
|
||||
test("layout serialization can flush a staged setting and read the confirmed latest value", async () => {
|
||||
let staged = { pointSize: 2 };
|
||||
let confirmed = { pointSize: 1 };
|
||||
let resolveCommit;
|
||||
const committer = createLatestAsyncCommitter({
|
||||
commit() {
|
||||
return new Promise((resolve) => {
|
||||
resolveCommit = resolve;
|
||||
});
|
||||
},
|
||||
onSettled({ value, applied }) {
|
||||
if (applied) confirmed = value;
|
||||
},
|
||||
});
|
||||
|
||||
const saveLayout = async () => {
|
||||
committer.enqueue(staged);
|
||||
await committer.waitForIdle();
|
||||
return { sceneSettings: confirmed };
|
||||
};
|
||||
|
||||
const savePromise = saveLayout();
|
||||
staged = { pointSize: 9 };
|
||||
let saved = false;
|
||||
void savePromise.then(() => {
|
||||
saved = true;
|
||||
});
|
||||
await Promise.resolve();
|
||||
assert.equal(saved, false);
|
||||
|
||||
resolveCommit(true);
|
||||
const layout = await savePromise;
|
||||
assert.deepEqual(layout, { sceneSettings: { pointSize: 2 } });
|
||||
});
|
||||
Reference in New Issue
Block a user