summaryrefslogtreecommitdiffhomepage
path: root/src/features
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-12 16:28:07 +0900
committerAdam Malczewski <[email protected]>2026-06-12 16:28:07 +0900
commit4001274e3ba25a3946df1e9f2dc82ca6781cd2bf (patch)
tree24af95e69bda5c38ab7eefd6b71d55b4c247040a /src/features
parente6f6bd86eab07954d8f06e740659969c3dfecc7f (diff)
downloaddispatch-web-4001274e3ba25a3946df1e9f2dc82ca6781cd2bf.tar.gz
dispatch-web-4001274e3ba25a3946df1e9f2dc82ca6781cd2bf.zip
feat(cache-warming): consume CR-4 lifecycle — tab-close cancel + scope-aware subscriptions
- closeTab now POSTs /conversations/:id/close (abort in-flight turn + stop/disable warming server-side); disconnect still leaves both running ([email protected]) - syncSubscriptions honors catalog scope ([email protected]): global surfaces are not re-subscribed on conversation switch - fix(ws): the surface-message parser dropped the conversationId echo (CR-4d was ours, not the backend's) — preserved + unit-tested - secondsUntilNext: 3s stale guard — a past nextWarmAt renders as waiting, not 0s - re-pinned + re-mirrored [email protected] / [email protected] - scripts/probe-cache-warming.ts: live CR-4 probe (default-off, future nextWarmAt, repeated warms, mid-turn close abort, idempotent re-close) — 17/17 against bin/up
Diffstat (limited to 'src/features')
-rw-r--r--src/features/cache-warming/logic/view-model.test.ts10
-rw-r--r--src/features/cache-warming/logic/view-model.ts14
2 files changed, 22 insertions, 2 deletions
diff --git a/src/features/cache-warming/logic/view-model.test.ts b/src/features/cache-warming/logic/view-model.test.ts
index 3d6f6d0..d5ea901 100644
--- a/src/features/cache-warming/logic/view-model.test.ts
+++ b/src/features/cache-warming/logic/view-model.test.ts
@@ -215,6 +215,14 @@ describe("secondsUntilNext (authoritative, from nextWarmAt)", () => {
expect(secondsUntilNext(10_000, 10_000)).toBe(0);
expect(secondsUntilNext(250_000, 10_000)).toBe(240);
expect(secondsUntilNext(70_000, 10_000)).toBe(60);
- expect(secondsUntilNext(5_000, 999_999)).toBe(0); // already past
+ });
+
+ it("treats a nextWarmAt past the stale grace as not scheduled (belt-and-braces)", () => {
+ // Within the 3s grace an on-time warm may briefly read "0s"…
+ expect(secondsUntilNext(10_000, 11_000)).toBe(0);
+ expect(secondsUntilNext(10_000, 13_000)).toBe(0);
+ // …but beyond it the value is stale → null (the "waiting…" state).
+ expect(secondsUntilNext(10_000, 13_001)).toBeNull();
+ expect(secondsUntilNext(5_000, 999_999)).toBeNull();
});
});
diff --git a/src/features/cache-warming/logic/view-model.ts b/src/features/cache-warming/logic/view-model.ts
index f7740d7..eb105f6 100644
--- a/src/features/cache-warming/logic/view-model.ts
+++ b/src/features/cache-warming/logic/view-model.ts
@@ -232,11 +232,23 @@ export function observeWarm(
}
/**
+ * Grace before a PAST `nextWarmAt` is treated as "not scheduled" (→ the
+ * "waiting…" state instead of a perpetual "0s"). The backend pushes the FUTURE
+ * `nextWarmAt` after every warm (CR-4b) and `null` while generating/disabled, so
+ * this is a belt-and-braces guard that should never trigger — it only matters
+ * against a stale/buggy emitter, and the small window lets an on-time warm show
+ * "0s" for the second it takes to complete.
+ */
+const STALE_NEXT_WARM_MS = 3000;
+
+/**
* Seconds until the next automatic warm, AUTHORITATIVE: derived straight from the
* backend's `nextWarmAt` epoch-ms (never FE-anchored/guessed). `null` when nothing
- * is scheduled (disabled, or a turn is generating so the timer is cancelled).
+ * is scheduled (disabled, or a turn is generating so the timer is cancelled) — or
+ * when `nextWarmAt` is stale (further than the grace into the past).
*/
export function secondsUntilNext(nextWarmAt: number | null, now: number): number | null {
if (nextWarmAt === null) return null;
+ if (now - nextWarmAt > STALE_NEXT_WARM_MS) return null;
return Math.max(0, Math.ceil((nextWarmAt - now) / 1000));
}