Skip to content

feat(m4-12): Cache API on the VM — caches/CacheStorage/Cache (D-19 PR-1)#275

Merged
send merged 10 commits into
mainfrom
feat/11-cache-api-vm
Jun 3, 2026
Merged

feat(m4-12): Cache API on the VM — caches/CacheStorage/Cache (D-19 PR-1)#275
send merged 10 commits into
mainfrom
feat/11-cache-api-vm

Conversation

@send

@send send commented Jun 3, 2026

Copy link
Copy Markdown
Owner

WHATWG Service Workers §5 Cache API on the bytecode VM (slot #11-cache-api-vm), the first of the D-19 3-PR carve-out. Window-realm only, fully independent of the SW thread.

Surfaces (11, all real)

  • caches.{open,has,delete,keys,match} (CacheStorage §5.5)
  • Cache.{match,matchAll,put,delete,keys} (§5.4)
  • CacheQueryOptions (ignoreSearch / ignoreMethod / ignoreVary), CacheStorage.match cacheName restriction
  • Vary matching (incl. Vary:* put-reject), Cache.put rejections: non-GET / 206 / disturbed-body / Vary:*

Architecture (plan §4)

  • Storage: OriginStorageManager::cache_connection() hands out the origin Arc<Mutex<SqliteConnection>> (Send+Sync, shareable to the SW thread in PR-2); CacheBackend wraps it behind with_conn (DR-A). PR-1 lazily mints an in-memory backend on HostData (boa parity); the shell-wired origin handle lands with PR-2/D-26.
  • Async: deferred PendingTask::CacheDeliver { promise_id, outcome } — the backend call runs synchronously at the native, the marshalled outcome settles the Promise at the drain tail (DR-A.1; the single IdbDeliver-style delivery model, GC-rooted in gc/roots.rs).
  • ECS-native: 2 payload-free ObjectKind brands (CacheStorage / Cache) + cache_handle_states side-store (name only); trace / retain / unbind / proto_roots all wired.
  • Layering: all open/has/delete/keys + match/put/Vary algorithm stays in the engine-independent elidex-cache-api crate; the VM only marshals JsValueCachedEntry/Response/Request.

Deferred — Cache.add / addAll (NOT installed)

They fetch-then-put, which needs a fetch-broker promise continuation the VM lacks; a boa-style synchronous empty-response stub would corrupt the cache. Honest absence over a stub → new slot #11-cache-add-fetch-integration (spec-core, eligible). Net cap impact on landing: 47→48 (D-19 program net +1→+2). Decided during implementation (the plan had scoped these into PR-1).

Tests

  • 25 window-realm JS tests (round-trips, all CacheQueryOptions, Vary match/mismatch, cross-cache + cacheName, all put rejections, illegal constructors, synthetic-url, add/addAll absence)
  • 4 elidex-cache-api compute_vary_key tests

Gate

cargo fmtmise run ci (green) → /code-review/simplify/review/elidex-review (5-axis) → Step 4.5 fix-delta re-verification (clean). Findings fixed: GC proto_roots rooting (defensive vs delete globalThis.caches), synthetic response.url, disturbed-body put-reject, missing-arg reject, Vary→crate move, WebIDL §3.7.7 citation.

Recommend squash-merge (2 commits: feat + review-fixes).

🤖 Generated with Claude Code

send and others added 2 commits June 3, 2026 17:34
WHATWG Service Workers §5 Cache API on the bytecode VM (slot
`#11-cache-api-vm`), the first of the D-19 3-PR carve-out. Window-realm
only, fully independent of the SW thread.

Surfaces (real): caches.{open,has,delete,keys,match} (§5.5) +
Cache.{match,matchAll,put,delete,keys} (§5.4) + CacheQueryOptions
(ignoreSearch/ignoreMethod/ignoreVary) + Vary matching (incl. Vary:*
put-reject) + non-GET / 206 put-reject.

Architecture (plan §4):
- Storage: `OriginStorageManager::cache_connection()` hands out the
  origin `Arc<Mutex<SqliteConnection>>` (Send+Sync, shareable to the SW
  thread later); `CacheBackend` wraps it behind `with_conn` (DR-A). For
  PR-1 the VM lazily mints an in-memory backend (boa parity) installed on
  HostData; the shell-wired origin handle lands with PR-2/D-26.
- Async: deferred `PendingTask::CacheDeliver { promise_id, outcome }` —
  the backend call runs synchronously at the native, the marshalled
  outcome settles the Promise at the drain tail (DR-A.1; the single
  IdbDeliver-style delivery model, GC-rooted in gc/roots.rs).
- ECS-native: 2 payload-free ObjectKind brands (CacheStorage / Cache) +
  `cache_handle_states` side-store (name only); trace/retain/unbind wired.
- All open/has/delete/keys + match/put/Vary algorithm stays in the
  engine-independent `elidex-cache-api` crate (Layering mandate); the VM
  only marshals JsValue↔CachedEntry/Response/Request.

Cache.add / addAll are intentionally NOT installed — they fetch then put,
which needs a fetch-broker promise continuation the VM lacks; a boa-style
synchronous empty-response stub would corrupt the cache. Deferred to slot
`#11-cache-add-fetch-integration` (honest absence over a stub).

22 window-realm tests; full `mise run ci` green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
/code-review + /elidex-review (5-axis) findings, all verified:

- GC: root cache_storage_prototype / cache_prototype in the gc/collect.rs
  proto_roots array (+ size 182→184 in gc/roots.rs). Without this,
  `delete globalThis.caches` / `delete globalThis.Cache` lets the cached
  prototype be swept while VmInner still hands its ObjectId to the next
  caches.open() — the IDB/intrinsic-prototype defensive-rooting invariant
  (elidex-review Axis 2 IMP; the only finding the dry-run missed).
- Spec: response.url of a cached synthetic Response must stay "" across a
  put/match round-trip — drop the request-URL fallback in
  build_response_from_entry (code-review).
- Spec §5.4.5: Cache.put now rejects a disturbed/locked-body Response
  instead of silently caching an empty body (code-review).
- WebIDL: match/delete reject a missing (0-arg) required request; single
  point in resolve_request (code-review).
- Layering: move the Vary→match-key derivation into elidex-cache-api
  (entry::compute_vary_key, next to its consumer entry_matches) so
  host/cache only marshals (elidex-review Axis 1 MIN). +4 crate tests.
- Citation: WebIDL §3.7.2.1 → §3.7.7 "Operations" (webref-verified;
  elidex-review Axis 4 IMP).

+4 elidex-cache-api Vary tests, +4 window-realm cache tests (synthetic-url,
disturbed-body, missing-arg). Full `mise run ci` green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 3, 2026 11:25

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements the WHATWG Service Workers §5 Cache API on the bytecode VM (window-realm only), introducing VM-side caches/Cache bindings that marshal to an engine-independent elidex-cache-api SQLite backend and settle Promises via a deferred PendingTask::CacheDeliver delivery model.

Changes:

  • Adds Cache API VM surfaces (caches.*, Cache.*, CacheQueryOptions) with GC rooting, side-store handle state, and structured-clone rejection.
  • Introduces a shareable Cache API storage connection hook (OriginStorageManager::cache_connection) and HostData backend plumbing (with in-memory fallback for tests/headless).
  • Adds Cache API JS tests and elidex-cache-api Vary-key unit tests.

Reviewed changes

Copilot reviewed 22 out of 23 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
crates/storage/elidex-storage-core/src/origin_manager.rs Adds cache_connection() to hand out an origin Cache API Arc<Mutex<SqliteConnection>>.
crates/script/elidex-js/src/vm/vm_api.rs Clears cache_handle_states on Vm::unbind() to prevent stale handle reuse across rebind.
crates/script/elidex-js/src/vm/tests/tests_cache.rs Adds window-realm Cache API JS surface tests.
crates/script/elidex-js/src/vm/tests/mod.rs Registers the new Cache API test module under engine feature.
crates/script/elidex-js/src/vm/object_kind.rs Introduces ObjectKind::CacheStorage and ObjectKind::Cache payload-free brands.
crates/script/elidex-js/src/vm/mod.rs Adds cache_handle_states and prototype slots to VmInner.
crates/script/elidex-js/src/vm/init.rs Initializes Cache API state fields in Vm::new() setup.
crates/script/elidex-js/src/vm/host/structured_clone.rs Marks Cache API objects as unclonable for structured clone.
crates/script/elidex-js/src/vm/host/pending_tasks.rs Adds PendingTask::CacheDeliver and dispatch wiring for deferred Promise settlement.
crates/script/elidex-js/src/vm/host/mod.rs Exposes host::cache module under engine.
crates/script/elidex-js/src/vm/host/cache/register.rs Registers caches global and installs CacheStorage/Cache illegal constructors + prototypes.
crates/script/elidex-js/src/vm/host/cache/natives.rs Implements the CacheStorage/Cache native methods (Promise-returning) and backend calls.
crates/script/elidex-js/src/vm/host/cache/mod.rs Adds Cache API host module, delivery model, and backend lazy-install helpers.
crates/script/elidex-js/src/vm/host/cache/marshal.rs Implements JS↔CachedEntry/Request/Response marshalling and option parsing.
crates/script/elidex-js/src/vm/host/cache/backend.rs Adds CacheBackend wrapper around Arc<Mutex<SqliteConnection>>.
crates/script/elidex-js/src/vm/host_data.rs Stores optional cache_backend in HostData + install/borrow accessors.
crates/script/elidex-js/src/vm/globals.rs Hooks Cache API registration into globals init ordering (after Request/Response prototypes).
crates/script/elidex-js/src/vm/gc/trace.rs Declares Cache API kinds as payload-free (no fan-out) for tracing.
crates/script/elidex-js/src/vm/gc/roots.rs Expands proto root set and marks PendingTask::CacheDeliver held values.
crates/script/elidex-js/src/vm/gc/collect.rs Roots Cache API prototypes + prunes cache_handle_states on sweep.
crates/script/elidex-js/Cargo.toml Adds elidex-cache-api as an engine feature dependency.
crates/api/elidex-cache-api/src/entry.rs Adds compute_vary_key() + unit tests for Vary key derivation.
Cargo.lock Adds elidex-cache-api dependency to the workspace lockfile.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/script/elidex-js/src/vm/host/cache/natives.rs
Comment thread crates/script/elidex-js/src/vm/host/cache/natives.rs
Comment thread crates/script/elidex-js/src/vm/host/cache/natives.rs
Comment thread crates/script/elidex-js/src/vm/host/cache/natives.rs Outdated
Comment thread crates/script/elidex-js/src/vm/host/cache/natives.rs Outdated
Comment thread crates/script/elidex-js/src/vm/host/cache/natives.rs Outdated
Comment thread crates/script/elidex-js/src/vm/host/cache/natives.rs
Comment thread crates/script/elidex-js/src/vm/host/cache/natives.rs
Comment thread crates/script/elidex-js/src/vm/host/cache/natives.rs Outdated
Cache API (#11-cache-api-vm / D-19 PR-1): the 8 read-side ops were
swallowing backend `CacheError`s (`unwrap_or(false)` / `unwrap_or_default()`
/ `.ok().flatten()`), contradicting the module docstring's stated contract
that every synchronous failure rejects the Promise — and diverging from
`open()`/`put()` which already `.map_err(..TypeError)?`. Propagate the error
uniformly so a SQLite/IO fault rejects the Promise instead of returning a
silently-wrong `false`/empty/`undefined` (Copilot #1-8):

  caches.has / caches.delete / caches.keys / caches.match /
  cache.match / cache.matchAll / cache.delete / cache.keys

Reject wiring is the same `finish` -> CacheDelivery::Reject path already
covered by the put_rejects_* / vary_star tests; the read-op error path has
no JS-reachable deterministic trigger in the in-memory harness (a
fault-injection seam with no production analog would be a stub), so no new
unit test — all success paths stay green under the existing 25 cache tests
+ mise ci.

cache.keys() doc (Copilot #9): dropped the false "creation order" promise.
Spec (§5.4.7) orders by the request-response list (insertion order); the
pre-existing backend (`store::keys`, M4-8.5) returns request-URL order.
Closing that backend gap is out of this PR's layer scope -> deferred to new
slot #11-cache-keys-insertion-order.

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 23 changed files in this pull request and generated 4 comments.

Comment thread crates/script/elidex-js/src/vm/host/cache/natives.rs Outdated
Comment thread crates/script/elidex-js/src/vm/vm_api.rs
Comment thread crates/script/elidex-js/src/vm/tests/tests_cache.rs
Comment thread crates/script/elidex-js/src/vm/host/cache/marshal.rs Outdated
Cache API (#11-cache-api-vm / D-19 PR-1):

- Cache.put required-arg gating (Copilot #1): cache_put_outcome wrapped the
  request arg as Some(&req_arg) (req_arg = args.first().unwrap_or(Undefined)),
  so c.put() with 0 args bypassed resolve_request's required-arg reject and
  coerced undefined as a URL. Pass args.first() straight through (parity with
  match/delete) so a 0-arg call rejects with the WebIDL TypeError. Regression
  test put_requires_a_request_argument (also closes Copilot #3).

- CacheStorage.match cacheName: null (Copilot #4): read_cache_name_option
  treated null as absent. Per WebIDL a non-nullable DOMString member coerces
  null -> "null" (it is *present*); drop the null guard so {cacheName: null}
  restricts to a cache named "null" (parity with cache_name_arg). Regression
  test caches_match_cache_name_null_restricts_to_literal_null.

- vm_api.rs unbind cfg guard (Copilot #2): FALSE POSITIVE. cache_handle_states
  .clear() is already inside the #[cfg(feature="engine")] block opened at
  vm_api.rs:521; the sibling idb_*_states clears (same engine-gated fields)
  sit in the same block and compile under non-engine builds — no extra guard
  needed.

mise ci green (10699 tests).

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 23 changed files in this pull request and generated 3 comments.

Comment thread crates/script/elidex-js/src/vm/host/cache/marshal.rs
Comment thread crates/script/elidex-js/src/vm/host/cache/marshal.rs
Comment thread crates/api/elidex-cache-api/src/entry.rs Outdated
Cache API (#11-cache-api-vm / D-19 PR-1):

- Vary multi-valued request header (Copilot #3, IMPORTANT): compute_vary_key
  and entry_matches both snapshotted only the *first* request header line
  matching a Vary token via `.find()`. Fetch `Headers.get` joins same-name
  field-lines with ", ", so a request with multiple values for a Vary-
  selected header (e.g. via Headers.append) could get a wrong cache hit/miss
  (different multi-value sets sharing a first value → false hit). Added a
  shared join_header_values helper (comma-join all matching lines) used by
  both halves. Crate test vary_key_joins_multivalued_request_header.

- CacheQueryOptions primitive argument (Copilot #1 + #2, one root): a
  non-nullish, non-object options arg was silently treated as an empty
  dictionary. Per WebIDL §3.2.17 step 1 a primitive is NOT a valid
  dictionary -> TypeError (it is NOT ToObject-boxed to read prototype
  members, contra the suggested fix). Added the dictionary-type gate to
  parse_query_options (undefined/null -> empty, object -> read, else ->
  TypeError); since it runs before read_cache_name_option, the cacheName
  path (#2) is covered transitively. JS test match_rejects_primitive_options.

mise ci green (10700 tests).

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 23 changed files in this pull request and generated 4 comments.

Comment thread crates/script/elidex-js/src/vm/host/cache/natives.rs Outdated
Comment thread crates/script/elidex-js/src/vm/host/cache/marshal.rs
Comment thread crates/script/elidex-js/src/vm/host/cache/marshal.rs
Comment thread crates/api/elidex-cache-api/src/entry.rs
Cache API (#11-cache-api-vm / D-19 PR-1):

- Response.redirected lost through Cache (Copilot #2, IMPORTANT):
  entry_from_response stored response_url_list with len <= 1, so
  build_response_from_entry (redirected = url_list.len() > 1) always
  reconstructed redirected=false. Capture ResponseState.redirected and, when
  set, store the [request, final] URL endpoints so the round-trip preserves
  both observables Fetch §2.2.6 derives from the URL list (`url` = last,
  `redirected` = size > 1). No JS regression test: the window test harness
  has no fetch-redirect path to mint a redirected Response (the non-redirected
  paths stay green under the existing put/match tests).

- Cache.put response-arg arity (Copilot #1): a 1-arg `c.put(req)` rejected
  with "response is not a Response" instead of the WebIDL arity error. Added
  an explicit `args.len() < 2` gate -> "2 arguments required, but only N
  present." Regression test put_requires_the_response_argument.

- Missing-arg message specificity (Copilot #3): resolve_request hard-coded a
  generic "Failed to execute a Cache method" label. Parameterized it with the
  WebIDL-style "'<op>' on '<interface>'" label at all 6 call sites so the
  message matches the rest of the VM.

- join_header_values allocation (Copilot #4): the R3 helper collected an
  intermediate Vec<&str> before join on every Vary comparison. Build the
  String directly (first-flag join, preserves ", " semantics incl. empty
  values) to keep entry_matches allocation-light.

mise ci green (10701 tests).

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 23 changed files in this pull request and generated no new comments.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 23 changed files in this pull request and generated 3 comments.

Comment thread crates/script/elidex-js/src/vm/host/cache/mod.rs
Comment thread crates/script/elidex-js/src/vm/host/cache/marshal.rs
Comment thread crates/script/elidex-js/src/vm/host/cache/marshal.rs
Cache API (#11-cache-api-vm / D-19 PR-1), CRITICAL (memory safety):

The Cache build paths held freshly-allocated ObjectIds in Rust locals across
allocations that can trigger GC (`alloc_object` runs the tracing collector
before insertion, and its contract requires callers to root such ids). On a
collection in that window the intermediate is swept and its id recycled →
use-after-free / wrong object. Fixed by rooting across the allocation window
(`push_temp_root` single-value guard; `push_stack_scope` multi-value guard for
the accumulation loops) — matching the `alloc_object` contract and the
`create_array_object` / `response_ctor` precedents:

- settle_async (Copilot #1): root the staged Resolve/Reject value across
  `create_promise` until the GC-rooted PendingTask captures it.
- build_response_from_entry (Copilot #2): root the Response across
  `create_headers` (+ body / state insert).
- build_request_from_entry (Copilot #3): root the Request across
  `create_headers`.
- matchAll / keys array accumulation (same-pattern audit, not Copilot-flagged):
  `create_array_object` self-roots its Vec, but the per-element build loop left
  prior elements unrooted across the next `build_*`/`create_headers` GC window;
  accumulate onto the rooted VM stack via `push_stack_scope`.

`caches.keys()` (cache *names*) and `build_cache_object` are unaffected
(string array / no second allocation).

No deterministic JS regression test: the bug only fires when a collection
lands in the unrooted window inside the native, and the adaptive `gc_threshold`
(reset to `max(live*128, 32768)` per collect) scales with live count — which
the rooting fix itself raises — so the instant can't be forced from script
without an internal mid-native GC hook. Verified by review against the
documented contract, like the rest of the VM's GC-safety. mise ci green.

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 23 changed files in this pull request and generated 1 comment.

Comment thread crates/script/elidex-js/src/vm/host/cache/marshal.rs
Cache API (#11-cache-api-vm / D-19 PR-1):

- headers_to_owned allocation (Copilot, MINOR): the helper cloned the whole
  HeadersState.list into a temp Vec before converting StringIds to owned
  Strings. `headers_states` and `strings` are disjoint VmInner fields, so the
  list can be borrowed in place (both shared borrows coexist) — dropped the
  clone. Reduces per-call overhead on the cache match / put marshalling paths.

mise ci green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@send send requested a review from Copilot June 3, 2026 13:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 23 changed files in this pull request and generated 1 comment.

Comment thread crates/script/elidex-js/src/vm/object_kind.rs Outdated
send and others added 2 commits June 3, 2026 22:38
Cache API (#11-cache-api-vm / D-19 PR-1):

- Cache ObjectKind doc (Copilot, MINOR): the receiver-ops list named
  add / addAll, which this PR intentionally does not install (deferred to
  slot #11-cache-add-fetch-integration). Dropped them from the list so the
  doc matches the installed surface.

mise ci green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cache API (#11-cache-api-vm / D-19 PR-1), MIN comment-only:

- marshal.rs (Axis 4): the `redirected` / `url` getter definitions live in
  Fetch §5.5 (Response class), not §2.2.6 — §2.2.6 defines the underlying URL
  list concept. Re-attributed the getter semantics to §5.5, kept §2.2.6 for
  the URL-list (substance was already correct; anchor was imprecise).
- tests_cache.rs GC-safety NOTE (Axis 3 framing catch): sharpened the
  no-deterministic-test rationale — the decisive blocker is that no script
  runs inside the native's unrooted window (unlike the abort-dispatch GC test
  where the JS listener body allocates in-window and trips gc_threshold), not
  the adaptive-threshold scaling (which compounds but is settable).

elidex-review fix-delta verdict: 0 CRIT / 0 IMP / 2 MIN / 3 FP. F2 (slot
ledger registration for #11-cache-keys-insertion-order) is a landing-time
action. mise ci green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 3, 2026 14:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 23 changed files in this pull request and generated 1 comment.

Comment thread crates/api/elidex-cache-api/src/entry.rs
@send send merged commit 520f86e into main Jun 3, 2026
7 checks passed
send added a commit that referenced this pull request Jun 3, 2026
)

Add a calibration entry so /elidex-review Axis 3 (Pragmatic shortcut)
catches docstring-contract ↔ body inconsistency: `Result`-returning host
natives (esp. promise-returning `*_outcome` family) whose body swallows a
backend/IO `Result` via `unwrap_or`/`unwrap_or_default`/`.ok().flatten()`
while the file/fn docstring or a sibling op (`open`/`put`) states a
"failure → reject/propagate" contract → silent-wrong-on-error + self-
inconsistency.

Origin: PR #275 (Cache API) Copilot R1 — 8 IMP error-swallow findings the
pre-push /elidex-review did not catch. MECE home is Axis 3 (the docstring
contract was correct; the body violated it = pragmatic error-swallow), not
Axis 4 (citation-accuracy only). Adds the Detect rule + a MECE ownership
line. Skill-only diff; no Rust paths touched.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
send added a commit that referenced this pull request Jun 5, 2026
* feat(sw): navigator.serviceWorker client container — register/lifecycle/identity (D-19 PR-3)

Closes #11-service-workers-vm — the window-realm `navigator.serviceWorker`
binding over the existing shell SW infra (the 3rd of the D-19 carve-out after
Cache #275 + SW-thread #280).

Surface (WHATWG SW §3.1/§3.2/§3.4):
- ServiceWorkerContainer (brand-via-prototype singleton): register /
  getRegistration(s) / ready / controller / startMessages / onmessage /
  oncontrollerchange.
- ServiceWorkerRegistration: scope / installing / waiting / active /
  updateViaCache / update / unregister / onupdatefound.
- ServiceWorker: scriptURL / state / postMessage / onstatechange.
- DR-B' back-channel: `Vm::deliver_sw_client_update` (7th `deliver_*`
  member) settles register()/unregister() promises + fires statechange /
  updatefound / controllerchange / message; harness-driven over the
  engine-indep `SwClientUpdate` contract (event_loop.rs→VM wire = D-26).

Architecture: per-realm object identity via the `wrapper_intern` seam
(WrapperOwner::Scope + WrapperKind::{ServiceWorkerRegistration,ServiceWorker})
so `reg === getRegistration()` / `reg.active === controller` / worker identity
survives state transitions; op-segregated pending-promise lists force-marked +
a registry-walk GC mark loop; coordinator emits SwRegistered(true)/
SwStateChanged/SwControllerSet (boa no-op consumer, zero regression). Validation
stays in elidex-api-sw via the typed `SwRegisterError`. Folds the PR-2 `Client`
`[SameObject]` bug (NG-1) through the same intern machinery.

43 SW tests (14 window-realm client + NG-1 + 27 PR-2 + ...); 10768 workspace
tests pass.

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

* fix(sw): apply /code-review findings — container ObjectKind, deliver ordering, updateViaCache

- Altitude: give the container an `ObjectKind::ServiceWorkerContainer` brand
  (uniform with the other VmObject EventTargets) instead of a brand-via-prototype
  + `dispatch_target` `Ordinary if sw_container==id` special-case; drop the now-dead
  `sw_container_prototype` field.
- deliver: gate `ControllerSet` on registry-presence + only-on-change (no spurious
  controllerchange on out-of-scope same-origin tabs); drain+settle register waiters
  BEFORE firing `updatefound` (reentrant same-scope register() can't be settled by
  this deliver); factor the new-installing `updatefound` edge-detect into one helper.
- message queue: a `message` listener (onmessage / addEventListener) now enables the
  queue + flushes the buffer (SW §3.4.6), not just startMessages().
- updateViaCache: parse `options.updateViaCache`, carry it in SwClientRequest::Register,
  and seed the registry entry from SwClientUpdate::Registered so the getter is live.
+ 3 tests (updateViaCache round-trip, out-of-scope controller ignored, onmessage flush).

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

* fix(sw): apply /elidex-review + /simplify findings — citations, predicate, dedup

elidex-review (5-axis, 0 CRIT):
- Axis 4: correct the systematic off-by-one §number cluster — the spec inserts a
  "Getting X instances" §.1 subsection per interface that the citations didn't
  count. scriptURL §3.1.1→§3.1.2, state §3.1.2→§3.1.3, postMessage §3.1.3→§3.1.4;
  registration scope §3.2.1→§3.2.6, updateViaCache §3.2.6→§3.2.7 (+ types.rs /
  deliver.rs siblings); the "Register" algorithms are dfns, not §3.1 (the
  interface) → §3.4.3 + algorithm names; container scope-match §8.1 (= Acks) →
  "Match Service Worker Registration" algorithm.
- Axis 1: move the ServiceWorkerState WebIDL string into the engine-indep crate
  as SwState::as_str() (uniform with UpdateViaCache::as_str()), drop the host-side
  re-impl.
- Axis 2: unify the `active`-slot predicate via SwState::is_active_slot()
  ({activating, activated}) so `ready`'s lazy resolve matches `.active`/the
  deliver — a page reading `.ready` while its worker is `activating` now resolves.

/simplify:
- factor the rooted-settle body into one `settle_rooted` (was 4 copies);
- collapse the three byte-identical `fire_*` event firers into `fire_simple`;
- deliver_state_changed: one `get_mut` instead of 3 registry lookups + an
  `.expect`.

+ ready_lazily_resolves_for_activating_worker test. 10772 workspace tests pass.

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

* fix(sw): PR283 Copilot review — 3 findings resolved

- ControllerSet: also gate on `matches_scope(scope, document_url)`, not just
  `contains_key` — with multiple same-origin registrations, a non-controlling
  one must not become the controller / fire controllerchange.
- ServiceWorker.scriptURL: capture it as an immutable readonly own-data prop at
  worker-object creation (was a getter reading the registry → "" after
  `unregister`), so a JS-held redundant worker keeps its original URL (SW §3.1.2);
  `state` stays a getter (mutable → "redundant").
- container.rs: module docstring updated to the ObjectKind brand (was stale
  "brand-via-prototype").

+ 2 regression tests. 10774 workspace tests pass; fix-delta re-review CLEAN
(layering + data-flow).

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

* docs(sw): PR283 Codex review — add #match-service-worker-registration dfn anchor (MIN)

Codex Axis-4 MIN: the SW "Match Service Worker Registration" algorithm cite in
deliver_controller_set (and the sibling cite in container.rs getRegistration)
named the algorithm without its dfn anchor. Add `#match-service-worker-registration`
to both (webref-verified). Comment-only.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

2 participants