feat(m4-12): Cache API on the VM — caches/CacheStorage/Cache (D-19 PR-1)#275
Merged
Conversation
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>
There was a problem hiding this comment.
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-apiVary-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.
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>
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>
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>
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>
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>
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>
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>
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>
This was referenced Jun 3, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.matchcacheNamerestrictionVary:*put-reject),Cache.putrejections: non-GET / 206 / disturbed-body / Vary:*Architecture (plan §4)
OriginStorageManager::cache_connection()hands out the originArc<Mutex<SqliteConnection>>(Send+Sync, shareable to the SW thread in PR-2);CacheBackendwraps it behindwith_conn(DR-A). PR-1 lazily mints an in-memory backend onHostData(boa parity); the shell-wired origin handle lands with PR-2/D-26.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 singleIdbDeliver-style delivery model, GC-rooted ingc/roots.rs).ObjectKindbrands (CacheStorage/Cache) +cache_handle_statesside-store (name only); trace / retain / unbind /proto_rootsall wired.elidex-cache-apicrate; the VM only marshalsJsValue↔CachedEntry/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
url, add/addAll absence)elidex-cache-apicompute_vary_keytestsGate
cargo fmt→mise run ci(green) →/code-review→/simplify→/review→/elidex-review(5-axis) → Step 4.5 fix-delta re-verification (clean). Findings fixed: GCproto_rootsrooting (defensive vsdelete globalThis.caches), syntheticresponse.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