offline set in the manifest, the app also opens with no network: Pylon
serves a web app manifest, a service worker, and the pages to keep on the
device.
Turn it on
Addoffline to buildManifest:
Pylon then does these things:
- It serves
/manifest.webmanifestfrom this config. Apublic/manifest.webmanifestorapp/manifest.webmanifestfile replaces it. - It adds the manifest link, the theme color, and the home-screen icon tags to every page.
- It writes a service worker for each build and serves it at
/pylon-sw.js. - It registers the worker in production.
pylon devdoes not register it, and removes one that an earlier build left.
What the worker keeps
When the worker installs, it downloads these files:- The build files that the pages of
routesload: scripts, chunks, stylesheets, and fonts. - The web app manifest and the icons.
- The HTML of each route in
routesthat has no dynamic segment.
/api/*, /auth/*, the sync connection, or the dev endpoints. Data comes from the sync engine, not from the HTTP cache.
When a page load fails because there is no network, the worker answers with the kept HTML for that route. A route with a dynamic segment, such as /leads/[id], uses the start URL’s HTML. The client then renders the real URL from the local replica. A load of a route that is not in routes opens the start URL.
On an offline route, a slow network gets 3 seconds. Then the worker shows the kept page.
Render rule for offline routes
An offline route renders with no server. Follow these rules on the page and its layouts:- Read data with
db.useQueryordb.useQueryOne. These read the local replica. - Do not read
serverData. There is no server to answer it. - Do not read
props.auth,props.headers, orprops.cookiesduring the render. Useprops.session.existsfor a signed-in check, anduseSession()for the user.
pylon lint warns about an offline route that reads serverData (PYL008), and about an offline route that names no page (PYL009).
The kept HTML carries one bit of the session: signed in or not. When that bit changes, the page tells the worker, and the worker gets the HTML again.
Unsynced and failed writes
Writes made with no network wait in a queue on the device and are sent when the network returns. Show the user what is waiting and what the server refused withuseOutbox():
retry(w.id, { door: "14B" }). A failed write is sent again only as the user who made it.
Rows written offline stay on screen after the app restarts. The sync engine applies the queued writes to the local replica again when it starts.
When two people edit the same field of one row offline, the server keeps the edit that arrives last. Both writes succeed, so neither person is told. Edits to different fields of the row are both kept.
Photos taken offline
uploadFile needs the network. queueUpload keeps the file on the device and returns at once. The leader tab uploads queued files one at a time when there is a network, then sets the file on its record.
queueUploadstores the file in IndexedDB.- It sets
fieldandurlFieldon the record tolocal:<localId>with a queued write.useFileUrlshows alocal:value from the copy on the device. - When the upload finishes, a second queued write sets
fieldto the file id andurlFieldto the file URL.
- A file is uploaded only as the user who queued it. When another user signs in, the queued file is discarded.
- A file the server refuses (type, size, policy) stays in the outbox as a failed write with
kind: "upload". It is not sent again until you retry it. Retry and dismiss it like any failed write. - A dropped connection leaves the file queued. It is sent again on the next reconnect.
- The queue holds at most 200 MB (
maxQueuedUploadByteson the sync engine). Past that,queueUploadthrowsUploadQueueFullError. - Another device sees
local:<localId>until the upload finishes.useFileUrlreturnsnullfor it.
<FileUpload offline attach={...} /> in @pylonsync/client does the same for a file picker. Give attach as a function to insert a row per file:
Instant navigation
A navigation to an offline route renders from the device, with no page fetch, also when the network is good. A tap on a tab bar paints in the same frame. To get the same for a route that is not inroutes, export navigation = "client" from its page. See Instant navigation.
New deploys
Each build gets a new worker with its own caches. The new worker installs in the background and waits. It takes over when every tab of the app is closed, or when the page tells it to. Then it deletes the caches of the old build. A deploy never changes the code of a page that is open by itself. An app on the iOS home screen can stay open in the background for days. Show the user that a new version is available withuseAppUpdate():
The hook checks
/_pylon/version when the page becomes visible, and every five minutes while it is visible. It also learns of a new worker when the worker finishes installing.
apply() does these things in sequence:
- It waits until the outbox has no pending writes. A write made offline waits for the network, so
apply()waits too. Failed writes do not block it. - It waits for the new worker to finish installing, for up to 30 seconds.
- It tells the new worker to take over, and reloads the page once when it does.
apply() in the next 60 seconds does not reload. Thus a page that calls apply() automatically cannot reload in a loop.
/_pylon/version returns the latest build and is never cached:
<AppUpdateBar /> shows a minimal notice with an Update button, and renders nothing when there is no update. It has no styles. Give it a className, or style [data-pylon-app-update]. To use your own components, use the hook.
Under pylon dev, in an app without offline, and during the server render, updateReady is false and apply() does nothing.
Changes to the name or the icon
iOS reads the app’s name, icons, display mode, and start URL only when the user adds the app to the home screen. A later deploy does not change them. When one of these changes,reinstallRecommended becomes true on an iOS home-screen app. Tell the user to remove the app from the home screen and add it again. Android and desktop browsers update an installed app from the manifest, so reinstallRecommended stays false there.
The install field of /_pylon/version is a hash of these values, and of the icon files in public/.
Install the app
- iOS Safari: tap Share, then Add to Home Screen.
- Android Chrome: tap the menu, then Install app.
startUrl, with no browser UI when display is "standalone".