Verify identity persistence on HTML5 (sys.save writes to IDBFS) #17

Closed
opened 2026-08-19 12:06:45 +00:00 by yuris · 1 comment
Owner

Identity persistence is proven on native and unverified on HTML5, and there is a concrete reason to doubt it rather than assume it.

stdb.sys_storage uses sys.save / sys.load. On HTML5 that writes to Emscripten's IDBFS, which is an in-memory filesystem flushed to IndexedDB asynchronously — a write is not durable at the moment sys.save returns. A page closed before the flush loses the token, and the player silently gets a new identity on the next load. That is exactly the failure #8 fixed for the header case, arriving by a different route.

The native tier proves this properly: tools/conformance.sh runs the engine twice, the second time with no STDB_TOKEN, and fails unless both runs report the same identity. The HTML5 harness only does a single page load, so it proves the ?token= transport works but says nothing about persistence.

Work

  • Extend tools/html5-conformance.mjs with a second load of the same page, no STDB_TOKEN in the query string, reusing the same browser profile so IndexedDB survives
  • Assert both loads report the same SMOKE: identity 0x…, mirroring the native check
  • If the token does not survive, fix it in stdb/init.lua rather than in the harness — most likely an explicit IDBFS sync, or storing the token in localStorage via html5.run on HTML5

Acceptance

The harness fails if the second load reports a different identity. Whichever way it lands, the finding gets written into the README's identity-persistence section — a token that quietly does not persist is worse than one that documented itself as not persisting.

Note the profile handling: the harness currently creates a throwaway --user-data-dir per run and deletes it, which is right for isolation but means IndexedDB cannot survive between runs. The two loads must share one profile within a single run.

Identity persistence is proven on native and **unverified on HTML5**, and there is a concrete reason to doubt it rather than assume it. `stdb.sys_storage` uses `sys.save` / `sys.load`. On HTML5 that writes to Emscripten's IDBFS, which is an in-memory filesystem flushed to IndexedDB asynchronously — a write is not durable at the moment `sys.save` returns. A page closed before the flush loses the token, and the player silently gets a new identity on the next load. That is exactly the failure #8 fixed for the header case, arriving by a different route. The native tier proves this properly: `tools/conformance.sh` runs the engine twice, the second time with no `STDB_TOKEN`, and fails unless both runs report the same identity. The HTML5 harness only does a single page load, so it proves the `?token=` transport works but says nothing about persistence. ## Work - [ ] Extend `tools/html5-conformance.mjs` with a second load of the same page, no `STDB_TOKEN` in the query string, reusing the same browser profile so IndexedDB survives - [ ] Assert both loads report the same `SMOKE: identity 0x…`, mirroring the native check - [ ] If the token does not survive, fix it in `stdb/init.lua` rather than in the harness — most likely an explicit IDBFS sync, or storing the token in `localStorage` via `html5.run` on HTML5 ## Acceptance The harness fails if the second load reports a different identity. Whichever way it lands, the finding gets written into the README's identity-persistence section — a token that quietly does not persist is worse than one that documented itself as not persisting. Note the profile handling: the harness currently creates a throwaway `--user-data-dir` per run and deletes it, which is right for isolation but means IndexedDB cannot survive between runs. The two loads must share one profile within a single run.
Author
Owner

Verified, and it persists. 7dc33bc.

What the harness does now

Load 1 is handed a token in the query string as before. On its PASS, the harness navigates the same page in the same browser process with STDB_TOKEN removed, and requires both loads to reach PASS reporting the same identity — the shape the native tier has always used across two engine processes.

Two details the description called out, both handled:

  • One profile, two loads. The throwaway --user-data-dir stays throwaway; the second load happens within the same run, so IndexedDB is the same origin in the same Chromium. Isolation between runs is preserved, which is what that profile was for.
  • Phase 2 restarts again. restarted resets before the second load, so load 2 bounces the server exactly as native run 2 does rather than getting a softer test than the first.

Also refreshed the timeout per load: two full phase sequences do not fit in one 120s budget, and the CI runner already spends ~13s of it in reconnect backoff.

[html5] SMOKE: identity 0xc200d7a169271ff6e3d9d6e96809606eaae5045178f08f519c05380e4c88dfb0
[html5] SMOKE: PASS procedure round trip; failure surfaced as an error
[html5] load 1 passed — reloading without a token (identity persistence)
[html5] SMOKE: identity 0xc200d7a169271ff6e3d9d6e96809606eaae5045178f08f519c05380e4c88dfb0
[html5] SMOKE: PASS procedure round trip; failure surfaced as an error
[html5] PASS (identity 0xc200d7…dfb0 survived a reload)

No fix was needed in stdb/init.lua. The IDBFS flush happens well before the reload, so sys.save / sys.load work unmodified on HTML5 — no explicit sync, no localStorage fallback via html5.run.

The check can actually fail

A green assertion that has never gone red is not evidence, so I ran a negative control: storage = false in the smoke client, everything else identical.

[html5] SMOKE: identity 0xc200d7a169271ff6e3d9d6e96809606eaae5045178f08f519c05380e4c88dfb0
[html5] load 1 passed — reloading without a token (identity persistence)
[html5] SMOKE: identity 0xc200d72c114b95dbf537edf26ef4a5cce76333a35a37084881a1521b0f7e3e35
[html5] identity NOT persisted across loads: '0xc200d7a1…' then '0xc200d72c…'
[html5] FAIL

Exit 1, both identities printed. Note the failure mode this guards: load 2 still reports PASS on its own terms, because a brand-new anonymous identity connects perfectly happily. That is the same silent-success trap #8 found with the dropped Authorization header — nothing errors, the player is just someone else now. The control was reverted; only the harness change is committed.

What this does not prove

The harness reloads roughly 15s after the token is saved, so it demonstrates the flush happens, not the flush is prompt. A tab closed in the second between sys.save returning and IDBFS reaching IndexedDB is still untested and still plausible. README now states that limit rather than advertising durability nobody measured — if a shutdown path ever depends on it, that is the case to test.

Closing: the acceptance criterion is met (the harness fails on identity mismatch) and the finding is in the README's identity-persistence section.

Verified, and it persists. `7dc33bc`. ## What the harness does now Load 1 is handed a token in the query string as before. On its `PASS`, the harness navigates the **same page in the same browser process** with `STDB_TOKEN` removed, and requires both loads to reach `PASS` reporting the same identity — the shape the native tier has always used across two engine processes. Two details the description called out, both handled: - **One profile, two loads.** The throwaway `--user-data-dir` stays throwaway; the second load happens *within* the same run, so IndexedDB is the same origin in the same Chromium. Isolation between runs is preserved, which is what that profile was for. - **Phase 2 restarts again.** `restarted` resets before the second load, so load 2 bounces the server exactly as native run 2 does rather than getting a softer test than the first. Also refreshed the timeout per load: two full phase sequences do not fit in one 120s budget, and the CI runner already spends ~13s of it in reconnect backoff. ``` [html5] SMOKE: identity 0xc200d7a169271ff6e3d9d6e96809606eaae5045178f08f519c05380e4c88dfb0 [html5] SMOKE: PASS procedure round trip; failure surfaced as an error [html5] load 1 passed — reloading without a token (identity persistence) [html5] SMOKE: identity 0xc200d7a169271ff6e3d9d6e96809606eaae5045178f08f519c05380e4c88dfb0 [html5] SMOKE: PASS procedure round trip; failure surfaced as an error [html5] PASS (identity 0xc200d7…dfb0 survived a reload) ``` No fix was needed in `stdb/init.lua`. The IDBFS flush happens well before the reload, so `sys.save` / `sys.load` work unmodified on HTML5 — no explicit sync, no `localStorage` fallback via `html5.run`. ## The check can actually fail A green assertion that has never gone red is not evidence, so I ran a negative control: `storage = false` in the smoke client, everything else identical. ``` [html5] SMOKE: identity 0xc200d7a169271ff6e3d9d6e96809606eaae5045178f08f519c05380e4c88dfb0 [html5] load 1 passed — reloading without a token (identity persistence) [html5] SMOKE: identity 0xc200d72c114b95dbf537edf26ef4a5cce76333a35a37084881a1521b0f7e3e35 [html5] identity NOT persisted across loads: '0xc200d7a1…' then '0xc200d72c…' [html5] FAIL ``` Exit 1, both identities printed. Note the failure mode this guards: **load 2 still reports `PASS`** on its own terms, because a brand-new anonymous identity connects perfectly happily. That is the same silent-success trap #8 found with the dropped `Authorization` header — nothing errors, the player is just someone else now. The control was reverted; only the harness change is committed. ## What this does not prove The harness reloads roughly 15s after the token is saved, so it demonstrates *the flush happens*, not *the flush is prompt*. A tab closed in the second between `sys.save` returning and IDBFS reaching IndexedDB is still untested and still plausible. README now states that limit rather than advertising durability nobody measured — if a shutdown path ever depends on it, that is the case to test. Closing: the acceptance criterion is met (the harness fails on identity mismatch) and the finding is in the README's identity-persistence section.
yuris closed this issue 2026-08-19 14:51:29 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
yuris/spacetimedb-defold#17
No description provided.