Public: anyone can read this; the crew can change it.
Sentinel: shared:01m23vygyffx5hg2f5vm8r74sr:2hvdu8l540 is past its budget: trevato/forge: "after a write, signed-in home is answered from memory within the budget and says what to swap" failed o… #8029
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/8002-sentinel-shared-01m23vygyffx5hg2f5vm8r74 → main 3 commits fork 2 threads · 2 unresolved
2 files
changed +18
−2
src/web/studio/home_last.ts +2 −1
| @@ -73,10 +73,11 @@ export async function homeRegion( | ||
| 73 | 73 | if (!current && Date.now() - (drawing.get(key) ?? 0) >= REDRAW_GAP_MS) { |
| 74 | 74 | if (drawing.size > 1024) for (const [k, at] of drawing) if (at !== Number.MAX_SAFE_INTEGER) drawing.delete(k); |
| 75 | 75 | drawing.set(key, Number.MAX_SAFE_INTEGER); |
| 76 | // Read when the redraw is decided, not when the timer fires: a write that lands in between must leave the copy stale, not current. | |
| 77 | const before = stamp; | |
| 76 | 78 | setTimeout(() => { |
| 77 | 79 | void (async () => { |
| 78 | 80 | try { |
| 79 | const before = writeStamp(db); | |
| 80 | 81 | remember(db, viewerId, name, variant, (await draw()).text, before, access); |
| 81 | 82 | } catch { |
| 82 | 83 | // The page's own swap asks again and shows the failure. |
test/home-memory.test.ts +16 −1
| @@ -6,7 +6,8 @@ | ||
| 6 | 6 | import { afterAll, beforeAll, expect, test } from "bun:test"; |
| 7 | 7 | import { snappyFixture, type SnappyFixture } from "../bench/snappy.ts"; |
| 8 | 8 | import { forgetAll, forgetRegion, recall, remember } from "../src/core/region_memory.ts"; |
| 9 | import { homeVariant } from "../src/web/studio/home_last.ts"; | |
| 9 | import { homeRegion, homeVariant } from "../src/web/studio/home_last.ts"; | |
| 10 | import { raw } from "../src/web/html.ts"; | |
| 10 | 11 | |
| 11 | 12 | let f: SnappyFixture; |
| 12 | 13 | beforeAll(async () => { |
| @@ -88,3 +89,17 @@ test("a copy is recalled only for the access it was drawn under, and forgetting | ||
| 88 | 89 | forgetRegion(db, "bea"); |
| 89 | 90 | expect(recall(db, "bea", "feed", "", "m.1")).toBeUndefined(); |
| 90 | 91 | }); |
| 92 | ||
| 93 | test("a write between deciding a redraw and its timer firing leaves the redrawn copy stale", async () => { | |
| 94 | const db = f.app.db; | |
| 95 | forgetAll(db); | |
| 96 | const request = new Request("http://x/"); | |
| 97 | const draw = () => raw("<p>drawn</p>"); | |
| 98 | const look = () => homeRegion(db, request, "race-viewer", "feed", "?race", "m", draw); | |
| 99 | await look(); // first look: drawn and remembered current | |
| 100 | db.query("UPDATE repo SET description = 'first' WHERE id = ?1").run(f.repos[0]!.id); | |
| 101 | expect((await look()).text).toContain("data-refresh="); // stale: a redraw is decided, its timer pending | |
| 102 | db.query("UPDATE repo SET description = 'second' WHERE id = ?1").run(f.repos[0]!.id); // lands before the timer fires | |
| 103 | await Bun.sleep(50); | |
| 104 | expect((await look()).text).toContain("data-refresh="); | |
| 105 | }); |
new above
offline
Correct fix, but add a regression test that writes between this line and the timer firing.