diff --git a/server/src/computer/snapshot-store.ts b/server/src/computer/snapshot-store.ts index 157638f5..450c31fc 100644 --- a/server/src/computer/snapshot-store.ts +++ b/server/src/computer/snapshot-store.ts @@ -145,6 +145,13 @@ export function createInMemorySnapshotStore(): SnapshotStore { const snapshots = new Map(); return { save: async (computerId, snapshot) => { + // Only ever forward, the same rule the table's `setWhere` applies, because the two stores have + // to agree about when a save wins. Two snapshots of one computer can complete out of order in a + // single process as easily as across two, and a test that reaches for this store because it has + // no database would otherwise be told a different story about what the gateway resolves + // against: the older page here, the newer one in a deployment. + const held = snapshots.get(computerId); + if (held && held.snapshotId >= snapshot.snapshotId) return; snapshots.set(computerId, snapshot); }, load: async (computerId) => snapshots.get(computerId), diff --git a/server/tests/computer-snapshot-store.test.ts b/server/tests/computer-snapshot-store.test.ts index c0fedd28..3f30d10e 100644 --- a/server/tests/computer-snapshot-store.test.ts +++ b/server/tests/computer-snapshot-store.test.ts @@ -67,6 +67,35 @@ describe("the in-memory snapshot store", () => { expect(loaded?.elements.get("e9")?.name).toBe("Cancel"); }); + test("an older snapshot arriving late does not overwrite the newer one", async () => { + // The property #46 established, asked of this store rather than of the table. A test that reaches + // for the in-memory store because it has no database must not be told a different story about + // when a save wins: two snapshots of one computer can complete out of order here too, and the + // generation is what decides between them in both implementations. + const store = createInMemorySnapshotStore(); + await store.save( + "default", + snapshot(8, [{ ref: "e9", role: "button", name: "Cancel" }]), + ); + + await store.save( + "default", + snapshot(7, [{ ref: "e9", role: "button", name: "Submit order" }]), + ); + + // A save of the generation already held loses too, the way `setWhere`'s `lt` refuses it: the + // stored snapshot is the one that generation named, and a second delivery of it carries nothing + // newer to say. + await store.save( + "default", + snapshot(8, [{ ref: "e9", role: "button", name: "Submit order" }]), + ); + + const loaded = await store.load("default"); + expect(loaded?.snapshotId).toBe(8); + expect(loaded?.elements.get("e9")?.name).toBe("Cancel"); + }); + test("clearing forgets the snapshot, so nothing resolves against a wiped computer", async () => { const store = createInMemorySnapshotStore(); await store.save(