Skip to content

60-net: Q2 fast movement channel + Q6 local asset cache - #435

Merged
AlexZ005 merged 7 commits into
feat/60from
feat/60-net
Oct 23, 2026
Merged

AlexZ005 merged 7 commits into
feat/60from
feat/60-net

Conversation

@AlexZ005

Copy link
Copy Markdown
Collaborator

Lane 60-net — roadmap 60 Q2 (fast movement channel) + Q6 (local asset cache). Base v1.41.0.

Q2 — fast movement channel (src/lib/fastChannel.js)

  • A second, negotiated data channel per peer on the existing RTCPeerConnection, {ordered: false, maxRetransmits: 0}, for latest-pose streams only: camera, vrhands, streamed move (drag, held objects, physics bodies, possess). Edits, committed moves and requests stay on the reliable conn.
  • ps sequence numbers per sender; a receive-side stale filter drops older poses, on both channels.
  • A trailing keyframe: when a key goes quiet, its last pose is re-sent once on the reliable channel.
  • A pose that would queue behind more than 256 KB of unsent data is skipped.
  • Fallback: a peer without fc: 1 (an older build), a channel that isn't open yet, or a message over 12 KB all go reliable, byte-identical to before.
  • Wire changes are additive: fc: 1 on modules, a fastchan hello (on the ALWAYS_ALLOWED floor), and ps. peers.send(data, {fast: true}) and broadcast(payload, opts) gain an optional second argument.

Q6 — local asset cache (src/lib/assetCache.js + assetCacheCore.js)

  • OPFS, keyed by content hash (or by url for pinned CDN files), with an LRU under a cap (default 1 GB) and Cache Storage as the fallback.
  • Covers pack files (packCache keeps its API; its old Cache Storage copy is migrated once, then deleted), skies, CC0 downloads, converted textures and animation refs.
  • Settings ▸ Storage shows use by kind, the size limit and Clear. The Storage breakdown gains a "Cached downloads" row.
  • navigator.storage.persist() is asked once, through a toast that gives the reason.
  • The app shell is never cached; the no-cache service-worker rule stands.

Proof

  • vitest 2522/2522 (31 new: fastChannel, assetCacheCore); check-ratchet 298 against a baseline of 299; npm run build passes.
  • e2e fast-channel-loss passes. It runs two real peers through a WebRTC loss shim: reliable = retransmit after 200 ms with head-of-line blocking, fast = dropped. 20 Hz, 400 poses per phase, with rendering paused so the measurement times the wire.
loss reliable p95 fast p95
0 % 57 ms 57 ms
5 % 249 ms 74 ms
10 % 249 ms (max 1.3 s) 99 ms (max 154 ms)

Edits arrive complete and in order on both channels, the final pose lands exactly, and the ordered-channel control applies 400/400.

  • e2e fast-channel-fallback passes: a peer without the channel gets every pose over the reliable conn.
  • e2e asset-cache-opfs passes:
    • the second load makes 0 requests and gets 2 cache hits;
    • persist() is asked once;
    • nothing from the app shell is cached;
    • Clear empties OPFS.
  • Regression battery: 10 of 12 green. cc0-asset-browser and possess fail identically on base 1.41.0 (A/B), so they are not this branch.

Owed on device

  • N6 on lossy Wi-Fi: avatar smoothness.
  • Quest: VR hands over the fast channel.

🤖 Generated with Claude Code

AlexZ005 and others added 7 commits October 23, 2026 15:38
…nel for pose streams

- fastChannel.js (leaf): a negotiated {ordered:false, maxRetransmits:0} data channel on each
  PeerJS connection's own RTCPeerConnection (id 7, no extra signaling), opened only when the
  other side advertises `fc: 1` in the modules handshake + a `fastchan` hello down the same conn
- every pose message (camera, vrhands, move) carries `ps`, one monotonic counter per sender;
  receivers drop a pose older than the newest applied for that (sender, key) — on both channels
  and inside batch envelopes, so a late streamed move never overwrites the committed one
- trailing keyframe: a stream's last pose is re-sent reliably (same ps) once the key goes
  quiet for 250 ms, to the peers that got it unreliably
- streams marked fast: avatar camera (not when its extras change), VR hands (not on a rep
  switch), gizmo drag, physics bodies, VR held objects (not the release), play-mode grabs,
  possess rides; everything else stays on the reliable conn
- graceful fallback: older peers / closed channel / >12 KB messages go reliable; kill switch
  safeStorage fastChannel=off; `fastchan` on the cloudHooks ALWAYS_ALLOWED floor
- debug hook __stores.fastChannel (links, stale filter, keyframes, pose tap for the loss e2e)
- unit: tests/unit/fastChannel.test.js (18)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r a cap

- assetCacheCore.js (zero-import leaf): url keys (+ a version tag for build-bundled files),
  derived keys (source hash + kind + settings, field order irrelevant), the index shape and
  its normalizer, usage (a shared blob counted once), the LRU eviction plan, orphans
- assetCache.js: one blob per SHA-256 in OPFS (tp-asset-cache/blobs) + index.json; the same
  model on CacheStorage where OPFS cannot write; a plain fetch where neither exists. Reconciles
  orphan blobs / missing blobs on open; index flushed on a 400 ms debounce and on pagehide
- kinds wired: pack files (packCache.js is now a url-keyed view over the cache, same API; 39 P5's
  CacheStorage copies are migrated once and that cache deleted), bundled skies (hdriRuntime,
  keyed by the build version), CC0 downloads (cc0Fetch.fetchBytes), converted textures (the
  import optimizer's output keyed by source hash + options + version), pack anim refs
- navigator.storage.persist() asked once, from a toast with the reason, the first time something
  is cached (assetCache:persistAsked); never on boot, never again
- never the app shell: only content urls reach it
- unit: tests/unit/assetCacheCore.test.js (13); place-drag-cache reads the store through
  packCache.cachedPackBuffer instead of CacheStorage

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…uites

- Settings ▸ Storage (StorageSettings.svelte, Workspace group): what the cache holds by kind,
  Clear, the size limit (256 MB / 512 MB / 1 GB / 2 GB, LRU), the persistence state with
  "Ask the browser", and the Storage breakdown button
- Storage breakdown: a "Cached downloads" category (skies, converted textures, downloaded
  files), one row per kind, reclaimed through the cache's own delete
- e2e: asset-cache-opfs (second load served from OPFS with no request, persist asked once,
  no shell in the index, Settings Clear, the 39 P5 migration), fast-channel-loss (p95 pose gap
  at 0/5/10 % loss before/after through a WebRTC loss shim), fast-channel-fallback

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…of queueing it stale

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…h pages paused, edits read from .text, a cadence check per phase

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…al pose land (10 % back-off outlasts a fixed 1.5 s)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r message swapped same-ms releases), ordered-channel control check

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@AlexZ005
AlexZ005 merged commit 27a8f53 into feat/60 Oct 23, 2026
9 checks passed
@AlexZ005
AlexZ005 deleted the feat/60-net branch October 23, 2026 23:08
@AlexZ005

Copy link
Copy Markdown
Collaborator Author

In main via 60-int: released in v1.44.0 (#476).

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