Fix app stream/timeline ordering desync and init lifecycle race (#73)
* fix(app): enforce canonical stream sequencing and init resolution * refactor: extract timeline and stream reducers to pure functions
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { classifySessionTimelineSeq } from "./session-timeline-seq-gate";
|
||||
import {
|
||||
deriveBootstrapTailTimelinePolicy,
|
||||
shouldResolveTimelineInit,
|
||||
} from "./session-timeline-bootstrap-policy";
|
||||
|
||||
describe("deriveBootstrapTailTimelinePolicy", () => {
|
||||
it("always replaces on explicit reset without catch-up cursor", () => {
|
||||
const policy = deriveBootstrapTailTimelinePolicy({
|
||||
direction: "after",
|
||||
reset: true,
|
||||
epoch: "epoch-1",
|
||||
endCursor: { seq: 200 },
|
||||
isInitializing: false,
|
||||
hasActiveInitDeferred: false,
|
||||
});
|
||||
|
||||
expect(policy.replace).toBe(true);
|
||||
expect(policy.catchUpCursor).toBeNull();
|
||||
});
|
||||
|
||||
it("forces baseline replace and canonical catch-up for init tail race", () => {
|
||||
const advancedCursor = { epoch: "epoch-1", endSeq: 205 };
|
||||
const tailSeqStart = 101;
|
||||
const tailSeqEnd = 200;
|
||||
|
||||
let acceptedWithoutBootstrap = 0;
|
||||
for (let seq = tailSeqStart; seq <= tailSeqEnd; seq += 1) {
|
||||
const decision = classifySessionTimelineSeq({
|
||||
cursor: advancedCursor,
|
||||
epoch: "epoch-1",
|
||||
seq,
|
||||
});
|
||||
if (decision === "accept" || decision === "init") {
|
||||
acceptedWithoutBootstrap += 1;
|
||||
}
|
||||
}
|
||||
expect(acceptedWithoutBootstrap).toBe(0);
|
||||
|
||||
const policy = deriveBootstrapTailTimelinePolicy({
|
||||
direction: "tail",
|
||||
reset: false,
|
||||
epoch: "epoch-1",
|
||||
endCursor: { seq: 200 },
|
||||
isInitializing: true,
|
||||
hasActiveInitDeferred: true,
|
||||
});
|
||||
|
||||
expect(policy.replace).toBe(true);
|
||||
expect(policy.catchUpCursor).toEqual({
|
||||
epoch: "epoch-1",
|
||||
endSeq: 200,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not replace non-bootstrap, non-reset responses", () => {
|
||||
const policy = deriveBootstrapTailTimelinePolicy({
|
||||
direction: "tail",
|
||||
reset: false,
|
||||
epoch: "epoch-1",
|
||||
endCursor: { seq: 200 },
|
||||
isInitializing: false,
|
||||
hasActiveInitDeferred: false,
|
||||
});
|
||||
|
||||
expect(policy.replace).toBe(false);
|
||||
expect(policy.catchUpCursor).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldResolveTimelineInit", () => {
|
||||
it("resolves tail init when the tail response arrives", () => {
|
||||
expect(
|
||||
shouldResolveTimelineInit({
|
||||
hasActiveInitDeferred: true,
|
||||
isInitializing: true,
|
||||
initRequestDirection: "tail",
|
||||
responseDirection: "tail",
|
||||
reset: false,
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not resolve tail init when an after response arrives first", () => {
|
||||
expect(
|
||||
shouldResolveTimelineInit({
|
||||
hasActiveInitDeferred: true,
|
||||
isInitializing: true,
|
||||
initRequestDirection: "tail",
|
||||
responseDirection: "after",
|
||||
reset: false,
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("resolves after init when an after response arrives", () => {
|
||||
expect(
|
||||
shouldResolveTimelineInit({
|
||||
hasActiveInitDeferred: true,
|
||||
isInitializing: true,
|
||||
initRequestDirection: "after",
|
||||
responseDirection: "after",
|
||||
reset: false,
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user