@xcwds/plugin-update

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.state UpdateState
app.update.subscribe (listener: (state: UpdateState) => void) => () => void
Calls listener now and on every change; returns a function that stops it.
app.update.askBeforeReload boolean
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: markBusy names and onBeforeReload reasons.
app.update.markBusy (name: string, isBusy: () => boolean) => () => void
Marks name busy while isBusy() 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's handover(). 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.

Edit this page on GitHub