This page shows how to turn the SPA's offline layer or its WebSocket module off, or remove either from your product.
The SPA opens with no network and shows every list it last saw. A person can add, edit and delete notes and tags offline, and the changes are sent when the connection is back. A change the server cannot take is shown as a conflict for the person to settle; nothing is overwritten on its own.
How it works, in four Concepts pages:
- How offline reads work: what the device keeps, and the three levels.
- How offline writes sync: the queue, conflicts and tags.
- Sign-out and the device's data: what a sign-out deletes and keeps.
- The app shell: the service worker, and why offline tests use the build.
To make a new aggregate readable or writable offline, follow aggregates.md.
Both modules run unless their switch says false.
| Variable | /config.json key |
What false does |
|---|---|---|
SPA_REALTIME |
realtime |
No socket. The page reads and writes over HTTP and sees others' changes within about 30 seconds. |
SPA_OFFLINE |
offline |
No device copy, no queue, no remembered user. Every screen needs the network. |
In a deployment, set the variable in
infra/envs/.env.prod. Compose passes it to thespacontainer.Deploy:
deno task deploy
In development, edit apps/spa/public/config.json instead.
true, false, 1 and 0 are accepted. Any other value is ignored with a warning in the
browser's console, and the module runs (apps/spa/src/runtime-config.ts).
With the offline layer off, the service worker still caches the app's files, so the page loads
fast. Copies written before the switch are deleted at the next start. The e2e specs
e2e/realtime-off.e2e.ts and e2e/local-data.e2e.ts run the core flows with each module off.
A queue left behind is sent once, then deleted. At each signed-in start with the layer off, the
app looks for the person's queue databases without opening them. It sends what it finds in order
(offline/send-once.ts), then deletes them. A write that cannot reach the server keeps the queue
for the next try. A write the server refuses is dropped, since there is no conflict screen, and a
warning names up to three of them.
- Delete the folder
apps/spa/src/realtime/. - In
apps/spa/src/modules.ts, delete the./realtime/index.tsimport and its uses:- the
socketoption ofcreateAppCallPort; - the
if (switches.realtime)branch ofstartModules; - the
disconnectRealtimecalls; - the
realtimeswitch.
- the
- Make
advanceGroupCursoran empty function, and makeconnectionDisplayalways answer"idle". - Delete
tests/realtime-removal.test.tsand its entry ininfra/scripts/test.ts. - Delete the e2e specs that test the socket itself:
e2e/groups-socket.e2e.tsande2e/realtime-resume.e2e.ts.
The API's socket route (/api/ws) can stay: with no socket clients, its hints reach nobody.
A product that does not want offline deletes the layer and keeps an online SPA with live updates.
The guard test tests/offline-removal.test.ts fails when a file that imports the layer is missing
from these steps.
Delete the folder
apps/spa/src/offline/, and the filesapps/spa/src/sw-options.tsandapps/spa/src/sw-options.test.ts.In
apps/spa/src/sw.ts, delete the@spy4x/platform/browser/offline-shelland./sw-options.tsimports and theinstallOfflineShell(...)call. Keep the push handlers, and add back the one listenerSWUpdaterneeds:scope.addEventListener("message", (event) => { if (event.data?.action === "skipWaiting") scope.skipWaiting() })In
apps/spa/src/modules.ts, delete:- the
./offline/imports and the "Local data" part of the file; watchUnsentChanges,reviewConflictsand theUnsentWaitandUnsentViewtypes, which are in that part;- the
startOfflineLayerandsendLeftQueuecalls instartModules,sendLeftQueueitself, and thedatabasesoption; - the
stopOfflineLayercall inforgetModules; - the
offlineswitch.
- the
In the same file, keep the
LocalReadsandcreateLocalReadsre-export andnoLocalReads, and end that part with:export const localReads = noLocalReads export const benchmarkLocalData: BenchmarkLocalData | null = nullThe level 2 stores then read from the server and keep nothing, with no change to them or their tests. The
nullcloses the benchmark page, which measures this layer.In
apps/spa/src/app.tsx:bootstrapSession(recallUser)becomesbootstrapSession()and loses itswipeSignedOutstep;- in the effect that remembers the user, drop
rememberUser, and callclearBrowserStorage()where it calledwipeSignedOut(); - delete
wipeSignedOut, and theflushOutboxline ofreads.
In
apps/spa/src/state/notes.ts, delete thecurrentLayerandofflineNotesimports, and exportcreateNotesStore(onlineNotes). The optionalreadLocaldependency and itsshowLocalcall can go too.In
apps/spa/src/state/tags.ts, delete thecurrentLayerandofflineTagsimports, and exportcreateTagsStore(onlineTags).In
apps/spa/src/views/AppShell.tsx:- delete the
currentLayerimport, and make the search'sdeviceNotesanswer[]; - delete
useSignOutand thewatchUnsentChanges,reviewConflictsandUnsentSignOutimports, and passuseSignOutNow()asonSignOut.
- delete the
Delete
apps/spa/src/views/UnsentSignOut.tsxandapps/spa/src/views/UnsentSignOut.test.tsx.In
apps/spa/src/views/NotesView.tsx,apps/spa/src/views/NoteEditorView.tsxandapps/spa/src/views/TagsView.tsx, delete theOfflineStatusimport and element.Delete the specs
e2e/offline.e2e.ts,e2e/local-data.e2e.ts,e2e/search-offline.e2e.ts,e2e/tags-offline.e2e.tsande2e/notes-restore-offline.e2e.ts, and their names inOFFLINE_SPECSofinfra/scripts/e2e-local-plan.ts.Delete
tests/offline-removal.test.tsand its group ininfra/scripts/test.ts.
The app then reads and writes over the network only. The server's changes read (group.changes)
stays and is not called; the notes store pages the REST list instead.