App updates on the user's terms, for an @xcwds app on @xcwds/sveltekit. A new
version installs in the background and waits; the page offers it, and it takes over only when
the user taps Update. Until then a relaunch keeps the old version, since precached files come
from the old version's cache.
// xcwds.config.ts
import { defineConfig } from '@xcwds/core';
import offline from '@xcwds/plugin-offline';
import update from '@xcwds/plugin-update';
export default defineConfig({ brand, plugins: [offline(), update({ askBeforeReload: true })] });
<!-- src/routes/+layout.svelte, inside <App> -->
<script lang="ts">
import UpdateBanner from '@xcwds/plugin-update/UpdateBanner.svelte';
</script>
<UpdateBanner />
Options
| Option | Default | What it does |
|---|---|---|
checkEveryMs |
3600000 |
How often an open page looks for a new version (0 turns it off). It also looks on launch, when the app comes back into view and when the browser goes online. |
askBeforeReload |
true |
While something is busy, the banner asks before Update or Reload goes ahead. |
marker |
xcwds:just-updated |
The sessionStorage key the update hands over in (see below). Set it to the key an app used before moving onto @xcwds. |
Busy work
A reload throws away what a page holds in memory: a running timer, an unsaved form. Say so in either of two ways, and the banner shows "Finish your โฆ first." and asks before reloading:
// State a component already holds:
onMount(() => useApp().update?.markBusy('timer', () => running));
// Or a hook in any client plugin:
app.addHook('onBeforeReload', () => (running ? 'timer' : undefined));
Other tabs
When one tab applies the update, the new worker takes over every tab. A hidden tab with nothing busy reloads quietly; any other tab shows "Updated in another tab" with a Reload button.
Handing over to the new version
Update leaves a marker in sessionStorage for the reloaded page: app.update.state.justUpdated
is true on that first load. Plugins can carry values across with it: app.update.carry(name, () => value) adds one to the marker when it is written (the old version's code runs it), and
app.update.handover() reads them in the new version (null when this load isn't an update;
{} for a marker without values, such as one an app wrote before @xcwds).
@xcwds/plugin-changelog carries the newest entry the old version had, so What's new knows what's
new even on a device that never opened it.
app.update
state (available, reloadNeeded, and justUpdated on the first load after an update, for
what's-new notes), subscribe(listener), check(), busyReasons(), markBusy(name, isBusy),
carry(name, value), handover(),
apply() (what Update does) and reload() (what Reload does). To change how the banner looks,
pass text to <UpdateBanner>, set --xcwds-update-bg, --xcwds-update-fg and
--xcwds-update-accent, or render your own from app.update.
The worker entry answers the page's { type: 'SKIP_WAITING' } message with
app.worker.skipWaiting(); nothing else makes a new version take over.
API
From the package's types and doc comments.
Options (UpdateOptions)
checkEveryMs?number- How often to look for a new version while the app is open, in ms (0: only on launch and when the app comes back). Defaults to an hour.
askBeforeReload?boolean- When something a reload would interrupt is running, ask before reloading. Defaults to
true. marker?string- The session storage key the old version sets just before it reloads into the new one.
Defaults to
xcwds:just-updated; set it to the key an app used before it moved onto
Decorators
app.update?AppUpdate- From
@xcwds/plugin-update.app.update.stateUpdateStateapp.update.subscribe(listener: (state: UpdateState) => void) => () => void- Calls
listenernow and on every change; returns a function that stops it. app.update.askBeforeReloadboolean- From the options: ask before reloading while something is busy.
app.update.check() => Promise<void>- Looks for a new version now.
app.update.busyReasons() => Promise<string[]>- What a reload would interrupt now:
markBusynames andonBeforeReloadreasons. app.update.markBusy(name: string, isBusy: () => boolean) => () => void- Marks
namebusy whileisBusy()returns true; returns a function that unmarks it. app.update.apply() => void- Switches to the waiting version and reloads (what Update does).
app.update.reload() => void- Reloads into the version another tab applied (what Reload does).
app.update.carry(name: string, value: () => unknown) => () => void- Hands a value to the next version: when this page reloads into an update,
value()(JSON) is saved for the new version'shandover(). Returns a function that stops it. app.update.handover() => Readonly<Record<string, unknown>> | null- What the previous version carried over when it reloaded into this one (by name), or null when this page load isn't an update. Browser only; it can be read before the app boots.