summaryrefslogtreecommitdiffhomepage
path: root/src/app/store.svelte.ts
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
commit46d6b48d5229bd8cd8eb61efd0a0275349de7caf (patch)
tree1afce4d7e7eb14acca6fc1cea0b162d28760d086 /src/app/store.svelte.ts
parent9469b3d56be7d5a1b7fe781a5f6e6eb213e46439 (diff)
downloaddispatch-web-46d6b48d5229bd8cd8eb61efd0a0275349de7caf.tar.gz
dispatch-web-46d6b48d5229bd8cd8eb61efd0a0275349de7caf.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/app/store.svelte.ts')
-rw-r--r--src/app/store.svelte.ts29
1 files changed, 27 insertions, 2 deletions
diff --git a/src/app/store.svelte.ts b/src/app/store.svelte.ts
index df92b31..2837bb5 100644
--- a/src/app/store.svelte.ts
+++ b/src/app/store.svelte.ts
@@ -256,6 +256,23 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore {
socket?.send({ type: "chat.unsubscribe", conversationId });
}
+ /**
+ * Tell the backend the user EXPLICITLY closed this conversation's tab
+ * (`POST /conversations/:id/close`): aborts any in-flight turn (it seals with
+ * `reason: "aborted"`) and stops + DISABLES its cache-warming (persisted OFF).
+ * Distinct from a disconnect / `chat.unsubscribe`, which deliberately leave
+ * both running. Fire-and-forget: a failure is non-fatal (worst case the
+ * warming keeps running until a later close/toggle), and the endpoint is
+ * idempotent server-side.
+ */
+ function closeConversation(conversationId: string): void {
+ void fetchImpl(`${httpBase}/conversations/${encodeURIComponent(conversationId)}/close`, {
+ method: "POST",
+ }).catch(() => {
+ // Non-fatal — see doc comment.
+ });
+ }
+
/** The conversation the surfaces should scope to (undefined for a draft). */
function focusedConversationId(): string | undefined {
return tabsStore.activeConversationId ?? undefined;
@@ -289,7 +306,12 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore {
function syncSubscriptions(): void {
const cid = focusedConversationId();
for (const entry of protocol.catalog) {
- const result = protocolSubscribe(protocol, entry.id, cid);
+ // A GLOBAL surface ignores conversation scope — subscribe it WITHOUT an id
+ // so a conversation switch doesn't churn a redundant unsubscribe+subscribe
+ // round trip ([email protected] catalog `scope`; ABSENT = assume
+ // conversation-scoped, the conservative pre-0.2.0 policy).
+ const scoped = entry.scope === "global" ? undefined : cid;
+ const result = protocolSubscribe(protocol, entry.id, scoped);
protocol = result.state;
for (const msg of result.outgoing) {
socket?.send(msg);
@@ -489,7 +511,10 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore {
closeTab(conversationId: string): void {
tabsStore.closeTab(conversationId);
- // Stop watching the closed conversation's turns (does NOT stop the turn).
+ // The user is DONE with this chat for now: abort any in-flight turn and
+ // stop + disable its cache-warming, server-side.
+ closeConversation(conversationId);
+ // Stop watching the closed conversation's turns.
unsubscribeChat(conversationId);
const store = chatStores.get(conversationId);
if (store !== undefined) {