Update protocol
This page is the behaviour every host must implement. It is the normative description of how a phone takes an update and how it recovers from a bad one.
Host update algorithm
Section titled “Host update algorithm”Run it on UI start, on resume and every 30 minutes in the foreground. Never block the first paint on it.
- Fetch the pointer (5 second timeout) and decode it strictly.
- If
pointer.seq < state.pending, droppending: the release was pulled. - If
pointer.seq <= current, or it is inbad, orpointer.seq <= state.pending, stop.
- If
- Rollout gate.
fnv1a32(installId + ":" + seq) % 100 < rollout. If it fails, stop.installIdis a random UUID created on first launch. Never use a device id. - Fetch
manifests/<seq>.json(at most 1 MB) and its.sig. - Run checks 1 to 6 of the canonical order. Any failure means stop and log. Never retry the same
seqwithin the run. - Check the total size. If the files not already in the content-addressed store add up to more than the app’s cap (default 50 MB), stop.
- Download each missing file to a temporary name, verify its sha256 and size (check 7), then rename it to
cas/<hash>. - Data requirements. Make sure every
requires.datapair has been synced at least once. If the phone is offline, stop: the bundle is not pending until its data is there. - Make it pending. Write the manifest and signature, and set
state.pending = seq. Ifurgentis set, emitbundle.updateReady; the web app may then callbundle.applyUpdate, which reloads the UI. Otherwise the bundle applies at the next UI start.
State file
Section titled “State file”State lives in <appSupport>/valence/state.json. Writes are atomic: write a temporary file, flush, then rename.
{ "installId": "…", "port": 23817, "current": 42, "previous": 41, "pending": 43, "trial": null, "trialFails": 0, "serving": null, "bad": [39], "floor": 40 }| Field | Meaning |
|---|---|
current |
The last committed bundle. |
previous |
The bundle before it. |
pending |
Downloaded and verified, waiting for the next UI start. |
trial |
Being tried, not yet confirmed. |
trialFails |
How many times the trial has failed. |
serving |
The trial’s seq while it is on screen and unconfirmed, otherwise null. |
bad |
Bundles marked bad on this phone. |
floor |
The high-water mark. It never goes down. |
State machine
Section titled “State machine”Transitions run when the host widget is about to load a bundle (UI start), never in main() or a background isolate. iOS background launches and workmanager runs must not consume a trial.
At UI start:
- A newer built-in bundle wins. If the built-in bundle’s
seqis abovecurrent(a store update shipped it), install it through the same checks, skipping check 3. Setprevious = currentandcurrent = builtin, and droppendingandtrialif they are at or below it. - A crashed trial. If
serving == trial, the last UI start showed the trial and the process died before it was confirmed. Count a failure. - No trial but a pending bundle: set
trial = pending, clearpendingand resettrialFailsto 0. - Serve. If there is a trial, serve it and set
serving = trial. Otherwise servecurrent, or the built-in bundle if there is none.
Counting a failure
Section titled “Counting a failure”A failure is counted in exactly one place, from step 2 or from a ready timeout:
- increment
trialFailsand setserving = null; - if
trialFailsreaches 2, add the trial tobadand cleartrialandtrialFails; - otherwise keep the trial, so the next UI start tries it once more. One failure can be a user who opened and closed the app at once.
The ready timer
Section titled “The ready timer”- It counts 15 seconds of foreground time, starting at the entry page’s
onPageStarted, and pauses while the app is in the background. - It commits only if
app.ready()comes from the page serving the trial. Aready()from acurrentpage never commits anything. - On commit:
previous = current,current = trial,trial = null,trialFails = 0,serving = null,floor = max(floor, manifest.floor), then run garbage collection. - On timeout: count a failure, then reload
currentright away.
Garbage collection
Section titled “Garbage collection”It runs only after a commit.
- Keep the manifests for
current,previous,pending,trialand the built-in bundle, with every file they reference. - Delete every other manifest and file.
- Delete each
(bucket, schema)pair no kept manifest requires. Writable buckets are never deleted this way.
Rollback
Section titled “Rollback”Rolling back means rolling forward. Release the last good commit with valence bundle so it gets a new, higher seq, then point the channel at it. Pointing the channel at an older seq only cancels pending downloads, as in step 1 of the algorithm. Phones still trying a bad bundle revert on their own.
Backups
Section titled “Backups”Exclude all of <appSupport>/valence/ from device backups, so a restore starts clean from the built-in bundle with a new installId. The SQLite database lives outside it and is backed up.