Conversation
bplatz
left a comment
There was a problem hiding this comment.
Approving. Reviewed the native/shared surface and the server-side security posture — not the two new crates (fluree-db-browser ~4k lines, fluree-db-wasm, the JS shell), so don't read this as a full sign-off on those. Four inline items; the first and last are the ones I'd want settled before merge.
Verified clean:
- The storage trait additions are inert:
query_guard/miss_registerare default-Noneplus mechanical forwarding throughArc<dyn …>,StorageContentStore,BranchedContentStore. No native implementor overrides them; every consumer is cfg-gated. - The
query.rsnon-residency arm is the original code verbatim — production planning/execution untouched. - The NS-record conditional GET is correct:
no-cacheforces revalidation so authz is re-checked every time, and the 304 is ordered after every authz/serving gate. The bytes-digest ETag over the composite is the right call. - "No pre-existing test exercises
PeerSubscriptionTask" — grepped, accurate.
| /// and immutable, so a cached copy is valid forever. `private` because the | ||
| /// response was authorized by a bearer token; the public-visibility tier | ||
| /// will relax this to `public` for ledgers that opt in. | ||
| const OBJECT_CACHE_CONTROL: &str = "private, max-age=31536000, immutable"; |
There was a problem hiding this comment.
Suggest adding Vary: Authorization to the object responses.
These are authorized by bearer token and cached private, max-age=31536000, immutable. private is the browser's own cache, shared across sessions on one profile; immutable means it never revalidates. So after principal A reads a ledger, principal B on the same browser profile gets those CIDs served from cache with the server never seeing B's token.
The comment here already reasons about the token (private "because the response was authorized by a bearer token") — this is the next step of that thought. Vary puts the header in the cache key so a different token is a different entry.
The NS route doesn't need it (no-cache revalidates, authz re-runs). Cost is low in practice since the peer's real cache is IndexedDB with its own CID verification — the HTTP tier is a bonus, not the mechanism.
| ns_record: NsRecord, | ||
| ) { | ||
| async fn notify_mgr(&self, mgr: &Arc<LedgerManager>, ns_record: NsRecord) { | ||
| let ledger_id = ns_record.ledger_id.clone(); |
There was a problem hiding this comment.
This file is −377/+152 — a restructure plus two deliberate behavior changes (source_branch/branches from the wire, unparseable ledger_id degrading to verbatim rather than erroring the stream) — in live replication code with no test coverage. The six new tests cover the lifted pump, not this task.
You flag the gap and note it predates the PR; agreed on both. What I'd still suggest: either an integration test pinning the two deltas, or split the behavior changes into their own commit separate from the lift. As it stands a regression here has no failing test and no clean bisect boundary between "the refactor broke it" and "the intended change broke it."
| // execution completes, so it cannot be re-run transparently and is | ||
| // excluded on residency-mode peers. | ||
| #[cfg(any(target_arch = "wasm32", feature = "residency"))] | ||
| let (batches, plan_ms, exec_ms) = { |
There was a problem hiding this comment.
Suggest factoring the plan+execute body into one async fn that both arms call, so they differ only in the retry wrapper.
As written this is two copies of the main query entry path, and per the dev-dep feature unification (raised on #1714), cargo test -p fluree-db-api and every api bench compile the residency arm — the cfg(not(…)) production arm isn't compiled in those binaries at all. So nothing in the api suite exercises what ships, and drift between the arms would be invisible.
In #1714 the duplicated code was error-path only, which made this tolerable. Here it's the main path, so the exposure is larger.
| /// (the `fluree-*` policy/tracking headers, `idempotency-key`, trace ids, | ||
| /// `range`, `if-none-match`, `last-event-id`, …) without a list to keep in | ||
| /// sync; with `allow_origin(Any)` and no credentials it has exactly the | ||
| /// trust posture `*` had. The expose list makes response metadata — the |
There was a problem hiding this comment.
Wording: "exactly the trust posture * had" is not quite right, and the difference is the point of the change. * deliberately excludes Authorization, so cross-origin authenticated requests were impossible before and are possible now — strictly more permissive, deliberately.
Worth stating that way, because the consequence is real for internal deployments: a page the user visits can now reach an internal Fluree server with an Authorization header and read the response. Not CSRF — no ambient credentials — but it widens the browser-as-pivot surface.
Follow-up rather than this PR: cors_enabled is a bool with allow_origin(Any) hardcoded. Now that browser clients are a first-class use case, an origin allow-list is worth having.
1defc7e to
4d81372
Compare
|
Thanks — all four addressed, including the two you wanted settled before merge.
The duplicated query entry path. Factored into one CORS wording. Corrected, and in the code comment as well as the description — the claim was in both. It now says this is strictly more permissive than On the scope of your review: understood, and noted — the two new crates not being covered is worth someone's eyes before this lands, and I'd rather that be explicit than assumed. |
9f2901f to
3dc32d0
Compare
CORS: mirror the preflight's Access-Control-Request-Headers instead of
the blanket Any. The Fetch spec excludes Authorization from the literal
* wildcard that Any emits, so bearer-token requests from spec-conforming
browsers failed preflight; mirroring covers authorization and every
other header the API reads (fluree-* policy/tracking headers,
idempotency-key, trace ids, range, if-none-match) with no allow-list to
keep in sync, and with allow_origin(Any) and no credentials it has the
trust posture * had. Also expose the Range/CAS/x-fdb-* response metadata
to page JavaScript and cache preflight results.
GET /storage/objects/{cid}: ETag (the CID) + immutable Cache-Control on
200 and 206 responses; If-None-Match -> 304 checked only after the
authorization and serving gates so revalidation can't leak existence
and costs no storage I/O. The * validator form is not honored (the
route answers before consulting storage).
GET /storage/ns/{alias}: ETag derived from the serialized record (a
truncated SHA-256 of the exact bytes served, computed after the serving
tiers are resolved) so head CIDs, retracted, branches, and serving all
invalidate it; Cache-Control: private, no-cache; If-None-Match -> 304.
Tests pin 200/206/304 header behavior, weak/list validators, the
out-of-scope 404, ETag change on head advance and on child-branch
creation without a commit, preflight mirroring of documented client
headers, and CORS headers on no-leak 404s.
ProxyStorage and ProxyNameService now perform all network I/O through a new transport::HttpTransport trait (execute(TransportRequest) -> TransportResponse with http::StatusCode/HeaderMap and zero-copy Bytes bodies). The wire protocol — URL construction, address<->CID parsing, status mapping, client-side CID verification, FLKB content negotiation — stays in the shared clients and is now transport-agnostic; a browser (wasm32) fetch transport can be injected via the new from_api_base_with_transport constructors. Native behavior is preserved: the default ReqwestTransport keeps the per-client timeouts (60s blocks / 30s NS lookups) and the historical error-message taxonomy (timeout/connect/request/body per call site), and public constructor signatures are untouched, so server and CLI consumers compile with zero diffs. One deliberate delta: the transport reads the response body for every status where the old code left non-200 bodies unread — observable only if that read fails, and the TransportResponse docs state the full-buffering contract. TransportRequest implements Debug by hand with the authorization value redacted, so a transport impl logging the request can never leak the bearer token (pinned by a test). The trait keeps Send futures on every target deliberately: the engine's storage traits box Send futures unconditionally, so a ?Send transport could never carry a ProxyStorage impl — a wasm fetch implementation bridges over channels instead (documented on the module). New tests inject a canned transport end-to-end: URL/bearer-header formation, CID verification (accept + reject), and 403->NotFound no-leak mapping, proving the seam carries the full client logic.
…t impl Nothing takes a generic T: HttpTransport bound - the proxy clients hold Arc<dyn HttpTransport> and auto-deref to the vtable. The blanket impl shadowed that deref, boxing every dispatch twice on the CAS read path for no consumer. A comment now records why it is deliberately absent.
…parser Two additive proxy-client APIs for cache layers — the consumer is the browser peer's CAS cache (fluree-db-browser), which keeps verified blocks resident and must avoid re-copying them: - ProxyStorage::read_object_bytes(address) -> Bytes: the raw-endpoint fetch (CID-verified) returning the transport's buffer uncopied. read_bytes/read_bytes_hint now call it and convert at their own boundary, so native behavior and copy counts are unchanged. - cid_and_ledger_from_address is pub: cache layers key by CID without re-implementing the address layout; the mapping stays pinned by this module's round-trip tests (including the exhaustiveness guard). Byte-neutral natively: no caller changes, no wire changes.
…ds for wasm32 The proxy clients, HttpTransport seam, and CID integrity verification are runtime-agnostic; everything else — sync/pull/clone, SSE watchers, config store, origin fetchers, ReqwestTransport — is native-only and now sits behind cfg(not(target_arch = "wasm32")) with reqwest/tokio/rand/ parking_lot/async-stream moved to native-only target dependencies. verify_object_integrity moves to a runtime-agnostic integrity module (origin:: and crate-root re-export paths unchanged); the default constructors that build ReqwestTransport are native-only, wasm callers inject a transport via from_api_base_with_transport. fluree-db-nameservice is consumed with default-features=false behind a new default 'native' feature, so native dependency resolution is byte-identical. CI: the wasm32 job now checks -p fluree-db-nameservice-sync alongside fluree-db-api, so the crate's wasm buildability can no longer drift (it was previously unverified — the E-doc's claim was vacuous). Consumer: fluree-db-browser implements HttpTransport over fetch and builds ProxyStorage/ProxyNameService on wasm32 via the injection constructors.
Two small core surfaces the residency retry loop and the load-time novelty prefetch build on: `ContentStore::query_guard` / `StorageRead::query_guard` (default `None`, forwarded through `Arc<dyn Storage>`, `Arc<dyn ContentStore>`, `StorageContentStore`, and `BranchedContentStore`, which composes its ancestry chain into one handle) hands out an opaque RAII `InFlightGuard`. A store with a residency tier freezes eviction while any guard is alive — the no-eviction-while-in-flight policy that keeps the fetch-pins contract true across a retry loop's rounds, so every byte a round observed stays resident for the re-run and progress is monotone. The guard rides the storage traits rather than a concrete browser type so the api-level loop needs no dependency on the browser crate: whichever store is behind the ledger's content store supplies its own payload. `DictNovelty` gains read-only iteration (`subjects.iter_entries()`, `strings.iter_values()`, via a new `NsVecBiDict::iter_entries`): the entries query-time overlay translation reverse-looks-up against the persisted dictionaries, which a residency-mode load prefetches.
Overlay translation resolves every novelty subject and string against the persisted reverse trees through sync lookups at query time (`find_subject_id_by_parts` / `find_string_id`), so on a residency-mode peer each cold reverse-tree leaf would cost a retry round — the F8 novelty miss source. `prefetch_novelty_reverse_wants` computes the routed leaf set for a novelty entry iterator (the exact key encodings the lookups use, over both reverse trees), skips resident leaves, and pins the rest in one concurrent round through the fetch-pins contract (`fetch_wants`) — no filesystem involvement, so it is wasm-safe. No-op outside residency mode. Unit test builds a multi-leaf CAS-backed reverse tree over a miss-injecting store: cold lookups miss with a typed, registered NeedFetch; one prefetch round pins the whole routed set (asserted >= 2 leaves — a set, not one object); every subsequent lookup is a pure hit recording no wants. Mutation-checked: no-opping the prefetcher fails the test on "wanted 0".
Promotes the drain/fetch/re-run loop the residency tests ran by hand into the real query path. Under `cfg(any(wasm32, feature = "residency"))`, `query_with_options` — the frozen-`GraphDb` entry, so a round never sees another snapshot's index — wraps plan+execute in the loop: on any execution error it calls `RetryBudget::after_error` against the store backing the snapshot's binary index (resolved from the range provider, so the loop drains exactly the store the sync reads went through); progress re-runs the round, no wants surfaces the real error. Each round gets a FRESH fuel tracker — the Tracker is Arc-shared, so reusing one would bill earlier rounds' work against the final round's budget — and the store's in-flight guard is held across all rounds so pinned bytes cannot evict between them. The streaming entry stays unwrapped (rows are emitted before execution completes, so a mid-stream miss cannot be re-run transparently) and is documented as excluded on residency-mode peers. Default native builds compile the pre-residency path unchanged; the `residency` feature exists so native tests can drive the loop. F8(b) wiring: both ledger-load provider sites prefetch the novelty reverse-dict want set (`prefetch_novelty_translation`) right after the range provider is installed, so overlay translation lookups on a cold peer are pure hits instead of a retry round per reverse-tree leaf. The recovery suite now drives the PRODUCTION loop: one direct `Fluree::query` / `query_connection` call must complete each path (scan, one-shot fast path, dir-only COUNT(DISTINCT) walk, policy f:query) with results identical to a plain native instance, misses positively observed, and the register drained. Mutation-checked: disabling the loop's retry arm fails the fast-path and policy tests (the scan and dir-walk paths are absorbed by the operator-frame retry, which is the point of having both). A new novelty test commits data past the index, loads on a residency instance, proves a translation lookup is a pure post-prefetch hit, and completes the combined persisted+novelty query through the loop.
… storage, IndexedDB cache The engine side of the browser peer (the JS shell lives elsewhere): - protocol/bridge: IoJob channel protocol + IoHandle; WasmFetchTransport implements the HttpTransport seam by enqueueing jobs and awaiting a oneshot, so engine-visible futures stay Send while a spawn_local driver task owns every JS handle (fetch/AbortController/IndexedDB). - residency: CID-keyed in-memory tier serving resolve_cached_bytes — O(1) Arc-clone hits, LRU byte budget, pin support with a query-duration PinSet, typed working-set-exceeds-budget errors. - cas: BrowserCasStorage wraps a Raw-mode ProxyStorage (which owns URL formation + CID verification) with the residency tier, IndexedDB write-behind via the driver, request coalescing, and a bounded fetch width. Implements the Storage traits + the residency hook; exposes ensure_resident/prefetch as residency-first entry points (fetched blocks cost exactly two copies: JS->Bytes, Bytes->Arc<[u8]>). - budget: pure LRU byte-budget index for the persistent cache, rebuilt from the meta store at open, natively tested. - driver (wasm32-only): fetch execution per the transport contract (Ok for any status, credentials omit, CORS mode, AbortController timeouts, Timeout/Connect/Request/Body classification, one copy out of JS memory) and the IndexedDB blocks+meta stores with planned eviction and batched access-time flushes. - connect: build_peer assembles FlureeBuilder::memory().build_with(cas, NameServiceMode::ReadOnly(ProxyNameService)) over any IoHandle (native tests use a mock driver); connect() (wasm32) starts the real driver. Native unit tests cover the bridge round-trip, coalescing (8 readers -> 1 fetch), residency LRU/pins/budget errors, cache-hit/write-behind/ integrity-rejection/403-NotFound paths, bounded prefetch width, and head resolution through the proxy nameservice via the injected transport. Browser-only paths (fetch, IndexedDB) have wasm-bindgen-test coverage in tests/browser.rs.
…governor, bounded write-behind, batch-first fetch Four design-review findings folded in: - F5 (pin attribution): eviction from the residency tier now runs only while no query is in flight. BrowserCasStorage::query_guard() marks a query (the retry loop holds one across its rounds); a mid-flight insert that would need eviction gets a typed EvictionDeferred, and the async caller waits one bounded release interval (budget_wait, served natively by a tokio timer and on wasm by a driver Sleep job so the engine-side future stays Send) before failing typed. This is what upholds the engine's fetch-pins contract without per-query pin attribution through the sync hook. - F4 (memory governor): BrowserIoConfig::from_max_memory derives the residency budget, write-behind bound, and fetch width from one ceiling; the rustdoc names what the crate cannot govern (engine operator memory, and the forward-pack readers pinning pack bytes for the store's lifetime — an engine-side gap owned by the read-path work). - F9 (write-behind backpressure): a byte-bounded WriteBehindGauge. Every CachePut carries a permit released only when the driver finishes the IndexedDB write; fetch completion acquires the permit, so a lagging IndexedDB stalls the fetch pipeline instead of queueing unbounded block clones. Peak/outstanding surface in CasStats. - F1/F2 (batch-first wants): fetch_cids / fetch_cids_pinned make a drained want-set resident concurrently (bounded width, coalesced) and pin successes into a PinSet — the fetch half of the miss-register drain loop; address_for maps CID+ledger to the canonical address. New tests: gauge admission/blocking/oversize, deferred-insert wait and typed failure, backpressure peak bound, want-batch fetch+pin, tier deferral semantics, release wakeups, governor derivation (37 native tests total).
… fetching Rebased onto the restacked transport (the duplicate core hook dropped in favor of read-path's canonical StorageRead::resolve_cached_bytes / miss_register pair on groundwork). BrowserCasStorage now participates in the sync residency tier fully: it holds a MissRegister and returns it from StorageRead::miss_register (forwarded to the engine through StorageContentStore), so every sync miss the binary-index read path takes against this storage is recorded for a retry frame to drain. fetch_wants(ledger, wants, pins) adapts a drained Want set onto the batch fetch path for shell-side callers that hold the ledger context. The new integration test is the load-bearing proof: the engine's landed RetryBudget::after_error, running against the exact StorageContentStore bridge the engine assembles, records a sync miss into this storage's register, drains it, fetches through ContentStore::get (our read_bytes, which makes the bytes resident — the fetch-pins contract, upheld under a held query guard), verifies residency, and the re-run hits with one fetch total. An empty register correctly reports 'real error' to the loop. 38 native tests; browser suite unchanged (4 headless-Chrome tests).
…p into a shared head-stream pump fluree-db-nameservice-sync grows head_stream::run_head_stream — the peer subscription loop parameterized over everything runtime-specific: the connection (SseChunkSource; ReqwestSseSource natively, a channel fed by the browser driver's fetch-stream on wasm), the timer (Sleeper), the consumer (HeadSink), and a watch-channel stop signal. It reuses the crate's existing server_sse payload parser and lifts the server's ExponentialBackoff verbatim (configurable multiplier, +/-25% jitter, reset on clean stream end, 401/403 and token-load failures fatal). The watch/server_sse modules are un-gated for wasm32 (pure code); rand moves to both targets for the jitter; wasm gets tokio/sync for the stop signal. Six native pump tests cover chunk-split parsing, backoff growth and reset, malformed-frame skipping, and both stop paths. fluree-db-server's PeerSubscriptionTask now drives the shared pump: the task keeps its public shape (new/spawn, PeerSubscriptionError with its is_fatal taxonomy retained for compat) and a PeerEventSink owns what events mean for a peer — state watermarks, ledger-cache notify, preload on connect, retraction eviction — with the previous log lines kept. Two deliberate behavior deltas, both strict improvements: (1) ledger records now carry source_branch/branches from the wire into LedgerManager::notify (the old local conversion hardcoded None/0 — lineage the peer needs for BranchedContentStore, cf. the proxy client's finding #11 regression test); (2) an SSE record with an unparseable ledger_id degrades to name=ledger_id instead of erroring the event. NOTE: no existing integration test exercises the subscription task; the gate here is the full server suite plus the pump's own unit tests.
The browser half of the lifted head-stream loop: - protocol/driver: a dedicated SseOpen job — streaming deliberately does not ride the HttpTransport seam (whose contract is full-body buffering). The driver fetch-streams the events URL (credentials omit, CORS), forwards raw ReadableStream chunks over a channel, and the chunk receiver dropping cancels the reader and aborts the fetch. Reconnect delays ride the existing Sleep job (DriverSleeper). - heads: ChannelSseSource (the pump's connect = an SseOpen job; driver-gone is fatal), PeerHeadSink (ledger updates go through LedgerManager::notify — the native peer's incremental refresh, so an open peer re-opens at the new head between queries while in-flight queries keep their frozen views — then fan out to callbacks; retractions evict via disconnect_ledger; graph-source events ignored in the v1 peer), HeadChange + HeadTracker (stop() or drop stops the pump). - BrowserPeer: on_head_change(f) registry for the shell, head_stream(ledgers) -> (HeadTracker, future) for any-executor spawning, start_head_tracking (wasm, spawn_local); reconnect knobs on BrowserIoConfig. Tests: native end-to-end through the mock driver (events URL + accept/authorization headers asserted, callback receives the HeadChange, tracker stops the pump) and a REAL-browser wasm-bindgen test where a data:text/event-stream URL flows through the driver's actual ReadableStream path into the shared pump (stop-on-first-event; noted: a data URL cannot exercise reconnect or non-2xx classification — the pump's native unit tests carry those).
…nd engine)
New workspace crate wrapping fluree-db-api (default-features = false, the
shape the wasm32 CI check enforces) behind #[wasm_bindgen] exports for the
browser package in fluree-db-wasm/js. Named fluree-db-wasm to match the
crate family: it IS the wasm binding of fluree-db, nothing more.
Runtime model: single-threaded on the worker event loop; no tokio runtime is
ever constructed (async exports become Promises; the engine's detached spawns
already route through the spawn_detached seam to spawn_local). Queries run
against frozen GraphDb snapshots held in a handle slab, never a live alias,
so a head advance - or the future peer mode's fetch-and-re-run loop - cannot
move the view mid-query (design review F6); the streaming query entry is
deliberately not bound, buffered results only. One memory setting: the
constructor's max_memory_bytes becomes each query's memory budget via
QueryCancellation::set_memory_limit, so an oversized query dies as a typed
out_of_memory (507) instead of trapping the worker (review F4); the JS shell
owns trap recycling for whatever still gets through.
Errors cross the boundary as JS Errors with stable {code, status} props
(not_found / conflict / invalid_input / cancelled / out_of_memory /
unsupported / internal) mapped from ApiError.
Root Cargo.toml adds the member plus [profile.wasm-release] (inherits
release, opt-level=s) - inert for every native build; it exists because the
shipped .wasm is download-size-bound and wasm-pack cannot select custom
profiles, so the package build drives cargo + wasm-bindgen-cli directly.
wasm-bindgen-tests run in a dedicated worker (the package's real hosting
context) in headless Chrome and assert on returned rows, receipt t, snapshot
isolation across an update, and error codes - positive ran-markers, never
didn't-crash.
…, smoke The JS half of the browser shell (name placeholder pending the npm org decision). Main-thread module (src/index.ts) contains no wasm: it spawns the dedicated engine worker (src/worker.ts) and speaks the typed protocol in src/protocol.ts; the .wasm streams+compiles lazily inside the worker on first use. Public types are our own TS declarations, not the wasm-bindgen internals. Crash model (review F4): a wasm trap poisons the instance; the worker marks the error fatal, and the proxy rejects in-flight calls with the typed error (engine_crashed, or out_of_memory for allocation traps), terminates, then respawns + re-inits a fresh worker so the Playground object stays alive. The single maxMemoryBytes option (default: quarter of navigator.deviceMemory clamped [256 MiB, 2 GiB]; 512 MiB where unavailable) rides the init message into the engine's per-query budget. Frozen views (review F6): Ledger.query freezes the head per call worker-side; Ledger.snapshot() exposes the frozen view for multi-query consistency. Buffered results only - no streaming surface exists on the protocol. Result transport: engine emits UTF-8 JSON bytes; default posts the buffer in the transfer list (zero-copy, size-independent), clone mode structured-clones a string instead; both selectable per call, side-by-side timer in the demo. scripts/build.mjs: cargo --profile wasm-release -> wasm-bindgen --target web -> wasm-opt (explicit post-MVP feature flags) -> tsc -> size report via node's zlib (gzip -9 + brotli -q 11). Drives cargo directly because wasm-pack cannot select custom cargo profiles and the root release profile is native-tuned; the CLI version is checked against Cargo.lock (glue/module ABI is version-locked). scripts/smoke-browser.mjs: dependency-free CDP client driving headless Chrome against demo/smoke.html over plain HTTP with deliberately NO isolation headers (the hosting contract); exits 0 only on the page's positive pass marker with the expected bound rows. scripts/serve.mjs: static server with correct wasm/module MIME types.
Runs on the pinned toolchain next to the wasm32 check job: (1) wasm-pack test --headless --chrome runs the crate's wasm-bindgen-tests inside a dedicated worker in a real browser engine, asserting on returned rows - positive ran-markers; (2) the npm package is built (wasm-release profile + wasm-opt + tsc) and the SHIPPED artifact is driven end-to-end (main proxy -> module worker -> wasm) over CDP with no COOP/COEP headers, then the size report is printed. RUSTFLAGS restates the getrandom cfg because a job-level env RUSTFLAGS masks .cargo/config.toml (same note as the wasm32 job). wasm-bindgen-cli is installed at the exact Cargo.lock version; ubuntu-latest supplies a matching Chrome + chromedriver pair.
The api retry loop reaches storage only through dyn StorageRead, so the crate's eviction-freeze guard is now exposed via the trait's query_guard (wrapped in the opaque core InFlightGuard), mirroring miss_register. The new test exercises the dyn path specifically: freeze while held, release on drop.
…nned vars order, smoke robustness Everything below was found by actually executing the browser build (the compile gates were green throughout): - build.rs: raise the wasm shadow stack to 8 MiB via -zstack-size (covers the cdylib and the wasm-bindgen-test executables). The 1 MiB default overflowed in the dev-profile browser tests as a 'memory access out of bounds' trap; the workspace already documents this codebase's unoptimized async futures need 8 MiB native (RUST_MIN_STACK in .cargo/config.toml) - same futures, same fix. rustc places the stack first in linear memory, so an overflow traps instead of corrupting, and untouched stack pages cost address space, not resident memory. - index.ts: Omit over the Request union collapsed it to common keys and rejected every op-specific field; use a distributive Omit. - tests/playground.rs: head.vars arrives in the engine's var-registry order, not SELECT-clause order (same on the HTTP surface; the W3C JSON results format mandates no order) - assert the set, not the sequence. - smoke-browser.mjs: resolve bare Chrome candidates through `which` instead of returning the first name unchecked (macOS has no google-chrome on PATH - launching the app binary to probe would hand off to a running instance and hang); cleanup retries and never decides the exit code (the SIGKILLed Chrome can still be flushing its temp profile).
…eview H-1, H-2) H-1: a budget-full residency tier no longer bricks in-query cold fetches. Eviction previously froze whenever ANY query guard was live — including the querying context's own — so once the LRU filled to budget (the steady state of a cache), every cold fetch stalled budget_wait and failed typed, forever. Eviction is now governed by an epoch-tick rule: each QueryGuard records the tier-clock tick at begin_query, every observation (resolve hit, insert incl. its already-resident return) bumps last_use under the same lock, so entries whose last_use predates the OLDEST live guard's begin tick provably belong to no in-flight query's working set and stay evictable. Monotone progress (the engine's fetch-pins contract) is preserved structurally: anything a live query touched carries a tick at or after its begin. Eviction plans before it applies (a deferred insert never wastes cached bytes), EvictionDeferred now fires only when even unobservable entries cannot make room, guard drops always wake deferred waiters (any drop can advance the epoch), and remove/clear_unpinned obey the same rule so a shell-side free-memory call cannot break a running query. Tests pin both required properties: the bricking scenario (tier full, guard live, cold fetch succeeds by shedding pre-guard leftovers — at tier and storage level, the latter proving no budget_wait stall) and monotonicity (observed bytes survive another context's fetch; a fully-observed tier still defers). H-2: write-behind backpressure is now real. The gauge permit was acquired AFTER the fetch slot was released, so a lagging IndexedDB let completed fetches park in acquire() while their freed slots admitted more fetches — unbounded un-persisted clones, the exact OOM the gauge exists to prevent. The permit is now acquired INSIDE the fetch-slot scope (at most max_concurrent_fetches blocks between fetch-complete and gauge admission; no deadlock — permits are released by IndexedDB writes, which never take fetch slots), and prefetch/fetch_cids swap unbounded join_all for buffer_unordered at the fetch width. New test wedges the persist path (permits never released) and proves fetch admission stalls at 1 admitted + width parked, un-persisted bytes bounded at the budget, and the batch drains once persists resume. Also folded from the review's mediums in these files: the native missed-wakeup race on the deferred-insert wait (release interest is now registered via Notified::enable BEFORE the re-attempt — the gauge's own pattern); a cancelled coalescing leader no longer fails its followers (bounded re-election with a residency re-check, replacing the spurious 'was cancelled' error); and fetch_cids_pinned reports a CID evicted before its pin landed as a failure instead of silently claiming it pinned.
- SSE idle zombie: each ReadableStream read now races the chunk receiver's closed() signal, so a stopped pump releases an IDLE stream's connection immediately (cancel + abort) instead of on the next chunk — and a connect whose ready send fails no longer streams into the void. Matters because SSE shares the ~6 per-host connection slots with block fetches. - IndexedDB index consistency: put() applies planned evictions to the in-memory index (and drops the victims from the dirty set) BEFORE awaiting the transaction, and inserts the new key only after commit — the access-time flusher can no longer resurrect meta for a just-deleted victim (orphan meta inflating the rebuilt index) or write meta for a block that never lands. get() on a store miss drops the stale index entry so put()'s already-persisted skip cannot become permanent (failed tx, another tab's eviction). - Residency floor: from_max_memory clamps the residency budget to 32 MiB, so a tiny memory ceiling degrades (smaller cache) instead of bricking every ordinary leaflet read with ObjectExceedsBudget; the divide-before-multiply ordering is now commented as the 32-bit overflow guard it is. - Head-stream observability: the pump logs reconnect_in_ms before each backoff sleep, restoring the delay field the subscription-loop lift dropped. - Test hygiene: the browser-suite cache test polls for the write-behind landing instead of trusting a fixed 200 ms sleep (CI-box flake guard); stale pre-epoch doc comments on query_guard()/the release signal updated. NOTE: the review's low nit about test_ns_record_etag_and_conditional_get (stale composite-ETag doc, unpinned * validator) was already fixed on this branch at assembly — the doc describes the digest ETag and line 3413 pins * as a 200.
Pre-review C-1: the if-then-panic shape trips the workspace-denied manual_assert lint the moment --all-features CI compiles the residency test target (invisible while the PR is stacked — the base branch gets no CI). Verified: clippy clean and the suite still 5/5.
H-4: every Ledger/Snapshot object is generation-stamped and validated worker-side, so pre-crash objects reject typed (not_found / closed) instead of silently reading a recycled engine's state — the fresh worker reuses small handle numbers, which is exactly how the aliasing happened. A debugCrash test hook (deliberate trap) lets the JS smoke drive the whole path: crash -> typed engine_crashed -> stale snapshot not_found -> respawned engine answers. H-5: respawns are capped (3 consecutive) with exponential backoff; past the cap, or when the worker never boots at all, the channel goes terminal and everything rejects engine_unavailable. New codes engine_restarting / engine_unavailable documented. H-6: the default spawn is the literal new Worker(new URL(...)) shape bundlers' static detection requires; verified unbundled and via a zero-config vite build (scripts/vite-repro.mjs); the README now says plainly which bundlers are NOT covered by checks. Mediums: not_initialized re-init window closed; transact pre-gate (inputs over a quarter of the memory budget refused typed before the allocator can trap) with tests; smoke hardened. Also: the memory-budget browser test now pins the typed-rejection contract via grouped aggregation (GROUP_EST_BYTES-charged, 4,096 groups against a 64 KiB budget) — cross joins and GROUP_CONCAT payloads are not charged today, and the first version of this test could only time out the harness or blow chromedriver's 10 MB response cap trying.
…ed wasm-pack, packed-tarball smoke, ns-ETag doc - the wasm32 job now checks fluree-db-browser and wasm-smoke runs its real-browser suites (review H-3: they previously never executed in CI) - wasm-pack installed via the pinned installer action, not curl|sh - the npm smoke drives the packed tarball, so a files-allowlist regression fails in CI instead of consumers' installs - the ns-ETag test's doc-comment describes the digest ETag actually served and pins '*' as deliberately unhonored (stale text from the abandoned composite design) - probe lockfile picks up the rebased workspace's dep graph
…s the native deferred-insert wait; the wasm arm waits via a driver Sleep job. The wasm32 CI job now gates warnings, so this would fail CI.
std::env::temp_dir() PANICS on wasm32 ("no filesystem on this
platform"). ledger_manager's cache-dir default was gated for that, but
view/fluree_ext.rs called it raw at two sites in load_graph_db_inner —
an ungated impl, so both are reachable on wasm and would trap the
engine if that path runs there.
Scope honestly: these are LATENT sites, not a diagnosed crash. A trap
was observed opening an indexed ledger through the JS shell and first
attributed here by inspection, but that attribution came from a grep
rather than a symbolized frame and did not hold — the trap cleared on a
base that still contains these lines, fixed instead by degrading the
disk-cache directory create where there is no filesystem. These calls
remain a real hazard on their own terms; that is why they are fixed.
Rather than add a third copy of the cfg, ledger_manager now owns a
temp_cache_dir(leaf) helper that is the ONE place knowing the wasm arm
exists, and both fluree_ext sites route through it. Each site keeps its
own leaf name, so native paths are unchanged.
These bodies are authorized by a bearer token and served `private, max-age=31536000, immutable`. `private` keeps them out of shared caches, but the browser's own cache is per PROFILE, not per principal, and `immutable` means it never revalidates — so once principal A read a ledger, principal B on the same browser profile was served those CIDs straight from cache, with the server never seeing B's token and never getting the chance to refuse it. `Vary: Authorization` puts the token in the cache key, so a different token is a different entry. Applied to all three object responses — 200, 206 and the 304, which refreshes the entry and so must carry the same key. The nameservice routes deliberately do not need this: they are `no-cache`, so every use revalidates and authorization re-runs on the server first. Pinned in test_object_endpoint_immutable_caching_and_conditional_get and mutation-checked: reverting this file with the assertions kept fails it.
… `*` The comment claimed that with `allow_origin(Any)` and no credentials, `AllowHeaders::mirror_request()` had "exactly the trust posture \`*\` had". It does not, and the difference is the point of the change rather than a side effect of it: `*` deliberately excludes `Authorization`, so cross-origin AUTHENTICATED requests were impossible before and are possible now. The consequence is real for an internal deployment — a page the user visits can reach an internal Fluree server with an `Authorization` header and read the response. Not CSRF, since there are no ambient credentials and the page must already hold a token, but it does widen the browser-as-pivot surface, and it is why an origin allow-list is now worth having (`cors_enabled` is a bool with `allow_origin(Any)` hardcoded). Comment only; no behavior change.
…re carried `peer/subscription.rs` is -377/+152: a restructure plus two deliberate behavior changes, in live replication code. The six tests that came with it cover the lifted SSE pump, not these — so a regression here would have had no failing test and no clean bisect boundary between "the refactor broke it" and "the intended change broke it". Pinned at the conversion site that owns them. `source_branch`/`branches` are carried from the wire; they were hardcoded to `None`/`0` regardless of what the server sent, so a branched ledger arrived at the peer looking unbranched. The test also pins that their ABSENCE still defaults cleanly, so an older server stays readable. An unparseable `ledger_id` degrades to the verbatim id plus the wire's `branch` rather than erroring the event and tearing down the stream: one unreadable record should not stop a peer receiving every other ledger's updates. The test asserts the fixture really is an id the canonical parser rejects, so it cannot quietly stop exercising the fallback. The first is mutation-checked against the hardcoded values.
The residency and production arms were two copies of the main query entry path, differing only in which fuel tracker they passed. They now call one `plan_and_execute_round` and differ only in the retry wrapper around it. This matters more than ordinary de-duplication because of the dev-dependency feature unification: `fluree-db-api`'s dev-deps enable `residency`, and under `resolver = "2"` that unifies into every target needing dev-deps — so `cargo test -p fluree-db-api` and every api bench compile the RESIDENCY arm, and the production `cfg(not(...))` arm is not built there at all. Two copies meant nothing in the api suite exercised what ships, and drift between them would have been invisible. Once per query, not per row, so it is not on any hot path, and an `async fn` is a state machine rather than a boxed allocation. Ordering inside the retry loop is unchanged: fresh tracker per round, floor charged, then plan+execute, with the store guard and retry budget still outside the loop. Verified building in all three configurations (production native, residency, wasm32); api suite 3319 green, residency retry 5/5.
The wasm-smoke job sets a job-level RUSTFLAGS carrying the wasm
`getrandom_backend="wasm_js"` cfg. The "Install wasm-bindgen CLI" step is a
NATIVE host `cargo install`, and getrandom 0.3.4+ now hard-errors
("the wasm_js backend requires the wasm_js feature") when that cfg is set for
a non-wasm target — so the step started failing on ecosystem drift, with no
change to our code. Clear RUSTFLAGS for this one step, exactly as the
fluree-server native build step in the same job already does.
…awn seam The commit windows in tx_builder detach onto their own task via a bare tokio::spawn (the cancellation shield from 9e68d18: a caller dropped mid-window must abandon the WAIT, never the commit — or the emptied cache slot hands the genesis placeholder to every parked reader). On wasm32-unknown-unknown there is no tokio runtime, so tokio::spawn panics (TryCurrentError) and every playground transact trapped the instance — 5 of 6 wasm-bindgen playground tests failed the moment the wasm-smoke job could actually run them (the failure was previously masked by the getrandom install break). Fix: `wasm_compat::spawn_shielded` — the same seam shape as the existing `spawn_detached`, but with the result awaited. Native: `tokio::spawn`, exactly the call the two shield sites made directly (JoinError detail preserved in the mapped message). wasm32: the work runs to completion on its own `spawn_local` task — the browser event loop is the executor — and the result returns over a oneshot, so the shield property is identical: a caller dropping the returned future cannot cancel the commit window. The spawn is eager (at call, not first poll) on both targets. Verified: cargo check -p fluree-db-api --all-targets clean; wasm32 clippy --tests -D warnings clean; wasm-pack test --headless --chrome fluree-db-wasm 6/6 green in real Chrome (was 1/6 at this head), and fluree-db-browser 5/5.
This is the second and final PR of the wasm program: everything that turns PR-1's "the engine runs on wasm32" into product. It's deliberately over-scoped as one reviewable stack (per our fewer-bigger-PRs preference) — 13 commits in four layers, each layer gated independently before assembly. Stacked on
wasm/groundwork; nothing here touches the native engine paths beyond what's called out below.What you can actually do at this HEAD:
cd fluree-db-wasm/js && npm install && node scripts/build.mjs && node scripts/serve.mjs→ an interactive playground at127.0.0.1:8787/demo/(create ledger → JSON-LD insert/upsert/update → SPARQL + JSON-LD queries, plus a transfer-vs-clone transport timer). Headless proof:node scripts/smoke-browser.mjs— the CI-shaped smoke drives the shipped npm package over CDP and passes with init ~900 ms, insert ~740 ms, query ~675 ms on the dev box.fluree-db-browser::connect(api_base, token, config)yields aBrowserPeerover the peer-proxy wire contract: CID-verified fetches through a channel-bridged transport, an IndexedDB CAS cache with verify-once-then-trust and LRU byte budget, a synchronous residency tier feeding the engine's read path, SSE head tracking, and the production retry loop that makes cold queries converge. (The JS shell's peer mode — wiringBrowserPeerinto the worker protocol — is the one deliberately-deferred integration; the constraints are enumerated influree-db-wasm/js's protocol notes and both sides were built to slot together.)The four layers
1. Server browser-readiness + the transport seam (
fluree-db-server,fluree-db-nameservice-sync). CORS now usesAllowHeaders::mirror_request()— the first cut hand-listed allowed headers and our internal review caught that it would have regressed every browser client that sendsfluree-identity/fluree-track-*/Idempotency-Key(the wildcard it replaced doesn't coverAuthorization, which is the whole reason to touch this; mirroring covers both). Object GETs getETag: "{cid}"+immutablecaching (CID-addressed bytes are the textbook case — this also slots the browser's HTTP cache underneath IndexedDB for free); the NS record gets an ETag over the served bytes (a digest, not the{commit_t}:{index_t}composite we first tried —retracted/branches/servingall change without a commit, which review also caught) with 304 handling ordered after every authz/serving gate so a 304 can never become an existence oracle (test-pinned, including out-of-scope-token → 404 with a matching validator). The proxy clients now sit on anHttpTransportseam (plain-data request/response,Bytesbodies, no reqwest types) so a wasm fetch impl can slot in; native behavior is preserved to the error-message level (all four call sites diffed line-by-line in review), andReqwestTransport+ the native machinery are target-gated sofluree-db-nameservice-syncgenuinely builds for wasm32 — which CI now checks.2. The residency retry loop + freshness (
fluree-db-api,fluree-db-binary-index,fluree-db-core). The production form of PR-1's machinery:query_with_optionson residency builds (wasm32, or theresidencydev feature natively) wraps execution in drain-wants → concurrent fetch-and-pin → re-run, holding the storage's query guard across rounds (a new default trait method mirroringmiss_register; the guard is what makes progress monotone — eviction is frozen while any query is in flight) with a fresh fuel tracker per round so retries don't bill earlier rounds against the final budget. Scans rarely reach this loop at all: the operator-frame retry insidebinary_scanabsorbs leaf misses in place (mutation-tested — disabling the outer loop only fails the one-shot fast-path and policy tests, exactly the intended division of labor). Freshness keeps novelty replay (no new consistency mode) and closes its miss source by prefetching the overlay's reverse-dict leaves at ledger load, pinned by a mutation-checked multi-leaf unit test. Streaming query entry is excluded on wasm by design (rows emit before completion; can't re-run). Not yet wrapped: the cypher/tracked/dataset entries — named follow-ups, same pattern.3.
fluree-db-browser— the engine-side browser I/O crate. ASend + Syncchannel bridge to aspawn_localdriver that owns fetch/AbortController/IndexedDB (the storage traits box Send futures, so a JsFuture-holding transport can't exist — this is the shape that satisfies them);WasmFetchTransportimplementing the seam to its documented contract (Ok for any HTTP status — status semantics stay client logic;credentials: 'omit'; exactly one JS→wasm copy per block, documented); the residency tier (Arc-clone hits, pin sets, batch-first coalesced fetches, no-eviction-while-in-flight with a bounded deferred-insert wait); one memory governor deriving every budget from a single ceiling; write-behind IndexedDB persistence with fetch-admission backpressure (a lagging IDB stalls the pipeline instead of queueing clones). The integration test to start review at runs the engine's landedRetryBudget::after_errorthrough the realcontent_store_forbridge over this storage — miss → register → drain → fetch → pin → re-run with one network fetch, and empty-register → "real error" discrimination — every step positively asserted. 4 real-browser tests (headless Chrome) cover fetch round-trips, error classification, and IndexedDB persistence across reopen.4.
fluree-db-wasm+@fluree/db-wasm(name pending the npm-org decision) — the JS shell. The engine in a dedicated worker behind a typed protocol; a main-thread proxy with hand-written TS types; per-call frozen snapshot handles (an update mid-session provably doesn't leak into an open snapshot — test-pinned) and no streaming surface; a crash model where a wasm trap marks the worker fatal, in-flight calls reject with typedengine_crashed/out_of_memory, and the proxy recycles the worker so thePlaygroundobject survives;maxMemoryBytes(default ¼ ofdeviceMemory, clamped) applied per query. SSE head tracking rides a shared pump lifted from the server's peer subscription loop (parameterized over connection/timer/sink; the server keeps its public shape and drives the same pump), consumed browser-side as a dedicated streaming driver job — with a realReadableStreamtest in Chrome. Ships with awasm-smokeCI job that executes the browser build (bindgen tests in a dedicated worker + the shipped package over CDP), not just compiles it.Performance and build characteristics, transparently
Native first, because it's the thing we rank highest. The shared-crate surface of this PR is deliberately tiny: the server header/CORS commit, the
HttpTransportseam under the proxy clients (reviewed line-by-line for behavior preservation down to error-message text; per HTTP round trip it adds oneasync_traitbox and a small headerVec— invisible next to the network — and the response body staysBytesuncopied), the SSE-pump lift (server keeps its public shape and drives the same pump), and two default trait methods on the storage traits (query_guard/miss_register, defaultNone/None— native implementors never override them and the retry loop that consults them doesn't compile into default native builds). Everything else is two brand-new crates. The full native battery is green at this HEAD — workspace check, nameservice-sync 96, proxy_integration 34 (serial), browser crate 41, residency suites 5/5 + 380, query lib 1416, fmt/clippy per crate — and since this touches perf-relevant paths thebench-comparegate will annotate TIME and PEAK_MEM on the PR; treat that as part of the review.Wasm runtime characteristics, measured not guessed (dev box, headless Chrome, shipped package over CDP, no isolation headers): engine init ~901 ms (includes wasm streaming-compile + worker spawn + first instantiation — a one-time cost per page), JSON-LD insert ~739 ms, SPARQL query ~675 ms on a fresh micro-ledger. Result transfer between worker and main thread uses transferred
ArrayBuffers rather than structured clone where size warrants it — the measured gap that motivated this is ~300 ms vs ~6.6 ms at 32 MB. On the peer read path: a synchronous residency hit is anArcclone (zero copy); a fetched block is copied exactly twice (JS→Bytes,Bytes→Arc<[u8]>— the seam contract's price, documented incas.rs), and theVec-returning legacyStoragemethods pay a third at their boundary, which the residency-first entry points avoid. IndexedDB writes are write-behind and can never delay a read; a lagging IDB applies backpressure to fetch admission instead of queueing clones.What a cold open actually costs, measured (not in this PR to fix, but reviewers should have the number): opening a 200-row ledger in a browser peer issues 18 object GETs — and on a 100 ms link they arrive as 16 sequential waves, 15 of them a single object, because the peer discovers what it needs a round at a time. Roughly 37 serialized round trips to move 20 KB. Two consequences worth knowing while reading the CORS commit:
Authorizationmakes every object GET non-simple, so each one costs anOPTIONSfirst (~1.9 s of a ~5.8 s cold open at 100 ms RTT), and theAccess-Control-Max-Agewe set genuinely helps the repeated-URL routes but buys nothing on the object route, because preflight caching is keyed per URL and every CID has its own URL. Also measured:Rangeis CORS-safelisted (so the ranged leaf reads planned later add no preflight of their own), and a stable batch URL preflights exactly once ever. None of this is urgent — objects areimmutable-cached, so it is paid once per cold profile and never again — but it says where the next work goes: a batch object endpoint (one preflight, ~4× fewer requests, drops into the driver's already-batch-shapedfetch_cids) and widening what each discovery round learns. The peer is discovery-limited, not width-limited:max_concurrent_fetchesdefaults to 8 and currently sits unused because the engine never knows about 8 objects at once.Build/size: shipped wasm (fat LTO,
-Os,wasm-opt -Os): 8.99 MB raw / 3.31 gzip / 2.28 brotli, 26 KB JS glue. A pure-size build (-Oz, one env flip documented inbuild.mjs) lands at 7.48 / 2.83 / 2.01. We shipped-Oson the speed-first mandate —-Oztrades speed-relevant codegen for ~12% wire size and I'd rather hold the line on speed until someone shows the download matters; flip is one env var if the team disagrees. For calibration: this binary still links r2rml, the reasoner, BM25, and Cypher — feature-slicing a slim profile is available headroom we deliberately haven't spent (DuckDB-wasm ships ~11 MB compressed; we're at 2.3 with everything in).Decisions that warrant scrutiny (and the alternatives they beat)
AllowHeaders::mirror_request(), not an allow-list. Our first cut hand-listed headers and internal review proved it would regress every documentedfluree-*/Idempotency-Keybrowser caller (the wildcard it replaced doesn't coverAuthorization— the reason to touch this at all — but covered everything else). With nothing to keep in sync. It is not, however, "exactly the wildcard's trust posture" — an earlier draft said that and review correctly pushed back, because the difference is the point of the change:*deliberately excludesAuthorization, so cross-origin authenticated requests were impossible before and are possible now. This is strictly more permissive, on purpose. The consequence is real for internal deployments — a page the user visits can reach an internal Fluree server with anAuthorizationheader and read the response. That is not CSRF (no ambient credentials; the page must already hold a token), but it widens the browser-as-pivot surface. The code comment now says this rather than the softer version. Follow-up, deliberately not smuggled in here:cors_enabledis a bool withallow_origin(Any)hardcoded, and now that browser clients are a first-class use case an origin allow-list is worth having.{commit_t}:{index_t}composite we tried first — review found four ways the representation changes without a commit (retracted, branch count,serving, drop-recreate), each of which would have pinned a stale 304 in browsers indefinitely. Bytes-digest is strictly stronger than any field list.resolve_cached_bytes(&cid)hit — a query that hits bytes another query fetched would hold no pin, and its working set could vanish between retry rounds (livelock). Tier-global freeze is coarser but is what makes retry progress provably monotone. The cost: a memory-hungry query can hold the tier open; the boundedbudget_wait+ typed failure is the escape.HttpTransport. Rejected — every CAS consumer wants whole verified blocks (CID verification needs the full payload anyway), and the one genuine streaming consumer (SSE) gets a purpose-built job type instead of complicating the seam every block read rides.+atomicsbuild (wasm-bindgen-rayon) needs a pinned nightly, a second artifact, and pushes COOP/COEP isolation headers onto every embedding page (breaks OAuth popups; no Safaricredentialless). Deferred behind a profiling trigger, not ideology — the ladder (OPFS sync reads → multi-worker → threads) is documented and each rung is additive.-Osover-Oz— argued in the build section above.Two deliberate server behavior deltas (flagged for review, not buried)
The SSE lift carries two argued changes: head-change notifications now populate
source_branch/branchesfrom the wire (previously hardcodedNone/0— branch lineage the peer client provably needs), and an unparseableledger_idin one event degrades to using it verbatim as the name instead of erroring the whole stream. Both are in the lift commit's body with rationale; neither has a behavioral test upstream because — honest note — no pre-existing integration test exercisesPeerSubscriptionTaskat all. The pump got six unit tests (parse/backoff-growth-and-reset/failure/malformed-frame/stop) in the lift, but the peer-subscription coverage gap predates this PR and is worth its own issue.What this PR does not do
No peer mode in the JS shell yet (the
BrowserPeer↔ worker-protocol wiring is enumerated and deferred — the protocol needs an unsolicited-event channel first, and a recycled worker's replayed init must never carry a raw token, so the token flow needs its callback bridge). No cypher/tracked/dataset retry wrapping. No OPFS cache tier, no ranged/pack-paged fetches, no wasm threads — all deliberately Phase-3, gated on measurements. The public/anonymous server tier stays its own future PR before any hosted playground.Review map: start with
fluree-db-browser/src/cas.rs(the storage + theafter_errorintegration test), thenfluree-db-api/src/view/query.rs(the loop), then the SSE lift (fluree-db-nameservice-sync/src/head_stream.rsagainst the oldpeer/subscription.rs), then the shell'sprotocol.ts/worker.ts. The server header commit is small and its tests tell the story. The Cargo diffs are wide but mechanical.Pre-review remediation (before undrafting)
Before asking anyone to spend review time here, we ran this PR through an internal blind pre-review (a reviewer with no context on the work, applying the repo's own review rubric, checking out the PR like any colleague's). Verdict was request-changes (one CI-gate red, six highs) — all resolved. Every finding was then either fixed or rebutted with evidence — the remediation commits are on the branch, findings referenced in their messages. Highlights: a clippy lint only reachable under
--all-features(invisible while stacked — the base branch gets no CI) fixed; the residency tier's eviction now uses epoch-tick semantics (entries older than the oldest live query guard are evictable), closing a warm-full-tier scenario where every cold fetch would stall then fail — with the monotone-progress property re-proven by tests at tier, storage, and api-loop levels; write-behind backpressure actually applies now (permit acquired inside the fetch slot,buffer_unorderedbatches) with a stalled-persist throttling test;fluree-db-browser's five real-browser suites now run in CI (they previously never executed anywhere); JS snapshot handles are generation-stamped so pre-crash objects reject typed instead of silently reading a recycled engine's state, respawns are capped with backoff, and the worker-spawn pattern is the bundler-statically-detectable shape (Vite verified by a repro script; other bundlers honestly marked unverified). One engine fact surfaced while making the memory-budget browser test deterministic, worth its own follow-up: the per-query memory budget charges grouped-aggregation state (GROUP_EST_BYTES) and hash-join lanes, but plain cross-join row retention andGROUP_CONCATpayload bytes are not charged today — the test pins the typed-rejection contract on a charged lane and says so.Review remediation
Four items from review, all addressed; the two flagged as wanted-before-merge are the first and last.
Vary: Authorizationon CAS object responses — a real cross-principal cache leak, now fixed. These bodies are authorized by bearer token and servedprivate, max-age=31536000, immutable.privatekeeps them out of shared caches, but the browser's own cache is per profile, not per principal, andimmutablemeans it never revalidates — so after principal A read a ledger, principal B on the same browser profile was served those CIDs directly, with the server never seeing B's token and never getting to refuse it.Varyputs the token in the cache key. Pinned at all three object responses (200, 206, 304) intest_object_endpoint_immutable_caching_and_conditional_get, and mutation-checked: reverting the source with the assertions kept fails the test. The NS routes deliberately don't need it —no-cacherevalidates, so authorization re-runs server-side before anything is served.The two subscription behavior deltas are now pinned. Review's point stood:
peer/subscription.rsis −377/+152, a restructure carrying two deliberate behavior changes, and the six new tests covered the lifted SSE pump rather than those changes — so a regression would have had no failing test and no clean bisect boundary between "the refactor broke it" and "the intended change broke it". Two tests added at the conversion site (server_sse.rs): one pinssource_branch/branchesbeing carried from the wire (they were hardcodedNone/0, so a branched ledger arrived looking unbranched) and that their absence still defaults cleanly for an older server; the other pins that an unparseableledger_iddegrades to the verbatim id plus the wire'sbranchrather than tearing down the stream, with a precondition assertion that the fixture really is an id the canonical parser rejects, so the test cannot go vacuous. The first is mutation-checked against the hardcoded values.The duplicated query entry path is factored into one
plan_and_execute_round. Both cfg arms now call it and differ only in the retry wrapper. This one is worth stating precisely because it interacts with the dev-dependency feature unification raised on #1714:cargo test -p fluree-db-apiand every api bench compile the residency arm, so the productioncfg(not(...))arm is not built there at all — two copies of the main query path meant nothing in the api suite exercised what ships, and drift between them would have been invisible. The call is once per query, not per row, so it is not on any hot path; verified building in all three configurations (production native, residency, wasm32).Caught up to
main, and where the browser-driver hardening livesRebased onto current
main(verified semantically, not just textually). A second independent adversarial review then covered the two new crates (which the human review explicitly excluded). Its findings are fixed in the stacked child #1733, not here, because they are entangled with lq-smoke's token-cell evolution (pre-this-PR there is noTokenCell, so the driver-open/deadline/timer commits cannot be cleanly cherry-picked down without pulling a dependency chain). This is a strict stack that merges in order, so #1715 → #1733 lands the browser peer as a unit: the driver-open hang, theu32::MAX-is-immediate timer, the SSE connect timeout, the unverified-cache and token-Debugissues, and the buffer bounds are all closed by the time #1733 merges. #1715's own head is the transport/CORS/header surface the human review approved; its browser-crate hardening completes in #1733.