Skip to content

offline: make startup and browsing work without a connection (#854) - #1694

Open
Suncadgeek wants to merge 8 commits into
pulsejet:masterfrom
Suncadgeek:pr-offline-web
Open

offline: make startup and browsing work without a connection (#854)#1694
Suncadgeek wants to merge 8 commits into
pulsejet:masterfrom
Suncadgeek:pr-offline-web

Conversation

@Suncadgeek

Copy link
Copy Markdown

The service worker and the data and image caches already put most of the offline pieces in place, but several startup paths still block on the network, so in practice launching the app offline (or on a flaky connection) shows a blank page. This series fixes them one by one; each commit is independently reviewable. It also addresses the report in #854 that the whole app waits to connect before showing anything (#854 (comment)).

Bug fixes:

  • The image worker opened its cache asynchronously at startup, and the first viewport of preview requests raced past the check while it was still undefined, going straight to the network. Offline, the whole first screen stayed on placeholders forever even though every preview was cached.

  • On Android WebView, worker script requests are not routed through the service worker at all, so the image worker cannot even start while offline. The worker is now health-checked once with a short deadline and requests fall back to the main thread (cache first) when it does not answer.

  • The precache stored entries keyed by the webpack output name, which carries the content hash as a ?v= parameter, while pages request the same scripts with the Nextcloud version as ?v=. No page request ever matched the precache, so deployed updates never reached browsers until the runtime cache expired. Revisions are moved out of the URL, the ?v= parameter is ignored when matching, and updated workers activate immediately.

  • The static configuration request had no timeout and much of the app waits on it. A first start with a failed request rendered nothing at all, permanently. It is now bounded, retried in the background with backoff, and the app shows a loading indicator instead of a blank page in that state.

  • The on-this-day widget blocked the first timeline render while revalidating over the network. When cached content is available it now revalidates in the background.

Behavior improvements:

  • HTML pages are served stale-while-revalidate, so startup does not depend on the network round trip and takes constant time regardless of connection quality.

  • The most recent days (up to 500 photos) are prefetched in the background after the timeline loads in the native app, so recently synced content can be browsed offline even if it was never displayed. Throttled to once per hour and served through the existing cache-first worker path, so anything already cached costs nothing.

  • The user is told once when cached content is being shown offline, and the timeline refreshes when connectivity returns.

  • Client-side timeouts are treated as network errors so they do not surface as error toasts.

Test environment: Nextcloud 34.0.0 (PHP 8.4, Docker) with Memories 8.0.1, self-hosted on fiber. Measured on a Pixel 7 (GrapheneOS, Vanadium WebView): offline cold start went from a permanently blank page to skeleton in about 4 seconds and cached photos in about 7, and browsing weeks of never-displayed days offline works. Online startup is unchanged or slightly faster. Also verified on a Samsung Galaxy S10 (stock Android and WebView) running the unmodified store build of the app against the same server, with identical results.

Add a network timeout to the NetworkFirst route for HTML pages, so
that the app shell is served from cache when the network is slow or
dead instead of blocking the startup indefinitely.

Register the service worker immediately when running inside the
native app: sessions there can be short-lived, and the service
worker must take control early for offline startup to work.
The first render of the timeline waited on the network in several
places, so starting the app offline (or on a connection that drops
packets, e.g. behind a reconnecting VPN) showed a blank page for up
to a minute, or forever on a first start:

- The on-this-day widget blocked the first timeline render while
  revalidating over the network. When cached content is available,
  revalidate in the background instead.

- The static configuration request had no timeout and the whole app
  waits on it. Bound it to 5 seconds and retry in the background with
  backoff on failure; a new bus event tells UserConfig components to
  pick up late-loaded values. Without this, a first start with a
  failed request rendered nothing at all (timeline_path _unknown_);
  also show a loading icon instead of a blank page in that state.

- Treat client-side timeouts (ECONNABORTED) as network errors so
  they do not surface as error toasts.

- Tell the user once when cached content is shown offline, and
  refresh the timeline when connectivity returns (online event).
The image worker opened the Cache API storage asynchronously at
startup, but the first batch of preview requests arrives before the
open completes. Those requests silently skipped the cache lookup
(imageCache is still undefined) and went straight to the network
queue. Online this is invisible; offline the whole first viewport
stays on placeholders forever even though every preview is cached.

Keep the opening promise and await it before matching.
On Android WebView, worker script requests are not routed through the
service worker, so the image worker cannot start at all while offline:
the fetch of its own script hangs, and every preview request queues
against a worker that will never answer. No error surfaces anywhere.

Race the worker fetch against a timeout and serve the image from the
main thread (cache first) when it expires. Once the worker is marked
unresponsive further requests skip the wait; a later successful worker
response re-enables it.
After the timeline loads over the network in the native app, warm the
offline caches in the background with the most recent days (up to 500
photos): the day details go to the data cache and the previews to the
image cache, through the existing cache-first worker path so anything
already cached costs nothing. Throttled to once per hour.
Two startup paths waited on network timeouts before rendering content
that was available locally:

- The data cache name awaited the remote configuration just to read
  the version. Race it against the persisted version (1s) so cached
  days render without waiting for the config request to fail.

- The image worker fallback waited 5s on the first image request.
  Replace the per-request timeout with a single ping health check
  (2s deadline): if the worker does not answer, all requests go to
  the main thread immediately; if it does, requests are never raced
  against a timer, so slow networks cannot cause double fetches.
  Re-probe every 30s so the worker is used again once it can start.
Startup no longer waits on the network round trip for the HTML page:
serve the cached copy immediately and revalidate in the background.
This makes the app start in constant time regardless of connection
quality, which matters most on slow mobile networks.

The embedded initial state and request token can be one navigation
stale. The timeline refreshes itself over the API on every load, and
the request token remains valid for the session lifetime; renewal
goes through the network-only /csrftoken route.
The precache stored entries keyed by the webpack output name, which
carries the content hash as a ?v= parameter. Pages request the same
scripts with a different ?v= (the Nextcloud version), so no page
request ever matched the precache: scripts were served forever by the
runtime cache-first route, and deploying a new version had no effect
until that cache expired.

Move the content hash into the workbox revision so the cache key is
the bare URL, ignore the ?v= parameter when matching, and activate
updated workers immediately (skipWaiting): revisioned entries keep an
already-open page consistent.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant