Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 50 additions & 12 deletions src/lib/state.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -700,8 +700,21 @@ export function applyServerSettings(s: UserSettings): void {
setSystemPrompts(s.systemPrompts ?? []);
}

// One-way latch flipped by `haltBackgroundWork()` when a newer build is
// detected. Module-scoped (not on the reactive `app` object) because it
// must survive a lock/unlock cycle - if the user signs out and back in
// after the update banner appears, we still don't want to fire fresh
// workers against soon-to-be-stale code. The page reload through
// `applyUpdate()` is the only thing that clears it.
let workersHalted = false;

function startBackgroundWorkers(config: AppConfig): void {
if (!app.supabase) return;
// A newer build was announced (see `haltBackgroundWork()` below) -
// don't fire workers that we'd just tear down. This covers the race
// where `onNeedRefresh` lands between `activate()` and the settings
// fetch that gates worker startup.
if (workersHalted) return;
// Each manager acquires a cross-tab lock before spawning its
// worker, so another tab holding the lock will make these calls
// hang internally - they're fire-and-forget by design, never
Expand Down Expand Up @@ -818,15 +831,19 @@ async function loadSettingsThenStartWorkers(config: AppConfig): Promise<void> {
startBackgroundWorkers(config);
}

export function lock(): void {
// Tear all workers down before clearing services — each manager
// releases its Web Lock here so a queued tab can take over as soon
// as we're gone. Order doesn't matter; the locks are independent.
// Each handle's stop() is a no-op when start() never fired
// (module never loaded) and dispatches through the captured
// import Promise otherwise - so a sign-out that races a still-
// loading manager chunk still runs the teardown when the chunk
// lands.
/**
* Tear down every background worker plus the usage poller. Each
* manager handle's stop() is a no-op when start() never fired (module
* never loaded) and dispatches through the captured import Promise
* otherwise - so a teardown that races a still-loading manager chunk
* still runs when the chunk lands. Order doesn't matter; the locks are
* independent.
*
* Used by `lock()` (sign-out releases each Web Lock so a queued tab
* can take over) and by `haltBackgroundWork()` (newer build detected,
* stop processing on the old code until the user reloads).
*/
function stopBackgroundWorkers(): void {
embeddings.stop();
reflection.stop();
summary.stop();
Expand All @@ -835,10 +852,31 @@ export function lock(): void {
journal.stop();
wiki.stop();
wikiLibrarian.stop();
// Tear down the usage poller and wipe the cache so rows billed
// against the previous API key don't leak into a subsequent
// unlock-with-different-config.
stopUsagePolling();
}

/**
* Halt all background work because a newer build is waiting. Called
* from `update.svelte.ts::onNeedRefresh` once the SW reports a waiting
* version. Latches the `workersHalted` flag so that a subsequent
* sign-out + sign-in inside the same page session cannot accidentally
* fire workers back up on stale code - the only way out of the halted
* state is the page reload that `applyUpdate()` performs.
*
* The update poller itself lives in update.svelte.ts and is not in the
* worker list - it's what's announcing the new build, so it has to
* keep running. Same for the in-flight chat loop: it's user-initiated,
* not background, and the user gets to finish their turn before the
* update banner asks them to reload.
*/
export function haltBackgroundWork(): void {
if (workersHalted) return;
workersHalted = true;
stopBackgroundWorkers();
}

export function lock(): void {
stopBackgroundWorkers();
app.config = null;
app.supabase = null;
app.venice = null;
Expand Down
9 changes: 9 additions & 0 deletions src/lib/update.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

import { registerSW } from 'virtual:pwa-register';
import { createLogger } from './logger.svelte';
import { haltBackgroundWork } from './state.svelte';

const log = createLogger('update');

Expand Down Expand Up @@ -123,6 +124,14 @@ export function initUpdateWatcher(): void {
controller: !!navigator.serviceWorker?.controller,
});
updateState.available = true;
// A newer build is waiting. Halt every background worker on
// this tab - they'd otherwise keep processing leases against
// soon-to-be-stale code until the user clicks Reload. The
// halt is one-way; the page reload through `applyUpdate()` is
// what clears it. The update poller itself keeps running -
// it's what flipped `available`, and it stays in
// update.svelte.ts so the halt doesn't touch it.
haltBackgroundWork();
},
onRegisteredSW(swUrl, registration) {
// Routine SW-registered confirmation - fires on every load; the
Expand Down
Loading