diff --git a/CHANGELOG.md b/CHANGELOG.md
index 42b9ff0..6ea372b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,57 @@ While pre-1.0, the public API may change between 0.x releases.
## [Unreleased]
+### Added
+
+- **SSR support (experimental, ADR-0011)** — built on TanStack DB's merged
+ SSR API (`DbClient` `dehydrate()`/`hydrate()`, PR
+ [#1564](https://github.com/TanStack/db/pull/1564), shipped in
+ `@tanstack/db` 0.8.0).
+ - **Server:** `readSyncSnapshot(req, request)` — one consistent
+ `{rows, cursor}` read over the DO binding, no WebSocket. The required
+ `request` runs through `parseAttachment`: one auth gate for the socket
+ and the read path. The cursor is a durable high-water mark
+ (`max(currentSeq, drainCursor)` — robust to retention pruning); `"0"`
+ honestly means "no resume point". RPC rows normalize BLOB
+ `ArrayBuffer → Uint8Array` for wire-codec parity (ADR-0017).
+ - **Client:** `SsrSnapshotTransport` (read-only; per-request; swapped at
+ the new structural `Transport` seam), syncMeta cursor round-trip
+ (`{v, cursor, where-fingerprint}`; fail-loud-but-safe import/merge),
+ `since` on the first sub, `seedCursor` (a late chunk regresses and
+ replays via a forced reconnect), always-armed eager snapshot reconcile
+ (authoritative set semantics — no flash-to-empty, no stranded deletes),
+ on-demand transient catch-up with honest truncate for unresumable rows.
+ - **Wire (additive):** `uptodate` gains optional `sub` (a catch-up's
+ terminal is sub-scoped); the first `sub` may carry `since`.
+- **New upstream contracts adopted:** `commit()` receipts
+ (`SyncAppliedReceipt`, 0.8.5) — subset loads settle only once rows are
+ visible; `markError` (0.8.2) — a failed first connect fails `preload()`
+ loud with the cause instead of hanging (a retried `preload()` recovers);
+ per-subset load failures reject that subset's promise (0.8.4);
+ `withCollectionConfigFactory` (0.8.0) — `doCollectionOptions` configs work
+ as `collectionOptions(id, …)` descriptors with fresh adapter state per
+ `DbClient`.
+
+### Changed
+
+- **Peer dependency: `@tanstack/db >= 0.8.5`** (was `>= 0.6.0`) — the SSR
+ hooks shipped in 0.8.0; 0.8.5 carries the `commit()`-receipt contract and
+ descriptor reuse this adapter adopts. The 0.6-era API is otherwise
+ unchanged: the full pre-lift suite passes on 0.8.5 without modification.
+- The transport ignores STREAM frames from an abandoned socket
+ (identity-guarded message dispatch): only the current socket speaks for the
+ stream; dropped frames are re-covered by the resubscribe catch-up from the
+ applied cursor. ID-scoped receipts (`committed`/`rejected`/`page`) still
+ settle their waiters from a stale socket — they are not re-covered by any
+ replay — but never advance the cursor.
+
+### Fixed
+
+- `unsubscribe` during an in-flight `subscribe`'s connect no longer sends the
+ sub after the socket opens — previously the server persisted a ghost
+ subscription (ADR-0019) with no local consumer until the socket dropped.
+ Pre-existing; surfaced by the SSR lift's adversary review.
+
## [0.6.0] — 2026-07-27
### Added
diff --git a/README.md b/README.md
index f5c52bc..9481042 100644
--- a/README.md
+++ b/README.md
@@ -267,6 +267,57 @@ same socket: `transport.call.(args)` (typed sugar) or the low-level
`transport.sendCall("clearRoom", undefined)` — both mint the txId for you and
resolve with the command's result on `committed`.
+### 4. SSR (experimental)
+
+Built on TanStack DB's SSR support (`DbClient` `dehydrate()`/`hydrate()` and
+the `exportSyncMeta`/`importSyncMeta`/`mergeSyncMeta` sync hooks, shipped in
+`@tanstack/db` 0.8.0 — this adapter requires ≥ 0.8.5). Why/how trade-offs
+live in [ADR-0011](./docs/adr/0011-ssr-dehydrate-hydrate.md).
+
+On the worker, render through a **per-request** `DbClient` backed by one
+snapshot read per subscription — no WebSocket from the render path:
+
+```ts
+// Route loader / server handler (per request!)
+import { DbClient, collectionOptions } from "@tanstack/db"
+import { doCollectionOptions, SsrSnapshotTransport } from "tanstack-durable-object-sync/client"
+
+const stub = env.CHAT_DO.get(env.CHAT_DO.idFromName(sessionId))
+// `request` is the incoming (claims-bearing) Request — the DO runs it through
+// parseAttachment, the SAME auth gate as the WebSocket upgrade.
+const transport = new SsrSnapshotTransport({
+ read: (req) => stub.readSyncSnapshot(req, request),
+})
+const db = new DbClient()
+const messages = db.collection(
+ collectionOptions("messages", () =>
+ doCollectionOptions({ transport, table: "messages", getKey: (m) => m.id }),
+ ),
+)
+await messages.preload()
+return { dbState: db.dehydrate() } // rows + our resume cursor (opaque syncMeta)
+```
+
+In the browser, hydrate before going live. The collection is ready
+immediately with the dehydrated rows (stale-while-revalidate); the first sub
+resumes from the dehydrated cursor, so the catch-up applies exactly what
+changed while the HTML was in flight — updates *and* deletes:
+
+```ts
+const db = new DbClient()
+db.hydrate(dbState) // or from @tanstack/react-db
+const messages = db.collection(
+ collectionOptions("messages", () =>
+ doCollectionOptions({ transport: wsTransport, table: "messages", getKey: (m) => m.id }),
+ ),
+)
+```
+
+Mutations during SSR throw (`SsrReadOnlyError`). `readSyncSnapshot` is callable
+by any worker holding the DO binding, and its required `request` argument runs
+through `parseAttachment` — **one auth gate for both the socket and the read
+path**, so a tenant check in `parseAttachment` can't be bypassed by SSR.
+
---
## Examples
@@ -288,6 +339,10 @@ browser-verified.
an inbox) behind one Worker: one transport per DO, each typed by its own `Api`
so `transport.call.*` is scoped to that DO's commands, and a cross-DO feed
merged client-side (the DO never joins — ADR-0001).
+- **[`examples/ssr`](./examples/ssr)** — TanStack Start on Cloudflare with
+ `routerWithDbClient`: a loader-preloaded page (rows in the server HTML,
+ WebSocket resumes from the dehydrated cursor) and a Suspense-streaming page
+ (`useLiveSuspenseQuery`, result streamed into the document). ADR-0011.
> [!TIP]
> Using on-demand with `orderBy` + `limit`? Add a **range index** on the order
diff --git a/docs/adr/0011-ssr-dehydrate-hydrate.md b/docs/adr/0011-ssr-dehydrate-hydrate.md
new file mode 100644
index 0000000..b2e0d26
--- /dev/null
+++ b/docs/adr/0011-ssr-dehydrate-hydrate.md
@@ -0,0 +1,354 @@
+# 0011 — SSR: dehydrate on the worker, hydrate to the cursor
+
+**Status:** Accepted (experimental — designed against TanStack DB draft PR
+[#1564](https://github.com/TanStack/db/pull/1564), lifted onto the PR **as
+merged** in `@tanstack/db` 0.8.0; see *Amendments* below). Generalizes
+ADR-0002 C1's flush-before-`committed` barrier to *all* cursor-advancing
+emissions (C1′ below).
+
+## Context
+
+TanStack DB's draft SSR PR adds `DbClient` with row-level
+`dehydrate()`/`hydrate()` and three opaque sync-config hooks —
+`exportSyncMeta(): unknown`, `importSyncMeta(meta)`,
+`mergeSyncMeta(current, incoming)`. Facts about the upstream design that
+constrain ours (verified against the PR, not its docs — it has none for
+adapter authors):
+
+- Hydration applies rows as **synced upserts** (`committed: true,
+ immediate: true`) **before** `importSyncMeta` runs. An adapter cannot veto
+ row application; correctness must come from post-connect catch-up.
+- Dehydrated state has **no tombstones**. It is a snapshot *at* the exported
+ cursor, never a delta. All delete-correctness is the adapter's problem.
+- Hydration does **not** mark the collection ready; readiness stays under the
+ adapter's `markReady()`.
+- The hooks live on the (potentially module-scoped, request-shared) sync
+ config. Our options creator takes the transport as an argument, so on the
+ server our options are **per-request by construction** — we sidestep the
+ upstream cross-request-leak hazard, and document per-request creation as a
+ requirement.
+- TanStack sync-write semantics: a sync `insert` for an existing key throws
+ `DuplicateKeySyncError` unless values deep-equal; a sync `update` for a
+ missing key upserts (our move-in path already relies on this).
+
+Our model is one ordered stream per DO and a **single client cursor**
+(`appliedSeq`) advanced only at commit boundaries (ADR-0001/0002). The server
+already serves `since` on *any* sub — windowed catch-up within the retention
+floor, honest `reset` below it (ADR-0009). SSR support is therefore mostly:
+get a snapshot + cursor out of the DO without a WebSocket, round-trip the
+cursor through the dehydrated state, and make the first sub carry `since`.
+
+## Decision
+
+### D1 — Socketless snapshot read: `readSyncSnapshot` RPC
+
+`SyncDurableObject` gains a public RPC method:
+
+```ts
+readSyncSnapshot(
+ req: { collection: string; where?: unknown; orderBy?: unknown; limit?: number },
+ request: Request, // REQUIRED — runs through parseAttachment, the one auth gate
+): Promise<{ rows: Array>; cursor: string }>
+```
+
+Same compile path as the `fetch` frame (`compileSubsetQuery`); the gate awaits
+*before* the reads, so rows and cursor are still taken at one position
+(synchronous SQLite between them). Throws on unknown collection or unsupported
+predicate — fail loud; RPC propagates.
+
+Trust model: the binding limits callers to first-party workers, and the
+REQUIRED `request` argument runs through **`parseAttachment` — the same gate
+as the WS upgrade**. The worker passes the claims-bearing Request it already
+forges (or forwards) for the socket path; a rejecting `parseAttachment`
+rejects the read. Two paths, one gate: an author's tenant check cannot be
+silently bypassed by the snapshot read (grill-session finding — an earlier
+draft had no gate here, inverting the WS path's safe-by-default shape). The
+minted claims are also the seam where uniform read-scoping would land, on
+subs and snapshots alike — note that today *neither* path filters rows by
+identity; `parseAttachment` is connection/read-level gating, and the
+client-supplied `where` is shaping, not security.
+
+**The exported cursor is a durable high-water mark** — `max(MAX(_sync_changes
+.seq), drain_cursor)` — *not* bare `currentSeq()`, because retention can prune
+the changelog empty while the table has rows, and a bogus cursor `0` against
+live rows would let a delete that lands between render and hydration strand a
+stale row forever (adversarial-review finding). Cursor `"0"` therefore honestly
+means "no resume point": the client omits `since` and reconciles (D4).
+
+### D2 — `SsrSnapshotTransport`, and `Transport` as an interface
+
+What `doCollectionOptions` consumes becomes a structural `Transport` interface
+(satisfied by `WebSocketTransport` unchanged). `SsrSnapshotTransport` implements
+it for server rendering: constructor takes `read: (req) => Promise<{rows,
+cursor}>` (the author passes `(req) => stub.readSyncSnapshot(req, request)`,
+closing over the request's claims; no Cloudflare
+types in the client build). `subscribe` performs one read and synthesizes
+`onSnap*`/`onSnapEnd`; `connect()` resolves immediately (so on-demand
+`loadSubset` during a server `preload()` works unchanged); its cursor is the
+**min** across reads; `sendMut`/`sendCall`/`fetch` throw `SsrReadOnlyError`.
+SSR is read-only.
+
+Min is not merely the *safe* joint resume point (replay is idempotent;
+skipping is not) — it is *self-consistent-making*: a render's reads land at
+slightly different positions (milliseconds of DO time apart), and the first
+catch-up from min replays exactly that skew window, converging every
+dehydrated row to one position. Because the changelog `seq` is one stream
+across all collections on the DO, the min is also a coherent position for
+every collection sharing the transport — no per-collection reset risk.
+Per-table cursor tracking (`cursorFor(table)`) was considered and rejected:
+permanent interface surface to avoid a transient milliseconds-wide replay.
+
+### D3 — syncMeta carries the cursor; the first sub carries `since`
+
+`doCollectionOptions` implements the hooks:
+
+- `exportSyncMeta → { v: 1, cursor: transport.appliedCursor, where? }` —
+ `where` is a fingerprint (the codec envelope) of the eager filter the rows
+ were dehydrated under. A cursor is only a sound resume point *for that
+ filter*: catch-up emits changed keys only, so an **unchanged** out-of-filter
+ hydrated row would never be reconciled away (second-review finding).
+- `importSyncMeta` — validate (`v` unknown / malformed cursor → throw); a
+ fingerprint mismatch (deploy skew) refuses the cursor and downgrades to the
+ always-sound snapshot-reconcile path (`hydratedCursor = "0"`, transport
+ unseeded); otherwise stash `hydratedCursor` and `transport.seedCursor(c)`.
+- `mergeSyncMeta → min(cursor)` — min is self-healing: a late/stale chunk's
+ rows are applied upstream before we're consulted, and a min cursor makes the
+ next catch-up replay exactly the clobbered window.
+
+`seedCursor(c)` may **regress** `appliedSeq` (claiming a *shorter* applied
+prefix is always safe). A regress while LIVE cannot replay on the same socket:
+boundary frames the server already sent (full duplex) would dispatch after the
+regress and re-advance the cursor past the repair window (second-review
+blocker). It therefore **forces a reconnect** — the old socket's queued
+boundaries stop counting (`advance` suppressed; their data still applies,
+idempotently) and the fresh socket resubscribes from the seed. One mechanism
+for early and late hydration; no second cursor, no ack channel.
+
+With a `hydratedCursor` (consumed once at sync start; cleared in the sync
+cleanup fn — after a collection GC the rows are wiped, so a retained cursor
+would resume over an empty store and silently lose data):
+
+- **Eager**: the first sub carries `since`; `markReady()` immediately (rows are
+ present; catch-up arrives as `d`+`uptodate`, which never fires `snap-end`).
+ Be explicit about what this changes: **hydration redefines `ready` as
+ "renderable", not "synced"** — `isReady` is true on the server pass (no
+ socket will ever exist) and stays true offline with stale rows. That is the
+ stale-while-revalidate contract, deliberately. An app that wants a
+ "catching up → live" signal (a SyncIndicator) doesn't need new API: the
+ transport already exposes it — `awaitSeq(String(BigInt(dehydratedCursor) +
+ 1n))` resolves at the first post-hydration boundary, i.e. caught up. Not
+ README material (sharp-edged); recorded here for when someone asks.
+ Below the retention floor the server answers `reset` → truncate + fresh
+ snapshot — which DOES flash empty between the truncate's commit and the
+ snapshot's (unlike the cursor-`"0"` reconcile path). Accepted, not fixed:
+ the dehydrated cursor is seconds old, so falling below the floor requires
+ `changelogRetentionMs` (default 2 days) shorter than the HTML's flight time
+ — pathological config, not a reachable state. Unifying it would need the
+ client to skip the truncate and let snapshot set-semantics reconcile, but a
+ `reset` is also the only terminal for a REJECTED sub (no snapshot follows),
+ where skipping the truncate keeps stale rows forever — the one outcome
+ ranked worst throughout this design. The reset-cause ambiguity is harmless
+ today (rejection is dev-loud; below-floor is pathological) and becomes
+ worth a wire-level distinction — likely alongside the incarnation epoch —
+ when client-side persistence (an LRU'd local db) makes days-old cursors
+ routine. Future scope, deliberately not now.
+- **On-demand**: **one transient unfiltered catch-up sub**
+ (`since = hydratedCursor`, no `where`) that unsubscribes at *its own*
+ sub-scoped terminal — never at a broadcast boundary, which can precede its
+ frames. The dehydrated rows are the union of the server-loaded subsets;
+ per-subset `since` is unsound for any subset the dehydrated state didn't
+ cover, and subset-tracking still leaves overlapping-`where` stale-delete
+ holes. One unfiltered catch-up covers every changed key (always-emit ⇒
+ synthetic deletes included) in the seconds-wide render→hydrate window.
+ **Semantic cost, accepted and documented**: changes to rows outside any
+ hydrated subset land in the collection during that window (bounded by
+ change volume). The leaked rows' staleness is **unobservable**: a live
+ query whose predicate matches one has a server sub with that same
+ predicate, whose snapshot/deltas converge it at observation time
+ (update-if-exists) — stale only while nothing looks, fresh by the time
+ anything does. Eager rendering of stale/leaked data is acceptable against
+ the snappy client-first UI it buys; the residual cost is memory, bounded
+ by seconds of change volume. `markReady()` **gates on the catch-up sub frame being
+ sent** (not completed): `loadSubset` subs fire only after ready, so on the
+ single ordered socket the catch-up always precedes subset snapshots
+ (second-review finding — `connect().then(markReady)` alone races).
+ Wire note: the transient's teardown depends on the server scoping the
+ catch-up terminal (`uptodate.sub`); against a pre-0011 server the terminal
+ arrives unscoped and the transient sub never tears down (an unfiltered
+ live sub leaks until the socket drops). Matter-of-fact, not mitigated:
+ pre-1.0, client/server version skew is not a supported configuration —
+ the worker ships the bundle and the DO from one deploy.
+ When the hydrated rows are **unresumable** — cursor `"0"`, or the server
+ `reset`s the catch-up below the floor — on-demand **truncates** them
+ (the reset path also unsubscribes immediately so the trailing unfiltered
+ resnapshot is dropped unhandled). A full-table snapshot was rejected here —
+ and the principled line between this and the tolerated catch-up leak above
+ (both are "stale while unobserved, fresh when observed") is **on-demand's
+ memory contract**: memory proportional to what you observe. A seconds-wide
+ window of changed keys respects that contract asymptotically; a full-table
+ snapshot breaks it categorically — unbounded in table size, on the mode
+ whose purpose is not loading the table. The truncate refuses to convert
+ on-demand into accidental-eager. Eager keeps the no-flash reconcile;
+ on-demand keeps honesty.
+
+### D4 — Snapshot reconciliation (and two pre-existing bugs fixed)
+
+Adversarial review (gpt-5.5) rejected the obvious "insert-if-absent" guard for
+snapshots — `snap-end` advances the cursor, so *skipping* a fresher snapshot
+value and then dropping the socket loses that write forever. Instead:
+
+- **C1′ (server)**: `broadcaster.flushOne(ws)` before any synchronous
+ cursor-advancing emission (`handleSub` snapshot, `emitCatchUp`). C1 said
+ "deltas flush before `committed`"; C1′ says **a socket's pending coalesced
+ deltas always precede any cursor boundary on that socket**. This fixes a
+ pre-existing bug independent of SSR: a multi-collection reconnect's catch-up
+ `uptodate` could advance the cursor past another collection's still-buffered
+ delta (drop before the tick ⇒ lost write).
+- **`onSnap` writes update-if-exists** (snapshot value wins). With C1′ a
+ snapshot value is never staler than the held row, so this converges; it also
+ absorbs `DuplicateKeySyncError` when a subset snapshot lands over hydrated
+ rows that changed since dehydration. (`loadMore`'s page path keeps its
+ insert-if-absent: `page` frames never advance the cursor, and a page *can*
+ be staler than a held row.)
+- **Key-reconcile is ALWAYS armed for eager subs** (grill-session
+ generalization; never for on-demand subset subs, whose snapshot must not
+ delete other subsets' rows): an eager snapshot is authoritative set
+ semantics over synced rows, period — at `snap-end`, held synced keys
+ absent from the snapshot are deleted. For the normal empty-at-first-
+ snapshot flow it is a no-op (and boundary-free: `begin` opens only when a
+ delete is due); for ANY path where synced rows precede a snapshot —
+ hydration with no resume point, a refused foreign-filter cursor, meta that
+ failed validation — it is what prevents a server-deleted held row from
+ being stale forever. An EMPTY snapshot still reconciles (zero keys is an
+ authoritative set — second-review blocker). Honest set semantics without a
+ truncate's flash-to-empty (SSR exists for first paint). Presence checks
+ steer by `syncedData`, never the combined view — optimistic overlays are
+ invisible to sync writes by design.
+- **The syncMeta hooks fail loud but SAFE** (grill-session finding): upstream
+ applies a chunk's rows BEFORE `mergeSyncMeta`/`importSyncMeta` run — a
+ validation throw cannot veto them, so throwing alone would leave applied
+ rows with no reconcile intent (and, on-demand, no truncate): stale
+ forever. Both hooks set `hydratedCursor = "0"` (the always-sound
+ snapshot-reconcile / truncate route) BEFORE throwing — the version skew
+ still surfaces to the app, and the state left behind converges. This is
+ also the gradual-upgrade path: a future `v: 2` payload degrades old
+ clients safely and loudly; no per-version fallback logic.
+- **`onDelta` maps `insert` → `update` when the key exists** — catch-up emits
+ the latest CDC op per key, so a delete-then-reinsert since the cursor arrives
+ as `insert` against a held key and would throw. Pre-existing on reconnect
+ catch-up too; fixed for both.
+
+### D5 — Packaging
+
+Peer dependency `@tanstack/db >= 0.8.5`: the syncMeta hooks shipped in 0.8.0
+(unchanged from the draft), and 0.8.5 carries the `commit()`-receipt contract
+and descriptor-reuse-by-id this adapter adopts (see Amendments). No
+self-branding via `Symbol.for` — the returned config carries upstream's own
+`withCollectionConfigFactory` marker, so `collectionOptions(id, () =>
+doCollectionOptions(...))` descriptors materialize fresh adapter state per
+`DbClient`. (The draft era vendored PR-branch tarballs; those are gone —
+everything builds against released packages.) Everything lands as
+**experimental** in the changelog.
+
+## Known limitations
+
+- **No incarnation epoch.** A cursor from a pre-storage-reset DO whose new
+ changelog already reaches past it would catch up silently-wrong. The exposure
+ window for SSR is seconds and requires a storage reset inside it; fixing it
+ properly is a protocol rev (an epoch in `_sync_meta` + a hello/epoch frame),
+ deliberately deferred. Pre-existing for in-page reconnects too.
+- ~~**Upstream is a draft.**~~ Resolved: PR #1564 merged 2026-08-17 and
+ shipped in `@tanstack/db` 0.8.0 with the three hook signatures **byte-for-
+ byte unchanged**. The semantics *around* them did change — see Amendments.
+
+## Consequences
+
+- SSR first paint with no WebSocket from the render path, no idle timers, no
+ hibernation impact (the RPC is a plain request).
+- The single-cursor inversion survives intact: `since` at first sub is a
+ bootstrap parameter, `seedCursor` only ever claims a shorter prefix, and
+ confirmation still rides the one stream.
+- C1′ and the `onDelta` normalization harden reconnect for all clients, SSR or
+ not.
+
+## Amendments — 2026-08 lift onto merged upstream (`@tanstack/db` 0.8.5)
+
+The design above was written against the draft PR. The PR merged (0.8.0,
+2026-08-17) with the hook names/signatures intact but reshaped semantics
+around them, and this repo's `main` moved 0.4.0 → 0.6.0 (ADR-0015..0019)
+underneath the branch. The lift changed the following — each a deliberate
+decision, not drift:
+
+- **`exportSyncMeta` can return `undefined`, and the `Transport` seam gains
+ `hasPosition`.** Merged upstream consults `exportSyncMeta()` for the
+ *current* meta on **every** hydrated chunk and routes the incoming meta
+ through `mergeSyncMeta` when current exists (draft: merge only across
+ chunks). A fresh browser transport exporting `{cursor:"0"}` would win the
+ MIN-merge against every real dehydrated cursor — silently downgrading all
+ hydration to the snapshot-reconcile path, making D3's cursor resume dead
+ code. The claim basis is now: the transport's position if it has one
+ (`hasPosition`), else the unconsumed `hydratedCursor`, else **no meta at
+ all**. `"0"` stays a *real* claim exactly where it is one — an SSR read
+ against a DO with no history (`SsrSnapshotTransport.hasPosition` is true
+ after any read, even at 0; a live `WebSocketTransport` can never claim 0).
+- **`readSyncSnapshot` lives on the `Syncable` mixin as a public method** —
+ DO RPC dispatches only on public instance members, so this is one
+ deliberate addition to ADR-0015's four-method collision surface. The gate
+ is the mixin's configured `parseAttachment` hook (same contract). D1's
+ hand-rolled ordering note is obsolete: `compileSubsetQuery` now defaults
+ `ORDER BY rowid` for every subset read (ADR-0015 era). RPC rows also
+ normalize BLOB `ArrayBuffer → Uint8Array` — structured clone would leak the
+ bare buffer where the wire codec normalizes it (ADR-0017 parity).
+- **D3's forced regress-reconnect is re-derived against ADR-0016.** It is
+ *voluntary* — not a network failure — so it bypasses the backoff policy
+ entirely: no attempt consumed, no delay, and a custom `reconnectDelay`
+ policy cannot declare it terminal. A failed open falls back into the normal
+ policy-driven retry. The draft-era `suppressAdvance` flag is **replaced by a
+ socket-identity guard on message dispatch** (only the current socket speaks
+ for the STREAM): the flag protected the cursor but still let an abandoned
+ socket's queued frames dispatch data, and its reset-at-install left a race
+ window. Stream frames (`snap`/`snap-end`/`d`/`uptodate`/`reset`) from a
+ stale socket are dropped — the resubscribe catch-up re-covers them
+ idempotently. ID-scoped receipts (`committed`/`rejected`/`page`) are NOT
+ re-covered by any replay, so a stale socket may still settle those waiters
+ — it just never advances the cursor (codex adversary: a committed mutation
+ must not be reported as timed out because a late hydration chunk forced a
+ reconnect first). Main's own ADR-0016 machinery already carried the
+ scheduling-time `reconnecting` flag and the stale-close guard this branch
+ originally invented, so those SSR commits dropped out.
+- **New upstream contracts adopted** (released after the draft): `commit()`
+ receipts (`SyncAppliedReceipt`, 0.8.5) — snapshot terminals and cursor
+ load-more settle `loadSubset` only once rows are visible; `markError`
+ (0.8.2) — a failed ready-gate/first-subscribe fails `preload()` loud with
+ the cause, and a retried `preload()` recovers (error → ready); subset-load
+ failures reject that subset's promise (0.8.4 `loadSubset:error`), not the
+ collection; `withCollectionConfigFactory` (0.8.0) as in D5. Not adopted
+ (documented follow-up): `LoadSubsetOptions.signal` — cooperative
+ cancellation of a shared refcounted sub needs its own design; loads
+ complete correctly without it.
+- **A second codex adversary round on the lift itself** hardened five more
+ edges, each pinned by a test: `parseSyncMeta` rejects a NEGATIVE cursor (it
+ would ride `since` to the server, draw a full snapshot the on-demand
+ catch-up handler discards, and leak the transient sub forever);
+ `mergeSyncMeta` with mismatched fingerprints yields the honest `"0"` under
+ our fingerprint (MIN alone could let the matching side's cursor smuggle
+ foreign-filter rows past import's check); `exportSyncMeta` is
+ settlement-gated (while any commit receipt is unsettled it claims the last
+ fully-settled position — the boundary cursor is not yet proof of applied
+ rows, and a dehydrate in that window must under-claim); a REJECTED receipt
+ fails its subset load / readiness rather than resolving it; and the
+ on-demand ready-gate failure path heals — the catch-up terminal and every
+ completed subset also `markReady()` (idempotent), so error → ready recovery
+ actually happens. Plus one pre-existing transport fix: `unsubscribe` during
+ an in-flight `subscribe`'s connect no longer sends a ghost sub the server
+ would persist (ADR-0019) with no local consumer.
+- **Upstream now natively reconciles hydration-seeded keys** (a later adapter
+ `insert` of a seeded key is applied as an update — `hydrationSeedKeys`,
+ 0.8.0). D4's held-key upsert conversion stays: it is belt-and-braces on the
+ hydration path and load-bearing for the mid-session reconnect catch-up,
+ which upstream's seed tracking does not cover.
+- **Not lifted:** the upstream live-query layer (`preloadLiveQuery`,
+ `HydrationBoundary`, `@tanstack/react-router-with-db`) needs nothing from
+ the adapter — dehydrated live-query results ride upstream's own state, and
+ source-collection hydration is exactly the D3 path. The example app is the
+ right home for that surface.
diff --git a/docs/adr/README.md b/docs/adr/README.md
index 112f09d..cd3f6fe 100644
--- a/docs/adr/README.md
+++ b/docs/adr/README.md
@@ -18,6 +18,7 @@ explains the displacement.
| [0008](./0008-orphaned-cdc-triggers.md) | Orphaned CDC triggers when a collection is removed | Accepted |
| [0009](./0009-changelog-time-retention.md) | Changelog time-based retention; reset stale reconnects | Accepted |
| [0010](./0010-typed-mutations-collection-manifest.md) | Typed mutations via a collection-row manifest on `SyncRegistry` | Accepted (manifest superseded by 0014) |
+| [0011](./0011-ssr-dehydrate-hydrate.md) | SSR: dehydrate on the worker, hydrate to the cursor | Accepted (experimental; generalizes 0002 C1 → C1′; amended for merged upstream + 0015/0016 lift) |
| [0012](./0012-wire-input-hardening.md) | Wire-input hardening: frame-shape guards, inbound limits, sanitized execute errors | Accepted |
| [0013](./0013-predicate-floor-one-evaluator.md) | Filtered-subscription membership: one evaluator is the source of truth; the floor is the verified-agreeing set | Accepted |
| [0014](./0014-object-sync-schema.md) | `defineSync`: one schema value, mutations on the collection, commands on the connection | Accepted (supersedes 0001 D11 builder; closes 0010 manifest) |
diff --git a/examples/ssr/.gitignore b/examples/ssr/.gitignore
new file mode 100644
index 0000000..0293926
--- /dev/null
+++ b/examples/ssr/.gitignore
@@ -0,0 +1 @@
+.tanstack/
diff --git a/examples/ssr/README.md b/examples/ssr/README.md
new file mode 100644
index 0000000..89d67e6
--- /dev/null
+++ b/examples/ssr/README.md
@@ -0,0 +1,93 @@
+# ssr — tanstack-durable-object-sync example
+
+Server-side rendering end to end (ADR-0011): a TanStack Start app on Cloudflare
+Workers reads a `todos` collection from the sync DO **without a WebSocket**,
+dehydrates it into the router payload, hydrates in the browser for an instant
+first paint, then goes live over the socket and converges — catch-up from the
+dehydrated cursor delivers whatever changed while the HTML was in flight.
+Stale-while-revalidate, never a flash of empty.
+
+Built on the **released** TanStack DB SSR API (`@tanstack/db` ≥ 0.8.5,
+`@tanstack/react-db` ≥ 0.3.5) and the official
+[`@tanstack/react-router-with-db`](https://www.npmjs.com/package/@tanstack/react-router-with-db)
+Start adapter — `routerWithDbClient(router, dbClient)` handles DbProvider,
+dehydrate/hydrate through the router, and Suspense query streaming. The only
+app-specific part is this library's transport seam.
+
+The example depends on the local package (`"tanstack-durable-object-sync":
+"file:../.."`), so build it first:
+
+## Run
+
+```sh
+npm run build --prefix ../.. # build the library's dist/ (file: dep)
+npm install
+npm run dev # vite dev with the Cloudflare plugin (runs in workerd)
+```
+
+Open the printed URL (default http://localhost:5173).
+
+- `npm run build` — production build (client + worker)
+- `npm run typecheck` — `tsc --noEmit`
+- `npm run deploy` — build then `wrangler deploy`
+
+## Pages
+
+`/` is a plain landing page.
+
+### `/live-query` — loader preload + `useLiveQuery`
+
+The baseline SSR round trip. The route loader materializes the collection on
+the request's DbClient and calls `preload()` — one snapshot read from the DO
+via `readSyncSnapshot`. `routerWithDbClient` dehydrates the normalized rows
+**plus the resume cursor** (our opaque `syncMeta`) into the router payload and
+hydrates the browser client from it. Rows are in the raw HTML; the status line
+flips `ssr → hydrated` on mount and `catching up → live` once the WebSocket
+resumes from the dehydrated cursor and converges — updates *and* deletes made
+while the HTML was in flight are applied. Adds and toggles are optimistic.
+Open a second tab to watch them sync.
+
+### `/live-suspense-query` — Suspense streaming + `useLiveSuspenseQuery`
+
+No loader preload: the query is discovered mid-render. On the server the
+component suspends while the snapshot transport reads the DO, and
+`routerWithDbClient` streams the pending query result into the document — the
+streamed shell shows the fallback, then the rows arrive, then the browser's
+live WebSocket result replaces the snapshot once sync converges. The
+"show only open" toggle changes the structured query IR (a new derived query
+identity), which re-suspends until the new query computes.
+
+## Shape
+
+One worker serves everything (`src/server.ts`): WebSocket upgrades on `/sync/*`
+go straight to the DO; every other request is the Start app via
+`@tanstack/react-start/server-entry`.
+
+- `src/todos-do.ts` — `TodosDO` (`todos` table + insert/update/delete
+ mutations, `defineSync` object-schema API), seeded with three rows on first
+ create. `readSyncSnapshot` comes with `SyncDurableObject`.
+- `src/lib/todos.ts` — ONE shared collection descriptor:
+ `collectionOptions("todos", (client) => doCollectionOptions({...}))`. The
+ factory runs once per DbClient and pulls that environment's transport out of
+ the client's dependency bag. The descriptor id matches the table name
+ (`todos`) everywhere — that match routes dehydrated rows back into the
+ collection on hydrate.
+- `src/router.tsx` — the wiring (ADR-0011 D2 seam, released form). `getRouter()`
+ runs per request on the server and once per tab in the browser; it builds a
+ `DbClient` carrying the transport dependency — `SsrSnapshotTransport` over
+ `stub.readSyncSnapshot(req, getRequest())` on the server (the incoming
+ Request goes through `parseAttachment`, the SAME auth gate as the WS
+ upgrade), `WebSocketTransport` to `/sync/main` in the browser — and hands
+ both to `routerWithDbClient`. No manual DbProvider, HydrationBoundary, or
+ serverFn: the adapter dehydrates the server client into the router stream
+ and hydrates the browser client from it.
+- `src/routes/live-query.tsx`, `src/routes/live-suspense-query.tsx` — the two
+ consumers. Reads go through the descriptor in `from`; imperative writes
+ through `useDbClient().collection(todosCollection)`.
+
+The vite config dedupes `@tanstack/db`: the `file:` link would otherwise
+resolve the library's peer import from the repo root's node_modules — a second
+physical copy, which breaks the Symbol-branded `collectionOptions`.
+
+The library's own `tests/ssr-*.test.ts` pin the dehydrate → hydrate → converge
+contract; this example is the in-framework showcase.
diff --git a/examples/ssr/package-lock.json b/examples/ssr/package-lock.json
new file mode 100644
index 0000000..b4e2a11
--- /dev/null
+++ b/examples/ssr/package-lock.json
@@ -0,0 +1,4591 @@
+{
+ "name": "tanstack-do-db-ssr-example",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "tanstack-do-db-ssr-example",
+ "dependencies": {
+ "@tanstack/db": "^0.8.5",
+ "@tanstack/react-db": "^0.3.5",
+ "@tanstack/react-router": "^1.170.0",
+ "@tanstack/react-router-with-db": "^0.1.0",
+ "@tanstack/react-start": "^1.168.0",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "tanstack-durable-object-sync": "file:../.."
+ },
+ "devDependencies": {
+ "@cloudflare/vite-plugin": "^1.40.0",
+ "@cloudflare/workers-types": "^5.20260825.1",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "@vitejs/plugin-react": "^5.1.0",
+ "typescript": "^5.9",
+ "vite": "^7.3.0",
+ "wrangler": "^4"
+ }
+ },
+ "../..": {
+ "version": "0.6.0",
+ "license": "MIT",
+ "dependencies": {
+ "@msgpack/msgpack": "^3.0.0"
+ },
+ "devDependencies": {
+ "@cloudflare/vitest-pool-workers": "0.18.7",
+ "@cloudflare/workers-types": "^4.20260518.1",
+ "@tanstack/db": "^0.8.5",
+ "typescript": "^5.7",
+ "vitest": "4.1.10",
+ "wrangler": "^4"
+ },
+ "peerDependencies": {
+ "@tanstack/db": ">=0.8.5"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
+ "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.8",
+ "@babel/types": "^7.29.8",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
+ "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.8"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
+ "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
+ "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template/node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
+ "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.8",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.8",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.8",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
+ "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@cloudflare/kv-asset-handler": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz",
+ "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==",
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/@cloudflare/unenv-preset": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz",
+ "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==",
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "peerDependencies": {
+ "unenv": "2.0.0-rc.24",
+ "workerd": ">1.20260305.0 <2.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "workerd": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@cloudflare/vite-plugin": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/vite-plugin/-/vite-plugin-1.54.0.tgz",
+ "integrity": "sha512-9jQEA7t4QvjsLbdBPwtTquLMFi5XRy16/6CBJ0BbOgWqx3vPWv0gpwD5attTsbbQq1bL52cTKZmZLqgZCPSViQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cloudflare/unenv-preset": "2.16.1",
+ "miniflare": "5.20260825.0-alpha",
+ "unenv": "2.0.0-rc.24",
+ "workerd": "1.20260825.1",
+ "wrangler": "4.126.0",
+ "ws": "8.21.0"
+ },
+ "bin": {
+ "cf-vite": "bin/cf-vite"
+ },
+ "peerDependencies": {
+ "vite": "^6.1.0 || ^7.0.0 || ^8.0.0",
+ "wrangler": "^4.126.0"
+ }
+ },
+ "node_modules/@cloudflare/workerd-darwin-64": {
+ "version": "1.20260825.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260825.1.tgz",
+ "integrity": "sha512-oHu38dwaUuzAilyTb0QkQ1YxU/kzqzIQybCvQKAhiK1CGtQS9h0MmjIZYogv3g8cFGGY2k+Wxs0wV9hHK8z78g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@cloudflare/workerd-darwin-arm64": {
+ "version": "1.20260825.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260825.1.tgz",
+ "integrity": "sha512-ak5zh8YGEjxQQ78bVo7gzU+tcg2fSFxMIjOPZtWk56a/rIYLbGu6ECcliqnYfMUlragg68H0JuVpfdr3BR5Alw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@cloudflare/workerd-linux-64": {
+ "version": "1.20260825.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260825.1.tgz",
+ "integrity": "sha512-bNQvzz6NemWAwixDRz1fQa5T+E5lS4xpB7A/H/72ULxrjVpHmq8CGFPSbdmRp3dvgBjZTgp7wHdGLISLSVd9Gg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@cloudflare/workerd-linux-arm64": {
+ "version": "1.20260825.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260825.1.tgz",
+ "integrity": "sha512-a5E61YsnNCHHQMnmYsbVXInzeYqFqAMwm/wo16dWW4klXDr6T1bm7u1h5G7ZkxVM7+rtc69oe0yVHjDEqzpYVg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@cloudflare/workerd-windows-64": {
+ "version": "1.20260825.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260825.1.tgz",
+ "integrity": "sha512-EokzVY2suzRSzeURi2HpHnySR5mo6aF5V1klFIqFOZp2YJyXXTI8AvQgYzhlmGTZY3LNL41jjkUQunOM2OErgQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@cloudflare/workers-types": {
+ "version": "5.20260826.1",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-5.20260826.1.tgz",
+ "integrity": "sha512-hyW0QHvJYKhsMTAx1YZkbhcXBr17Brl1xZwvVExYmB5pAExJCCzAEouKNGwag2H+yBlqLay3X3kZoiJoGu2DRw==",
+ "dev": true,
+ "license": "MIT OR Apache-2.0"
+ },
+ "node_modules/@cspotcode/source-map-support": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.9"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.3",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
+ "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
+ "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
+ "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
+ "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
+ "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
+ "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
+ "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
+ "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
+ "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
+ "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
+ "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
+ "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
+ "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
+ "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
+ "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
+ "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
+ "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
+ "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
+ "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
+ "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
+ "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
+ "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
+ "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
+ "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@img/colour": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.35.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.2.tgz",
+ "integrity": "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.3.1"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.35.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.2.tgz",
+ "integrity": "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.3.1"
+ }
+ },
+ "node_modules/@img/sharp-freebsd-wasm32": {
+ "version": "0.35.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.2.tgz",
+ "integrity": "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "dependencies": {
+ "@img/sharp-wasm32": "0.35.2"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.1.tgz",
+ "integrity": "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.1.tgz",
+ "integrity": "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.1.tgz",
+ "integrity": "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.1.tgz",
+ "integrity": "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-ppc64": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.1.tgz",
+ "integrity": "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-riscv64": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.1.tgz",
+ "integrity": "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.1.tgz",
+ "integrity": "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.1.tgz",
+ "integrity": "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.1.tgz",
+ "integrity": "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.1.tgz",
+ "integrity": "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.35.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.2.tgz",
+ "integrity": "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.3.1"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.35.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.2.tgz",
+ "integrity": "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.3.1"
+ }
+ },
+ "node_modules/@img/sharp-linux-ppc64": {
+ "version": "0.35.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.2.tgz",
+ "integrity": "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-ppc64": "1.3.1"
+ }
+ },
+ "node_modules/@img/sharp-linux-riscv64": {
+ "version": "0.35.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.2.tgz",
+ "integrity": "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-riscv64": "1.3.1"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.35.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.2.tgz",
+ "integrity": "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.3.1"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.35.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.2.tgz",
+ "integrity": "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.3.1"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.35.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.2.tgz",
+ "integrity": "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.1"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.35.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.2.tgz",
+ "integrity": "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.3.1"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.35.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.2.tgz",
+ "integrity": "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==",
+ "dev": true,
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^1.11.1"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-webcontainers-wasm32": {
+ "version": "0.35.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.2.tgz",
+ "integrity": "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "@img/sharp-wasm32": "0.35.2"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-arm64": {
+ "version": "0.35.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.2.tgz",
+ "integrity": "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-ia32": {
+ "version": "0.35.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.2.tgz",
+ "integrity": "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.35.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.2.tgz",
+ "integrity": "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/lzma-linux-x64-gnu": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
+ "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^22.20 || ^24.12 || >=25"
+ }
+ },
+ "node_modules/@oozcitak/dom": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-2.0.2.tgz",
+ "integrity": "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w==",
+ "license": "MIT",
+ "dependencies": {
+ "@oozcitak/infra": "^2.0.2",
+ "@oozcitak/url": "^3.0.0",
+ "@oozcitak/util": "^10.0.0"
+ },
+ "engines": {
+ "node": ">=20.0"
+ }
+ },
+ "node_modules/@oozcitak/infra": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-2.0.2.tgz",
+ "integrity": "sha512-2g+E7hoE2dgCz/APPOEK5s3rMhJvNxSMBrP+U+j1OWsIbtSpWxxlUjq1lU8RIsFJNYv7NMlnVsCuHcUzJW+8vA==",
+ "license": "MIT",
+ "dependencies": {
+ "@oozcitak/util": "^10.0.0"
+ },
+ "engines": {
+ "node": ">=20.0"
+ }
+ },
+ "node_modules/@oozcitak/url": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-3.0.0.tgz",
+ "integrity": "sha512-ZKfET8Ak1wsLAiLWNfFkZc/BraDccuTJKR6svTYc7sVjbR+Iu0vtXdiDMY4o6jaFl5TW2TlS7jbLl4VovtAJWQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@oozcitak/infra": "^2.0.2",
+ "@oozcitak/util": "^10.0.0"
+ },
+ "engines": {
+ "node": ">=20.0"
+ }
+ },
+ "node_modules/@oozcitak/util": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-10.0.0.tgz",
+ "integrity": "sha512-hAX0pT/73190NLqBPPWSdBVGtbY6VOhWYK3qqHqtXQ1gK7kS2yz4+ivsN07hpJ6I3aeMtKP6J6npsEKOAzuTLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.0"
+ }
+ },
+ "node_modules/@poppinss/colors": {
+ "version": "4.1.6",
+ "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz",
+ "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kleur": "^4.1.5"
+ }
+ },
+ "node_modules/@poppinss/dumper": {
+ "version": "0.6.5",
+ "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz",
+ "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@poppinss/colors": "^4.1.5",
+ "@sindresorhus/is": "^7.0.2",
+ "supports-color": "^10.0.0"
+ }
+ },
+ "node_modules/@poppinss/exception": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz",
+ "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
+ "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.0.tgz",
+ "integrity": "sha512-70TeIFezKKy65LgAVyQh+w94/gjWhvPWaLaGGeMEgVrPkQhuj/M5bAYYZzIFUj9Y69oHyTm5Um/R6gcLh4A8JA==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.0.tgz",
+ "integrity": "sha512-YC86tYIHK6M1IV+wbzO+Bxk8RCBr6ZyWYgWxUCzaZD8mc8rrFoIJDNzDrkHBYRc/wKdrsIXmm6/F7NzrAO+OrA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.0.tgz",
+ "integrity": "sha512-oI+ECtUcli0y0fi4xpW82GdPIXdTkI8G8DSjG2LRuw09fPAGykaWYH/hXxiKuTxiAjiPSTIIuYUqof5Z2hShWw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.0.tgz",
+ "integrity": "sha512-NwV+1s7TiKrMe4owHyKB/dTLD7ZJD0YEBEhIz+hvav1Cu1GReJjF+rsdNwjzENQeIAbE/CoNiaAc5Vz2h5DPAA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.0.tgz",
+ "integrity": "sha512-tWtHBTu5gOPK4u4Urtk4qAHW3zZ9rQAmbssO8gp7ELvGTGI3aCiq6NqyTQ0PCIg7KbHJF2UkGDDs77YZGxfjCA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.0.tgz",
+ "integrity": "sha512-2qPoJiwTvtHQ27NnYvTnsgk8laXWYuVmNESG8WFZBcEPKLfZ3I27qBJarjVRQtwGeYyRfq5ZowHXih9lm2BItw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.0.tgz",
+ "integrity": "sha512-FQwsTRvLNuHoTdICABJQfbPUSEueISGmnpT06tXTMpfprf5NiKLSXKA0A+w45wJnCmZAnzgqBwbt6ARFuyOi5w==",
+ "cpu": [
+ "arm"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.0.tgz",
+ "integrity": "sha512-BBVTXziw8mY1a4ZbWME9tZyfzqXCDPqaC7Z3heQ29p5dkvXzwL0NwelO8zLa8c3RBKvl3YTuSnBgsBhYBtwjIw==",
+ "cpu": [
+ "arm"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.0.tgz",
+ "integrity": "sha512-w2Iyy9+RqKwx3d9qWMKsJg0FfRBsY0/pXNv0mCQ3ueRvJI6+QAScfD4nrMlzFLs2HNVW6Ew+mtZfDl9b7Ew5/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.0.tgz",
+ "integrity": "sha512-YK++KtrFRHYE0P6/RtYEAy9t8F37znP+K03RrIuLPYOL6SVlObRumf/0OE4V/h63xL9DwkWbNssZfmA9hawuDA==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.0.tgz",
+ "integrity": "sha512-aBfOG6fP7YkkPmTqPwufRJeFyz7WPpECv9XNbnsk9+vg7rxdih0lbtEel7jcRng4LZrrmU3FfitCFyEj4BWDWg==",
+ "cpu": [
+ "loong64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.0.tgz",
+ "integrity": "sha512-LGaHEOeHNAag9VuS1Crs5DFg4RrU9MPi2nVnNJk9DTePx/B6RRYKVmrIXt2h7YOJlwjaFJ6lwtFDliZxScTLrQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.0.tgz",
+ "integrity": "sha512-jClvk+J0FC3b7Udvegiw5/4hErbHtmsNsQgENnKXDWtNCJXsJYZH5WURvu7imDOO38xYml24eeh5x3A04ppwCw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.0.tgz",
+ "integrity": "sha512-0OJlaGK+8+B777Ql5okIpD7ua5Ro9+VB9Ve0OKa28OQJZ1RbuUBVNHK/e3pr4BROqsyPl1JrPO1ZxJseCNffcA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.0.tgz",
+ "integrity": "sha512-Ygsx+HoNH7afwi1bTIXbnTvVnsO+zurPLSYxybV1hHFVU72OWOCl6v05ql/z0hkpAPx+DK7Kn9Bi7MayCcjLTA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.0.tgz",
+ "integrity": "sha512-pDQxtMGb+OvG3fLwR2OkZlSd47hW+kWg4BYMG/++sR6RqorQccwPTDsxda5hPwiIeIErAnCF9ma3SAU06bdQtQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.0.tgz",
+ "integrity": "sha512-0BnUG9mS8I4SSHr3XsxVhuCMEiu+rX61xxZF5vujso4LaiAGFZFxvDjg6Xn6tLPNTUAfuCvQYas4LMQMVsKRSQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.0.tgz",
+ "integrity": "sha512-Adu/VttB1dpPNW+FEacrZ+xVm9tFty84+RrFzsqlFaPxoJB+9XXyDGtp5dCOoBwGBIEVH0To7lExFXEx0BIF4A==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.0.tgz",
+ "integrity": "sha512-NQ3bDvjUbFKmP23671xUlXtKmqVsUBd6M4PQCvbmNtOy06hnQIdKHy8oG/6S3R/S6He1JgPk6A5VT+prAJMYEw==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.0.tgz",
+ "integrity": "sha512-u2eDAl4+0aFvA13GxlGBtTI3SS3sdgwgtV0HyjZ0QaQVCgNE+jqNGey+GtxWiq+wxr/UycAx/OnfJzApCFamvA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.0.tgz",
+ "integrity": "sha512-XvRb5vfW3wAZQ+ZUG21AnHHDKtNcw99eigzEhjr//NZ3u7SoBaPP0seSc7FgP7p1epAEdAoZckMW9WY/+4w70w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.0.tgz",
+ "integrity": "sha512-iZPmniy4kNBf5yo2RezbkYNNK5HPbXE9+g+twnbqSng7dtLEJy1SKoxiE/ni4FDacjyuZpEeb9U054N4EoKHYw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.0.tgz",
+ "integrity": "sha512-mFBBd+LF37fnE8JnYUOH+imj0aPFPK30vpar4ehJkgnLj9sZn8ZxiRENmLtgIwxK7TC8klF6N57fxdNBwQoqOA==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.0.tgz",
+ "integrity": "sha512-ujeqEY3B+zbGn3Z4Q03cUBG/LGWnBJncVT36WER31LcOsQk9+1dmINKKtvmmfChUvRbK1G0R8OhMWFgHgaZtAw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.0.tgz",
+ "integrity": "sha512-hncn90N4sOky0L2LKE5oESKLbxCPeVo4eLA2LSMoDzM+879ml4WSr+Rr4DWknNIVVvS1Hirkc9hx02W6YxS8rQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@sindresorhus/is": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz",
+ "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/is?sponsor=1"
+ }
+ },
+ "node_modules/@speed-highlight/core": {
+ "version": "1.2.24",
+ "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.24.tgz",
+ "integrity": "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "license": "MIT"
+ },
+ "node_modules/@tanstack/db": {
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/@tanstack/db/-/db-0.8.5.tgz",
+ "integrity": "sha512-0bzsEWb9B0f6ADQjW/W1KCS/f8HBFLPz/Wqq52yqSjmvyKDAEvfOzaKdMXF38j3eR/oigwKj4qmw3Soni+BLNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@tanstack/db-ivm": "0.1.19",
+ "@tanstack/pacer-lite": "^0.2.1"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.7"
+ }
+ },
+ "node_modules/@tanstack/db-ivm": {
+ "version": "0.1.19",
+ "resolved": "https://registry.npmjs.org/@tanstack/db-ivm/-/db-ivm-0.1.19.tgz",
+ "integrity": "sha512-3VGvgNXAPSVqny/pL8Y6n/txnaZzV49WmgSQ9N5eyCQNgMtuOEOJHKDatqqCy3gDxVu6pkhSHDKxcNFOtxMouw==",
+ "license": "MIT",
+ "dependencies": {
+ "fractional-indexing": "^3.2.0",
+ "sorted-btree": "^1.8.1"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.7"
+ }
+ },
+ "node_modules/@tanstack/history": {
+ "version": "1.162.1",
+ "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.162.1.tgz",
+ "integrity": "sha512-DR9t6lfLVdrjgCwpglrR9DR7Ok8/HlXjcOE+goWXF3zyuLUO/ug7vMbSFxTqrQTtbRghJfyhmIZ0S6LhPIy44w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/pacer-lite": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/pacer-lite/-/pacer-lite-0.2.2.tgz",
+ "integrity": "sha512-eQ1MyLKCHyXiH7NbdmB80W77OhiMgGBUb+qDx/8WMGbwg5Lf/NlfD0TfNYAqY77i8V3AxoDoYdICrQE5ADw4Yw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-db": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-db/-/react-db-0.3.5.tgz",
+ "integrity": "sha512-O5zjaaZRWNYF+j0S3nidUsp2jSO0ib5MiqH65a72pJ8OAB+sLRMN15GkoY/UEKWJhTAVCjm33yIQ4bb7Oc7h/Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/db": "0.8.5",
+ "use-sync-external-store": "^1.6.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@tanstack/react-router": {
+ "version": "1.170.32",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.170.32.tgz",
+ "integrity": "sha512-SIpxvaTKco100a5ZR3ePmArbhtm3XOx+w1dpGYY9gxHDta4iXSKDdQuhLonwJbIMkVJsU1rwXf0UDHMrF/1snw==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/history": "1.162.1",
+ "@tanstack/react-store": "^0.9.3",
+ "@tanstack/router-core": "1.171.27",
+ "isbot": "^5.1.22"
+ },
+ "engines": {
+ "node": ">=20.19"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": ">=18.0.0 || >=19.0.0",
+ "react-dom": ">=18.0.0 || >=19.0.0"
+ }
+ },
+ "node_modules/@tanstack/react-router-with-db": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-router-with-db/-/react-router-with-db-0.1.0.tgz",
+ "integrity": "sha512-yZdsrkW3//J1TZ7m/K8tr8B5K8lQU/2zazhlJ9ywVL+wW9+nsFkhRaeqwGK/LagRMP6rKpQx4jy5QSY4mcFh+g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@tanstack/react-db": ">=0.2.1",
+ "@tanstack/react-router": ">=1.43.2",
+ "@tanstack/router-core": ">=1.127.0",
+ "react": ">=18.0.0",
+ "react-dom": ">=18.0.0"
+ }
+ },
+ "node_modules/@tanstack/react-start": {
+ "version": "1.168.49",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-start/-/react-start-1.168.49.tgz",
+ "integrity": "sha512-iQb1ZoEHqvZMGLR4G7v3tTtgL06yv/bwxvGP3waVHxVn7bRpyopM44YbOluaGkcJzc13ZTvdLKWYNAN3bxMX/Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/react-router": "1.170.32",
+ "@tanstack/react-start-client": "1.168.30",
+ "@tanstack/react-start-rsc": "0.1.48",
+ "@tanstack/react-start-server": "1.167.37",
+ "@tanstack/router-utils": "1.162.2",
+ "@tanstack/start-client-core": "1.170.27",
+ "@tanstack/start-plugin-core": "1.171.39",
+ "@tanstack/start-server-core": "1.169.31",
+ "pathe": "^2.0.3"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "@rsbuild/core": "^2.0.0",
+ "react": ">=18.0.0 || >=19.0.0",
+ "react-dom": ">=18.0.0 || >=19.0.0",
+ "vite": ">=7.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rsbuild/core": {
+ "optional": true
+ },
+ "@vitejs/plugin-rsc": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@tanstack/react-start-client": {
+ "version": "1.168.30",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-start-client/-/react-start-client-1.168.30.tgz",
+ "integrity": "sha512-qsZuykUl1EF0/rc1bin1RtjFzz07YMOTBzhOstSDzbOVm/WKf1QKFTN+qAZi74xlbXSWNuT2MiFRyg4RKwj8iw==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/react-router": "1.170.32",
+ "@tanstack/router-core": "1.171.27",
+ "@tanstack/start-client-core": "1.170.27"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": ">=18.0.0 || >=19.0.0",
+ "react-dom": ">=18.0.0 || >=19.0.0"
+ }
+ },
+ "node_modules/@tanstack/react-start-rsc": {
+ "version": "0.1.48",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-start-rsc/-/react-start-rsc-0.1.48.tgz",
+ "integrity": "sha512-UglRdTMuF3c4dvzL/gh4dMVbMWHsPy8ZgTQdT2qpTlyt1b/3m+R40tBFdsfTgw76VTXivW+qV/ih0PN9000XTw==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/react-router": "1.170.32",
+ "@tanstack/router-core": "1.171.27",
+ "@tanstack/router-utils": "1.162.2",
+ "@tanstack/start-client-core": "1.170.27",
+ "@tanstack/start-fn-stubs": "1.162.0",
+ "@tanstack/start-plugin-core": "1.171.39",
+ "@tanstack/start-storage-context": "1.167.29",
+ "pathe": "^2.0.3"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "@rspack/core": ">=2.0.0-0",
+ "@vitejs/plugin-rsc": ">=0.5.30",
+ "react": ">=18.0.0 || >=19.0.0",
+ "react-dom": ">=18.0.0 || >=19.0.0",
+ "react-server-dom-rspack": ">=0.0.2"
+ },
+ "peerDependenciesMeta": {
+ "@rspack/core": {
+ "optional": true
+ },
+ "@vitejs/plugin-rsc": {
+ "optional": true
+ },
+ "react-server-dom-rspack": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@tanstack/react-start-server": {
+ "version": "1.167.37",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-start-server/-/react-start-server-1.167.37.tgz",
+ "integrity": "sha512-cODHpFU8vIm7AdHii9W3NEuwyruNmT5wLDZjRsJLT9Jp+7FACnfJrRbvxnp1ldSv/9mxcHKi/OgwbdHQeMZcAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/react-router": "1.170.32",
+ "@tanstack/router-core": "1.171.27",
+ "@tanstack/start-server-core": "1.169.31"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": ">=18.0.0 || >=19.0.0",
+ "react-dom": ">=18.0.0 || >=19.0.0"
+ }
+ },
+ "node_modules/@tanstack/react-store": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.9.3.tgz",
+ "integrity": "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/store": "0.9.3",
+ "use-sync-external-store": "^1.6.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/@tanstack/router-core": {
+ "version": "1.171.27",
+ "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.171.27.tgz",
+ "integrity": "sha512-wDwSLvoLwIaNcnx9UNcN9Mb7Y8QwCYq1U1RQZwyN186gnkIoIYI2SOxy8VqH1vFigbkHkk4FmwMAQlghPgDK2g==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/history": "1.162.1",
+ "cookie-es": "^3.0.0",
+ "seroval": "^1.6.2",
+ "seroval-plugins": "^1.6.2"
+ },
+ "engines": {
+ "node": ">=20.19"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/router-generator": {
+ "version": "1.167.33",
+ "resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.167.33.tgz",
+ "integrity": "sha512-Z3lCWIPuRUMPmuI8Mm48x/s49TxmHOaFVZ52j1W1QKYrsFHyT6U/h9bqfHJDxfQ8kz7y9q+W1YPKZ15Ee7yuCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.5",
+ "@tanstack/router-core": "1.171.27",
+ "@tanstack/router-utils": "1.162.2",
+ "@tanstack/virtual-file-routes": "1.162.0",
+ "jiti": "^2.7.0",
+ "magic-string": "^0.30.21",
+ "prettier": "^3.5.0",
+ "zod": "^4.4.3"
+ },
+ "engines": {
+ "node": ">=20.19"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/router-plugin": {
+ "version": "1.168.35",
+ "resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.168.35.tgz",
+ "integrity": "sha512-foDAZKFqHXae+oFbIgcsSvy2QCVRn7XdS3nhwcRvD+ed6JrKPUP/1lQMsZLJqWycgR1vkZF7gs955KGa0NZQ0w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.5",
+ "@tanstack/router-core": "1.171.27",
+ "@tanstack/router-generator": "1.167.33",
+ "@tanstack/router-utils": "1.162.2",
+ "chokidar": "^5.0.0",
+ "unplugin": "^3.0.0",
+ "zod": "^4.4.3"
+ },
+ "engines": {
+ "node": ">=20.19"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "@rsbuild/core": ">=1.0.2 || ^2.0.0",
+ "@tanstack/react-router": "^1.170.32",
+ "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0",
+ "vite-plugin-solid": "^2.11.10 || ^3.0.0-0",
+ "webpack": ">=5.92.0"
+ },
+ "peerDependenciesMeta": {
+ "@rsbuild/core": {
+ "optional": true
+ },
+ "@tanstack/react-router": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ },
+ "vite-plugin-solid": {
+ "optional": true
+ },
+ "webpack": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@tanstack/router-utils": {
+ "version": "1.162.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.162.2.tgz",
+ "integrity": "sha512-hTWqJtqIFFdvuCl8WXNyrodp2L9zo2G37xKRrcVmVRWpAB2h+U1LuRAfS4tsFTiWOIoE/B+WDVFB8JpoEdw6jQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/generator": "^7.28.5",
+ "@babel/parser": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "ansis": "^4.1.0",
+ "babel-dead-code-elimination": "^1.0.12",
+ "diff": "^8.0.2",
+ "pathe": "^2.0.3",
+ "tinyglobby": "^0.2.15"
+ },
+ "engines": {
+ "node": ">=20.19"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/start-client-core": {
+ "version": "1.170.27",
+ "resolved": "https://registry.npmjs.org/@tanstack/start-client-core/-/start-client-core-1.170.27.tgz",
+ "integrity": "sha512-Ro6ZSM0NgYKDMxM0e8qyU4mBfnld2Zb74BA/9f4i35C0Y3IAA8Zxs/DIPfOo9VRYWp5c5n5Q8dRBn2qn0vtPhQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/router-core": "1.171.27",
+ "@tanstack/start-fn-stubs": "1.162.0",
+ "@tanstack/start-storage-context": "1.167.29",
+ "seroval": "^1.6.2"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/start-fn-stubs": {
+ "version": "1.162.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/start-fn-stubs/-/start-fn-stubs-1.162.0.tgz",
+ "integrity": "sha512-QWfUZ3Yo923tdQn38LyKMU8rcTw69zc+T4dAvgTWV4O56SqFRsGfS0lSWIMhJRwXIx/bvdi7nTUBDdZtTHtpTQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=22.12.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/start-plugin-core": {
+ "version": "1.171.39",
+ "resolved": "https://registry.npmjs.org/@tanstack/start-plugin-core/-/start-plugin-core-1.171.39.tgz",
+ "integrity": "sha512-Zyj6G4MDFLXcHYhPavhewCBo8dsxi3qvPk31zl/QtTzYzOY9rJHDDBGarKsJq20ziChmkGn/sttYqb4vGCxJDA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "7.27.1",
+ "@babel/core": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@tanstack/router-core": "1.171.27",
+ "@tanstack/router-generator": "1.167.33",
+ "@tanstack/router-plugin": "1.168.35",
+ "@tanstack/router-utils": "1.162.2",
+ "@tanstack/start-server-core": "1.169.31",
+ "exsolve": "^1.0.7",
+ "lightningcss": "^1.32.0",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "seroval": "^1.6.2",
+ "source-map": "^0.7.6",
+ "srvx": "^0.11.9",
+ "tinyglobby": "^0.2.15",
+ "ufo": "^1.5.4",
+ "vitefu": "^1.1.1",
+ "xmlbuilder2": "^4.0.3",
+ "zod": "^4.4.3"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "@rsbuild/core": "^2.0.0",
+ "vite": ">=7.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rsbuild/core": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@tanstack/start-server-core": {
+ "version": "1.169.31",
+ "resolved": "https://registry.npmjs.org/@tanstack/start-server-core/-/start-server-core-1.169.31.tgz",
+ "integrity": "sha512-56w8l+Fao01YCrmv0hzNxL3b3FRmqLRGdE11izZNQsW+1CcSYuehAM5khWKABuI1DI/UIBLGV7BwL4Dlg0eHCw==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/history": "1.162.1",
+ "@tanstack/router-core": "1.171.27",
+ "@tanstack/start-client-core": "1.170.27",
+ "@tanstack/start-storage-context": "1.167.29",
+ "fetchdts": "^0.1.6",
+ "h3-v2": "npm:h3@2.0.1-rc.20",
+ "seroval": "^1.6.2"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/start-storage-context": {
+ "version": "1.167.29",
+ "resolved": "https://registry.npmjs.org/@tanstack/start-storage-context/-/start-storage-context-1.167.29.tgz",
+ "integrity": "sha512-8qfprC5774XMRDQlMogkfiGpFLiBf0xDG4bMFUbfkSzpCAQwpLbgGY4Zwft22O9rKYq2vUXusKAdVjvJTUEquQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/router-core": "1.171.27"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/store": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.9.3.tgz",
+ "integrity": "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/virtual-file-routes": {
+ "version": "1.162.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.162.0.tgz",
+ "integrity": "sha512-uhOeFyxLcU41HzvrxsGpiWdcMbScY1EDgbZ5K7DVRMYInbLYWAC0EA/kx9wXAoSM8q82bUG2hRl8+EAjE6XAbA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.19"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.18",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
+ "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "19.2.5",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.5.tgz",
+ "integrity": "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^19.2.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
+ "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.29.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-rc.3",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.18.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ansis": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.3.1.tgz",
+ "integrity": "sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0"
+ },
+ "node_modules/babel-dead-code-elimination": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.12.tgz",
+ "integrity": "sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.23.7",
+ "@babel/parser": "^7.23.6",
+ "@babel/traverse": "^7.23.7",
+ "@babel/types": "^7.23.6"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.11.19",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.19.tgz",
+ "integrity": "sha512-Grytf1xOxOEMTGRwx6rLGKkTabd4vMg3VrKdj/7joCmV0qgh4QwMMO6xh34YEXQqirAuUdgQGa5orJQQ+69RBw==",
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/blake3-wasm": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",
+ "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.8",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
+ "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.11.12",
+ "caniuse-lite": "^1.0.30001809",
+ "electron-to-chromium": "^1.5.402",
+ "node-releases": "^2.0.53",
+ "update-browserslist-db": "^1.3.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001810",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
+ "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chokidar": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
+ "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^5.0.0"
+ },
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cookie-es": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz",
+ "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==",
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/diff": {
+ "version": "8.0.4",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz",
+ "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.415",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.415.tgz",
+ "integrity": "sha512-958V+Kbhtgz+SxXeEVKBjrlKRBIDAYvUJfwhjxMZ5S6ut9jAl7l9ZKBkBrvjyjZE36PabLUo2L8kEeV5O4vgJg==",
+ "license": "ISC"
+ },
+ "node_modules/error-stack-parser-es": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz",
+ "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.28.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
+ "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
+ "devOptional": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.2",
+ "@esbuild/android-arm": "0.28.2",
+ "@esbuild/android-arm64": "0.28.2",
+ "@esbuild/android-x64": "0.28.2",
+ "@esbuild/darwin-arm64": "0.28.2",
+ "@esbuild/darwin-x64": "0.28.2",
+ "@esbuild/freebsd-arm64": "0.28.2",
+ "@esbuild/freebsd-x64": "0.28.2",
+ "@esbuild/linux-arm": "0.28.2",
+ "@esbuild/linux-arm64": "0.28.2",
+ "@esbuild/linux-ia32": "0.28.2",
+ "@esbuild/linux-loong64": "0.28.2",
+ "@esbuild/linux-mips64el": "0.28.2",
+ "@esbuild/linux-ppc64": "0.28.2",
+ "@esbuild/linux-riscv64": "0.28.2",
+ "@esbuild/linux-s390x": "0.28.2",
+ "@esbuild/linux-x64": "0.28.2",
+ "@esbuild/netbsd-arm64": "0.28.2",
+ "@esbuild/netbsd-x64": "0.28.2",
+ "@esbuild/openbsd-arm64": "0.28.2",
+ "@esbuild/openbsd-x64": "0.28.2",
+ "@esbuild/openharmony-arm64": "0.28.2",
+ "@esbuild/sunos-x64": "0.28.2",
+ "@esbuild/win32-arm64": "0.28.2",
+ "@esbuild/win32-ia32": "0.28.2",
+ "@esbuild/win32-x64": "0.28.2"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/exsolve": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz",
+ "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==",
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fetchdts": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/fetchdts/-/fetchdts-0.1.7.tgz",
+ "integrity": "sha512-YoZjBdafyLIop9lSxXVI33oLD5kN31q4Td+CasofLLYeLXRFeOsuOw0Uo+XNRi9PZlbfdlN2GmRtm4tCEQ9/KA==",
+ "license": "MIT"
+ },
+ "node_modules/fractional-indexing": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/fractional-indexing/-/fractional-indexing-3.4.0.tgz",
+ "integrity": "sha512-8J3glhz2rrpKG6KmI7wmJo3zH1VjeOpN+vTJSw1fOyO+Viqq3zX6/5NGh6oaZB2qIAYdOYuu5Dz9xp4faOO0Pg==",
+ "license": "CC0-1.0",
+ "engines": {
+ "node": "^14.13.1 || >=16.0.0"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/h3-v2": {
+ "name": "h3",
+ "version": "2.0.1-rc.20",
+ "resolved": "https://registry.npmjs.org/h3/-/h3-2.0.1-rc.20.tgz",
+ "integrity": "sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg==",
+ "license": "MIT",
+ "dependencies": {
+ "rou3": "^0.8.1",
+ "srvx": "^0.11.13"
+ },
+ "bin": {
+ "h3": "bin/h3.mjs"
+ },
+ "engines": {
+ "node": ">=20.11.1"
+ },
+ "peerDependencies": {
+ "crossws": "^0.4.1"
+ },
+ "peerDependenciesMeta": {
+ "crossws": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/isbot": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.2.1.tgz",
+ "integrity": "sha512-dJ+LpKyClQZ7NG+j3OensC/mAZkGpukE9YUrgPYvAZj2doVL0edfDgywTUh5CXa0o+nW9a1V9e5+CJTX8+SxRw==",
+ "license": "Unlicense",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz",
+ "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
+ "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/kleur": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+ "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.33.0",
+ "lightningcss-darwin-arm64": "1.33.0",
+ "lightningcss-darwin-x64": "1.33.0",
+ "lightningcss-freebsd-x64": "1.33.0",
+ "lightningcss-linux-arm-gnueabihf": "1.33.0",
+ "lightningcss-linux-arm64-gnu": "1.33.0",
+ "lightningcss-linux-arm64-musl": "1.33.0",
+ "lightningcss-linux-x64-gnu": "1.33.0",
+ "lightningcss-linux-x64-musl": "1.33.0",
+ "lightningcss-win32-arm64-msvc": "1.33.0",
+ "lightningcss-win32-x64-msvc": "1.33.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+ "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+ "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+ "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+ "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+ "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+ "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+ "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+ "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+ "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+ "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+ "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/miniflare": {
+ "version": "5.20260825.0-alpha",
+ "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260825.0-alpha.tgz",
+ "integrity": "sha512-ZwlF6LuX43ilx9EwMRDKHenoGXiNdcKSyGx5aPaJhuozujVZasb2lRR7t3ojJZSnVzwDPsBBYCu8lLnc+/KIgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cspotcode/source-map-support": "0.8.1",
+ "sharp": "0.35.2",
+ "undici": "7.29.0",
+ "workerd": "1.20260825.1",
+ "ws": "8.21.0",
+ "youch": "4.1.0-beta.10"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.18",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+ "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
+ "devOptional": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.53",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz",
+ "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
+ "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.26",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
+ "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
+ "devOptional": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.17",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.9.6",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz",
+ "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==",
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.8",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
+ "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.8",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
+ "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.8"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
+ "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz",
+ "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.63.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.0.tgz",
+ "integrity": "sha512-T5vnZ2y4QqC3/4P+w2+JO+Q/OVdnPsv4XcSYJYMEn0R9/jjl5AgLwO9LAZMzP2lN71O6pypn91rB7lDstUkfrQ==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@napi-rs/lzma-linux-x64-gnu": "1.5.1",
+ "@rollup/rollup-android-arm-eabi": "4.63.0",
+ "@rollup/rollup-android-arm64": "4.63.0",
+ "@rollup/rollup-darwin-arm64": "4.63.0",
+ "@rollup/rollup-darwin-x64": "4.63.0",
+ "@rollup/rollup-freebsd-arm64": "4.63.0",
+ "@rollup/rollup-freebsd-x64": "4.63.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.63.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.63.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.63.0",
+ "@rollup/rollup-linux-arm64-musl": "4.63.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.63.0",
+ "@rollup/rollup-linux-loong64-musl": "4.63.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.63.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.63.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.63.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.63.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.63.0",
+ "@rollup/rollup-linux-x64-gnu": "4.63.0",
+ "@rollup/rollup-linux-x64-musl": "4.63.0",
+ "@rollup/rollup-openbsd-x64": "4.63.0",
+ "@rollup/rollup-openharmony-arm64": "4.63.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.63.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.63.0",
+ "@rollup/rollup-win32-x64-gnu": "4.63.0",
+ "@rollup/rollup-win32-x64-msvc": "4.63.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/rou3": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.8.1.tgz",
+ "integrity": "sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA==",
+ "license": "MIT"
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/seroval": {
+ "version": "1.6.4",
+ "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.6.4.tgz",
+ "integrity": "sha512-LErWMNS2RRFdu2RMA5u/PA59/IWs0XsikyEXGQ2/36iEWFrdG0ABmg17E17cikrv76891kOAMq3TkTFXpwAHXw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/seroval-plugins": {
+ "version": "1.6.4",
+ "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.6.4.tgz",
+ "integrity": "sha512-R0f1U9hmn38+dFMz6b6ab8lwucmw4AtiY7St+JPWudy1dm+Bs3g884nyrsH9Cy6rKpZKLYayXuMda9GZ/fl8JQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "seroval": "^1.0"
+ }
+ },
+ "node_modules/sharp": {
+ "version": "0.35.2",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.2.tgz",
+ "integrity": "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@img/colour": "^1.1.0",
+ "detect-libc": "^2.1.2",
+ "semver": "^7.8.4"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.35.2",
+ "@img/sharp-darwin-x64": "0.35.2",
+ "@img/sharp-freebsd-wasm32": "0.35.2",
+ "@img/sharp-libvips-darwin-arm64": "1.3.1",
+ "@img/sharp-libvips-darwin-x64": "1.3.1",
+ "@img/sharp-libvips-linux-arm": "1.3.1",
+ "@img/sharp-libvips-linux-arm64": "1.3.1",
+ "@img/sharp-libvips-linux-ppc64": "1.3.1",
+ "@img/sharp-libvips-linux-riscv64": "1.3.1",
+ "@img/sharp-libvips-linux-s390x": "1.3.1",
+ "@img/sharp-libvips-linux-x64": "1.3.1",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.1",
+ "@img/sharp-libvips-linuxmusl-x64": "1.3.1",
+ "@img/sharp-linux-arm": "0.35.2",
+ "@img/sharp-linux-arm64": "0.35.2",
+ "@img/sharp-linux-ppc64": "0.35.2",
+ "@img/sharp-linux-riscv64": "0.35.2",
+ "@img/sharp-linux-s390x": "0.35.2",
+ "@img/sharp-linux-x64": "0.35.2",
+ "@img/sharp-linuxmusl-arm64": "0.35.2",
+ "@img/sharp-linuxmusl-x64": "0.35.2",
+ "@img/sharp-webcontainers-wasm32": "0.35.2",
+ "@img/sharp-win32-arm64": "0.35.2",
+ "@img/sharp-win32-ia32": "0.35.2",
+ "@img/sharp-win32-x64": "0.35.2"
+ }
+ },
+ "node_modules/sharp/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sorted-btree": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/sorted-btree/-/sorted-btree-1.8.1.tgz",
+ "integrity": "sha512-395+XIP+wqNn3USkFSrNz7G3Ss/MXlZEqesxvzCRFwL14h6e8LukDHdLBePn5pwbm5OQ9vGu8mDyz2lLDIqamQ==",
+ "license": "MIT"
+ },
+ "node_modules/source-map": {
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
+ "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "devOptional": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/srvx": {
+ "version": "0.11.22",
+ "resolved": "https://registry.npmjs.org/srvx/-/srvx-0.11.22.tgz",
+ "integrity": "sha512-LqZxxBDMKuMAZzFzJnDCkFOrs9MZQZr0LvHiO/SuSZVdQaXD7xQ5UWTUxheJrQPve1qk9MG2B/yttUvJxw8egQ==",
+ "license": "MIT",
+ "bin": {
+ "srvx": "bin/srvx.mjs"
+ },
+ "engines": {
+ "node": ">=20.16.0"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "10.2.2",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
+ "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/tanstack-durable-object-sync": {
+ "resolved": "../..",
+ "link": true
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/ufo": {
+ "version": "1.6.4",
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz",
+ "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==",
+ "license": "MIT"
+ },
+ "node_modules/undici": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
+ "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
+ "node_modules/unenv": {
+ "version": "2.0.0-rc.24",
+ "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz",
+ "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pathe": "^2.0.3"
+ }
+ },
+ "node_modules/unplugin": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.3.0.tgz",
+ "integrity": "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "picomatch": "^4.0.4",
+ "webpack-virtual-modules": "^0.6.2"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@farmfe/core": "*",
+ "@rspack/core": "*",
+ "bun-types-no-globals": "*",
+ "esbuild": "*",
+ "rolldown": "*",
+ "rollup": "*",
+ "unloader": "*",
+ "vite": "*",
+ "webpack": "*"
+ },
+ "peerDependenciesMeta": {
+ "@farmfe/core": {
+ "optional": true
+ },
+ "@rspack/core": {
+ "optional": true
+ },
+ "bun-types-no-globals": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "rolldown": {
+ "optional": true
+ },
+ "rollup": {
+ "optional": true
+ },
+ "unloader": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ },
+ "webpack": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz",
+ "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.3.6",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz",
+ "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitefu": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz",
+ "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==",
+ "license": "MIT",
+ "workspaces": [
+ "tests/deps/*",
+ "tests/projects/*",
+ "tests/projects/workspace/packages/*"
+ ],
+ "peerDependencies": {
+ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webpack-virtual-modules": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
+ "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
+ "license": "MIT"
+ },
+ "node_modules/workerd": {
+ "version": "1.20260825.1",
+ "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260825.1.tgz",
+ "integrity": "sha512-ccS6TEaaRxgONKawiYGnFYUVGfr2pmN1b7mrNtw0ADVhFaYsEIVoCHnQ4UjhM9EJDzuaNgAWFY82nzVrjWpuOA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "workerd": "bin/workerd"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "optionalDependencies": {
+ "@cloudflare/workerd-darwin-64": "1.20260825.1",
+ "@cloudflare/workerd-darwin-arm64": "1.20260825.1",
+ "@cloudflare/workerd-linux-64": "1.20260825.1",
+ "@cloudflare/workerd-linux-arm64": "1.20260825.1",
+ "@cloudflare/workerd-windows-64": "1.20260825.1"
+ }
+ },
+ "node_modules/wrangler": {
+ "version": "4.126.0",
+ "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.126.0.tgz",
+ "integrity": "sha512-glDq/nxaeQwue0XMIeupfwlu2jVxnFs+wjDFT71DQuURN5LsVdCwu0Af/1ep10U5xr1rKPGhHDEDkKG9nxE/dg==",
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "dependencies": {
+ "@cloudflare/kv-asset-handler": "0.5.0",
+ "@cloudflare/unenv-preset": "2.16.1",
+ "blake3-wasm": "2.1.5",
+ "esbuild": "0.28.1",
+ "miniflare": "5.20260825.0-alpha",
+ "path-to-regexp": "6.3.0",
+ "unenv": "2.0.0-rc.24",
+ "workerd": "1.20260825.1"
+ },
+ "bin": {
+ "cf-wrangler": "bin/cf-wrangler.js",
+ "wrangler": "bin/wrangler.js",
+ "wrangler2": "bin/wrangler.js"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.3"
+ },
+ "peerDependencies": {
+ "@cloudflare/workers-types": "^5.20260825.1"
+ },
+ "peerDependenciesMeta": {
+ "@cloudflare/workers-types": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/android-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/android-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/android-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/linux-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/linux-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/@esbuild/win32-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/wrangler/node_modules/esbuild": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.1",
+ "@esbuild/android-arm": "0.28.1",
+ "@esbuild/android-arm64": "0.28.1",
+ "@esbuild/android-x64": "0.28.1",
+ "@esbuild/darwin-arm64": "0.28.1",
+ "@esbuild/darwin-x64": "0.28.1",
+ "@esbuild/freebsd-arm64": "0.28.1",
+ "@esbuild/freebsd-x64": "0.28.1",
+ "@esbuild/linux-arm": "0.28.1",
+ "@esbuild/linux-arm64": "0.28.1",
+ "@esbuild/linux-ia32": "0.28.1",
+ "@esbuild/linux-loong64": "0.28.1",
+ "@esbuild/linux-mips64el": "0.28.1",
+ "@esbuild/linux-ppc64": "0.28.1",
+ "@esbuild/linux-riscv64": "0.28.1",
+ "@esbuild/linux-s390x": "0.28.1",
+ "@esbuild/linux-x64": "0.28.1",
+ "@esbuild/netbsd-arm64": "0.28.1",
+ "@esbuild/netbsd-x64": "0.28.1",
+ "@esbuild/openbsd-arm64": "0.28.1",
+ "@esbuild/openbsd-x64": "0.28.1",
+ "@esbuild/openharmony-arm64": "0.28.1",
+ "@esbuild/sunos-x64": "0.28.1",
+ "@esbuild/win32-arm64": "0.28.1",
+ "@esbuild/win32-ia32": "0.28.1",
+ "@esbuild/win32-x64": "0.28.1"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.21.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
+ "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xmlbuilder2": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-4.0.3.tgz",
+ "integrity": "sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@oozcitak/dom": "^2.0.2",
+ "@oozcitak/infra": "^2.0.2",
+ "@oozcitak/util": "^10.0.0",
+ "js-yaml": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=20.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "license": "ISC"
+ },
+ "node_modules/youch": {
+ "version": "4.1.0-beta.10",
+ "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz",
+ "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@poppinss/colors": "^4.1.5",
+ "@poppinss/dumper": "^0.6.4",
+ "@speed-highlight/core": "^1.2.7",
+ "cookie": "^1.0.2",
+ "youch-core": "^0.3.3"
+ }
+ },
+ "node_modules/youch-core": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz",
+ "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@poppinss/exception": "^1.2.2",
+ "error-stack-parser-es": "^1.0.5"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/examples/ssr/package.json b/examples/ssr/package.json
new file mode 100644
index 0000000..f21d89f
--- /dev/null
+++ b/examples/ssr/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "tanstack-do-db-ssr-example",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite dev",
+ "build": "vite build",
+ "preview": "vite preview",
+ "deploy": "npm run build && wrangler deploy",
+ "typecheck": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@tanstack/db": "^0.8.5",
+ "@tanstack/react-db": "^0.3.5",
+ "@tanstack/react-router": "^1.170.0",
+ "@tanstack/react-router-with-db": "^0.1.0",
+ "@tanstack/react-start": "^1.168.0",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "tanstack-durable-object-sync": "file:../.."
+ },
+ "devDependencies": {
+ "@cloudflare/vite-plugin": "^1.40.0",
+ "@cloudflare/workers-types": "^5.20260825.1",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "@vitejs/plugin-react": "^5.1.0",
+ "typescript": "^5.9",
+ "vite": "^7.3.0",
+ "wrangler": "^4"
+ }
+}
diff --git a/examples/ssr/src/lib/todos.ts b/examples/ssr/src/lib/todos.ts
new file mode 100644
index 0000000..dd10fc4
--- /dev/null
+++ b/examples/ssr/src/lib/todos.ts
@@ -0,0 +1,32 @@
+// One collection descriptor, two transports (ADR-0011 D2): the server render
+// reads a snapshot from the DO, the browser goes live over WebSocket. The
+// descriptor is shared — `collectionOptions(id, factory)` gives every DbClient
+// a fresh adapter config, and the factory pulls the environment's transport
+// out of the client's dependency bag (injected in router.tsx). The id matches
+// the table name ("todos") on every side — that match is what routes the
+// dehydrated rows back into this collection on hydrate.
+
+import { collectionOptions } from "@tanstack/db"
+import { doCollectionOptions } from "tanstack-durable-object-sync/client"
+import type { Transport } from "tanstack-durable-object-sync/client"
+import type { TodosApi } from "../todos-do.ts"
+
+export interface Todo {
+ id: string
+ text: string
+ /** SQLite INTEGER 0/1 — kept raw so optimistic and confirmed rows are identical. */
+ done: number
+}
+
+/** DbClient dependency key: `() => Transport` — snapshot transport on
+ * the server, WebSocket transport in the browser (see router.tsx). */
+export const TODOS_TRANSPORT = "todosTransport"
+
+export const todosCollection = collectionOptions("todos", (client) => {
+ const createTransport = client.requireDependency<() => Transport>(TODOS_TRANSPORT)
+ return doCollectionOptions({
+ transport: createTransport(),
+ table: "todos",
+ getKey: (t) => t.id,
+ })
+})
diff --git a/examples/ssr/src/routeTree.gen.ts b/examples/ssr/src/routeTree.gen.ts
new file mode 100644
index 0000000..7a4b181
--- /dev/null
+++ b/examples/ssr/src/routeTree.gen.ts
@@ -0,0 +1,104 @@
+/* eslint-disable */
+
+// @ts-nocheck
+
+// noinspection JSUnusedGlobalSymbols
+
+// This file was automatically generated by TanStack Router.
+// You should NOT make any changes in this file as it will be overwritten.
+// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
+
+import { Route as rootRouteImport } from './routes/__root'
+import { Route as IndexRouteImport } from './routes/index'
+import { Route as LiveQueryRouteImport } from './routes/live-query'
+import { Route as LiveSuspenseQueryRouteImport } from './routes/live-suspense-query'
+
+const IndexRoute = IndexRouteImport.update({
+ id: '/',
+ path: '/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const LiveQueryRoute = LiveQueryRouteImport.update({
+ id: '/live-query',
+ path: '/live-query',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const LiveSuspenseQueryRoute = LiveSuspenseQueryRouteImport.update({
+ id: '/live-suspense-query',
+ path: '/live-suspense-query',
+ getParentRoute: () => rootRouteImport,
+} as any)
+
+export interface FileRoutesByFullPath {
+ '/': typeof IndexRoute
+ '/live-query': typeof LiveQueryRoute
+ '/live-suspense-query': typeof LiveSuspenseQueryRoute
+}
+export interface FileRoutesByTo {
+ '/': typeof IndexRoute
+ '/live-query': typeof LiveQueryRoute
+ '/live-suspense-query': typeof LiveSuspenseQueryRoute
+}
+export interface FileRoutesById {
+ __root__: typeof rootRouteImport
+ '/': typeof IndexRoute
+ '/live-query': typeof LiveQueryRoute
+ '/live-suspense-query': typeof LiveSuspenseQueryRoute
+}
+export interface FileRouteTypes {
+ fileRoutesByFullPath: FileRoutesByFullPath
+ fullPaths: '/' | '/live-query' | '/live-suspense-query'
+ fileRoutesByTo: FileRoutesByTo
+ to: '/' | '/live-query' | '/live-suspense-query'
+ id: '__root__' | '/' | '/live-query' | '/live-suspense-query'
+ fileRoutesById: FileRoutesById
+}
+export interface RootRouteChildren {
+ IndexRoute: typeof IndexRoute
+ LiveQueryRoute: typeof LiveQueryRoute
+ LiveSuspenseQueryRoute: typeof LiveSuspenseQueryRoute
+}
+
+declare module '@tanstack/react-router' {
+ interface FileRoutesByPath {
+ '/': {
+ id: '/'
+ path: '/'
+ fullPath: '/'
+ preLoaderRoute: typeof IndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/live-query': {
+ id: '/live-query'
+ path: '/live-query'
+ fullPath: '/live-query'
+ preLoaderRoute: typeof LiveQueryRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/live-suspense-query': {
+ id: '/live-suspense-query'
+ path: '/live-suspense-query'
+ fullPath: '/live-suspense-query'
+ preLoaderRoute: typeof LiveSuspenseQueryRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ }
+}
+
+const rootRouteChildren: RootRouteChildren = {
+ IndexRoute: IndexRoute,
+ LiveQueryRoute: LiveQueryRoute,
+ LiveSuspenseQueryRoute: LiveSuspenseQueryRoute,
+}
+export const routeTree = rootRouteImport
+ ._addFileChildren(rootRouteChildren)
+ ._addFileTypes()
+
+import type { getRouter } from './router.tsx'
+import type { createStart } from '@tanstack/react-start'
+declare module '@tanstack/react-start' {
+ interface Register {
+ ssr: true
+ router: Awaited>
+ }
+}
diff --git a/examples/ssr/src/router.tsx b/examples/ssr/src/router.tsx
new file mode 100644
index 0000000..43a9d9e
--- /dev/null
+++ b/examples/ssr/src/router.tsx
@@ -0,0 +1,65 @@
+// The SSR seam (ADR-0011 D2), wired the released way: one DbClient per server
+// request / per browser tab, handed to the router via context and
+// `routerWithDbClient` — which wraps the app in DbProvider, dehydrates the
+// server client into the router payload (rows + our resume cursor as opaque
+// syncMeta), hydrates the browser client from it, and streams late Suspense
+// query results. The only app-specific part is the transport dependency: a
+// snapshot read against the DO on the server, a WebSocket in the browser.
+
+import { DbClient } from "@tanstack/react-db"
+import { createRouter } from "@tanstack/react-router"
+import { routerWithDbClient } from "@tanstack/react-router-with-db"
+import { createIsomorphicFn } from "@tanstack/react-start"
+import { SsrSnapshotTransport, WebSocketTransport } from "tanstack-durable-object-sync/client"
+import type { SnapshotRead, Transport } from "tanstack-durable-object-sync/client"
+import { TODOS_TRANSPORT } from "./lib/todos.ts"
+import { routeTree } from "./routeTree.gen"
+import type { Env, TodosApi } from "./todos-do.ts"
+
+export type RouterContext = {
+ dbClient: DbClient
+}
+
+const createTransport = createIsomorphicFn()
+ .server(
+ (): (() => Transport) =>
+ () =>
+ // No WebSocket from the render path: each subscribe is ONE snapshot
+ // read over DO RPC. The worker-only modules are imported lazily so
+ // they never reach the browser bundle.
+ new SsrSnapshotTransport({
+ read: async (req) => {
+ const [{ env }, { getRequest }] = await Promise.all([
+ import("cloudflare:workers"),
+ import("@tanstack/react-start/server"),
+ ])
+ const ns = (env as unknown as Env).TODOS_DO
+ const stub = ns.get(ns.idFromName("main")) as unknown as {
+ readSyncSnapshot: (r: Parameters[0], request: Request) => ReturnType
+ }
+ // The DO runs the incoming request through parseAttachment — the
+ // SAME auth gate the WS upgrade gets. This app has no auth, but the
+ // shape means an app that does can't bypass its own check via SSR.
+ return stub.readSyncSnapshot(req, getRequest())
+ },
+ }),
+ )
+ .client(
+ (): (() => Transport) =>
+ () =>
+ new WebSocketTransport({
+ url: `${location.protocol === "https:" ? "wss:" : "ws:"}//${location.host}/sync/main`,
+ }),
+ )
+
+export function getRouter() {
+ // Per request on the server, once per tab in the browser. The collection
+ // factory (lib/todos.ts) pulls the matching transport out of this bag.
+ const dbClient = new DbClient({ [TODOS_TRANSPORT]: createTransport() })
+ const router = createRouter({
+ routeTree,
+ context: { dbClient },
+ scrollRestoration: true,
+ })
+ return routerWithDbClient(router, dbClient)
+}
diff --git a/examples/ssr/src/routes/__root.tsx b/examples/ssr/src/routes/__root.tsx
new file mode 100644
index 0000000..0f5b41b
--- /dev/null
+++ b/examples/ssr/src/routes/__root.tsx
@@ -0,0 +1,51 @@
+import { createRootRouteWithContext, HeadContent, Link, Outlet, Scripts } from "@tanstack/react-router"
+import * as React from "react"
+import type { RouterContext } from "../router.tsx"
+
+export const Route = createRootRouteWithContext()({
+ head: () => ({
+ meta: [
+ { charSet: "utf-8" },
+ { name: "viewport", content: "width=device-width, initial-scale=1" },
+ { title: "tanstack-durable-object-sync SSR todos" },
+ ],
+ }),
+ shellComponent: RootDocument,
+ component: RootLayout,
+})
+
+function RootDocument({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+
+ {children}
+
+
+
+ )
+}
+
+function RootLayout() {
+ return (
+ <>
+
+
+ >
+ )
+}
diff --git a/examples/ssr/src/routes/index.tsx b/examples/ssr/src/routes/index.tsx
new file mode 100644
index 0000000..a786634
--- /dev/null
+++ b/examples/ssr/src/routes/index.tsx
@@ -0,0 +1,30 @@
+// Landing page only — no DB here. The DbClient is app-scoped (router context,
+// see router.tsx), so each showcase page exercises its own SSR path while
+// client-side navigation between them shares one collection and one socket.
+
+import { createFileRoute, Link } from "@tanstack/react-router"
+
+export const Route = createFileRoute("/")({
+ component: Landing,
+})
+
+function Landing() {
+ return (
+
+
tanstack-durable-object-sync SSR showcase
+
+ One todos collection in a Durable Object, server-rendered two ways.
+
+
+
+ useLiveQuery — loader preload, dehydrate,
+ hydrate, converge live; rows are in the raw HTML.
+
+
+ useLiveSuspenseQuery — the query
+ suspends during the server render and its result is streamed.
+
+
+
+ )
+}
diff --git a/examples/ssr/src/routes/live-query.tsx b/examples/ssr/src/routes/live-query.tsx
new file mode 100644
index 0000000..8ab81d5
--- /dev/null
+++ b/examples/ssr/src/routes/live-query.tsx
@@ -0,0 +1,87 @@
+// The loader-preload SSR path: the route loader materializes the todos
+// collection on the request's DbClient and preloads it (one snapshot read from
+// the DO). routerWithDbClient dehydrates the normalized rows PLUS the resume
+// cursor (opaque syncMeta) into the router payload and hydrates the browser
+// client from it — data is present from the first (server) render, then the
+// WebSocket resumes from the dehydrated cursor and converges: catch-up applies
+// exactly what changed while the HTML was in flight, updates AND deletes.
+// Stale-while-revalidate, never a flash of empty.
+
+import { useDbClient, useLiveQuery } from "@tanstack/react-db"
+import { createFileRoute } from "@tanstack/react-router"
+import * as React from "react"
+import { todosCollection } from "../lib/todos.ts"
+
+export const Route = createFileRoute("/live-query")({
+ loader: async ({ context }) => {
+ // Explicit collection preload → normalized rows + syncMeta dehydrate.
+ // On client-side navigation this same line just awaits the live sync.
+ await context.dbClient.collection(todosCollection).preload()
+ },
+ component: LiveQueryPage,
+})
+
+function LiveQueryPage() {
+ // Materialize through the ambient DbClient for imperative writes; the same
+ // descriptor in `from` resolves to the same collection instance.
+ const todos = useDbClient().collection(todosCollection)
+ const [hydrated, setHydrated] = React.useState(false)
+ const [text, setText] = React.useState("")
+ const { data, isReady } = useLiveQuery({
+ query: (q) => q.from({ t: todosCollection }).orderBy(({ t }) => t.id, "asc"),
+ })
+
+ React.useEffect(() => setHydrated(true), [])
+
+ const add = () => {
+ const t = text.trim()
+ if (!t) return
+ // Optimistic: appears instantly, confirmed on the single stream.
+ todos.insert({ id: crypto.randomUUID(), text: t, done: 0 })
+ setText("")
+ }
+
+ return (
+
+
+
+
+ )
+}
diff --git a/examples/ssr/src/routes/live-suspense-query.tsx b/examples/ssr/src/routes/live-suspense-query.tsx
new file mode 100644
index 0000000..1b108b4
--- /dev/null
+++ b/examples/ssr/src/routes/live-suspense-query.tsx
@@ -0,0 +1,80 @@
+// The Suspense-streaming SSR path: NO loader preload. The query is discovered
+// mid-render by useLiveSuspenseQuery; on the server the component suspends
+// while the snapshot transport reads the DO, and routerWithDbClient streams
+// the pending query result into the document (the browser shows the fallback
+// from the streamed shell, then the streamed rows, then the live WebSocket
+// result once sync converges). Toggling the filter changes the structured
+// query IR — a new query identity, a new derived collection — which re-suspends
+// in the browser until it computes.
+
+import { eq } from "@tanstack/db"
+import { useDbClient, useLiveSuspenseQuery } from "@tanstack/react-db"
+import { createFileRoute } from "@tanstack/react-router"
+import * as React from "react"
+import { todosCollection } from "../lib/todos.ts"
+
+export const Route = createFileRoute("/live-suspense-query")({
+ component: SuspensePage,
+})
+
+function SuspensePage() {
+ const [hydrated, setHydrated] = React.useState(false)
+ const [openOnly, setOpenOnly] = React.useState(false)
+
+ React.useEffect(() => setHydrated(true), [])
+
+ return (
+
+
useLiveSuspenseQuery todos
+
+ {hydrated ? "hydrated" : "ssr"}
+
+
+ loading todos…
}>
+
+
+
+ )
+}
+
+function TodoRows({ openOnly }: { openOnly: boolean }) {
+ const todos = useDbClient().collection(todosCollection)
+ // Config-object form: the derived query identity is the structured IR, so
+ // flipping `openOnly` swaps in a new live query rather than reusing rows.
+ const { data } = useLiveSuspenseQuery({
+ query: (q) => {
+ const base = q.from({ t: todosCollection })
+ const scoped = openOnly ? base.where(({ t }) => eq(t.done, 0)) : base
+ return scoped.orderBy(({ t }) => t.id, "asc")
+ },
+ })
+
+ return (
+ <>
+
+ rows: {data.length}
+
+
+ {data.map((t) => (
+
+
+
+ ))}
+
+ >
+ )
+}
diff --git a/examples/ssr/src/server.ts b/examples/ssr/src/server.ts
new file mode 100644
index 0000000..e753c74
--- /dev/null
+++ b/examples/ssr/src/server.ts
@@ -0,0 +1,22 @@
+// Custom worker entry (wrangler `main`): ONE worker serves both halves —
+// WebSocket upgrades on /sync/* go straight to the DO, everything else is the
+// TanStack Start app (SSR + assets). The Start handler never sees the upgrade,
+// so hibernation stays intact.
+
+import handler from "@tanstack/react-start/server-entry"
+import type { Env } from "./todos-do.ts"
+
+export { TodosDO } from "./todos-do.ts"
+
+export default {
+ fetch(req: Request, env: Env, ctx: ExecutionContext): Response | Promise {
+ const url = new URL(req.url)
+ if (url.pathname.startsWith("/sync/")) {
+ const room = url.pathname.slice("/sync/".length) || "main"
+ return env.TODOS_DO.get(env.TODOS_DO.idFromName(room)).fetch(req)
+ }
+ // Start's RequestHandler takes (request, opts?) — env/ctx reach server
+ // code through the `cloudflare:workers` module, not positional args.
+ return handler.fetch(req)
+ },
+} satisfies ExportedHandler
diff --git a/examples/ssr/src/todos-do.ts b/examples/ssr/src/todos-do.ts
new file mode 100644
index 0000000..1098f74
--- /dev/null
+++ b/examples/ssr/src/todos-do.ts
@@ -0,0 +1,72 @@
+// SSR example — the sync DO. One `todos` collection plus the three row
+// mutations the browser client sends, authored with the object-schema API
+// (defineSync; ADR-0014). Depends on the local package (`file:../..`), so run
+// `npm run build` at the repo root before installing here.
+
+import { defineSync, SyncDurableObject } from "tanstack-durable-object-sync"
+import type { Todo } from "./lib/todos.ts"
+
+export interface Env {
+ TODOS_DO: DurableObjectNamespace
+}
+
+const UPDATABLE = new Set(["text", "done"])
+
+const sync = defineSync()
+
+const todosSchema = sync.schema({
+ collections: {
+ todos: sync.collection({
+ pk: "id",
+ mutations: {
+ insert: {
+ execute: ({ op, sql }) => {
+ sql.exec("INSERT INTO todos(id, text, done) VALUES (?, ?, ?)", op.cols.id, op.cols.text, op.cols.done)
+ },
+ },
+ update: {
+ // A toggle/edit sends a getChanges() diff; build the SET from the
+ // present keys, allowing only the updatable columns.
+ execute: ({ op, sql }) => {
+ const cols = op.cols as Record
+ const keys = Object.keys(cols).filter((k) => UPDATABLE.has(k))
+ if (keys.length === 0) return
+ const set = keys.map((k) => `"${k}" = ?`).join(", ")
+ sql.exec(`UPDATE todos SET ${set} WHERE id = ?`, ...keys.map((k) => cols[k]), op.key)
+ },
+ },
+ delete: {
+ execute: ({ op, sql }) => {
+ sql.exec("DELETE FROM todos WHERE id = ?", op.key)
+ },
+ },
+ },
+ }),
+ },
+})
+
+/** The schema's type — the client brands its transports with this so
+ * `doCollectionOptions` infers the `todos` row type (no runtime schema value). */
+export type TodosApi = typeof todosSchema
+
+export class TodosDO extends SyncDurableObject {
+ constructor(ctx: DurableObjectState, env: Env) {
+ super(ctx, env)
+ ctx.blockConcurrencyWhile(async () => {
+ // You own your schema (ADR-0007); the framework wires sync after.
+ this.sql.exec(`CREATE TABLE IF NOT EXISTS todos (
+ id TEXT PRIMARY KEY,
+ text TEXT NOT NULL,
+ done INTEGER NOT NULL DEFAULT 0
+ )`)
+ this.registerSync(todosSchema)
+ // Seed AFTER registerSync so the rows flow through CDC and the first
+ // render gets a real (nonzero) resume cursor. Direct SQL is fine here:
+ // boot precedes any socket, so there is nothing to broadcast (ADR-0006).
+ this.sql.exec(`INSERT OR IGNORE INTO todos(id, text, done) VALUES
+ ('seed-1', 'Server-render this list', 1),
+ ('seed-2', 'Hydrate without a flash of empty', 0),
+ ('seed-3', 'Converge live over WebSocket', 0)`)
+ })
+ }
+}
diff --git a/examples/ssr/tsconfig.json b/examples/ssr/tsconfig.json
new file mode 100644
index 0000000..8f59103
--- /dev/null
+++ b/examples/ssr/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
+ "jsx": "react-jsx",
+ "strict": true,
+ "skipLibCheck": true,
+ "allowImportingTsExtensions": true,
+ "noEmit": true,
+ "types": ["@cloudflare/workers-types", "vite/client"]
+ },
+ "include": ["src/**/*", "vite.config.ts"]
+}
diff --git a/examples/ssr/vite.config.ts b/examples/ssr/vite.config.ts
new file mode 100644
index 0000000..86ae58e
--- /dev/null
+++ b/examples/ssr/vite.config.ts
@@ -0,0 +1,16 @@
+import { cloudflare } from "@cloudflare/vite-plugin"
+import { tanstackStart } from "@tanstack/react-start/plugin/vite"
+import react from "@vitejs/plugin-react"
+import { defineConfig } from "vite"
+
+export default defineConfig({
+ resolve: {
+ // `tanstack-durable-object-sync` is a file: link to the repo root, whose
+ // own `@tanstack/db` peer import would resolve from the ROOT node_modules
+ // — a second physical copy. Two copies break the Symbol-branded
+ // collectionOptions and every instanceof across the boundary. Dedupe
+ // forces one copy: this example's.
+ dedupe: ["@tanstack/db"],
+ },
+ plugins: [cloudflare({ viteEnvironment: { name: "ssr" } }), tanstackStart(), react()],
+})
diff --git a/examples/ssr/wrangler.jsonc b/examples/ssr/wrangler.jsonc
new file mode 100644
index 0000000..934d9f1
--- /dev/null
+++ b/examples/ssr/wrangler.jsonc
@@ -0,0 +1,10 @@
+{
+ "name": "tanstack-do-db-ssr",
+ "main": "src/server.ts",
+ "compatibility_date": "2026-03-10",
+ "compatibility_flags": ["nodejs_compat"],
+ "durable_objects": {
+ "bindings": [{ "name": "TODOS_DO", "class_name": "TodosDO" }]
+ },
+ "migrations": [{ "tag": "v1", "new_sqlite_classes": ["TodosDO"] }]
+}
diff --git a/package-lock.json b/package-lock.json
index 6c96809..3703974 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,7 +14,7 @@
"devDependencies": {
"@cloudflare/vitest-pool-workers": "0.18.7",
"@cloudflare/workers-types": "^4.20260518.1",
- "@tanstack/db": "0.6.5",
+ "@tanstack/db": "^0.8.5",
"typescript": "^5.7",
"vitest": "4.1.10",
"wrangler": "^4"
@@ -2064,14 +2064,14 @@
"license": "MIT"
},
"node_modules/@tanstack/db": {
- "version": "0.6.5",
- "resolved": "https://registry.npmjs.org/@tanstack/db/-/db-0.6.5.tgz",
- "integrity": "sha512-gtCuAo4UtC9SR/kTMu5fVEff6qZ2R1FZi9X7MybtHKA6wve7RePifGG6qBI4OmMB+7juT5/+glNbnqZOrG0/pg==",
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/@tanstack/db/-/db-0.8.5.tgz",
+ "integrity": "sha512-0bzsEWb9B0f6ADQjW/W1KCS/f8HBFLPz/Wqq52yqSjmvyKDAEvfOzaKdMXF38j3eR/oigwKj4qmw3Soni+BLNg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
- "@tanstack/db-ivm": "0.1.18",
+ "@tanstack/db-ivm": "0.1.19",
"@tanstack/pacer-lite": "^0.2.1"
},
"peerDependencies": {
@@ -2079,9 +2079,9 @@
}
},
"node_modules/@tanstack/db-ivm": {
- "version": "0.1.18",
- "resolved": "https://registry.npmjs.org/@tanstack/db-ivm/-/db-ivm-0.1.18.tgz",
- "integrity": "sha512-+pZJiRKdoKRM5Epq9T7otD9ZJl82pRFauo7LKuJGrarjVKQ7r+QQlPe3kGdN9LEKSnuNGIWjX9OOY4M8kH4eLw==",
+ "version": "0.1.19",
+ "resolved": "https://registry.npmjs.org/@tanstack/db-ivm/-/db-ivm-0.1.19.tgz",
+ "integrity": "sha512-3VGvgNXAPSVqny/pL8Y6n/txnaZzV49WmgSQ9N5eyCQNgMtuOEOJHKDatqqCy3gDxVu6pkhSHDKxcNFOtxMouw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2418,9 +2418,9 @@
}
},
"node_modules/fractional-indexing": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/fractional-indexing/-/fractional-indexing-3.2.0.tgz",
- "integrity": "sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ==",
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/fractional-indexing/-/fractional-indexing-3.4.0.tgz",
+ "integrity": "sha512-8J3glhz2rrpKG6KmI7wmJo3zH1VjeOpN+vTJSw1fOyO+Viqq3zX6/5NGh6oaZB2qIAYdOYuu5Dz9xp4faOO0Pg==",
"dev": true,
"license": "CC0-1.0",
"engines": {
diff --git a/package.json b/package.json
index efc1b48..5d0359c 100644
--- a/package.json
+++ b/package.json
@@ -56,12 +56,12 @@
"@msgpack/msgpack": "^3.0.0"
},
"peerDependencies": {
- "@tanstack/db": ">=0.6.0"
+ "@tanstack/db": ">=0.8.5"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "0.18.7",
"@cloudflare/workers-types": "^4.20260518.1",
- "@tanstack/db": "0.6.5",
+ "@tanstack/db": "^0.8.5",
"typescript": "^5.7",
"vitest": "4.1.10",
"wrangler": "^4"
diff --git a/src/client/do-collection.ts b/src/client/do-collection.ts
index d6b8fd9..b964630 100644
--- a/src/client/do-collection.ts
+++ b/src/client/do-collection.ts
@@ -15,9 +15,15 @@
// loaded subset are confirmed and their optimistic overlay retired by a
// post-mutation empty sync commit (ADR-0002 C2, verified).
-import { compileSingleRowExpression, toBooleanPredicate, type CollectionConfig } from "@tanstack/db"
+import {
+ compileSingleRowExpression,
+ toBooleanPredicate,
+ withCollectionConfigFactory,
+ type CollectionConfig,
+} from "@tanstack/db"
+import { encode as codecEncode } from "../wire/codec.ts"
import type { MutOp, RowOp } from "../wire/frames.ts"
-import { type SubHandler, WebSocketTransport } from "./transport.ts"
+import type { SubHandler, Transport } from "./transport.ts"
let subSeq = 0
@@ -49,15 +55,47 @@ interface PendingMutationLike {
changes: unknown
}
+/** `true` = applied synchronously; a Promise settles when the rows are visible
+ * (@tanstack/db 0.8.5's SyncAppliedReceipt — loadSubset must await it). */
+type CommitReceipt = true | Promise
+
interface SyncParams {
collection: { get: (key: string) => unknown }
begin: (options?: { immediate?: boolean }) => void
write: (message: { type: RowOp; value?: unknown; key?: string }) => void
- commit: () => void
+ commit: (signal?: AbortSignal) => CommitReceipt
markReady: () => void
+ /** Fails the collection's readiness promises with the cause (@tanstack/db
+ * 0.8.2); recovery is a retried preload() once sync later succeeds.
+ * Optional only for bare test harnesses — real @tanstack/db provides it. */
+ markError?: (error?: unknown) => void
truncate: () => void
}
+/** The opaque payload that rides TanStack's dehydrated state (ADR-0011 D3).
+ * Shape is ours; `v` gates forward evolution loudly. `where` fingerprints the
+ * eager filter the rows were dehydrated under: a cursor is only a sound
+ * resume point FOR THAT FILTER (catch-up emits changed keys only — an
+ * unchanged out-of-filter hydrated row would never be reconciled away). */
+export interface DoSyncMeta {
+ v: 1
+ cursor: string
+ where?: string
+}
+
+function parseSyncMeta(meta: unknown): DoSyncMeta {
+ const m = meta as Partial | null
+ if (m == null || m.v !== 1 || typeof m.cursor !== "string" || (m.where !== undefined && typeof m.where !== "string")) {
+ throw new Error(`unrecognized sync meta (expected {v:1, cursor, where?}): ${JSON.stringify(meta)}`)
+ }
+ // Malformed throws; NEGATIVE is rejected too (codex review): seedCursor
+ // ignores it but `since:"-1"` would reach the server, which answers a
+ // full snapshot the on-demand catch-up handler discards — no terminal, the
+ // transient sub never tears down, and stale hydrated rows survive forever.
+ if (BigInt(m.cursor) < 0n) throw new Error(`negative sync-meta cursor: ${m.cursor}`)
+ return { v: 1, cursor: m.cursor, ...(m.where === undefined ? {} : { where: m.where }) }
+}
+
/** Subset of @tanstack/db's LoadSubsetOptions we consume. */
interface LoadSubsetOptions {
where?: unknown
@@ -78,8 +116,11 @@ function compilePredicate(where: unknown): (row: Record) => boo
/** Api-typed options: Row is inferred from the schema `Api` + `table`, so the
* client needs no runtime schema value. `getKey` and the row type follow. */
export interface DoApiCollectionOptions> {
- /** One transport per DO, parameterized by the same schema `Api`. */
- transport: WebSocketTransport
+ /** One transport per DO, parameterized by the same schema `Api`. In the
+ * browser a `WebSocketTransport`; during SSR an
+ * `SsrSnapshotTransport` — created PER REQUEST (ADR-0011 D2). Both
+ * satisfy the structural `Transport`. */
+ transport: Transport
/** Collection (table) name on the DO — a key of the schema's collections. */
table: K
/** Stable client-supplied key extractor (must match the server pk). */
@@ -99,11 +140,17 @@ export interface DoApiCollectionOptions> {
// )
//
// Explicit type args are optional (`doCollectionOptions(...)`).
+//
+// The returned config carries @tanstack/db's collection-config factory
+// (`withCollectionConfigFactory`), so `collectionOptions("id", () =>
+// doCollectionOptions({...}))` descriptors materialize with FRESH adapter
+// state per DbClient. The transport is the caller's: per-request SSR clients
+// must construct a per-request transport inside their own descriptor factory.
export function doCollectionOptions>(
opts: DoApiCollectionOptions,
): CollectionConfig & object, string>
export function doCollectionOptions(opts: {
- transport: WebSocketTransport
+ transport: Transport
table: string
getKey: (row: any) => string
id?: string
@@ -118,8 +165,44 @@ export function doCollectionOptions(opts: {
// Set by sync(); used by mutationFn to retire no-subset-match optimistic rows.
let emptyCommit: (() => void) | null = null
+ // SSR hydration's resume point (ADR-0011 D3). Set by importSyncMeta — which
+ // upstream calls AFTER applying the dehydrated rows as synced upserts, and
+ // possibly BEFORE sync() ever runs (lazy collections). Consumed exactly once
+ // at sync start and cleared in cleanup: after a collection GC the rows are
+ // wiped, so a retained cursor would resume over an empty store and silently
+ // lose everything below it.
+ let hydratedCursor: string | null = null
+
+ // Settlement gate for the syncMeta claim (codex review): the transport's
+ // cursor advances at commit BOUNDARIES, but a commit's SyncAppliedReceipt
+ // may settle later (application queued behind a persisting user
+ // transaction). Exporting the boundary cursor in that window would dehydrate
+ // pre-boundary rows under meta claiming the boundary — a resume that skips
+ // the gap forever. While any receipt is unsettled, exportSyncMeta claims the
+ // last fully-settled position instead (under-claiming is always safe: MIN
+ // semantics, idempotent replay).
+ let pendingReceipts = 0
+ let settledCursor = "0"
+
const sync = (params: SyncParams): SyncConfigResult => {
- const { collection, begin, write, commit, markReady, truncate } = params
+ const { collection, begin, write, commit, markReady, markError, truncate } = params
+ const consumeHydratedCursor = (): string | null => {
+ const hc = hydratedCursor
+ hydratedCursor = null
+ return hc
+ }
+ // Presence in SYNCED data — the combined view (collection.get) includes
+ // optimistic overlays, which sync writes must never be steered by: a key
+ // under an optimistic delete still exists synced (insert would throw), and
+ // an optimistic-only insert does not (update would not upsert the synced
+ // store the hydration correction targets). `_state.syncedData` is the same
+ // seam upstream's DbClient hydration itself writes through.
+ const syncedData = (): Map | null =>
+ (collection as { _state?: { syncedData?: Map } })._state?.syncedData ?? null
+ const syncedHas = (key: string): boolean => {
+ const sd = syncedData()
+ return sd ? sd.has(key) : collection.get(key) !== undefined
+ }
let open = false
const ensureBegin = (): void => {
if (!open) {
@@ -127,57 +210,200 @@ export function doCollectionOptions(opts: {
open = true
}
}
- const flush = (): void => {
- if (open) {
- commit()
- open = false
+ /** Book a commit's receipt against the settlement gate (see
+ * pendingReceipts). Returns the ORIGINAL receipt so callers still await
+ * and propagate rejection; the passive branch also keeps a fire-and-
+ * forget flush from surfacing as an unhandled rejection. */
+ const track = (receipt: CommitReceipt): CommitReceipt => {
+ const r = receipt as { then?: (a: () => void, b: () => void) => unknown }
+ if (r != null && typeof r.then === "function") {
+ pendingReceipts++
+ const settle = (): void => {
+ pendingReceipts--
+ // Everything booked has applied: the transport's position is a
+ // sound claim again from here.
+ if (pendingReceipts === 0) settledCursor = transport.appliedCursor
+ }
+ void r.then(settle, settle)
+ } else if (pendingReceipts === 0) {
+ settledCursor = transport.appliedCursor
}
+ return receipt
+ }
+ /** Commit the open transaction; the receipt settles when rows are visible
+ * (`true` = already are). The 0.8.5 loadSubset contract chains on it. */
+ const flush = (): CommitReceipt => {
+ if (!open) return true
+ open = false
+ return track(commit())
+ }
+ /** Run `onApplied` once `receipt` says the rows are visible; a REJECTED
+ * receipt (the application was aborted, 0.8.5) is a failure, not
+ * success — it must not resolve a subset load or readiness (codex
+ * review). Thenable-sniffed — anything non-promise (incl. `true`, and
+ * bare harness mocks returning void) means "already applied". */
+ const afterApplied = (receipt: CommitReceipt, onApplied: () => void, onFail: (e: unknown) => void): void => {
+ const r = receipt as { then?: (a: () => void, b: (e: unknown) => void) => unknown }
+ if (r != null && typeof r.then === "function") void r.then(onApplied, onFail)
+ else onApplied()
}
emptyCommit = (): void => {
flush()
begin()
- commit() // a standalone empty boundary; runs the direct-upsert clear path
+ track(commit()) // a standalone empty boundary; runs the direct-upsert clear path
}
- const makeHandler = (onReady: () => void): SubHandler => ({
- onSnap: (_key, row) => {
- ensureBegin()
- write({ type: "insert", value: row })
- },
- onSnapEnd: () => {
- flush()
- onReady()
- },
- onDelta: (op, key, cols) => {
- ensureBegin()
- if (op === "delete") write({ type: "delete", key: key as string })
- // A catch-up emits the LATEST op per changed key, so a key deleted-and-
- // reinserted while we were away arrives as "insert" for a key we still
- // HOLD — TanStack's sync write throws DuplicateKeySyncError on that
- // unless values deep-equal. Apply a held-key insert as the upsert it
- // semantically is (update upserts; the move-in contract, ADR-0002 C4).
- else if (op === "insert" && collection.get(key as string) !== undefined) write({ type: "update", value: cols })
- else write({ type: op, value: cols })
- },
- onUptodate: () => flush(),
- onReset: () => {
- flush()
- begin()
- truncate()
- commit()
- // A reset is also the only terminal signal for a REJECTED sub (the
- // server sends `reset` with no `snap-end` for an unsupported predicate
- // or unknown collection). Mark ready here too, or this subset's load
- // promise — and the live query's preload() — would hang forever. For a
- // compaction/rotation reset (a valid sub that re-snapshots) this is an
- // idempotent no-op: onSnapEnd's onReady() has already fired.
- onReady()
- },
- })
+ const makeHandler = (
+ onReady: () => void,
+ opts?: { reconcileSnapshots?: boolean; onFail?: (e: unknown) => void },
+ ): SubHandler => {
+ // Where a rejected receipt lands: a subset load rejects ITS promise; the
+ // collection-level default fails readiness loud (a later markReady —
+ // any successful snapshot — recovers, error → ready).
+ const onFail = opts?.onFail ?? ((e: unknown): void => markError?.(e))
+ // `reconcileSnapshots` (armed for every EAGER sub, never for on-demand
+ // subset subs — a subset snapshot must not delete other subsets' rows):
+ // a snapshot is authoritative SET semantics over the synced rows —
+ // held keys absent from it were deleted server-side, and snapshots
+ // carry no tombstones (ADR-0011 D4). Track each snapshot's keys and
+ // delete the rest at ITS boundary; no truncate, so a hydrated first
+ // paint never flashes empty. The set is per-snapshot (reset at every
+ // snap-end), and an EMPTY snapshot (zero snap frames — the server
+ // wiped the table) still reconciles everything away at the boundary.
+ let snapKeys: Set | null = null
+ return {
+ onSnap: (_key, row) => {
+ ensureBegin()
+ const key = getKey(row as Record)
+ if (opts?.reconcileSnapshots) (snapKeys ??= new Set()).add(key)
+ // A held key's snapshot row is an upsert: hydrated rows may have
+ // changed since dehydration, and a differing insert would throw
+ // DuplicateKeySyncError. With the C1′ barrier a snapshot row is
+ // never staler than the held synced row, so the snapshot wins.
+ write(syncedHas(key) ? { type: "update", value: row } : { type: "insert", value: row })
+ },
+ onSnapEnd: () => {
+ if (opts?.reconcileSnapshots) {
+ const seen = snapKeys // null ⇒ empty snapshot ⇒ empty authoritative set
+ snapKeys = null
+ const sd = syncedData()
+ if (!sd) throw new Error("snapshot reconcile requires collection._state.syncedData (incompatible @tanstack/db)")
+ for (const key of sd.keys()) {
+ // ensureBegin only when a delete is actually due — the common
+ // converged/empty case stays boundary-free.
+ if (!seen?.has(key)) {
+ ensureBegin()
+ write({ type: "delete", key })
+ }
+ }
+ }
+ afterApplied(flush(), onReady, onFail)
+ },
+ onDelta: (op, key, cols) => {
+ ensureBegin()
+ if (op === "delete") write({ type: "delete", key: key as string })
+ // A catch-up emits the LATEST op per changed key, so a key deleted-
+ // and-reinserted while we were away arrives as "insert" for a key we
+ // still HOLD — TanStack's sync write throws DuplicateKeySyncError on
+ // that unless values deep-equal. Apply a held-key insert as the
+ // upsert it semantically is (update upserts; move-in, ADR-0002 C4).
+ else if (op === "insert" && syncedHas(key as string)) write({ type: "update", value: cols })
+ else write({ type: op, value: cols })
+ },
+ onUptodate: () => flush(),
+ onReset: () => {
+ flush()
+ begin()
+ truncate()
+ // A reset is also the only terminal signal for a REJECTED sub (the
+ // server sends `reset` with no `snap-end` for an unsupported predicate
+ // or unknown collection). Mark ready here too, or this subset's load
+ // promise — and the live query's preload() — would hang forever. For a
+ // compaction/rotation reset (a valid sub that re-snapshots) this is an
+ // idempotent no-op: onSnapEnd's onReady() has already fired.
+ afterApplied(track(commit()), onReady, onFail)
+ },
+ }
+ }
if (syncMode === "on-demand") {
- // Ready as soon as connected; data arrives per loadSubset.
- void transport.connect().then(() => markReady())
+ // Hydration catch-up (ADR-0011 D3): the dehydrated rows are the union of
+ // whatever subsets the server render loaded — per-subset resume is
+ // unsound (a subset the render didn't cover has no since to resume
+ // from, and overlapping predicates leave stale-delete holes). ONE
+ // transient unfiltered sub from the dehydrated cursor covers every
+ // changed key (always-emit ⇒ synthetic deletes included) in the
+ // render→hydrate window, then unsubscribes at ITS terminal — never at a
+ // broadcast boundary, which can precede its own frames. Semantic cost
+ // (documented): rows outside any loaded subset that changed in the
+ // window land in the collection.
+ //
+ // With NO resume point ("0"), or when the server resets the catch-up
+ // (below the retention floor), the hydrated rows are honestly
+ // UNRESUMABLE: truncate. In on-demand a full snapshot would strand
+ // never-subscribed whole-table rows as permanently-stale state — worse
+ // than a one-roundtrip refetch of the live subsets. The reset path
+ // unsubscribes IMMEDIATELY so the server's trailing unfiltered
+ // resnapshot is dropped on the floor (no handler), and the subset subs
+ // repopulate right after.
+ //
+ // markReady gates on the catch-up sub FRAME being sent (not completed):
+ // loadSubset subs only fire after ready, so on the single ordered
+ // socket the catch-up's truncate/deltas always precede subset
+ // snapshots. Ready never waits for data — stale-while-revalidate.
+ const hc = consumeHydratedCursor()
+ let readyGate: Promise
+ if (hc !== null && hc !== "0") {
+ const catchupId = `${table}#hydrate#${++subSeq}`
+ const done = (): void => transport.unsubscribe(catchupId)
+ readyGate = transport.subscribe(
+ catchupId,
+ table,
+ {
+ onSnap: () => {}, // catch-ups never snapshot; reset's resnapshot is dropped (unsubbed)
+ onSnapEnd: () => {},
+ onDelta: makeHandler(() => {}).onDelta,
+ onUptodate: (ownTerminal) => {
+ flush()
+ if (ownTerminal) {
+ done()
+ // Also heals an earlier failed gate (error → ready, 0.8.2):
+ // the readyGate rejected, the policy-driven reconnect
+ // resubscribed this catch-up, and its terminal is the first
+ // proof the collection is usable again (codex review —
+ // idempotent when the gate already marked ready).
+ markReady()
+ }
+ },
+ onReset: () => {
+ flush()
+ begin()
+ truncate()
+ track(commit())
+ done() // before the trailing resnapshot frames arrive
+ markReady() // same healing as the terminal path
+ },
+ },
+ undefined,
+ undefined,
+ undefined,
+ hc,
+ )
+ } else if (hc === "0") {
+ // No resume point: drop the hydrated rows at sync start, honestly.
+ readyGate = transport.connect().then(() => {
+ begin()
+ truncate()
+ track(commit())
+ })
+ } else {
+ readyGate = transport.connect()
+ }
+ // A failed gate fails readiness loud (markError; preload() rejects with
+ // the cause) instead of hanging — a later retried preload() recovers
+ // once the transport's policy-driven reconnect succeeds (0.8.2).
+ void readyGate.then(markReady, (e) => markError?.(e))
+
// Distinct `where` -> one refcounted server subscription.
const loaded = new Map }>()
const keyOf = (o: LoadSubsetOptions): string => JSON.stringify(o.where ?? null)
@@ -214,7 +440,9 @@ export function doCollectionOptions(opts: {
for (const r of page) {
if (collection.get(getKey(r)) === undefined) write({ type: "insert", value: r })
}
- flush()
+ // 0.8.5 contract: a subset load settles only once its rows are visible.
+ const receipt = flush()
+ if (receipt !== true) await receipt
}
const loadSubset = (o: LoadSubsetOptions): true | Promise => {
@@ -226,15 +454,28 @@ export function doCollectionOptions(opts: {
return existing.ready
}
let resolve!: () => void
- const ready = new Promise((r) => {
- resolve = r
+ let reject!: (e: unknown) => void
+ const ready = new Promise((res, rej) => {
+ resolve = res
+ reject = rej
})
const subId = `${table}#${key}`
loaded.set(key, { subId, refs: 1, ready })
// Forward orderBy/limit so the INITIAL snapshot is the bounded window
// (recent N), not the whole where-subset. The live sub's predicate is
// still `where`, so entering rows (e.g. new messages) are delivered.
- void transport.subscribe(subId, table, makeHandler(resolve), o.where, o.orderBy, o.limit)
+ // A send failure or rejected receipt rejects THIS load (0.8.4 surfaces
+ // it per-subscription as loadSubset:error), not the whole collection.
+ // A completed subset also (re)marks ready — the recovery path out of a
+ // failed ready-gate's error state (idempotent otherwise).
+ const handler = makeHandler(
+ () => {
+ resolve()
+ markReady()
+ },
+ { onFail: reject },
+ )
+ void transport.subscribe(subId, table, handler, o.where, o.orderBy, o.limit).catch(reject)
return ready
}
@@ -252,12 +493,49 @@ export function doCollectionOptions(opts: {
}
}
- return { loadSubset, unloadSubset, cleanup: () => transport.close() }
+ return {
+ loadSubset,
+ unloadSubset,
+ cleanup: () => {
+ hydratedCursor = null // GC wiped the rows; a retained cursor would lie
+ transport.close()
+ },
+ }
}
- // eager
- void transport.subscribe(eagerSubId, table, makeHandler(markReady), where)
- return () => transport.unsubscribe(eagerSubId)
+ // eager — reconcile is ALWAYS armed: an eager snapshot is authoritative
+ // set semantics over synced rows, period (ADR-0011 D4). For the normal
+ // empty-at-first-snapshot flow it is a no-op; for ANY path where synced
+ // rows precede a snapshot — hydration with no resume point, hydration
+ // whose meta failed validation (rows land before importSyncMeta; no
+ // veto), futures we haven't imagined — it is what prevents a
+ // server-deleted held row from being stale forever. C1′ makes it sound
+ // mid-session too: a held synced key absent from a snapshot is deleted.
+ {
+ const hc = consumeHydratedCursor()
+ const handler = makeHandler(markReady, { reconcileSnapshots: true })
+ if (hc !== null) {
+ // Hydrated (ADR-0011 D3): the rows were applied upstream as synced
+ // upserts before we ran. Resume from the dehydrated cursor (server
+ // catch-up; below the floor an honest reset + resnapshot) — or, with
+ // no resume point ("0"), take a fresh snapshot and reconcile it.
+ // Ready NOW: stale-while-revalidate is the explicit SSR contract —
+ // first paint renders the hydrated rows, the boundary converges them.
+ void transport
+ .subscribe(eagerSubId, table, handler, where, undefined, undefined, hc === "0" ? undefined : hc)
+ .catch((e) => markError?.(e))
+ markReady()
+ } else {
+ // A first-connect failure fails readiness loud (preload() rejects);
+ // the policy-driven reconnect keeps retrying and the eventual
+ // snapshot's markReady recovers the collection (error → ready, 0.8.2).
+ void transport.subscribe(eagerSubId, table, handler, where).catch((e) => markError?.(e))
+ }
+ }
+ return () => {
+ hydratedCursor = null // GC wiped the rows; a retained cursor would lie
+ transport.unsubscribe(eagerSubId)
+ }
}
const mutationFn = async (params: {
@@ -294,15 +572,108 @@ export function doCollectionOptions(opts: {
}
}
- return {
+ // SSR syncMeta hooks (ADR-0011 D3) — called by TanStack's DbClient
+ // dehydrate/hydrate (@tanstack/db ≥0.8.0, PR #1564 as merged). The eager
+ // `where` fingerprint is the codec envelope — stable for the same
+ // constructor code; a cross-deploy false mismatch merely downgrades to the
+ // (always-sound) snapshot-reconcile path.
+ const whereFingerprint = where == null ? undefined : codecEncode(where)
+ // Merged-upstream contract (client.ts applyRows): on every hydrated chunk,
+ // upstream asks exportSyncMeta() for the CURRENT meta and — when it exists —
+ // routes the incoming meta through mergeSyncMeta. A fresh browser-side
+ // adapter must therefore export UNDEFINED, not {cursor:"0"}: it holds no
+ // claim, and a "0" claim would win the MIN-merge against every real
+ // dehydrated cursor, silently downgrading all hydration to the
+ // snapshot-reconcile path. "0" stays a REAL claim ("no resume point" — the
+ // honest-truncate route); no-claim is the absence of meta. On the server the
+ // SSR transport's reads establish the position this exports.
+ const exportSyncMeta = (): DoSyncMeta | undefined => {
+ // While a commit's receipt is unsettled the boundary cursor is not yet a
+ // sound claim — export the last fully-settled position instead (see
+ // pendingReceipts). Under-claiming is always safe.
+ const live = pendingReceipts === 0 ? transport.appliedCursor : settledCursor
+ const cursor = transport.hasPosition ? live : hydratedCursor
+ if (cursor === null) return undefined
+ return {
+ v: 1,
+ cursor,
+ ...(whereFingerprint === undefined ? {} : { where: whereFingerprint }),
+ }
+ }
+ const importSyncMeta = (meta: unknown): void => {
+ // Upstream applies the dehydrated rows BEFORE this runs — there is no
+ // veto. So a validation failure must fail loud AND fail safe: the rows
+ // are in syncedData regardless, and silently skipping our bookkeeping
+ // would start sync down the no-resume path with no reconcile intent —
+ // a server-deleted hydrated row would then be stale forever. Set the
+ // safe state ("0" → snapshot + reconcile) FIRST, then throw so the
+ // version/corruption skew still surfaces to the app.
+ let m: DoSyncMeta
+ try {
+ m = parseSyncMeta(meta)
+ } catch (e) {
+ hydratedCursor = "0"
+ throw e
+ }
+ if (m.where === whereFingerprint) {
+ hydratedCursor = m.cursor
+ transport.seedCursor(m.cursor)
+ } else {
+ // The rows were dehydrated under a DIFFERENT eager filter: the cursor
+ // is not a sound resume point for ours (see DoSyncMeta). "0" routes the
+ // sync start to snapshot + reconcile; the transport cursor stays
+ // unseeded so a bootstrap-window reconnect resnapshots too.
+ hydratedCursor = "0"
+ }
+ }
+ const mergeSyncMeta = (current: unknown, incoming: unknown): DoSyncMeta => {
+ // Same fail-loud-but-SAFE contract as importSyncMeta: upstream calls
+ // merge (then import) AFTER applying the chunk's rows, so a parse throw
+ // here also can't veto anything — and upstream never reaches
+ // importSyncMeta when merge throws, which would skip the safety net.
+ let a: DoSyncMeta
+ let b: DoSyncMeta
+ try {
+ a = parseSyncMeta(current)
+ b = parseSyncMeta(incoming)
+ } catch (e) {
+ hydratedCursor = "0"
+ throw e
+ }
+ // Fingerprint skew between the two sides means SOME chunk's rows were
+ // dehydrated under a filter that is not ours — and they were APPLIED (no
+ // veto). MIN would let the matching side's cursor survive the merge and
+ // sail through import's fingerprint check, leaving the foreign rows with
+ // no catch-up that covers them (codex review). No sound joint resume
+ // point exists: return the honest "0" (snapshot-reconcile / on-demand
+ // truncate route) under OUR fingerprint so import routes it there.
+ if (a.where !== b.where) {
+ return { v: 1, cursor: "0", ...(whereFingerprint === undefined ? {} : { where: whereFingerprint }) }
+ }
+ // MIN is self-healing: a late chunk's rows were already applied over
+ // newer state (no veto); resuming from the EARLIER position replays the
+ // window idempotently and re-freshens whatever the chunk clobbered.
+ return BigInt(a.cursor) <= BigInt(b.cursor) ? a : b
+ }
+
+ const options = {
id: opts.id ?? table,
getKey,
syncMode,
- sync: { sync, rowUpdateMode: "partial" },
+ sync: { sync, rowUpdateMode: "partial", exportSyncMeta, importSyncMeta, mergeSyncMeta },
onInsert: mutationFn,
onUpdate: mutationFn,
onDelete: mutationFn,
- } as unknown as CollectionConfig
+ }
+ // Descriptor opt-in (@tanstack/db ≥0.8.0): a `collectionOptions("id", …)`
+ // descriptor over this config materializes per DbClient through this
+ // factory, giving each client FRESH adapter state (hydratedCursor, subIds).
+ // The transport is deliberately not recreated — it is the caller's; SSR
+ // callers construct a per-request transport in their own factory closure.
+ return withCollectionConfigFactory(
+ options as never,
+ () => doCollectionOptions(opts as Parameters[0]) as never,
+ ) as unknown as CollectionConfig
}
/** What our sync() returns: a cleanup fn (eager) or the on-demand handlers. */
diff --git a/src/client/index.ts b/src/client/index.ts
index c23ac67..ea0af12 100644
--- a/src/client/index.ts
+++ b/src/client/index.ts
@@ -13,6 +13,11 @@ export {
MutationRejectedError,
WebSocketTransport,
} from "./transport.ts"
-export type { ReconnectDelayFn, SubHandler, TransportOptions, WebSocketLike } from "./transport.ts"
+export type { ReconnectDelayFn, SubHandler, Transport, TransportOptions, WebSocketLike } from "./transport.ts"
export { doCollectionOptions, WriteOutsideSubError } from "./do-collection.ts"
-export type { CollectionName, DoApiCollectionOptions, RowOf } from "./do-collection.ts"
+export type { CollectionName, DoApiCollectionOptions, DoSyncMeta, RowOf } from "./do-collection.ts"
+// SSR (@tanstack/db ≥0.8.0 DbClient dehydrate/hydrate; ADR-0011). Create one
+// SsrSnapshotTransport PER REQUEST and pass `(req) => stub.readSyncSnapshot(req, request)`
+// — the same claims-bearing Request the WS upgrade gets (one auth gate, both paths).
+export { SsrReadOnlyError, SsrSnapshotTransport } from "./ssr-transport.ts"
+export type { SnapshotRead } from "./ssr-transport.ts"
diff --git a/src/client/ssr-transport.ts b/src/client/ssr-transport.ts
new file mode 100644
index 0000000..f4deb7e
--- /dev/null
+++ b/src/client/ssr-transport.ts
@@ -0,0 +1,113 @@
+// SsrSnapshotTransport — the server-rendering half of ADR-0011 D2.
+//
+// Implements the same structural `Transport` the WebSocket transport does, so
+// `doCollectionOptions` runs unchanged inside a per-request DbClient on the
+// worker: each subscribe is ONE snapshot read (rows + durable cursor), synthesized
+// as onSnap*/onSnapEnd. No socket, no timers, no live deltas — render, dehydrate,
+// throw away.
+//
+// The reader is injected as a plain function so this file carries no Cloudflare
+// types; the author passes `(req) => stub.readSyncSnapshot(req, request)` (the
+// SyncDurableObject RPC), closing over the same claims-bearing Request the WS
+// upgrade gets — parseAttachment is the ONE auth gate for both paths.
+//
+// SSR is read-only: mutations during render are a design error, not a queue —
+// they throw. Create one transport (and one options object) PER REQUEST; a
+// module-scope instance would leak cursor state across requests (the upstream
+// hooks offer no per-instance identity — see ADR-0011 "Context").
+
+import { decode, encode } from "../wire/codec.ts"
+import type { ClientFrame } from "../wire/frames.ts"
+import type { SubHandler, Transport } from "./transport.ts"
+
+/** TanStack's expression IR arrives as class instances (Func/Ref/Value), which
+ * structured clone — and therefore DO RPC — rejects. The wire tagged-value
+ * codec already flattens them to plain data preserving bigint/Date/±Inf, so a
+ * round-trip gives the reader a clone-safe request (same shape the WS frames
+ * carry). */
+function plain(v: unknown): unknown {
+ return v === undefined ? undefined : decode(encode(v))
+}
+
+export class SsrReadOnlyError extends Error {
+ constructor(operation: string) {
+ super(
+ `${operation} during SSR: the snapshot transport is read-only. ` +
+ `Mutations belong on the live client after hydration.`,
+ )
+ this.name = "SsrReadOnlyError"
+ }
+}
+
+/** One snapshot read — rows plus the durable high-water cursor at one position. */
+export type SnapshotRead = (req: {
+ collection: string
+ where?: unknown
+ orderBy?: unknown
+ limit?: number
+}) => Promise<{ rows: Array>; cursor: string }>
+
+export class SsrSnapshotTransport implements Transport {
+ /** phantom — carries `Api` so `doCollectionOptions` infers the collection set
+ * from the SSR transport exactly as it does from `WebSocketTransport`. */
+ declare readonly __api?: Api
+ /** MIN across reads — multiple subsets read at different positions can only
+ * safely resume from the EARLIEST one (replay is idempotent; skipping is
+ * not). Null until the first read. */
+ private cursor: bigint | null = null
+
+ constructor(private readonly opts: { read: SnapshotRead }) {}
+
+ /** Lowest position across this render's reads (stringified bigint). */
+ get appliedCursor(): string {
+ return String(this.cursor ?? 0n)
+ }
+
+ /** True after ANY read — a read that returned cursor 0 (a DO with no
+ * history) is a REAL "no resume point" claim and must dehydrate as such;
+ * only a transport that never read holds no claim. */
+ get hasPosition(): boolean {
+ return this.cursor !== null
+ }
+
+ async connect(): Promise {
+ // Nothing to open — but resolving lets on-demand mode markReady as usual.
+ }
+
+ async subscribe(
+ _subId: string,
+ collection: string,
+ handler: SubHandler,
+ where?: unknown,
+ orderBy?: unknown,
+ limit?: number,
+ _since?: string,
+ ): Promise {
+ const { rows, cursor } = await this.opts.read({ collection, where: plain(where), orderBy: plain(orderBy), limit })
+ const c = BigInt(cursor)
+ this.cursor = this.cursor === null || c < this.cursor ? c : this.cursor
+ // Key is derived by the adapter via getKey(row); snap keys are advisory.
+ for (const row of rows) handler.onSnap(undefined, row)
+ handler.onSnapEnd()
+ }
+
+ unsubscribe(): void {
+ // One-shot reads hold nothing to release.
+ }
+
+ sendMut(_frame: Extract): Promise<{ result?: unknown }> {
+ return Promise.reject(new SsrReadOnlyError("mutation"))
+ }
+
+ fetch(_frame: Extract): Promise> {
+ return Promise.reject(new SsrReadOnlyError("cursor fetch"))
+ }
+
+ seedCursor(): void {
+ // Hydrating INTO a render is meaningless — reads define this cursor.
+ }
+
+ close(): void {
+ // Nothing held.
+ }
+}
diff --git a/src/client/transport.ts b/src/client/transport.ts
index 6ae64e2..efe70a1 100644
--- a/src/client/transport.ts
+++ b/src/client/transport.ts
@@ -31,10 +31,48 @@ export interface SubHandler {
onSnap(key: unknown, row: unknown): void
onSnapEnd(): void
onDelta(op: RowOp, key: unknown, cols: Record | undefined): void
- onUptodate(): void
+ /** `ownTerminal` is true only for a sub-scoped boundary addressed to THIS
+ * subscription (a catch-up's terminal, ADR-0011 D3) — a transient
+ * subscription may tear itself down on it, but never on a broadcast
+ * boundary, which can precede its own catch-up frames. */
+ onUptodate(ownTerminal?: boolean): void
onReset(): void
}
+/** The transport surface `doCollectionOptions` consumes — structural, so the
+ * WebSocket transport and the SSR snapshot transport are interchangeable
+ * (ADR-0011 D2). Generic + branded on `Api` so row/table typing survives the
+ * seam: `WebSocketTransport` and `SsrSnapshotTransport` both satisfy
+ * it, and `doCollectionOptions` still infers the collection set from `Api`. */
+export interface Transport {
+ /** phantom — carries `Api` through the structural interface so inference at
+ * `doCollectionOptions` recovers it (never `unknown`). */
+ readonly __api?: Api
+ connect(): Promise
+ subscribe(
+ subId: string,
+ collection: string,
+ handler: SubHandler,
+ where?: unknown,
+ orderBy?: unknown,
+ limit?: number,
+ since?: string,
+ ): Promise
+ unsubscribe(subId: string): void
+ sendMut(frame: Extract): Promise<{ result?: unknown }>
+ fetch(frame: Extract): Promise>
+ close(): void
+ readonly appliedCursor: string
+ /** True once this transport has established a stream position of its own —
+ * even position 0 (an SSR read against a DO with no history is a REAL
+ * claim: "no resume point", the honest-truncate route). False means no
+ * claim at all (a fresh browser transport) — exportSyncMeta then exports
+ * nothing rather than a spurious "0" that would win a MIN-merge against
+ * every real dehydrated cursor (ADR-0011 D3, merged-upstream semantics). */
+ readonly hasPosition: boolean
+ seedCursor(cursor: string): void
+}
+
/** Cloudflare's inbound WebSocket edge cap, ~1 MiB (ADR-0018). An
* infrastructure FACT, not an application preference: both wire endpoints
* ship in this package and the only supported infra fixes the number, so it
@@ -192,6 +230,67 @@ export class WebSocketTransport {
return String(this.appliedSeq)
}
+ /** A live transport's position exists once anything has advanced (or seeded)
+ * the cursor — it can never claim position 0 (seedCursor("0") is a no-op),
+ * so 0 here honestly means "no claim yet". */
+ get hasPosition(): boolean {
+ return this.appliedSeq !== 0n
+ }
+
+ /**
+ * Claim a cursor position on behalf of externally-applied state — SSR
+ * hydration (ADR-0011 D3). The hydrated rows ARE the stream's prefix up to
+ * the dehydrated cursor, so claiming it keeps a bootstrap-window reconnect
+ * from re-snapshotting over them (a fresh snapshot carries no tombstones, so
+ * a row deleted server-side meanwhile would never be removed).
+ *
+ * The claim only ever SHRINKS relative to live progress: claiming a shorter
+ * applied prefix is always safe; claiming a longer one without data never
+ * is. A seed below the current position (a late streamed chunk — upstream
+ * has already applied its possibly-stale rows; there is no veto) regresses
+ * the cursor and resubscribes, so the catch-up replay re-freshens exactly
+ * the clobbered window. Replay is idempotent: latest-op-per-key, applied as
+ * upserts/deletes.
+ */
+ seedCursor(cursor: string): void {
+ const c = BigInt(cursor) // malformed cursor throws — fail loud, never guess
+ if (c <= 0n) return // "0" honestly means: no resume point to claim
+ if (c >= this.appliedSeq && this.appliedSeq !== 0n) return // never grow the claim
+ const wasLive = this.appliedSeq !== 0n && this.ws !== null
+ this.appliedSeq = c
+ if (wasLive && this.handlers.size > 0) {
+ // A live regress cannot replay on the SAME socket: boundary frames the
+ // server already sent (full duplex) would dispatch after the regress
+ // and re-advance the cursor past the repair window — then a drop
+ // resumes beyond it and the late chunk's clobbered rows stay stale
+ // forever. Force a reconnect instead: the old socket stops speaking for
+ // the stream (message dispatch is identity-guarded), and the FRESH
+ // socket resubscribes from the seed — clean ordering, replay guaranteed.
+ this.forceReconnect()
+ }
+ }
+
+ /** Abandon the current socket and reconnect NOW. A cursor-regress reconnect
+ * is voluntary — not a network failure — so it bypasses the backoff policy
+ * (no attempt consumed, no delay, and a custom policy cannot declare it
+ * terminal) but still runs the resubscribe path. A failed open falls back
+ * into the normal policy-driven retry via connect()'s failure path. */
+ private forceReconnect(): void {
+ const old = this.ws
+ this.ws = null // the identity guards now ignore the old socket entirely
+ this.connectPromise = null
+ try {
+ old?.close()
+ } catch {
+ /* already dead; the reconnect proceeds regardless */
+ }
+ this.reconnecting = true
+ this.clearReconnectTimer()
+ void this.connect().catch(() => {
+ /* retries route through connect()'s failure path (policy-driven) */
+ })
+ }
+
async connect(): Promise {
if (this.ws) return
if (this.connectPromise) return this.connectPromise
@@ -216,7 +315,20 @@ export class WebSocketTransport {
} catch {
/* some socket impls don't expose binaryType; codec handles AB/Uint8Array */
}
- ws.addEventListener("message", (ev) => this.onMessage(ev.data))
+ ws.addEventListener("message", (ev) => {
+ // Only the CURRENT socket speaks for the STREAM. An abandoned socket
+ // (forceReconnect regress, a superseded reconnect) can still deliver
+ // queued frames — applying its stream frames, or worse advancing the
+ // cursor on them, would claim positions the fresh socket's replay is
+ // about to own (ADR-0011 D3). Dropped stream frames are re-covered by
+ // the resubscribe catch-up from the applied cursor, idempotently.
+ // ID-scoped receipts (`committed`/`rejected`/`page`) are NOT
+ // re-covered by any replay, so a stale socket may still settle those
+ // waiters — it just never advances the cursor (codex review: a
+ // committed mutation must not be reported as timed out because a late
+ // hydration chunk forced a reconnect first).
+ this.onMessage(ev.data, this.ws !== ws)
+ })
ws.addEventListener("close", (ev) => {
// Only the CURRENT socket's close may detach/reconnect. A stale
// socket's late close (delivered after close()+connect() installed a
@@ -343,10 +455,18 @@ export class WebSocketTransport {
where?: unknown,
orderBy?: unknown,
limit?: number,
+ /** Resume point for the FIRST sub — SSR hydration's dehydrated cursor
+ * (ADR-0011 D3). One-shot: reconnects resume from `appliedCursor`. */
+ since?: string,
): Promise {
this.handlers.set(subId, { handler, collection, where, orderBy, limit })
await this.connect()
- this.sendFrame({ t: "sub", subId, collection, where, orderBy, limit })
+ // Unsubscribed while the connect was in flight (its `unsub` had no socket
+ // to ride): sending now would register a ghost subscription the server
+ // persists (ADR-0019) with no local handler — dead weight against the
+ // sub cap until the socket drops (codex review).
+ if (!this.handlers.has(subId)) return
+ this.sendFrame({ t: "sub", subId, collection, where, orderBy, limit, since })
}
unsubscribe(subId: string): void {
@@ -459,7 +579,7 @@ export class WebSocketTransport {
this.ws.send(data)
}
- private onMessage(data: unknown): void {
+ private onMessage(data: unknown, stale = false): void {
let frame: ServerFrame
try {
frame = this.codec.decode(data as ArrayBuffer | string) as ServerFrame
@@ -468,17 +588,24 @@ export class WebSocketTransport {
}
switch (frame.t) {
case "snap":
+ if (stale) return
this.handlers.get(frame.sub)?.handler.onSnap(frame.key, frame.row)
return
case "snap-end":
+ if (stale) return
this.handlers.get(frame.sub)?.handler.onSnapEnd()
this.advance(frame.seq)
return
case "d":
+ if (stale) return
this.handlers.get(frame.sub)?.handler.onDelta(frame.op, frame.key, frame.cols)
return
case "uptodate":
- for (const { handler } of this.handlers.values()) handler.onUptodate()
+ if (stale) return
+ // A sub-scoped terminal (a catch-up's) goes to its handler alone; a
+ // broadcast boundary (coalescer tick / barrier flush) goes to all.
+ if (frame.sub) this.handlers.get(frame.sub)?.handler.onUptodate(true)
+ else for (const { handler } of this.handlers.values()) handler.onUptodate(false)
this.advance(frame.seq)
return
case "committed": {
@@ -488,7 +615,7 @@ export class WebSocketTransport {
this.pendingTx.delete(frame.txId)
w.resolve({ result: frame.result })
}
- this.advance(frame.seq)
+ if (!stale) this.advance(frame.seq)
return
}
case "rejected": {
@@ -510,6 +637,7 @@ export class WebSocketTransport {
return
}
case "reset":
+ if (stale) return
if (frame.sub) this.handlers.get(frame.sub)?.handler.onReset()
else for (const { handler } of this.handlers.values()) handler.onReset()
return
diff --git a/src/server/changes.ts b/src/server/changes.ts
index 74dc8f4..4b2c8a5 100644
--- a/src/server/changes.ts
+++ b/src/server/changes.ts
@@ -226,6 +226,17 @@ export function currentSeq(sql: SqlStorage): number {
return Number(rows[0]?.s ?? 0)
}
+/**
+ * Durable high-water mark — the latest position the stream has reached, robust
+ * to retention pruning the changelog empty (`currentSeq` alone reads 0 then,
+ * which would hand SSR a bogus "no history" cursor for live rows; ADR-0011 D1).
+ * The drain cursor lives in `_sync_meta` and survives pruning; an undrained
+ * tail is covered by the MAX over the log itself.
+ */
+export function highWaterSeq(sql: SqlStorage): number {
+ return Math.max(currentSeq(sql), getDrainCursor(sql))
+}
+
/** Lowest `seq` still in the log — the retention floor for reconnect catch-up. */
export function minChangeSeq(sql: SqlStorage): number {
const rows = Array.from(
diff --git a/src/server/mixin.ts b/src/server/mixin.ts
index 6aaaed7..3c9a463 100644
--- a/src/server/mixin.ts
+++ b/src/server/mixin.ts
@@ -28,6 +28,7 @@ import {
currentSeq,
ensureTriggers,
getDrainCursor,
+ highWaterSeq,
hydrateRows,
initSchema,
minChangeSeq,
@@ -109,10 +110,27 @@ export interface SyncApi {
drainAndBroadcast(): void
}
+/** One consistent snapshot read for SSR (ADR-0011 D1): the request shape and
+ * result of `readSyncSnapshot`, callable over the DO binding as plain RPC. */
+export interface SyncSnapshotReq {
+ collection: string
+ where?: unknown
+ orderBy?: unknown
+ limit?: number
+}
+export interface SyncSnapshotRes {
+ rows: Array>
+ cursor: string
+}
+
/** The surface the mixin adds to `Base`. The four methods are real, runtime-
- * dispatched overrides so workerd finds them on the prototype. */
+ * dispatched overrides so workerd finds them on the prototype;
+ * `readSyncSnapshot` is public (not behind the `sync` facade) because DO RPC
+ * dispatches only on public instance members — one deliberate addition to the
+ * host-collision surface (ADR-0011 D1, amending ADR-0015's four-method rule). */
export interface SyncMixin {
readonly sync: SyncApi
+ readSyncSnapshot(req: SyncSnapshotReq, request: Request): Promise
fetch(request: Request): Promise
webSocketMessage(ws: WebSocket, message: string | ArrayBuffer): Promise
webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean): void | Promise
@@ -335,6 +353,52 @@ export function Syncable() {
// ---- runtime-dispatched overrides --------------------------------------
+ /**
+ * One consistent snapshot of a collection plus a durable resume cursor,
+ * WITHOUT a WebSocket — the SSR read path (ADR-0011 D1). Throws on an
+ * unknown collection or an un-lowerable predicate (fail loud; RPC
+ * propagates the error to the caller).
+ *
+ * `request` is REQUIRED and runs through the SAME gate as the WS upgrade:
+ * the configured `parseAttachment` — pass the claims-bearing Request the
+ * worker already forges (or forwards) for the socket path. One hook guards
+ * both paths, so an author's tenant check cannot be silently bypassed by
+ * the read path (and the minted claims are the seam where uniform
+ * read-scoping would land, on subs and snapshots alike). A rejecting
+ * parseAttachment rejects the RPC. The await happens BEFORE the reads:
+ * rows and cursor are still taken at one position (synchronous SQLite,
+ * no await between them).
+ *
+ * The cursor is the durable high-water mark, not bare `currentSeq` — see
+ * `highWaterSeq`. A cursor of "0" honestly means "no resume point" and the
+ * client must reconcile a fresh snapshot instead of catching up.
+ *
+ * BLOB parity (ADR-0017): the WS path's codec normalizes bare ArrayBuffer
+ * to Uint8Array; RPC structured clone would leak ArrayBuffer through, so
+ * rows are normalized here — one blob shape on both paths.
+ */
+ async readSyncSnapshot(req: SyncSnapshotReq, request: Request): Promise {
+ await this.#parseAttachmentHook(request) // the one gate (claims unused until read-scoping exists)
+ const coll = this.#registry.collections.get(req.collection)
+ if (!coll) throw new Error(`readSyncSnapshot: unknown collection '${req.collection}'`)
+ const query = compileSubsetQuery(req.collection, {
+ where: req.where,
+ orderBy: req.orderBy,
+ limit: req.limit,
+ })
+ const rows = Array.from(this.#sql.exec(query.sql, ...query.params)).map((row) => {
+ let out: Record | undefined
+ for (const [k, v] of Object.entries(row)) {
+ if (v instanceof ArrayBuffer) {
+ out ??= { ...row }
+ out[k] = new Uint8Array(v) as unknown as SqlStorageValue
+ }
+ }
+ return out ?? row
+ }) as Array>
+ return { rows, cursor: String(highWaterSeq(this.#sql)) }
+ }
+
override async fetch(req: Request): Promise {
if (req.headers.get("Upgrade") === "websocket" && this.#claimsUpgrade(req)) {
return this.#acceptSyncSocket(req)
@@ -1006,7 +1070,9 @@ export function Syncable() {
this.#send(ws, { t: "d", sub: sub.subId, key, op: change.op, cols: row, seq })
}
}
- this.#send(ws, { t: "uptodate", seq })
+ // Sub-scoped terminal: this catch-up is one subscription's bootstrap, not
+ // a socket-wide boundary (ADR-0011 D3). Still advances the client cursor.
+ this.#send(ws, { t: "uptodate", seq, sub: sub.subId })
}
/** Encode and send a server frame on one socket. Warns — and still sends
diff --git a/src/wire/frames.ts b/src/wire/frames.ts
index b2cd21b..bae1890 100644
--- a/src/wire/frames.ts
+++ b/src/wire/frames.ts
@@ -70,7 +70,11 @@ export type ServerFrame =
// Live delta; `cols` is a partial (top-level) patch, absent for delete.
| { t: "d"; sub: string; key: unknown; op: RowOp; cols?: Record; seq: Cursor }
// Batch boundary — client commits the buffered sync transaction here.
- | { t: "uptodate"; seq: Cursor }
+ // `sub` scopes a CATCH-UP's terminal to its subscription (ADR-0011 D3): a
+ // transient hydration catch-up must distinguish its own terminal from a
+ // coalescer/barrier boundary, or it unsubscribes early and drops its own
+ // deltas. Absent on broadcast boundaries (additive, backwards-compatible).
+ | { t: "uptodate"; seq: Cursor; sub?: string }
// Mutation receipt (the no-subscription-match path lives here; ADR-0002 C1/C2).
| { t: "committed"; txId: TxId; seq: Cursor; result?: unknown }
| { t: "rejected"; txId: TxId; error: { code?: string; message: string } }
diff --git a/tests/do-collection.test.ts b/tests/do-collection.test.ts
index cf332c5..c814edb 100644
--- a/tests/do-collection.test.ts
+++ b/tests/do-collection.test.ts
@@ -49,7 +49,7 @@ function startSync(transport: WebSocketTransport): { calls: Array
// sync lives on opts.sync.sync; invoke with spy controls (cast: type-only dep).
const syncConfig = (opts as unknown as { sync: { sync: (p: unknown) => void } }).sync
syncConfig.sync({
- collection: { get: () => undefined }, // adapter consults held keys (held-insert upsert)
+ collection: { get: () => undefined, _state: { syncedData: new Map() } }, // adapter consults synced rows
begin: () => calls.push(["begin"]),
write: (m: unknown) => calls.push(["write", m]),
commit: () => calls.push(["commit"]),
@@ -94,7 +94,7 @@ describe("doCollectionOptions (M3 adapter)", () => {
const adapter = doCollectionOptions({ transport: t, table: "messages", getKey: (r) => r.id })
const calls: Array = []
;(adapter as unknown as { sync: { sync: (p: unknown) => void } }).sync.sync({
- collection: { get: () => undefined },
+ collection: { get: () => undefined, _state: { syncedData: new Map() } },
begin: () => calls.push(["begin"]),
write: (m: unknown) => calls.push(["write", m]),
commit: () => calls.push(["commit"]),
diff --git a/tests/filtered-client.test.ts b/tests/filtered-client.test.ts
index 3454b53..57e62f8 100644
--- a/tests/filtered-client.test.ts
+++ b/tests/filtered-client.test.ts
@@ -52,7 +52,7 @@ function startFiltered(transport: WebSocketTransport, where: unknown):
const calls: Array = []
const adapter = doCollectionOptions({ transport, table: "messages", getKey: (r) => r.id, where })
;(adapter as unknown as { sync: { sync: (p: unknown) => void } }).sync.sync({
- collection: { get: () => undefined }, // adapter consults held keys (held-insert upsert)
+ collection: { get: () => undefined, _state: { syncedData: new Map() } }, // adapter consults synced rows
begin: () => calls.push(["begin"]),
write: (m: unknown) => calls.push(["write", m]),
commit: () => calls.push(["commit"]),
diff --git a/tests/read-sync-snapshot.test.ts b/tests/read-sync-snapshot.test.ts
new file mode 100644
index 0000000..a63bb1c
--- /dev/null
+++ b/tests/read-sync-snapshot.test.ts
@@ -0,0 +1,115 @@
+import { env, runInDurableObject } from "cloudflare:test"
+import { describe, expect, it } from "vitest"
+
+// WHY (ADR-0011 D1): SSR needs a snapshot + resume cursor out of the DO
+// WITHOUT a WebSocket. The cursor must be a DURABLE high-water mark — not
+// MAX(_sync_changes.seq), which retention can prune to 0 while the table still
+// has rows. A bogus cursor 0 against live rows means a delete landing between
+// render and hydration strands a stale row forever (the client can't resume,
+// and a fresh snapshot doesn't carry tombstones). These pin: rows+cursor read
+// at one position, predicate pushdown, fail-loud on unknown collections, and
+// the high-water surviving a pruned-empty changelog.
+
+type SnapshotReq = { collection: string; where?: unknown; orderBy?: unknown; limit?: number }
+type SnapshotRes = { rows: Array>; cursor: string }
+
+function stubFor(room: string): DurableObjectStub {
+ return env.SYNC_DO.get(env.SYNC_DO.idFromName(room))
+}
+
+/** Call over the binding like an SSR worker would (real RPC, not instance
+ * poking), passing the same claims-bearing Request the WS upgrade gets. */
+async function readSyncSnapshot(room: string, req: SnapshotReq, user = "anon"): Promise {
+ const stub = stubFor(room) as unknown as {
+ readSyncSnapshot: (r: SnapshotReq, request: Request) => Promise
+ }
+ return stub.readSyncSnapshot(req, new Request("https://example.com/ssr", { headers: { "x-user": user } }))
+}
+
+describe("readSyncSnapshot RPC (SSR read path, ADR-0011 D1)", () => {
+ it("runs the SAME gate as the WS upgrade: a rejecting parseAttachment rejects the read", async () => {
+ const room = `snap-gate-${crypto.randomUUID()}`
+ await runInDurableObject(stubFor(room), (_i, s) => {
+ s.storage.sql.exec("INSERT INTO messages(id,body) VALUES('a','secret')")
+ })
+ // The author's one auth hook guards both paths — a tenant check cannot be
+ // silently bypassed by the snapshot read.
+ await expect(readSyncSnapshot(room, { collection: "messages" }, "forbidden")).rejects.toThrow()
+ // ...and a passing identity reads normally.
+ const { rows } = await readSyncSnapshot(room, { collection: "messages" })
+ expect(rows).toHaveLength(1)
+ })
+
+ it("returns current rows and a cursor that resumes past them", async () => {
+ const room = `snap-${crypto.randomUUID()}`
+ await runInDurableObject(stubFor(room), (_i, s) => {
+ s.storage.sql.exec("INSERT INTO messages(id,body) VALUES('a','hi'),('b','yo')")
+ })
+
+ const { rows, cursor } = await readSyncSnapshot(room, { collection: "messages" })
+ expect(rows.map((r) => r.id).sort()).toEqual(["a", "b"])
+ // The cursor covers the snapshot: every change that produced these rows is
+ // at or below it, so a client resuming from it re-receives nothing.
+ expect(BigInt(cursor)).toBeGreaterThanOrEqual(2n)
+
+ // A later write is ABOVE the cursor — exactly what catch-up will deliver.
+ await runInDurableObject(stubFor(room), (_i, s) => {
+ s.storage.sql.exec("INSERT INTO messages(id,body) VALUES('c','new')")
+ })
+ const after = await readSyncSnapshot(room, { collection: "messages" })
+ expect(BigInt(after.cursor)).toBeGreaterThan(BigInt(cursor))
+ })
+
+ it("pushes the where predicate into the read", async () => {
+ const room = `snap-where-${crypto.randomUUID()}`
+ await runInDurableObject(stubFor(room), (_i, s) => {
+ s.storage.sql.exec("INSERT INTO messages(id,body) VALUES('a','keep'),('b','drop')")
+ })
+ // The serialized @tanstack/db IR shape a collection's `where` carries.
+ const where = { type: "func", name: "gt", args: [{ type: "ref", path: ["id"] }, { type: "val", value: "a" }] }
+ const { rows } = await readSyncSnapshot(room, { collection: "messages", where })
+ expect(rows.map((r) => r.id)).toEqual(["b"])
+ })
+
+ it("throws on an unknown collection (fail loud, not empty-success)", async () => {
+ const room = `snap-unknown-${crypto.randomUUID()}`
+ await runInDurableObject(stubFor(room), () => {}) // materialize schema
+ await expect(readSyncSnapshot(room, { collection: "nope" })).rejects.toThrow(/unknown collection/)
+ })
+
+ it("normalizes BLOB values to Uint8Array — parity with the wire codec (ADR-0017)", async () => {
+ const room = `snap-blob-${crypto.randomUUID()}`
+ await runInDurableObject(stubFor(room), (_i, s) => {
+ // SQLite affinity doesn't coerce blobs: a TEXT column stores this as BLOB
+ // and reads it back as ArrayBuffer — exactly what RPC structured clone
+ // would leak through where the WS codec normalizes to Uint8Array.
+ s.storage.sql.exec("INSERT INTO messages(id,body) VALUES('a', ?)", new Uint8Array([1, 2, 3]))
+ })
+ const { rows } = await readSyncSnapshot(room, { collection: "messages" })
+ expect(rows[0]!.body).toBeInstanceOf(Uint8Array)
+ expect(Array.from(rows[0]!.body as Uint8Array)).toEqual([1, 2, 3])
+ })
+
+ it("keeps a durable high-water cursor when retention has pruned the changelog empty", async () => {
+ const room = `snap-prune-${crypto.randomUUID()}`
+ await runInDurableObject(stubFor(room), (_i, s) => {
+ s.storage.sql.exec("INSERT INTO messages(id,body) VALUES('a','hi')")
+ })
+ const before = await readSyncSnapshot(room, { collection: "messages" })
+ expect(BigInt(before.cursor)).toBeGreaterThan(0n)
+
+ // Simulate retention pruning the whole log away (time passing). The drain
+ // cursor in _sync_meta is the durable survivor the high-water must use.
+ await runInDurableObject(stubFor(room), (_i, s) => {
+ s.storage.sql.exec(
+ "INSERT INTO _sync_meta(k,v) VALUES('drain_cursor', ?) ON CONFLICT(k) DO UPDATE SET v=excluded.v",
+ String(before.cursor),
+ )
+ s.storage.sql.exec("DELETE FROM _sync_changes")
+ })
+
+ const after = await readSyncSnapshot(room, { collection: "messages" })
+ expect(after.rows).toHaveLength(1) // table rows are untouched by retention
+ expect(BigInt(after.cursor)).toBeGreaterThanOrEqual(BigInt(before.cursor)) // never regresses to 0
+ })
+})
diff --git a/tests/ssr-adapter.test.ts b/tests/ssr-adapter.test.ts
new file mode 100644
index 0000000..715b33b
--- /dev/null
+++ b/tests/ssr-adapter.test.ts
@@ -0,0 +1,309 @@
+import { describe, expect, it } from "vitest"
+import { doCollectionOptions, type DoSyncMeta } from "../src/client/do-collection.ts"
+import type { Transport } from "../src/client/transport.ts"
+
+// WHY (ADR-0011 D3, adapter-level ordering): on-demand readiness must GATE on
+// the transient hydration catch-up sub being SENT — loadSubset subs only fire
+// after ready, so on the single ordered socket the catch-up's truncate/deltas
+// always precede subset snapshots. A markReady racing ahead (the bug: its
+// connect().then() was registered first) lets a subset snapshot land at a seq
+// the catch-up then stomps over — or, below the floor, lets the catch-up's
+// truncate WIPE an already-loaded subset. Also pins the syncMeta hook
+// contract: export shape, import validation, where-fingerprint downgrade,
+// min-merge.
+
+interface Msg {
+ id: string
+ body: string
+}
+
+/** Structural schema Api the branded transport carries, so `doCollectionOptions`
+ * infers Row = Msg for `table: "messages"` (matches `RowOf`/`CollectionName`). */
+type Api = { collections: { messages: { __row?: Msg } } }
+
+type Hooked = {
+ sync: {
+ sync: (p: unknown) => unknown
+ exportSyncMeta: () => DoSyncMeta
+ importSyncMeta: (m: unknown) => void
+ mergeSyncMeta: (a: unknown, b: unknown) => DoSyncMeta
+ }
+}
+
+function spyTransport(calls: Array): Transport {
+ return {
+ connect: async () => {
+ calls.push("connect")
+ },
+ subscribe: async (subId, _collection, _handler, _where, _orderBy, _limit, since) => {
+ calls.push(`sub:${subId}:since=${since ?? "none"}`)
+ },
+ unsubscribe: (subId: string) => {
+ calls.push(`unsub:${subId}`)
+ },
+ sendMut: () => Promise.reject(new Error("unused")),
+ fetch: () => Promise.reject(new Error("unused")),
+ close: () => {},
+ appliedCursor: "7",
+ hasPosition: true,
+ seedCursor: () => {
+ calls.push("seed")
+ },
+ }
+}
+
+const controls = {
+ collection: { get: () => undefined },
+ begin: () => {},
+ write: () => {},
+ commit: () => {},
+ truncate: () => {},
+}
+
+const flush = (): Promise => new Promise((r) => setTimeout(r, 0))
+
+describe("hydrated on-demand start ordering", () => {
+ it("ready waits for the catch-up sub to be SENT; the catch-up precedes any subset sub", async () => {
+ const calls: Array = []
+ const opts = doCollectionOptions({
+ transport: spyTransport(calls),
+ table: "messages",
+ getKey: (r) => r.id,
+ syncMode: "on-demand",
+ }) as unknown as Hooked
+ opts.sync.importSyncMeta({ v: 1, cursor: "5" })
+ opts.sync.sync({ ...controls, markReady: () => calls.push("ready") })
+ await flush()
+
+ const catchup = calls.findIndex((c) => c.startsWith("sub:messages#hydrate#") && c.endsWith("since=5"))
+ const ready = calls.indexOf("ready")
+ expect(catchup).toBeGreaterThanOrEqual(0)
+ expect(ready).toBeGreaterThan(catchup)
+ })
+
+ it("without hydration there is no catch-up sub and ready follows connect", async () => {
+ const calls: Array = []
+ const opts = doCollectionOptions({
+ transport: spyTransport(calls),
+ table: "messages",
+ getKey: (r) => r.id,
+ syncMode: "on-demand",
+ }) as unknown as Hooked
+ opts.sync.sync({ ...controls, markReady: () => calls.push("ready") })
+ await flush()
+ expect(calls.filter((c) => c.startsWith("sub:"))).toEqual([])
+ expect(calls).toContain("ready")
+ })
+
+ it("no resume point ('0'): hydrated rows are truncated, not left to go stale", async () => {
+ const calls: Array = []
+ const truncated: Array = []
+ const opts = doCollectionOptions({
+ transport: spyTransport(calls),
+ table: "messages",
+ getKey: (r) => r.id,
+ syncMode: "on-demand",
+ }) as unknown as Hooked
+ opts.sync.importSyncMeta({ v: 1, cursor: "0" })
+ opts.sync.sync({
+ ...controls,
+ truncate: () => truncated.push("truncate"),
+ markReady: () => calls.push("ready"),
+ })
+ await flush()
+ expect(truncated).toEqual(["truncate"])
+ expect(calls.filter((c) => c.startsWith("sub:"))).toEqual([]) // no unfiltered full snapshot
+ expect(calls).toContain("ready")
+ })
+})
+
+describe("syncMeta hooks", () => {
+ const eq = (field: string, value: unknown): unknown => ({
+ type: "func",
+ name: "eq",
+ args: [
+ { type: "ref", path: [field] },
+ { type: "val", value },
+ ],
+ })
+
+ function makeOpts(where?: unknown): Hooked {
+ return doCollectionOptions({
+ transport: spyTransport([]),
+ table: "messages",
+ getKey: (r) => r.id,
+ where,
+ }) as unknown as Hooked
+ }
+
+ it("export round-trips through import; the eager where is fingerprinted", () => {
+ const a = makeOpts(eq("body", "keep"))
+ const meta = a.sync.exportSyncMeta()
+ expect(meta).toMatchObject({ v: 1, cursor: "7" })
+ expect(typeof meta.where).toBe("string")
+ a.sync.importSyncMeta(meta) // same fingerprint: accepted (no throw)
+ })
+
+ it("a DIFFERENT where downgrades the cursor to the snapshot-reconcile path", async () => {
+ const calls: Array = []
+ const renderSide = makeOpts(eq("body", "keep"))
+ const meta = renderSide.sync.exportSyncMeta()
+
+ const clientSide = doCollectionOptions({
+ transport: spyTransport(calls),
+ table: "messages",
+ getKey: (r) => r.id,
+ where: eq("body", "other"),
+ }) as unknown as Hooked
+ clientSide.sync.importSyncMeta(meta)
+ expect(calls).not.toContain("seed") // an unsound cursor is never claimed
+ clientSide.sync.sync({ ...controls, markReady: () => {} })
+ await flush()
+ // The eager sub must NOT resume from the foreign cursor.
+ expect(calls.some((c) => c.startsWith("sub:") && c.endsWith("since=none"))).toBe(true)
+ })
+
+ it("rejects meta it does not understand — never resumes from garbage", () => {
+ const o = makeOpts()
+ expect(() => o.sync.importSyncMeta({ v: 2, cursor: "5" })).toThrow(/unrecognized sync meta/)
+ expect(() => o.sync.importSyncMeta({ v: 1, cursor: "not-a-seq" })).toThrow()
+ expect(() => o.sync.importSyncMeta(null)).toThrow(/unrecognized sync meta/)
+ })
+
+ it("unrecognized meta fails loud BUT safe: the rows already landed, so sync still reconciles", async () => {
+ // Upstream applies the chunk's rows BEFORE importSyncMeta — a throw can't
+ // veto them. If the throw also skipped our bookkeeping, sync would start
+ // down the non-hydrated path and a server-deleted hydrated row would be
+ // stale forever. The throw must leave the safe state behind: no resume
+ // point ("0") → snapshot + reconcile.
+ const calls: Array = []
+ const o = doCollectionOptions({
+ transport: spyTransport(calls),
+ table: "messages",
+ getKey: (r) => r.id,
+ }) as unknown as Hooked
+ expect(() => o.sync.importSyncMeta({ v: 99, cursor: "5" })).toThrow(/unrecognized sync meta/)
+ expect(calls).not.toContain("seed") // a cursor we can't read is never claimed
+ o.sync.sync({ ...controls, markReady: () => {} })
+ await flush()
+ // Snapshot path (no since) — where the always-armed eager reconcile lives.
+ expect(calls.some((c) => c.startsWith("sub:") && c.endsWith("since=none"))).toBe(true)
+ })
+
+ it("merge takes the EARLIER cursor — replay is idempotent, skipping is not", () => {
+ const o = makeOpts()
+ const merged = o.sync.mergeSyncMeta({ v: 1, cursor: "90" }, { v: 1, cursor: "100" })
+ expect(merged.cursor).toBe("90")
+ expect(o.sync.mergeSyncMeta({ v: 1, cursor: "100" }, { v: 1, cursor: "90" }).cursor).toBe("90")
+ })
+
+ it("a NEGATIVE cursor is rejected — and on-demand still lands on the safe truncate path", async () => {
+ // codex finding: BigInt("-1") parses, seedCursor ignores it, but
+ // `since:"-1"` on the wire makes the server answer a full snapshot the
+ // catch-up handler discards — no terminal, the transient sub never tears
+ // down, stale hydrated rows survive forever. Parse must refuse it; the
+ // fail-loud-but-safe contract then routes sync start to "0" (truncate).
+ const calls: Array = []
+ const truncated: Array = []
+ const o = doCollectionOptions({
+ transport: spyTransport(calls),
+ table: "messages",
+ getKey: (r) => r.id,
+ syncMode: "on-demand",
+ }) as unknown as Hooked
+ expect(() => o.sync.importSyncMeta({ v: 1, cursor: "-1" })).toThrow(/negative/)
+ expect(calls).not.toContain("seed")
+ o.sync.sync({ ...controls, truncate: () => truncated.push("truncate"), markReady: () => {} })
+ await flush()
+ expect(truncated).toEqual(["truncate"]) // safe "0" route, not a since:"-1" catch-up
+ expect(calls.filter((c) => c.startsWith("sub:"))).toEqual([])
+ })
+
+ it("merge with MISMATCHED fingerprints yields cursor '0' — no sound joint resume point", () => {
+ // codex finding: MIN alone can return the side whose fingerprint matches
+ // ours while the OTHER side's foreign-filter rows were already applied —
+ // import would then accept a cursor whose catch-up never covers them.
+ const o = makeOpts() // our fingerprint: undefined
+ const merged = o.sync.mergeSyncMeta({ v: 1, cursor: "50", where: "AAA" }, { v: 1, cursor: "100", where: "BBB" })
+ expect(merged.cursor).toBe("0")
+ expect(merged.where).toBeUndefined() // OUR fingerprint, so import routes it to "0"
+ // Same fingerprints (even a foreign one) still MIN — import does the
+ // ours-vs-theirs check.
+ expect(o.sync.mergeSyncMeta({ v: 1, cursor: "50", where: "AAA" }, { v: 1, cursor: "100", where: "AAA" }).cursor).toBe("50")
+ })
+})
+
+describe("commit receipts (0.8.5 SyncAppliedReceipt)", () => {
+ /** Spy transport that CAPTURES subscribe handlers so a test can drive them. */
+ function capturingTransport(calls: Array, handlers: Map): Transport {
+ const t = spyTransport(calls)
+ return {
+ ...t,
+ subscribe: async (subId, _c, handler) => {
+ calls.push(`sub:${subId}`)
+ handlers.set(subId, handler)
+ },
+ }
+ }
+
+ it("a REJECTED receipt fails the subset load — an aborted application is not success", async () => {
+ const calls: Array = []
+ const handlers = new Map()
+ const opts = doCollectionOptions({
+ transport: capturingTransport(calls, handlers),
+ table: "messages",
+ getKey: (r) => r.id,
+ syncMode: "on-demand",
+ }) as unknown as Hooked
+ const res = opts.sync.sync({
+ ...controls,
+ commit: () => Promise.reject(new Error("aborted application")),
+ markReady: () => {},
+ markError: () => {},
+ } as never) as { loadSubset: (o: unknown) => true | Promise }
+ await flush()
+ const load = res.loadSubset({}) as Promise
+ const settled = load.then(
+ () => "resolved",
+ (e: Error) => `rejected:${e.message}`,
+ )
+ await flush()
+ const h = handlers.get("messages#null")!
+ h.onSnap(undefined, { id: "a", body: "x" })
+ h.onSnapEnd() // flush → rejected receipt
+ await expect(settled).resolves.toBe("rejected:aborted application")
+ })
+
+ it("exportSyncMeta never claims a boundary whose receipt is unsettled", async () => {
+ // codex finding: the transport cursor advances at the boundary, but the
+ // commit's application can settle later — a dehydrate in that window would
+ // serialize pre-boundary rows under meta claiming the boundary.
+ const calls: Array = []
+ const handlers = new Map()
+ let settle!: () => void
+ const receipt = new Promise((r) => {
+ settle = r
+ })
+ const opts = doCollectionOptions({
+ transport: capturingTransport(calls, handlers),
+ table: "messages",
+ getKey: (r) => r.id,
+ }) as unknown as Hooked
+ expect(opts.sync.exportSyncMeta().cursor).toBe("7") // nothing pending: the live claim
+ opts.sync.sync({
+ ...controls,
+ collection: { get: () => undefined, _state: { syncedData: new Map() } },
+ commit: () => receipt,
+ markReady: () => {},
+ markError: () => {},
+ } as never)
+ await flush()
+ const h = handlers.get([...handlers.keys()][0]!)!
+ h.onSnap(undefined, { id: "a", body: "x" })
+ h.onSnapEnd() // flush → pending receipt
+ expect(opts.sync.exportSyncMeta().cursor).toBe("0") // under-claim, never the unproven "7"
+ settle()
+ await flush()
+ expect(opts.sync.exportSyncMeta().cursor).toBe("7") // settled: live claim again
+ })
+})
diff --git a/tests/ssr-cursor.test.ts b/tests/ssr-cursor.test.ts
new file mode 100644
index 0000000..3f73d08
--- /dev/null
+++ b/tests/ssr-cursor.test.ts
@@ -0,0 +1,309 @@
+import { env, runInDurableObject, SELF } from "cloudflare:test"
+import { describe, expect, it } from "vitest"
+import { type SubHandler, WebSocketTransport, type WebSocketLike } from "../src/client/transport.ts"
+import { createFrameCodec } from "../src/wire/frame-codec.ts"
+import type { ClientFrame, ServerFrame } from "../src/wire/frames.ts"
+
+// WHY (ADR-0011 D3): SSR hydration hands a client rows it did not stream — so
+// the FIRST sub must be able to resume from the dehydrated cursor (server
+// catch-up, not a redundant snapshot), and the transport must be able to claim
+// that position before/around live traffic:
+// - seedCursor before any advance: a drop in the bootstrap window otherwise
+// resubscribes from 0 → fresh snapshot over hydrated rows → a row deleted
+// server-side meanwhile is never removed (snapshots carry no tombstones).
+// - seedCursor AFTER live advance (a late streamed SSR chunk): upstream has
+// already applied the chunk's possibly-stale rows — we cannot veto. The
+// transport claims the SHORTER prefix (always safe) and resubscribes, so
+// the catch-up replay re-freshens exactly the clobbered window.
+
+interface Rec {
+ events: Array<[string, ...Array]>
+ handler: SubHandler
+}
+function recorder(): Rec {
+ const events: Array<[string, ...Array]> = []
+ return {
+ events,
+ handler: {
+ onSnap: (k, r) => events.push(["snap", k, r]),
+ onSnapEnd: () => events.push(["snap-end"]),
+ onDelta: (op, k, c) => events.push(["d", op, k, c]),
+ onUptodate: () => events.push(["uptodate"]),
+ onReset: () => events.push(["reset"]),
+ },
+ }
+}
+
+function makeTransport(room: string): WebSocketTransport {
+ return new WebSocketTransport({
+ url: `https://example.com/sync/${room}`,
+ reconnectDelay: 20,
+ open: async () => {
+ const res = await SELF.fetch(`https://example.com/sync/${room}`, { headers: { Upgrade: "websocket" } })
+ const ws = res.webSocket
+ if (!ws) throw new Error("no webSocket")
+ ws.accept()
+ return ws as unknown as WebSocketLike
+ },
+ })
+}
+
+async function waitFor(pred: () => boolean, timeoutMs = 3000): Promise {
+ const start = Date.now()
+ while (!pred()) {
+ if (Date.now() - start > timeoutMs) throw new Error("waitFor timeout")
+ await new Promise((r) => setTimeout(r, 5))
+ }
+}
+
+function stubFor(room: string): DurableObjectStub {
+ return env.SYNC_DO.get(env.SYNC_DO.idFromName(room))
+}
+
+async function snapshotCursor(room: string): Promise {
+ const stub = stubFor(room) as unknown as {
+ readSyncSnapshot: (r: { collection: string }, request: Request) => Promise<{ rows: Array; cursor: string }>
+ }
+ return (await stub.readSyncSnapshot({ collection: "messages" }, new Request("https://example.com/ssr", { headers: { "x-user": "anon" } }))).cursor
+}
+
+describe("transport cursor bootstrap (SSR hydration, ADR-0011 D3)", () => {
+ it("a FIRST sub carrying `since` gets a catch-up, not a snapshot", async () => {
+ const room = `ssr-since-${crypto.randomUUID()}`
+ await runInDurableObject(stubFor(room), (_i, s) => {
+ s.storage.sql.exec("INSERT INTO messages(id,body) VALUES('a','hydrated')")
+ })
+ const cursor = await snapshotCursor(room) // what dehydration exported
+ await runInDurableObject(stubFor(room), (_i, s) => {
+ s.storage.sql.exec("INSERT INTO messages(id,body) VALUES('b','missed')")
+ })
+
+ const t = makeTransport(room)
+ const { events, handler } = recorder()
+ await t.subscribe("s1", "messages", handler, undefined, undefined, undefined, cursor)
+ await waitFor(() => events.some((e) => e[0] === "uptodate"))
+
+ // The hydrated row is NOT re-streamed; only the post-cursor change is.
+ expect(events.some((e) => e[0] === "snap")).toBe(false)
+ expect(events.some((e) => e[0] === "snap-end")).toBe(false)
+ expect(events.some((e) => e[0] === "d" && e[2] === "b")).toBe(true)
+ expect(events.some((e) => e[0] === "d" && e[2] === "a")).toBe(false)
+ t.close()
+ })
+
+ it("seedCursor claims the dehydrated position before any advance", async () => {
+ const room = `ssr-seed-${crypto.randomUUID()}`
+ await runInDurableObject(stubFor(room), (_i, s) => {
+ s.storage.sql.exec("INSERT INTO messages(id,body) VALUES('a','hydrated')")
+ })
+ const cursor = await snapshotCursor(room)
+
+ const t = makeTransport(room)
+ expect(t.appliedCursor).toBe("0")
+ t.seedCursor(cursor)
+ expect(t.appliedCursor).toBe(cursor)
+ // A seed can never grow the claim without data.
+ t.seedCursor(String(BigInt(cursor) + 100n))
+ expect(t.appliedCursor).toBe(cursor)
+ t.close()
+ })
+
+ it("a late seed (streamed chunk after live advance) regresses the claim and replays the window", async () => {
+ const room = `ssr-late-${crypto.randomUUID()}`
+ await runInDurableObject(stubFor(room), (_i, s) => {
+ s.storage.sql.exec("INSERT INTO messages(id,body) VALUES('a','v1')")
+ })
+ const chunkCursor = await snapshotCursor(room) // a chunk dehydrated NOW...
+
+ const t = makeTransport(room)
+ const { events, handler } = recorder()
+ await t.subscribe("s1", "messages", handler, undefined, undefined, undefined, chunkCursor)
+ await waitFor(() => events.some((e) => e[0] === "uptodate"))
+
+ // ...but it arrives LATE: live sync has moved on past another write
+ // (driven through a real mut — raw SQL never broadcasts, ADR-0006).
+ const t2 = makeTransport(room)
+ await t2.sendMut({
+ t: "mut",
+ txId: `tx-${crypto.randomUUID()}`,
+ collection: "messages",
+ ops: [{ type: "update", key: "a", cols: { body: "v2" } }],
+ })
+ await waitFor(() => events.some((e) => e[0] === "d" && e[2] === "a"))
+ t2.close()
+ const advanced = t.appliedCursor
+ expect(BigInt(advanced)).toBeGreaterThan(BigInt(chunkCursor))
+ const before = events.length
+
+ // Upstream already applied the chunk's stale rows; the transport claims
+ // the shorter prefix and resubscribes — the replayed catch-up delivers the
+ // post-chunk window again (idempotent) and re-freshens clobbered rows.
+ t.seedCursor(chunkCursor)
+ expect(t.appliedCursor).toBe(chunkCursor)
+ await waitFor(() => events.slice(before).some((e) => e[0] === "d" && e[2] === "a"))
+ await waitFor(() => BigInt(t.appliedCursor) >= BigInt(advanced))
+ expect(events.slice(before).some((e) => e[0] === "snap")).toBe(false) // replay, not re-snapshot
+ t.close()
+ })
+
+ it("a stale pre-regress boundary cannot re-advance the claim; the fresh socket replays from the seed", async () => {
+ // Fully fake sockets: a live regress rides a RECONNECT because the old
+ // socket's already-queued boundary frames (full duplex) would otherwise
+ // re-advance the cursor past the repair window — then a drop would resume
+ // beyond it and the late chunk's clobbered rows would stay stale forever.
+ const codec = createFrameCodec()
+ interface Fake {
+ ws: WebSocketLike
+ sent: Array
+ emit: (type: string, ev: { data?: unknown }) => void
+ closeCalled: boolean
+ }
+ const makeFake = (): Fake => {
+ const listeners = new Map void>>()
+ const fake: Fake = {
+ sent: [],
+ closeCalled: false,
+ emit: (type, ev) => {
+ for (const l of listeners.get(type) ?? []) l(ev)
+ },
+ ws: {
+ send: (data) => fake.sent.push(codec.decode(data as ArrayBuffer | string) as ClientFrame),
+ close: () => {
+ fake.closeCalled = true // close event delivery is the TEST's choice
+ },
+ addEventListener: (type, l) => {
+ const arr = listeners.get(type) ?? []
+ arr.push(l)
+ listeners.set(type, arr)
+ },
+ removeEventListener: () => {},
+ },
+ }
+ return fake
+ }
+ const fakes: Array = []
+ const t = new WebSocketTransport({
+ url: "wss://fake",
+ reconnectDelay: 1,
+ open: () => {
+ const f = makeFake()
+ fakes.push(f)
+ return f.ws
+ },
+ })
+ const { handler } = recorder()
+ await t.subscribe("s1", "messages", handler)
+ const server = (frame: ServerFrame, fake = fakes.at(-1)!): void =>
+ fake.emit("message", { data: codec.encode(frame) })
+
+ server({ t: "snap-end", sub: "s1", seq: "100" })
+ expect(t.appliedCursor).toBe("100")
+
+ // Late chunk → regress. The transport must abandon this socket.
+ t.seedCursor("50")
+ expect(t.appliedCursor).toBe("50")
+ expect(fakes[0]!.closeCalled).toBe(true)
+
+ // A boundary the server sent BEFORE the close (still queued client-side)
+ // must not count: the claim holds at the seed.
+ server({ t: "uptodate", seq: "101" }, fakes[0]!)
+ expect(t.appliedCursor).toBe("50")
+
+ // Now the close lands; the fresh socket resubscribes FROM the seed...
+ fakes[0]!.emit("close", {})
+ await waitFor(() => fakes.length === 2 && fakes[1]!.sent.some((f) => f.t === "sub"))
+ const resub = fakes[1]!.sent.find((f) => f.t === "sub") as Extract
+ expect(resub.since).toBe("50")
+
+ // ...and its frames own the cursor again.
+ server({ t: "uptodate", seq: "102" })
+ expect(t.appliedCursor).toBe("102")
+ t.close()
+ })
+
+ it("an abandoned socket still settles ID-scoped receipts — but never advances the cursor", async () => {
+ // codex finding: a regress-reconnect must not convert a COMMITTED mutation
+ // into a timeout — receipts are not re-covered by any replay. Stream
+ // frames from the stale socket stay dropped (the catch-up re-covers them).
+ const codec = createFrameCodec()
+ const fakes: Array<{
+ ws: WebSocketLike
+ sent: Array
+ emit: (type: string, ev: { data?: unknown }) => void
+ }> = []
+ const makeFake = () => {
+ const listeners = new Map void>>()
+ const fake = {
+ sent: [] as Array,
+ emit: (type: string, ev: { data?: unknown }) => {
+ for (const l of listeners.get(type) ?? []) l(ev)
+ },
+ ws: {
+ send: (data: string | ArrayBuffer | ArrayBufferView) =>
+ fake.sent.push(codec.decode(data as ArrayBuffer | string) as ClientFrame),
+ close: () => {},
+ addEventListener: (type: string, l: (ev: { data?: unknown }) => void) => {
+ const arr = listeners.get(type) ?? []
+ arr.push(l)
+ listeners.set(type, arr)
+ },
+ removeEventListener: () => {},
+ } as WebSocketLike,
+ }
+ fakes.push(fake)
+ return fake
+ }
+ const t = new WebSocketTransport({ url: "wss://fake", reconnectDelay: 1, open: () => makeFake().ws })
+ const { handler } = recorder()
+ await t.subscribe("s1", "messages", handler)
+ const emit = (frame: ServerFrame, i = fakes.length - 1): void =>
+ fakes[i]!.emit("message", { data: codec.encode(frame) })
+
+ emit({ t: "snap-end", sub: "s1", seq: "100" })
+ const mut = t.sendMut({ t: "mut", txId: "tx-1", collection: "messages", ops: [] })
+ const page = t.fetch({ t: "fetch", fetchId: "f-1", collection: "messages" })
+ // Both frames must be ON the old socket before the regress abandons it.
+ await waitFor(() => fakes[0]!.sent.some((f) => f.t === "mut") && fakes[0]!.sent.some((f) => f.t === "fetch"))
+
+ t.seedCursor("50") // regress → the socket that carries tx-1/f-1 is abandoned
+ expect(t.appliedCursor).toBe("50")
+
+ // The stale socket's receipts settle their waiters...
+ emit({ t: "committed", txId: "tx-1", seq: "101" }, 0)
+ emit({ t: "page", fetchId: "f-1", rows: [{ id: "x" }], seq: "101" }, 0)
+ await expect(mut).resolves.toEqual({ result: undefined })
+ await expect(page).resolves.toEqual([{ id: "x" }])
+ // ...but never the cursor; and its stream frames are dropped outright.
+ expect(t.appliedCursor).toBe("50")
+ emit({ t: "uptodate", seq: "102" }, 0)
+ expect(t.appliedCursor).toBe("50")
+ t.close()
+ })
+
+ it("unsubscribing while a subscribe's connect is in flight sends no ghost sub", async () => {
+ // codex finding: the suspended subscribe() body would send its `sub` after
+ // the open completes even though the handler is gone — the server persists
+ // a subscription nothing consumes (ADR-0019) until the socket drops.
+ const codec = createFrameCodec()
+ let openNow!: (ws: WebSocketLike) => void
+ const sent: Array = []
+ const ws: WebSocketLike = {
+ send: (data) => sent.push(codec.decode(data as ArrayBuffer | string) as ClientFrame),
+ close: () => {},
+ addEventListener: () => {},
+ removeEventListener: () => {},
+ }
+ const t = new WebSocketTransport({
+ url: "wss://fake",
+ reconnectDelay: 1,
+ open: () => new Promise((r) => (openNow = r)),
+ })
+ const { handler } = recorder()
+ const sub = t.subscribe("s1", "messages", handler) // parks on the pending open
+ t.unsubscribe("s1") // no socket yet: nothing to send the unsub on
+ openNow(ws)
+ await sub
+ expect(sent.filter((f) => f.t === "sub")).toEqual([])
+ t.close()
+ })
+})
diff --git a/tests/ssr-hydration.test.ts b/tests/ssr-hydration.test.ts
new file mode 100644
index 0000000..6766188
--- /dev/null
+++ b/tests/ssr-hydration.test.ts
@@ -0,0 +1,287 @@
+import { createLiveQueryCollection, DbClient, collectionOptions, eq } from "@tanstack/db"
+import { env, runInDurableObject, SELF } from "cloudflare:test"
+import { describe, expect, it } from "vitest"
+import { doCollectionOptions } from "../src/client/do-collection.ts"
+import { SsrSnapshotTransport, type SnapshotRead } from "../src/client/ssr-transport.ts"
+import { WebSocketTransport, type WebSocketLike } from "../src/client/transport.ts"
+import type { ClientFrame } from "../src/wire/frames.ts"
+
+// WHY (ADR-0011, end to end): the whole point of SSR support is one specific
+// promise — the browser renders the worker's dehydrated rows IMMEDIATELY, then
+// CONVERGES to the DO's current truth without wedging, flashing empty, or
+// stranding a deleted row. Rows ride TanStack's DehydratedDbState; our cursor
+// rides the opaque syncMeta. These tests run the REAL upstream DbClient
+// (released ≥0.8.5 — PR #1564 as merged) on both sides: a server-side DbClient + snapshot
+// transport renders and dehydrates; a client-side DbClient hydrates over a
+// WebSocket transport; writes land on the DO between the two. Convergence —
+// updates applied, deletes applied, no DuplicateKeySyncError, even with no
+// resume point — is the contract.
+
+interface Msg {
+ id: string
+ body: string
+}
+
+/** Structural schema Api the branded transports carry, so `doCollectionOptions`
+ * infers Row = Msg for `table: "messages"`. */
+type Api = { collections: { messages: { __row?: Msg } } }
+
+function makeRead(room: string): SnapshotRead {
+ const stub = env.SYNC_DO.get(env.SYNC_DO.idFromName(room)) as unknown as {
+ readSyncSnapshot: (r: Parameters[0], request: Request) => ReturnType
+ }
+ return (req) => stub.readSyncSnapshot(req, new Request("https://example.com/ssr", { headers: { "x-user": "anon" } }))
+}
+
+function makeWsTransport(room: string): WebSocketTransport {
+ return new WebSocketTransport({
+ url: `https://example.com/sync/${room}`,
+ reconnectDelay: 20,
+ open: async () => {
+ const res = await SELF.fetch(`https://example.com/sync/${room}`, { headers: { Upgrade: "websocket" } })
+ const ws = res.webSocket
+ if (!ws) throw new Error("no webSocket")
+ ws.accept()
+ return ws as unknown as WebSocketLike
+ },
+ })
+}
+
+async function sql(room: string, ...statements: Array): Promise {
+ await runInDurableObject(env.SYNC_DO.get(env.SYNC_DO.idFromName(room)), (_i, s) => {
+ for (const stmt of statements) s.storage.sql.exec(stmt)
+ })
+}
+
+async function waitFor(pred: () => boolean, timeoutMs = 3000): Promise {
+ const start = Date.now()
+ while (!pred()) {
+ if (Date.now() - start > timeoutMs) throw new Error("waitFor timeout")
+ await new Promise((r) => setTimeout(r, 5))
+ }
+}
+
+/** The branded options DbClient wants, around our adapter. One per "process". */
+function makeOptions(
+ transport: WebSocketTransport | SsrSnapshotTransport,
+ syncMode?: "eager" | "on-demand",
+ where?: unknown,
+) {
+ return collectionOptions(
+ doCollectionOptions({ transport, table: "messages", getKey: (r) => r.id, syncMode, where }) as never,
+ ) as never
+}
+
+/** Server render: per-request DbClient + snapshot transport → dehydrated state. */
+async function serverRender(room: string, syncMode?: "eager" | "on-demand", where?: unknown) {
+ const transport = new SsrSnapshotTransport({ read: makeRead(room) })
+ const db = new DbClient()
+ const col = db.collection(makeOptions(transport, syncMode, where)) as unknown as {
+ preload: () => Promise
+ get: (k: string) => Msg | undefined
+ }
+ if (syncMode === "on-demand") {
+ const kept = createLiveQueryCollection((q) =>
+ q.from({ m: col as never }).where(({ m }: { m: Msg }) => eq(m.body, "keep")),
+ )
+ await kept.preload()
+ } else {
+ await col.preload()
+ }
+ return db.dehydrate()
+}
+
+const whereEq = (field: string, value: unknown): unknown => ({
+ type: "func",
+ name: "eq",
+ args: [
+ { type: "ref", path: [field] },
+ { type: "val", value },
+ ],
+})
+
+describe("SSR round trip: dehydrate on the worker, hydrate + converge in the browser", () => {
+ it("eager: hydrated rows render immediately, then converge (update applied, delete applied)", async () => {
+ const room = `rt-eager-${crypto.randomUUID()}`
+ await sql(room, "INSERT INTO messages(id,body) VALUES('a','v1'),('b','doomed'),('c','calm')")
+
+ const state = await serverRender(room)
+ const chunk = state.collections[0]!
+ expect(chunk.collectionId).toBe("messages")
+ expect(chunk.rows).toHaveLength(3)
+ expect(chunk.syncMeta).toMatchObject({ v: 1 })
+ const dehydratedCursor = (chunk.syncMeta as { cursor: string }).cursor
+ expect(BigInt(dehydratedCursor)).toBeGreaterThan(0n)
+
+ // While the HTML is in flight, the DO moves on: a changes, b dies.
+ await sql(room, "UPDATE messages SET body='v2' WHERE id='a'", "DELETE FROM messages WHERE id='b'")
+
+ // Browser: hydrate, then go live.
+ const ws = makeWsTransport(room)
+ const db = new DbClient()
+ db.hydrate(state as never)
+ const col = db.collection(makeOptions(ws)) as unknown as {
+ preload: () => Promise
+ get: (k: string) => Msg | undefined
+ size: number
+ }
+ await col.preload()
+
+ // First paint: the dehydrated rows, stale and ALL present — ready never
+ // waited for the socket (stale-while-revalidate, ADR-0011 D3).
+ expect(col.get("a")).toMatchObject({ body: "v1" })
+ expect(col.get("b")).toBeDefined()
+
+ // Convergence: catch-up applies the update AND the tombstone.
+ await waitFor(() => col.get("a")?.body === "v2" && col.get("b") === undefined)
+ expect(col.get("c")).toMatchObject({ body: "calm" })
+ expect(BigInt(ws.appliedCursor)).toBeGreaterThan(BigInt(dehydratedCursor))
+ ws.close()
+ })
+
+ it("eager with NO resume point (pruned log → cursor 0): snapshot reconcile removes the dead row", async () => {
+ const room = `rt-zero-${crypto.randomUUID()}`
+ await sql(room, "INSERT INTO messages(id,body) VALUES('a','hi'),('b','doomed')")
+ // Retention pruned everything; nothing was ever drained. High-water is
+ // honestly 0 — there is no resume point.
+ await sql(room, "DELETE FROM _sync_changes")
+
+ const state = await serverRender(room)
+ expect((state.collections[0]!.syncMeta as { cursor: string }).cursor).toBe("0")
+
+ await sql(room, "DELETE FROM messages WHERE id='b'") // dies while HTML is in flight
+
+ const ws = makeWsTransport(room)
+ const db = new DbClient()
+ db.hydrate(state as never)
+ const col = db.collection(makeOptions(ws)) as unknown as {
+ preload: () => Promise
+ get: (k: string) => Msg | undefined
+ }
+ await col.preload()
+ expect(col.get("b")).toBeDefined() // stale first paint, not a flash-to-empty
+
+ // The fresh snapshot is authoritative SET semantics: b is reconciled away.
+ await waitFor(() => col.get("b") === undefined)
+ expect(col.get("a")).toMatchObject({ body: "hi" })
+ ws.close()
+ })
+
+ it("eager with no resume point and a WIPED table: the empty snapshot still reconciles everything away", async () => {
+ const room = `rt-wipe-${crypto.randomUUID()}`
+ await sql(room, "INSERT INTO messages(id,body) VALUES('a','hi'),('b','yo')")
+ await sql(room, "DELETE FROM _sync_changes") // no resume point
+
+ const state = await serverRender(room)
+ expect(state.collections[0]!.rows).toHaveLength(2)
+
+ // Everything dies while the HTML is in flight: the catch-up snapshot has
+ // ZERO rows — which must still count as the authoritative (empty) set.
+ await sql(room, "DELETE FROM messages")
+
+ const ws = makeWsTransport(room)
+ const db = new DbClient()
+ db.hydrate(state as never)
+ const col = db.collection(makeOptions(ws)) as unknown as {
+ preload: () => Promise
+ size: number
+ }
+ await col.preload()
+ expect(col.size).toBe(2) // stale first paint
+ await waitFor(() => col.size === 0) // honest convergence, not stale-forever
+ ws.close()
+ })
+
+ it("a FUTURE-VERSIONED syncMeta throws from hydrate, yet the collection still converges", async () => {
+ const room = `rt-vskew-${crypto.randomUUID()}`
+ await sql(room, "INSERT INTO messages(id,body) VALUES('a','hi'),('b','doomed')")
+
+ const state = await serverRender(room)
+ // A newer serializer wrote meta this client can't read.
+ ;(state.collections[0]! as { syncMeta: unknown }).syncMeta = { v: 99, cursor: "999" }
+ await sql(room, "DELETE FROM messages WHERE id='b'") // dies while in flight
+
+ const ws = makeWsTransport(room)
+ const db = new DbClient()
+ const col = db.collection(makeOptions(ws)) as unknown as {
+ preload: () => Promise
+ get: (k: string) => Msg | undefined
+ }
+ expect(() => db.hydrate(state as never)).toThrow(/unrecognized sync meta/) // loud...
+ await col.preload()
+ expect(col.get("b")).toBeDefined() // rows landed regardless (no upstream veto)
+ await waitFor(() => col.get("b") === undefined) // ...but SAFE: reconcile converges
+ expect(col.get("a")).toMatchObject({ body: "hi" })
+ ws.close()
+ })
+
+ it("a CHANGED eager where between render and hydrate downgrades to snapshot reconcile", async () => {
+ const room = `rt-where-${crypto.randomUUID()}`
+ await sql(room, "INSERT INTO messages(id,body) VALUES('a','keep'),('b','other')")
+
+ // Rendered under where body='keep' — only 'a' is dehydrated, and the
+ // cursor is fingerprinted to THAT filter.
+ const state = await serverRender(room, undefined, whereEq("body", "keep"))
+ expect(state.collections[0]!.rows.map((r) => r.key)).toEqual(["a"])
+
+ // The browser ships a different filter (deploy skew). 'a' never changes
+ // after the render, so a since-catch-up would NEVER remove it — the
+ // foreign cursor must be refused and the snapshot reconciled instead.
+ const ws = makeWsTransport(room)
+ const db = new DbClient()
+ db.hydrate(state as never)
+ const col = db.collection(makeOptions(ws, undefined, whereEq("body", "other"))) as unknown as {
+ preload: () => Promise
+ get: (k: string) => Msg | undefined
+ }
+ await col.preload()
+ await waitFor(() => col.get("b") !== undefined && col.get("a") === undefined)
+ ws.close()
+ })
+
+ it("on-demand: transient catch-up converges hydrated subsets, then leaves (no eager leak)", async () => {
+ const room = `rt-od-${crypto.randomUUID()}`
+ await sql(room, "INSERT INTO messages(id,body) VALUES('a','keep'),('b','keep'),('c','drop')")
+
+ const state = await serverRender(room, "on-demand")
+ // Dehydrated = the loaded subset only.
+ expect(state.collections[0]!.rows.map((r) => (r.value as unknown as Msg).id).sort()).toEqual(["a", "b"])
+
+ // While in flight: b dies, d joins the subset.
+ await sql(room, "DELETE FROM messages WHERE id='b'", "INSERT INTO messages(id,body) VALUES('d','keep')")
+
+ const ws = makeWsTransport(room)
+ const db = new DbClient()
+ db.hydrate(state as never)
+ const colRaw = db.collection(makeOptions(ws, "on-demand"))
+ const col = colRaw as unknown as { get: (k: string) => Msg | undefined }
+ const kept = createLiveQueryCollection((q) =>
+ q.from({ m: colRaw as never }).where(({ m }: { m: Msg }) => eq(m.body, "keep")),
+ )
+ await kept.preload()
+
+ // Convergence across hydrated rows: the tombstone for b (only the
+ // transient catch-up can deliver it — b is gone from every fresh subset
+ // snapshot) and the new subset member d.
+ await waitFor(() => col.get("b") === undefined && col.get("d") !== undefined)
+ expect(col.get("a")).toMatchObject({ body: "keep" })
+
+ // The catch-up sub must be GONE: an unfiltered leftover would stream
+ // out-of-subset rows. Write e (outside) then f (inside) through a real
+ // mut; f's arrival is the ordering sentinel proving e had its chance.
+ const writer = makeWsTransport(room)
+ const mut = (id: string, body: string): Extract => ({
+ t: "mut",
+ txId: `tx-${id}-${crypto.randomUUID()}`,
+ collection: "messages",
+ ops: [{ type: "insert", key: id, cols: { id, body } }],
+ })
+ await writer.sendMut(mut("e", "drop"))
+ await writer.sendMut(mut("f", "keep"))
+ await waitFor(() => col.get("f") !== undefined)
+ expect(col.get("e")).toBeUndefined()
+ expect(col.get("c")).toBeUndefined() // never loaded; on-demand stayed on-demand
+ writer.close()
+ ws.close()
+ })
+})
diff --git a/tests/ssr-transport.test.ts b/tests/ssr-transport.test.ts
new file mode 100644
index 0000000..32222c7
--- /dev/null
+++ b/tests/ssr-transport.test.ts
@@ -0,0 +1,91 @@
+import { createCollection, createLiveQueryCollection, eq } from "@tanstack/db"
+import { env, runInDurableObject } from "cloudflare:test"
+import { describe, expect, it } from "vitest"
+import { doCollectionOptions } from "../src/client/do-collection.ts"
+import { SsrReadOnlyError, SsrSnapshotTransport, type SnapshotRead } from "../src/client/ssr-transport.ts"
+
+// WHY (ADR-0011 D2): server rendering must run the SAME collection adapter the
+// browser runs — one code path, swapped at the transport seam — with one
+// snapshot read per subscription and no socket. These pin: eager preload
+// materializes the DO's rows; on-demand loadSubset works under a server-side
+// live query preload (the upstream SSR fixture's flagship pattern); the
+// render's cursor is the durable high-water mark; and any write during SSR
+// fails loud as the design error it is.
+
+interface Msg {
+ id: string
+ body: string
+}
+
+/** Structural schema Api the branded transport carries, so `doCollectionOptions`
+ * infers Row = Msg for `table: "messages"`. */
+type Api = { collections: { messages: { __row?: Msg } } }
+
+/** Exactly what an SSR worker passes: the DO stub's RPC, as a function. */
+function makeRead(room: string): SnapshotRead {
+ const stub = env.SYNC_DO.get(env.SYNC_DO.idFromName(room)) as unknown as {
+ readSyncSnapshot: (r: Parameters[0], request: Request) => ReturnType
+ }
+ // The author closes over the claims-bearing Request; the transport's read
+ // contract stays {collection, where, ...} only.
+ return (req) => stub.readSyncSnapshot(req, new Request("https://example.com/ssr", { headers: { "x-user": "anon" } }))
+}
+
+async function seed(room: string, rows: Array<[string, string]>): Promise {
+ await runInDurableObject(env.SYNC_DO.get(env.SYNC_DO.idFromName(room)), (_i, s) => {
+ for (const [id, body] of rows) s.storage.sql.exec("INSERT INTO messages(id,body) VALUES(?,?)", id, body)
+ })
+}
+
+describe("SsrSnapshotTransport (server-side render path, ADR-0011 D2)", () => {
+ it("eager: preload materializes the DO's rows and a resumable cursor, no socket", async () => {
+ const room = `ssrt-eager-${crypto.randomUUID()}`
+ await seed(room, [
+ ["a", "hi"],
+ ["b", "yo"],
+ ])
+
+ const transport = new SsrSnapshotTransport({ read: makeRead(room) })
+ const messages = createCollection(
+ doCollectionOptions({ transport, table: "messages", getKey: (r) => r.id }),
+ )
+ await messages.preload()
+
+ expect(messages.size).toBe(2)
+ expect(messages.get("a")).toMatchObject({ id: "a", body: "hi" })
+ expect(BigInt(transport.appliedCursor)).toBeGreaterThan(0n) // dehydration exports this
+ })
+
+ it("on-demand: a server-side live query preload drives loadSubset through one read", async () => {
+ const room = `ssrt-od-${crypto.randomUUID()}`
+ await seed(room, [
+ ["a", "keep"],
+ ["b", "drop"],
+ ])
+
+ const transport = new SsrSnapshotTransport({ read: makeRead(room) })
+ const messages = createCollection(
+ doCollectionOptions({ transport, table: "messages", getKey: (r) => r.id, syncMode: "on-demand" }),
+ )
+ const kept = createLiveQueryCollection((q) => q.from({ m: messages }).where(({ m }) => eq(m.body, "keep")))
+ await kept.preload()
+
+ expect(kept.get("a")).toMatchObject({ id: "a", body: "keep" })
+ expect(kept.get("b")).toBeUndefined()
+ expect(BigInt(transport.appliedCursor)).toBeGreaterThan(0n)
+ })
+
+ it("rejects writes during SSR — read-only, fail loud", async () => {
+ const room = `ssrt-ro-${crypto.randomUUID()}`
+ await seed(room, [["a", "hi"]])
+
+ const transport = new SsrSnapshotTransport({ read: makeRead(room) })
+ const messages = createCollection(
+ doCollectionOptions({ transport, table: "messages", getKey: (r) => r.id }),
+ )
+ await messages.preload()
+
+ const tx = messages.insert({ id: "x", body: "nope" })
+ await expect(tx.isPersisted.promise).rejects.toThrow(SsrReadOnlyError)
+ })
+})
diff --git a/tests/test-worker.ts b/tests/test-worker.ts
index a1b5da4..07a7da5 100644
--- a/tests/test-worker.ts
+++ b/tests/test-worker.ts
@@ -213,7 +213,12 @@ export class SyncTestDO extends SyncDurableObject {
}
protected override parseAttachment(req: Request): Claims {
- return { userId: req.headers.get("x-user") ?? "anon" }
+ const userId = req.headers.get("x-user") ?? "anon"
+ // Sentinel for the auth-gate tests: parseAttachment is the ONE gate for both
+ // the WS upgrade and the SSR readSyncSnapshot read (ADR-0011 D1) — a rejecting
+ // identity must reject both paths, so a tenant check can't be bypassed by SSR.
+ if (userId === "forbidden") throw new Response("forbidden", { status: 403 })
+ return { userId }
}
}