diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d80763..5c14032 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,46 @@ While pre-1.0, the public API may change between 0.x releases. _Nothing yet._ +## [0.4.0-dev.0] — 2026-06-11 + +Prerelease on the `dev` dist-tag (`npm i tanstack-do-db-collection@dev`); does +not affect `latest` (0.3.1). The SSR adapter installs and imports against a +released `@tanstack/db`, but is **dormant until paired with the PR #1564 build** +(`dehydrate`/`hydrate`/`DbClient` and the hook calls are upstream and unreleased). + +### Added + +- **SSR support (experimental — ADR-0011; tracks TanStack DB draft PR + [#1564](https://github.com/TanStack/db/pull/1564), whose hook signatures may + change).** Dehydrate on the worker, hydrate to the cursor: + - `SyncDurableObject.readSyncSnapshot({ collection, where?, orderBy?, limit? }, request)` + — one consistent `{ rows, cursor }` read over the DO binding, no + WebSocket. The required `request` runs through `parseAttachment` — the + same auth gate as the WS upgrade, so one tenant check guards both paths. + The cursor is a durable high-water mark; `"0"` honestly means "no resume + point". + - `SsrSnapshotTransport` — runs the same `doCollectionOptions` inside a + per-request server `DbClient` (eager preload and on-demand + `loadSubset`/live-query preload both work); read-only, writes throw + `SsrReadOnlyError`. Create one per request. + - `doCollectionOptions` now implements `exportSyncMeta` / `importSyncMeta` + / `mergeSyncMeta` (`{ v: 1, cursor }`, opaque to TanStack; inert on older + `@tanstack/db`). A hydrated collection is ready immediately + (stale-while-revalidate), resumes its first sub from the dehydrated + cursor (server catch-up; honest reset below the retention floor), and + with no resume point reconciles a fresh snapshot as authoritative set + semantics — no flash-to-empty, no stranded deletes (an EMPTY snapshot + reconciles too). The cursor is fingerprinted to the eager `where`; a + changed filter refuses it and downgrades to snapshot reconcile. On-demand + mode adds one transient unfiltered catch-up sub (readiness gates on it + being sent) that unsubscribes at its own sub-scoped terminal; unresumable + hydrated rows are truncated, never left to go permanently stale. + Late/streamed chunks self-heal: the cursor claim only ever shrinks + (`seedCursor`), and a live regress rides a forced reconnect so stale + in-flight boundaries can't re-claim past the repair window. + - Wire: `uptodate` gains an optional `sub` (a catch-up's terminal is + sub-scoped; additive). `sub` frames accept `since` on first subscribe. + ## [0.3.2] — 2026-06-13 ### Added diff --git a/README.md b/README.md index a952e40..747f428 100644 --- a/README.md +++ b/README.md @@ -206,6 +206,50 @@ One `WebSocketTransport` per DO is shared by every collection on that DO (multiplexed over the single socket). Pass `where` to `doCollectionOptions` to sync only a matching subset. +### 4. SSR (experimental) + +Tracks TanStack DB's draft [`DbClient` SSR PR](https://github.com/TanStack/db/pull/1564); +the upstream hooks may change before release. 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-do-db-collection/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(doCollectionOptions({ transport, table: "messages", getKey: (m) => m.id })), +) +await messages.preload() +return { dbState: db.dehydrate() } // rows + our 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) +const messages = db.collection( + collectionOptions(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 @@ -218,6 +262,10 @@ browser-verified. - **[`examples/on-demand`](./examples/on-demand)** — `syncMode: 'on-demand'`: categorised items where each panel loads only its subset (`loadSubset`/ `unloadSubset`) and unopened categories are never synced. +- **[`examples/ssr`](./examples/ssr)** — server-side rendering (experimental): + a TanStack Start app on Cloudflare reads the DO **without a WebSocket** + (`readSyncSnapshot`), dehydrates into the route payload, hydrates for an instant + first paint, and converges live from the dehydrated cursor. - **[`examples/board`](./examples/board)** — the at-scale stress test: 5,000 tasks on one DO with a bounded window, `useLiveInfiniteQuery` cursor scroll-back, and a mutable order key so voting bumps a task to the top diff --git a/docs/adr/0002-adversarial-review-corrections.md b/docs/adr/0002-adversarial-review-corrections.md index 2854993..d48b5b6 100644 --- a/docs/adr/0002-adversarial-review-corrections.md +++ b/docs/adr/0002-adversarial-review-corrections.md @@ -2,7 +2,9 @@ **Status:** Accepted. Amends [ADR-0001](./0001-sync-architecture.md). C5's changelog-retention floor is refined by -[ADR-0009](./0009-changelog-time-retention.md). +[ADR-0009](./0009-changelog-time-retention.md). C1's flush-before-`committed` +barrier is generalized to ALL cursor-advancing emissions (C1′) by +[ADR-0011](./0011-ssr-dehydrate-hydrate.md). ## Context diff --git a/docs/adr/0011-ssr-dehydrate-hydrate.md b/docs/adr/0011-ssr-dehydrate-hydrate.md new file mode 100644 index 0000000..d8966f0 --- /dev/null +++ b/docs/adr/0011-ssr-dehydrate-hydrate.md @@ -0,0 +1,269 @@ +# 0011 — SSR: dehydrate on the worker, hydrate to the cursor + +**Status:** Accepted (experimental — tracks TanStack DB draft PR +[#1564](https://github.com/TanStack/db/pull/1564); the upstream hook signatures +may change before release). 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 + +Core stays compatible with released `@tanstack/db` (>= 0.6): the hooks are +additive and ignored by older versions; `since`-on-first-sub and `seedCursor` +are version-independent. No self-branding via `Symbol.for` — users wrap +`collectionOptions(doCollectionOptions(...))` for `DbClient`. Round-trip tests +and `examples/ssr` (TanStack Start on Cloudflare) build against packed +PR-branch tarballs vendored as **branch-only** devDependencies, removed when +upstream publishes. 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.** The hook signatures (per-config, no collection + argument, no veto in `mergeSyncMeta`) are likely to change; our surface is + one closure and three small hook bodies, kept deliberately thin. + +## 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. diff --git a/docs/adr/README.md b/docs/adr/README.md index e375d7c..d00a146 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -18,4 +18,5 @@ 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 | +| [0011](./0011-ssr-dehydrate-hydrate.md) | SSR: dehydrate on the worker, hydrate to the cursor | Accepted (experimental; generalizes 0002 C1 → C1′) | | [0012](./0012-wire-input-hardening.md) | Wire-input hardening: frame-shape guards, inbound limits, sanitized execute errors | Accepted | 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..e86e874 --- /dev/null +++ b/examples/ssr/README.md @@ -0,0 +1,101 @@ +# ssr — tanstack-do-db-collection 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 route 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. + +> **Experimental.** SSR support tracks TanStack DB **draft PR +> [#1564](https://github.com/TanStack/db/pull/1564)** (`DbClient`, +> `dehydrate`/`hydrate`, `collectionOptions`, `useLiveSuspenseQuery`). This +> example installs the vendored PR builds from `../../vendor` and pins +> `@tanstack/db` via npm `overrides` so exactly **one** copy resolves — two +> copies break the Symbol-branded `collectionOptions`. The upstream hook +> signatures may change before release. + +The example imports the library from source (`../../src`), so it always tracks +the current code. A published consumer would `import` from +`tanstack-do-db-collection` / `.../client` instead. + +## Run + +```sh +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 deploy` — build then `wrangler deploy` + +## Pages + +`/` is a plain landing page; the two showcase pages live under a shared layout: + +### `/live-query` — `useLiveQuery` + +The baseline SSR experience. Data is present from the first (server) render; +the status line flips `ssr → hydrated` on mount and `catching up → live` once +the socket converges. Adds and toggles are optimistic — instant locally, +confirmed on the single ordered stream. Open a second tab to watch them sync. + +### `/live-suspense-query` — `useLiveSuspenseQuery` + +The same collection consumed through React Suspense. What it demonstrates: + +- **Hydrated state does not suspend.** `db.hydrate()` applies the dehydrated + rows as a committed synced transaction — but upstream hydration does NOT + mark the collection ready; readiness is always the sync adapter's call. + It's this library's hydrated path that calls `markReady()` *synchronously* + at sync start, because the rows are already present — the explicit + stale-while-revalidate contract (ADR-0011 D3). So the first paint never + throws to the boundary, on the server or in the browser. View source: the + raw HTML contains the todo rows, **not** the fallback, and the + `fallback-count` on the page stays at 0 through hydration. +- **Query identity changes create a new derived collection.** The + "show only open" toggle changes the `where` clause; the structured query IR + is the identity, so flipping it swaps in a new live query (the re-suspension + path in the hook). In practice the fallback still never commits here: the + source collection is ready in memory, so the new derived query computes + synchronously and `useLiveSuspenseQuery` never reaches the throw. With this + library the fallback would only ever show for a source that isn't hydrated or + synced yet — which this app, by construction, never has. + +## 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), seeded with three rows on first create. +- `src/routes/_db.tsx` — the round trip, lifted to a **pathless layout** shared + by both pages. Its loader calls one `createServerFn` (server-only by + construction; the browser gets the payload instead of re-running the read) + that builds a **per-request** `DbClient` + `SsrSnapshotTransport` over + `stub.readSyncSnapshot`, preloads, and returns `db.dehydrate()`. The layout + component hydrates a fresh `DbClient` from that payload — **once per tab** — + and provides the collection to the pages. Lifting it means one loader, one + DbClient, one socket; per-page DbClients would open a fresh WebSocket on + every client-side navigation between the pages and never close the old one. +- `src/routes/_db.live-query.tsx`, `src/routes/_db.live-suspense-query.tsx` — + the two consumers, reading the shared collection via `useTodos()`. +- `src/lib/todos.ts` — the one collection shape, three transports (the + ADR-0011 D2 seam): snapshot in the loader, WebSocket in the browser, and an + inert transport for the worker's React render pass — `hydrate()` already + applied the rows, so that pass needs no data source. The collection id + defaults to the table name (`todos`) everywhere; that match is what routes + the dehydrated rows into the collection on hydrate. +- `src/lib/todos-context.ts` — the React context for the collection handle. + Deliberately **not** exported from the `_db` route file: Start code-splits + route files, so a context exported from one is evaluated twice (split + component module vs. direct import) and the provider and consumers end up + holding two different contexts. + +Verified manually (curl shows the rows — and no Suspense fallback — in both +pages' raw HTML; a headless browser confirmed hydration, the filter toggle, +and zero-fallback first paint). There is no automated e2e here — the library's +own `tests/ssr-*.test.ts` pin the contract. diff --git a/examples/ssr/package-lock.json b/examples/ssr/package-lock.json new file mode 100644 index 0000000..d87f781 --- /dev/null +++ b/examples/ssr/package-lock.json @@ -0,0 +1,4469 @@ +{ + "name": "tanstack-do-db-ssr-example", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tanstack-do-db-ssr-example", + "dependencies": { + "@msgpack/msgpack": "^3.0.0", + "@tanstack/db": "file:../../vendor/tanstack-db-0.6.7-pr1564.tgz", + "@tanstack/react-db": "file:../../vendor/tanstack-react-db-0.1.85-pr1564.tgz", + "@tanstack/react-router": "^1.170.0", + "@tanstack/react-start": "^1.168.0", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@cloudflare/vite-plugin": "^1.40.0", + "@cloudflare/workers-types": "^4.20260518.1", + "@types/react": "^19", + "@types/react-dom": "^19", + "@vitejs/plugin-react": "^5.1.0", + "typescript": "^5.9", + "vite": "^7.3.0", + "wrangler": "^4" + } + }, + "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.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@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.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "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.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "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.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "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.40.1", + "resolved": "https://registry.npmjs.org/@cloudflare/vite-plugin/-/vite-plugin-1.40.1.tgz", + "integrity": "sha512-hn7NH6gc2RNOThCJVTSRVvSpqSYVmoZrFQMjilTdwwsrvMD0Np8zM7pEDB1q5isSGy7F5D+dacRF6LiF4Z1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cloudflare/unenv-preset": "2.16.1", + "miniflare": "4.20260609.0", + "unenv": "2.0.0-rc.24", + "wrangler": "4.99.0", + "ws": "8.20.1" + }, + "bin": { + "cf-vite": "bin/cf-vite" + }, + "peerDependencies": { + "vite": "^6.1.0 || ^7.0.0 || ^8.0.0", + "wrangler": "^4.99.0" + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260609.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260609.1.tgz", + "integrity": "sha512-AK8tYLQm+8BqQMzjZ55ZfuhfIm1eCkj+Ykxz6kWXojdACwjjU03MrwdM9fBDdgzU3upXOs4e1scOFHySlfVQjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260609.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260609.1.tgz", + "integrity": "sha512-4kKXfr7ZHU6xQ/R9ShdSuj1A1bEouoRcHzUWdjnuMPBlRsAAVanlxAVYISotFUulLEinayOpRFbhpsfwzrpSSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260609.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260609.1.tgz", + "integrity": "sha512-T2Ebir2OPHAvvZ0HUh5mi1lN8q30sVi4lf7LIpc28AHoWtoOmJ0jA5AJK4IYJm1MKEbBldq+QsckaHOCQFmRpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260609.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260609.1.tgz", + "integrity": "sha512-INfcYoSsKqEIvPL69/3RkqYoP8WUR0VEN6loWN/3tekXLoJrVOj3E5NjIetsdS8MJN6zc3st/ae4bMuWRRzoDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260609.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260609.1.tgz", + "integrity": "sha512-EWhfxKI1aqUr7S8xuGxgmRCumEzB8iSsCIz6oEqJN+3pZuW3EWiKDGFW4EY1BmwNINLW1eO5VMGYb8Fj6FVYxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workers-types": { + "version": "4.20260610.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260610.1.tgz", + "integrity": "sha512-Mk/f3lUygeIHzQ4HnJjU/JvGg/kllgp9gISty9nylHE/2M2MFeKO+hgAKSgiPpmwUbuhewdYGgqFGgT/ADK0/g==", + "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.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz", + "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "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.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "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.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "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.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.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/@msgpack/msgpack": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz", + "integrity": "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==", + "license": "ISC", + "engines": { + "node": ">= 18" + } + }, + "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.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "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.16", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.16.tgz", + "integrity": "sha512-yNm/fYEcnpRjYduLMaddTK9XKYil6xB88+qFg79ZdZhHu1PadfoQmFW7pVTx7FZqMBNcUuThiAhxhENgtAO2/w==", + "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.6.7", + "resolved": "file:../../vendor/tanstack-db-0.6.7-pr1564.tgz", + "integrity": "sha512-7/msCXoE8oYjuYBLT7/wA7Bna8BGOo6XHegPeuknWFy3VyMlpafEhqS5+JQ4eZqiO3Mplnf+SpEPGahj15NYaA==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@tanstack/db-ivm": "0.1.18", + "@tanstack/pacer-lite": "^0.2.1" + }, + "peerDependencies": { + "typescript": ">=4.7" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "fractional-indexing": "^3.2.0", + "sorted-btree": "^1.8.1" + }, + "peerDependencies": { + "typescript": ">=4.7" + } + }, + "node_modules/@tanstack/history": { + "version": "1.162.0", + "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.162.0.tgz", + "integrity": "sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA==", + "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.1.85", + "resolved": "file:../../vendor/tanstack-react-db-0.1.85-pr1564.tgz", + "integrity": "sha512-QNtAfRkAY4go/WBsrdJ6AQfIMdEvJKaRO0uO5Q1LXzDM1C7dyolN352jDYW2lP8ZjYcvIc3W3r48LHP9gc2v6g==", + "license": "MIT", + "dependencies": { + "@tanstack/db": "0.6.7", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@tanstack/react-router": { + "version": "1.170.15", + "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.170.15.tgz", + "integrity": "sha512-GawYz7HEjj8rTUUDoT/SemDEVm63pZUO+2mOcXHY9Jl3EwMS5gFBnPu/2UvcrwRm1jN1k79fokc0d4aFmrLatg==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.162.0", + "@tanstack/react-store": "^0.9.3", + "@tanstack/router-core": "1.171.13", + "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-start": { + "version": "1.168.25", + "resolved": "https://registry.npmjs.org/@tanstack/react-start/-/react-start-1.168.25.tgz", + "integrity": "sha512-aHlg9YTSeL12gWrYIHAEzoncPHc5JUbQ60Sc26OQ7J1zcsXqdKwdcqaApG4YV12S/keFdbndHjxaiYkUcJlx7Q==", + "license": "MIT", + "dependencies": { + "@tanstack/react-router": "1.170.15", + "@tanstack/react-start-client": "1.168.13", + "@tanstack/react-start-rsc": "0.1.24", + "@tanstack/react-start-server": "1.167.19", + "@tanstack/router-utils": "1.162.2", + "@tanstack/start-client-core": "1.170.12", + "@tanstack/start-plugin-core": "1.171.17", + "@tanstack/start-server-core": "1.169.14", + "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.13", + "resolved": "https://registry.npmjs.org/@tanstack/react-start-client/-/react-start-client-1.168.13.tgz", + "integrity": "sha512-enr4hL0Fifqz7jO8Zy4CuEpunEfH1LbvMw/mRjG49j699Bo3CaR7mPDcgN/9tSSjjUT5ZDj9M6TiTp9cSgehww==", + "license": "MIT", + "dependencies": { + "@tanstack/react-router": "1.170.15", + "@tanstack/router-core": "1.171.13", + "@tanstack/start-client-core": "1.170.12" + }, + "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.24", + "resolved": "https://registry.npmjs.org/@tanstack/react-start-rsc/-/react-start-rsc-0.1.24.tgz", + "integrity": "sha512-8zBLV68t6byrbtIyKYNTCpcc7qFbb0kQiu0yFtFIvsi70fpBeG3VP8bmkN95/Cqpvz1lLio+E4JApRyV52MpxQ==", + "license": "MIT", + "dependencies": { + "@tanstack/react-router": "1.170.15", + "@tanstack/router-core": "1.171.13", + "@tanstack/router-utils": "1.162.2", + "@tanstack/start-client-core": "1.170.12", + "@tanstack/start-fn-stubs": "1.162.0", + "@tanstack/start-plugin-core": "1.171.17", + "@tanstack/start-server-core": "1.169.14", + "@tanstack/start-storage-context": "1.167.15", + "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.20", + "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.19", + "resolved": "https://registry.npmjs.org/@tanstack/react-start-server/-/react-start-server-1.167.19.tgz", + "integrity": "sha512-+eMpAwDreQvCwgX45MdUHTUCF/Wad36+PwQafe6W5wa3qVkGyN3P131ShGyRwT/0WwKa5EVGdW1zFgwby8UNqA==", + "license": "MIT", + "dependencies": { + "@tanstack/react-router": "1.170.15", + "@tanstack/router-core": "1.171.13", + "@tanstack/start-server-core": "1.169.14" + }, + "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.13", + "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.171.13.tgz", + "integrity": "sha512-+NOwEj1kO/6IGmpHRIZHasYxYWpyBQGNIZAST9aNrk9Q3YlU9SgqVnl1pbLa9qAKfeNdXQIRve0RQb/0kyDeDA==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.162.0", + "cookie-es": "^3.0.0", + "seroval": "^1.5.4", + "seroval-plugins": "^1.5.4" + }, + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/router-generator": { + "version": "1.167.17", + "resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.167.17.tgz", + "integrity": "sha512-xtB9tB2Ws0tWR6Pi7nc3Qk9IYgoh1mQCKWjHqIl9tf6BNUpKoqniJoPAQ4+LGrK8FeZYU0o0p/qlZEyj9FAulA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5", + "@tanstack/router-core": "1.171.13", + "@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.18", + "resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.168.18.tgz", + "integrity": "sha512-MofS28/axfnfnhOD2RSgJEaU882aX5RsAzhGz5Vc4XhAmvCjy919u9JrNs4QsTWFbTD1P7IJ8WFlFVsrg0pStg==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "@tanstack/router-core": "1.171.13", + "@tanstack/router-generator": "1.167.17", + "@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.15", + "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.12", + "resolved": "https://registry.npmjs.org/@tanstack/start-client-core/-/start-client-core-1.170.12.tgz", + "integrity": "sha512-gwtZRMPUIAxmDV2AIQUhC0kSW262SV7BkHXEgy5B1woHQdrdsELuGOdJwdweLxrjyefORxk+9MYGqDY0Cxn0bw==", + "license": "MIT", + "dependencies": { + "@tanstack/router-core": "1.171.13", + "@tanstack/start-fn-stubs": "1.162.0", + "@tanstack/start-storage-context": "1.167.15", + "seroval": "^1.5.4" + }, + "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.17", + "resolved": "https://registry.npmjs.org/@tanstack/start-plugin-core/-/start-plugin-core-1.171.17.tgz", + "integrity": "sha512-ngKkp3wn/U3nyeqZl7KcMzjbgTbcypC5ES7O92JpA5/tz4PufFOf5l+eX3pY+4Z6jE6Jb6ekQgnryG7XMjpK7Q==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "7.27.1", + "@babel/core": "^7.28.5", + "@babel/types": "^7.28.5", + "@tanstack/router-core": "1.171.13", + "@tanstack/router-generator": "1.167.17", + "@tanstack/router-plugin": "1.168.18", + "@tanstack/router-utils": "1.162.2", + "@tanstack/start-server-core": "1.169.14", + "exsolve": "^1.0.7", + "lightningcss": "^1.32.0", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "seroval": "^1.5.4", + "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.14", + "resolved": "https://registry.npmjs.org/@tanstack/start-server-core/-/start-server-core-1.169.14.tgz", + "integrity": "sha512-cSCTNbKARrkddPOfavF/soRFDxH+b+v3m4TeW6AvEy419R3E0ZsoZAm5UI6uNR1y4UU9WTOmaxLQ4nzIZPKmXg==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.162.0", + "@tanstack/router-core": "1.171.13", + "@tanstack/start-client-core": "1.170.12", + "@tanstack/start-storage-context": "1.167.15", + "fetchdts": "^0.1.6", + "h3-v2": "npm:h3@2.0.1-rc.20", + "seroval": "^1.5.4" + }, + "engines": { + "node": ">=22.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/start-storage-context": { + "version": "1.167.15", + "resolved": "https://registry.npmjs.org/@tanstack/start-storage-context/-/start-storage-context-1.167.15.tgz", + "integrity": "sha512-Jy0q4vdG6pv76N92+X+ag3fuOV2zINQagYyMN1/es7tPI1vzpKECIU8AqHqzI6ahkwaph7XDvmfUkiLJ3i4LOA==", + "license": "MIT", + "dependencies": { + "@tanstack/router-core": "1.171.13" + }, + "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.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "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.10.35", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.35.tgz", + "integrity": "sha512-honAfLBde0HAFLdNyBEfuuENkF6zR+ozxqxa/2zJKHBe1qzLqyTSeRKpdPEHAP03rlDGyQOPnCSxnVpVqQo9Mg==", + "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.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "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.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001797", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001797.tgz", + "integrity": "sha512-l8xKG+gwAIExZGl9FrF7KUwuOmk6wbEPC9Xoy/RtnWv1XG0Q4LFlagaLpUv3Kiza3W/wm27zy0yWJEieYKAP6w==", + "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.371", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.371.tgz", + "integrity": "sha512-e9htk9mAYL6AzmkEhSvVVw7IWGSBJ/Bqdn2eRyRLrj1g6sncN4WbFt5qnILYoCktktr45pyjIrOiRvBThQ808w==", + "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.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "devOptional": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "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.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "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.2.0", + "resolved": "https://registry.npmjs.org/fractional-indexing/-/fractional-indexing-3.2.0.tgz", + "integrity": "sha512-PcOxmqwYCW7O2ovKRU8OoQQj2yqTfEB/yeTYk4gPid6dN5ODRfU1hXd9tTVZzax/0NkO7AxpHykvZnT1aYp/BQ==", + "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.1.42", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.42.tgz", + "integrity": "sha512-/SXsVh7KpPRISrD4ffrGSxnTLlUBzEQUfWIusaJPrpJ93FW1P0YEZri5vAUkFsA0m2HRUhQRQadk2wJ+EeKowQ==", + "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.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "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.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "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": "4.20260609.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260609.0.tgz", + "integrity": "sha512-4ZfNh9ACDa/mKKQvTSO2vigyQS2MB7dEU02KRPle4FqL7S6nek+2Fq6WGzazZbt1OORYgb4OGVLnOCx+My2NNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.34.5", + "undici": "7.24.8", + "workerd": "1.20260609.1", + "ws": "8.20.1", + "youch": "4.1.0-beta.10" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "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.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "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.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "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.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", + "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.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.7" + } + }, + "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.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "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": { + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "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.5.4", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.4.tgz", + "integrity": "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/seroval-plugins": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.4.tgz", + "integrity": "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "seroval": "^1.0" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "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.16", + "resolved": "https://registry.npmjs.org/srvx/-/srvx-0.11.16.tgz", + "integrity": "sha512-bp07zRuycfTY43IjAvvTFnmnJi8ikW0VFiHwOhhYcVW/L4xQ1XY4PAd4Nuum1rsA17C39zL7x+CDhrn5AL32Rw==", + "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/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.24.8", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.8.tgz", + "integrity": "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==", + "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.0.0", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz", + "integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "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.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.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.20260609.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260609.1.tgz", + "integrity": "sha512-KF/Y/8f4VoXCk87NuU6RqmO0X5fdzcrxU3XzAgoPUpnH9t1ZyzRgX1O/9sJvjItxroCBTEBzKssda02Dz9i6BA==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260609.1", + "@cloudflare/workerd-darwin-arm64": "1.20260609.1", + "@cloudflare/workerd-linux-64": "1.20260609.1", + "@cloudflare/workerd-linux-arm64": "1.20260609.1", + "@cloudflare/workerd-windows-64": "1.20260609.1" + } + }, + "node_modules/wrangler": { + "version": "4.99.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.99.0.tgz", + "integrity": "sha512-i7GA2mZETTyq3ljWdEzM908FjLaMWZ1AaAHKaOJ8pFA/tonf2VqIWDyBGzKleIVBbNQxOTIY2wnbv0iaK3rC6g==", + "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.27.3", + "miniflare": "4.20260609.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260609.1" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=22.0.0" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20260609.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/wrangler/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/wrangler/node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/ws": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "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..f72aaee --- /dev/null +++ b/examples/ssr/package.json @@ -0,0 +1,33 @@ +{ + "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" + }, + "dependencies": { + "@msgpack/msgpack": "^3.0.0", + "@tanstack/db": "file:../../vendor/tanstack-db-0.6.7-pr1564.tgz", + "@tanstack/react-db": "file:../../vendor/tanstack-react-db-0.1.85-pr1564.tgz", + "@tanstack/react-router": "^1.170.0", + "@tanstack/react-start": "^1.168.0", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@cloudflare/vite-plugin": "^1.40.0", + "@cloudflare/workers-types": "^4.20260518.1", + "@types/react": "^19", + "@types/react-dom": "^19", + "@vitejs/plugin-react": "^5.1.0", + "typescript": "^5.9", + "vite": "^7.3.0", + "wrangler": "^4" + }, + "overrides": { + "@tanstack/db": "file:../../vendor/tanstack-db-0.6.7-pr1564.tgz" + } +} diff --git a/examples/ssr/src/lib/todos-context.ts b/examples/ssr/src/lib/todos-context.ts new file mode 100644 index 0000000..f184bb7 --- /dev/null +++ b/examples/ssr/src/lib/todos-context.ts @@ -0,0 +1,15 @@ +// Lives in lib, NOT in the `_db` route file: Start code-splits route files, so +// a context exported from one is evaluated twice (split component module vs. +// direct import) and the provider and consumers end up holding two different +// contexts — the SSR pass then renders the "outside the layout" error. + +import * as React from "react" +import type { TodosCollection } from "./todos.ts" + +export const TodosContext = React.createContext(null) + +export function useTodos(): TodosCollection { + const todos = React.useContext(TodosContext) + if (!todos) throw new Error("useTodos must be used under the /_db layout") + return todos +} diff --git a/examples/ssr/src/lib/todos.ts b/examples/ssr/src/lib/todos.ts new file mode 100644 index 0000000..d7b6cbe --- /dev/null +++ b/examples/ssr/src/lib/todos.ts @@ -0,0 +1,39 @@ +// One collection shape, three transports (ADR-0011 D2): the loader reads a +// snapshot from the DO, the browser goes live over WebSocket, and the worker's +// React render pass sits still on rows hydrate() already applied. The id +// defaults to the table name ("todos") on every side — that match is what lets +// hydrate() route the dehydrated rows into this collection. + +import { collectionOptions } from "@tanstack/db" +import { doCollectionOptions, SsrSnapshotTransport } from "../../../../src/client/index.ts" +import type { Transport } from "../../../../src/client/index.ts" + +export interface Todo { + id: string + text: string + /** SQLite INTEGER 0/1 — kept raw so optimistic and confirmed rows are identical. */ + done: number +} + +/** The branded options DbClient wants, around our adapter. One per DbClient; + * the `as never` casts bridge the vendored draft-PR types (see tests/ssr-*). */ +export function todosOptions(transport: Transport) { + return collectionOptions( + doCollectionOptions({ transport, table: "todos", getKey: (t) => t.id }) as never, + ) as never +} + +/** What the component's collection can do; `db.collection` on the draft-PR + * build is untyped, so the caller casts to this. */ +export interface TodosCollection { + insert: (t: Todo) => unknown + update: (key: string, fn: (draft: Todo) => void) => unknown +} + +/** The worker's render pass needs no data source — hydrate() applied the + * loader's rows before the first paint, and convergence is the browser's job. + * A never-resolving read keeps the snapshot transport inert (no second DO + * read, mutations still fail loud) for the lifetime of the request. */ +export function inertSsrTransport(): Transport { + return new SsrSnapshotTransport({ read: () => new Promise(() => {}) }) +} diff --git a/examples/ssr/src/routeTree.gen.ts b/examples/ssr/src/routeTree.gen.ts new file mode 100644 index 0000000..af757bb --- /dev/null +++ b/examples/ssr/src/routeTree.gen.ts @@ -0,0 +1,127 @@ +/* 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 DbRouteImport } from './routes/_db' +import { Route as IndexRouteImport } from './routes/index' +import { Route as DbLiveSuspenseQueryRouteImport } from './routes/_db.live-suspense-query' +import { Route as DbLiveQueryRouteImport } from './routes/_db.live-query' + +const DbRoute = DbRouteImport.update({ + id: '/_db', + getParentRoute: () => rootRouteImport, +} as any) +const IndexRoute = IndexRouteImport.update({ + id: '/', + path: '/', + getParentRoute: () => rootRouteImport, +} as any) +const DbLiveSuspenseQueryRoute = DbLiveSuspenseQueryRouteImport.update({ + id: '/live-suspense-query', + path: '/live-suspense-query', + getParentRoute: () => DbRoute, +} as any) +const DbLiveQueryRoute = DbLiveQueryRouteImport.update({ + id: '/live-query', + path: '/live-query', + getParentRoute: () => DbRoute, +} as any) + +export interface FileRoutesByFullPath { + '/': typeof IndexRoute + '/live-query': typeof DbLiveQueryRoute + '/live-suspense-query': typeof DbLiveSuspenseQueryRoute +} +export interface FileRoutesByTo { + '/': typeof IndexRoute + '/live-query': typeof DbLiveQueryRoute + '/live-suspense-query': typeof DbLiveSuspenseQueryRoute +} +export interface FileRoutesById { + __root__: typeof rootRouteImport + '/': typeof IndexRoute + '/_db': typeof DbRouteWithChildren + '/_db/live-query': typeof DbLiveQueryRoute + '/_db/live-suspense-query': typeof DbLiveSuspenseQueryRoute +} +export interface FileRouteTypes { + fileRoutesByFullPath: FileRoutesByFullPath + fullPaths: '/' | '/live-query' | '/live-suspense-query' + fileRoutesByTo: FileRoutesByTo + to: '/' | '/live-query' | '/live-suspense-query' + id: '__root__' | '/' | '/_db' | '/_db/live-query' | '/_db/live-suspense-query' + fileRoutesById: FileRoutesById +} +export interface RootRouteChildren { + IndexRoute: typeof IndexRoute + DbRoute: typeof DbRouteWithChildren +} + +declare module '@tanstack/react-router' { + interface FileRoutesByPath { + '/_db': { + id: '/_db' + path: '' + fullPath: '/' + preLoaderRoute: typeof DbRouteImport + parentRoute: typeof rootRouteImport + } + '/': { + id: '/' + path: '/' + fullPath: '/' + preLoaderRoute: typeof IndexRouteImport + parentRoute: typeof rootRouteImport + } + '/_db/live-suspense-query': { + id: '/_db/live-suspense-query' + path: '/live-suspense-query' + fullPath: '/live-suspense-query' + preLoaderRoute: typeof DbLiveSuspenseQueryRouteImport + parentRoute: typeof DbRoute + } + '/_db/live-query': { + id: '/_db/live-query' + path: '/live-query' + fullPath: '/live-query' + preLoaderRoute: typeof DbLiveQueryRouteImport + parentRoute: typeof DbRoute + } + } +} + +interface DbRouteChildren { + DbLiveQueryRoute: typeof DbLiveQueryRoute + DbLiveSuspenseQueryRoute: typeof DbLiveSuspenseQueryRoute +} + +const DbRouteChildren: DbRouteChildren = { + DbLiveQueryRoute: DbLiveQueryRoute, + DbLiveSuspenseQueryRoute: DbLiveSuspenseQueryRoute, +} + +const DbRouteWithChildren = DbRoute._addFileChildren(DbRouteChildren) + +const rootRouteChildren: RootRouteChildren = { + IndexRoute: IndexRoute, + DbRoute: DbRouteWithChildren, +} +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..3946535 --- /dev/null +++ b/examples/ssr/src/router.tsx @@ -0,0 +1,6 @@ +import { createRouter } from "@tanstack/react-router" +import { routeTree } from "./routeTree.gen" + +export function getRouter() { + return createRouter({ routeTree, scrollRestoration: true }) +} diff --git a/examples/ssr/src/routes/__root.tsx b/examples/ssr/src/routes/__root.tsx new file mode 100644 index 0000000..d555572 --- /dev/null +++ b/examples/ssr/src/routes/__root.tsx @@ -0,0 +1,28 @@ +import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router" +import * as React from "react" + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: "utf-8" }, + { name: "viewport", content: "width=device-width, initial-scale=1" }, + { title: "tanstack-do-db SSR todos" }, + ], + }), + shellComponent: RootDocument, + component: () => , +}) + +function RootDocument({ children }: { children: React.ReactNode }) { + return ( + + + + + + {children} + + + + ) +} diff --git a/examples/ssr/src/routes/_db.live-query.tsx b/examples/ssr/src/routes/_db.live-query.tsx new file mode 100644 index 0000000..59af4fd --- /dev/null +++ b/examples/ssr/src/routes/_db.live-query.tsx @@ -0,0 +1,79 @@ +// useLiveQuery over the hydrated collection: data is present from the first +// (server) render, `isReady` flips to live once the socket catches up. +// Stale-while-revalidate, never a flash of empty. + +import { useLiveQuery } from "@tanstack/react-db" +import { createFileRoute } from "@tanstack/react-router" +import * as React from "react" +import { useTodos } from "../lib/todos-context.ts" +import type { Todo, TodosCollection } from "../lib/todos.ts" + +export const Route = createFileRoute("/_db/live-query")({ + component: LiveQueryPage, +}) + +function LiveQueryPage() { + return +} + +function Todos({ todos }: { todos: TodosCollection }) { + const [hydrated, setHydrated] = React.useState(false) + const [text, setText] = React.useState("") + const { data, isReady } = useLiveQuery((q) => + q.from({ t: todos as never }).orderBy(({ t }: { t: Todo }) => t.id, "asc"), + ) as unknown as { data: Array; isReady: boolean } + + 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 ( +
+

useLiveQuery todos

+

+ {hydrated ? "hydrated" : "ssr"} + {" · "} + {isReady ? "live" : "catching up"} + {" · "} + rows: {data.length} +

+
    + {data.map((t) => ( +
  • + +
  • + ))} +
+
{ + e.preventDefault() + add() + }} + style={{ display: "flex", gap: 8 }} + > + setText(e.target.value)} + placeholder="new todo…" + style={{ flex: 1, padding: 8, borderRadius: 6, border: "1px solid #ccc" }} + /> + +
+
+ ) +} diff --git a/examples/ssr/src/routes/_db.live-suspense-query.tsx b/examples/ssr/src/routes/_db.live-suspense-query.tsx new file mode 100644 index 0000000..7b96fb1 --- /dev/null +++ b/examples/ssr/src/routes/_db.live-suspense-query.tsx @@ -0,0 +1,93 @@ +// useLiveSuspenseQuery over the hydrated collection. The point on display: +// hydration makes the source collection `ready` synchronously (the rows came +// with the document), so the FIRST paint never suspends — on the server or in +// the browser — and the raw HTML contains rows, not the fallback. Changing the +// query's identity (the where clause below) creates a new derived collection, +// which DOES suspend until it loads; the fallback counter makes that visible +// and testable. + +import { eq } from "@tanstack/db" +import { useLiveSuspenseQuery } from "@tanstack/react-db" +import { createFileRoute } from "@tanstack/react-router" +import * as React from "react" +import { useTodos } from "../lib/todos-context.ts" +import type { Todo, TodosCollection } from "../lib/todos.ts" + +export const Route = createFileRoute("/_db/live-suspense-query")({ + component: SuspensePage, +}) + +function SuspensePage() { + const todos = useTodos() + const [hydrated, setHydrated] = React.useState(false) + const [openOnly, setOpenOnly] = React.useState(false) + // Incremented by the fallback's mount effect: stays 0 if first paint never + // suspends (the claim under test), goes up when an identity change does. + const [fallbackCount, setFallbackCount] = React.useState(0) + + React.useEffect(() => setHydrated(true), []) + + return ( +
+

useLiveSuspenseQuery todos

+

+ {hydrated ? "hydrated" : "ssr"} + {" · "} + fallbacks shown: {fallbackCount} +

+ + setFallbackCount((c) => c + 1)} />}> + + +
+ ) +} + +function Fallback({ onShown }: { onShown: () => void }) { + // Effects never run during SSR, so a server-rendered fallback would still be + // visible in the raw HTML — the curl check covers that side. + React.useEffect(() => onShown(), [onShown]) + return

loading todos…

+} + +function TodoRows({ todos, openOnly }: { todos: TodosCollection; openOnly: boolean }) { + // Config-object form: the derived query identity includes the structured + // where clause, so flipping `openOnly` re-suspends (new collection) rather + // than silently reusing the old rows. + const { data } = useLiveSuspenseQuery({ + query: (q) => { + const base = q.from({ t: todos as never }) + const scoped = openOnly ? base.where(({ t }: { t: Todo }) => eq(t.done as never, 0)) : base + return scoped.orderBy(({ t }: { t: Todo }) => t.id, "asc") + }, + }) as unknown as { data: Array } + + return ( + <> +

+ rows: {data.length} +

+
    + {data.map((t) => ( +
  • + +
  • + ))} +
+ + ) +} diff --git a/examples/ssr/src/routes/_db.tsx b/examples/ssr/src/routes/_db.tsx new file mode 100644 index 0000000..10ba3e5 --- /dev/null +++ b/examples/ssr/src/routes/_db.tsx @@ -0,0 +1,91 @@ +// The SSR round trip (ADR-0011), lifted to a pathless layout so both showcase +// pages share it: a server function reads ONE snapshot from the DO and +// dehydrates it into the layout's loader payload; the browser hydrates that +// state into a fresh DbClient, paints immediately, then converges live over +// the WebSocket. One loader + one DbClient (one socket) per tab — per-page +// DbClients would open a fresh WebSocket on every client-side navigation +// between the pages, and the old one is never closed. + +import { DbClient } from "@tanstack/db" +import { DbProvider } from "@tanstack/react-db" +import { createFileRoute, Link, Outlet } from "@tanstack/react-router" +import { createServerFn } from "@tanstack/react-start" +import { getRequest } from "@tanstack/react-start/server" +import { env } from "cloudflare:workers" +import * as React from "react" +import { SsrSnapshotTransport, WebSocketTransport } from "../../../../src/client/index.ts" +import type { SnapshotRead } from "../../../../src/client/index.ts" +import { inertSsrTransport, todosOptions } from "../lib/todos.ts" +import type { TodosCollection } from "../lib/todos.ts" +import { TodosContext } from "../lib/todos-context.ts" +import type { Env } from "../todos-do.ts" + +// What actually rides the wire: plain JSON. Upstream's DehydratedDbState +// types `value`/`syncMeta` as `unknown`/`Record` (adapter- +// opaque by design), which Start's serializable validation can't see through +// — assert the boundary with the concrete shape this adapter produces +// (SQLite rows + {v, cursor[, where]}). +type SerializableDbState = { + collections: Array<{ + collectionId: string + rows: Array<{ key: string; value: Record }> + syncMeta?: { v: 1; cursor: string; where?: string } + }> +} + +// Server-only by construction (createServerFn): the browser never re-runs the +// DO read — it gets the dehydrated payload. One transport + one DbClient PER +// REQUEST; module scope would leak cursor state across requests (ADR-0011 D2). +const getDbState = createServerFn().handler(async () => { + 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 the read path. + const request = getRequest() + const transport = new SsrSnapshotTransport({ read: (req) => stub.readSyncSnapshot(req, request) }) + const db = new DbClient() + const todos = db.collection(todosOptions(transport)) as unknown as { preload: () => Promise } + await todos.preload() + return db.dehydrate() as SerializableDbState +}) + +export const Route = createFileRoute("/_db")({ + // Runs once per document request (whichever child is hit) and once per + // client-side entry into the subtree — the children never re-fetch it. + loader: async () => ({ dbState: await getDbState() }), + component: DbLayout, +}) + +function DbLayout() { + const { dbState } = Route.useLoaderData() + // One DbClient per browser tab, hydrated once from the loader payload. The + // transport seam (ADR-0011 D2): the worker's render pass sits still on the + // hydrated rows; the browser opens the real socket and converges. + const [{ db, todos }] = React.useState(() => { + const db = new DbClient() + db.hydrate(dbState as never) + const transport = import.meta.env.SSR + ? inertSsrTransport() + : new WebSocketTransport({ + url: `${location.protocol === "https:" ? "wss:" : "ws:"}//${location.host}/sync/main`, + }) + const todos = db.collection(todosOptions(transport)) as unknown as TodosCollection + return { db, todos } + }) + + return ( + + + + + + + ) +} diff --git a/examples/ssr/src/routes/index.tsx b/examples/ssr/src/routes/index.tsx new file mode 100644 index 0000000..1fa32aa --- /dev/null +++ b/examples/ssr/src/routes/index.tsx @@ -0,0 +1,31 @@ +// Landing page only — no DB here. The DbClient lives in the `_db` pathless +// layout so each showcase page exercises the SSR round trip on its own URL, +// while client-side navigation between them shares one socket. + +import { createFileRoute, Link } from "@tanstack/react-router" + +export const Route = createFileRoute("/")({ + component: Landing, +}) + +function Landing() { + return ( +
+

tanstack-do-db SSR showcase

+

+ One todos collection in a Durable Object, server-rendered two ways. View + source on either page: the rows are in the raw HTML. +

+
    +
  • + useLiveQuery — hydrate, paint, converge + live; explicit isReady state. +
  • +
  • + useLiveSuspenseQuery — same + data via Suspense; hydrated state does not suspend on first paint. +
  • +
+
+ ) +} 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..b31ea8b --- /dev/null +++ b/examples/ssr/src/todos-do.ts @@ -0,0 +1,65 @@ +// SSR example — the sync DO. One `todos` collection plus the three row +// mutations the browser client sends. Imports the library straight from source +// (../../../src) so the example tracks the real code; a published consumer +// would `import { ... } from "tanstack-do-db-collection"`. + +import { SyncDurableObject, SyncRegistry } from "../../../src/server/index.ts" +import type { Todo } from "./lib/todos.ts" + +export interface Env { + TODOS_DO: DurableObjectNamespace +} + +const UPDATABLE = new Set(["text", "done"]) + +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( + new SyncRegistry() + .defineCollection({ table: "todos", pk: "id" }) + .defineMutation({ + collection: "todos", + type: "insert", + execute: ({ op, sql }) => { + sql.exec("INSERT INTO todos(id, text, done) VALUES (?, ?, ?)", op.cols.id, op.cols.text, op.cols.done) + }, + }) + .defineMutation({ + collection: "todos", + type: "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) + }, + }) + .defineMutation({ + collection: "todos", + type: "delete", + execute: ({ op, sql }) => { + sql.exec("DELETE FROM todos WHERE id = ?", op.key) + }, + }), + ) + // 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..6f91a68 --- /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: { + // The library is imported from source (../../src), whose own `@tanstack/db` + // import would resolve from the REPO root's 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 + // vendored PR build. + 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 4bae81f..edf0386 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tanstack-do-db-collection", - "version": "0.3.2", + "version": "0.4.0-dev.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tanstack-do-db-collection", - "version": "0.3.2", + "version": "0.4.0-dev.0", "license": "MIT", "dependencies": { "@msgpack/msgpack": "^3.0.0" @@ -14,7 +14,7 @@ "devDependencies": { "@cloudflare/vitest-pool-workers": "0.12.21", "@cloudflare/workers-types": "^4.20260518.1", - "@tanstack/db": "0.6.5", + "@tanstack/db": "file:vendor/tanstack-db-0.6.7-pr1564.tgz", "typescript": "^5.7", "vitest": "3.2.4", "wrangler": "^4" @@ -1620,9 +1620,9 @@ "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.6.7", + "resolved": "file:vendor/tanstack-db-0.6.7-pr1564.tgz", + "integrity": "sha512-7/msCXoE8oYjuYBLT7/wA7Bna8BGOo6XHegPeuknWFy3VyMlpafEhqS5+JQ4eZqiO3Mplnf+SpEPGahj15NYaA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 631ef26..2b5a3e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tanstack-do-db-collection", - "version": "0.3.2", + "version": "0.4.0-dev.0", "description": "Sync a TanStack DB collection to a Cloudflare Durable Object over WebSockets — optimistic mutations, live queries, and single-ordered-stream write confirmation.", "type": "module", "license": "MIT", @@ -57,7 +57,7 @@ "devDependencies": { "@cloudflare/vitest-pool-workers": "0.12.21", "@cloudflare/workers-types": "^4.20260518.1", - "@tanstack/db": "0.6.5", + "@tanstack/db": "file:vendor/tanstack-db-0.6.7-pr1564.tgz", "typescript": "^5.7", "vitest": "3.2.4", "wrangler": "^4" diff --git a/src/client/do-collection.ts b/src/client/do-collection.ts index 80b0a2d..763152b 100644 --- a/src/client/do-collection.ts +++ b/src/client/do-collection.ts @@ -16,8 +16,9 @@ // post-mutation empty sync commit (ADR-0002 C2, verified). import { compileSingleRowExpression, toBooleanPredicate, 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 @@ -29,8 +30,10 @@ export class WriteOutsideSubError extends Error { } export interface DoCollectionOptions { - /** One transport per DO; shared by all collections on that DO. */ - transport: WebSocketTransport + /** One transport per DO; shared by all collections on that DO. In the + * browser a WebSocketTransport; during SSR an SsrSnapshotTransport — + * created PER REQUEST (ADR-0011 D2). */ + transport: Transport /** Collection (table) name on the DO. */ table: string /** Stable client-supplied key extractor (must match the server pk). */ @@ -62,6 +65,26 @@ interface SyncParams { 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)}`) + } + BigInt(m.cursor) // malformed cursor throws here — fail loud, never resume from garbage + return { v: 1, cursor: m.cursor, ...(m.where === undefined ? {} : { where: m.where }) } +} + /** Subset of @tanstack/db's LoadSubsetOptions we consume. */ interface LoadSubsetOptions { where?: unknown @@ -90,8 +113,33 @@ export function doCollectionOptions( // 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 + const sync = (params: SyncParams): SyncConfigResult => { const { collection, begin, write, commit, markReady, 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) { @@ -111,45 +159,140 @@ export function doCollectionOptions( 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 }): SubHandler => { + // `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 T) + 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 }) + } + } + } + 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; 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() + 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() + }, + } + } 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() + }, + onReset: () => { + flush() + begin() + truncate() + commit() + done() // before the trailing resnapshot frames arrive + }, + }, + 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() + commit() + }) + } else { + readyGate = transport.connect() + } + void readyGate.then(() => markReady()) + // Distinct `where` -> one refcounted server subscription. const loaded = new Map }>() const keyOf = (o: LoadSubsetOptions): string => JSON.stringify(o.where ?? null) @@ -224,12 +367,44 @@ export function doCollectionOptions( } } - 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) + markReady() + } else { + void transport.subscribe(eagerSubId, table, handler, where) + } + } + return () => { + hydratedCursor = null // GC wiped the rows; a retained cursor would lie + transport.unsubscribe(eagerSubId) + } } const mutationFn = async (params: { @@ -266,11 +441,68 @@ export function doCollectionOptions( } } + // SSR syncMeta hooks (ADR-0011 D3) — called by TanStack's DbClient + // dehydrate/hydrate (draft PR #1564); inert on older @tanstack/db versions. + // 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) + const exportSyncMeta = (): DoSyncMeta => ({ + v: 1, + cursor: transport.appliedCursor, + ...(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 + } + // 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 + } + return { id: opts.id ?? table, getKey, syncMode, - sync: { sync, rowUpdateMode: "partial" }, + sync: { sync, rowUpdateMode: "partial", exportSyncMeta, importSyncMeta, mergeSyncMeta }, onInsert: mutationFn, onUpdate: mutationFn, onDelete: mutationFn, diff --git a/src/client/index.ts b/src/client/index.ts index 8d725ab..85e5997 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -12,6 +12,11 @@ export { MutationRejectedError, WebSocketTransport, } from "./transport.ts" -export type { SubHandler, TransportOptions, WebSocketLike } from "./transport.ts" +export type { SubHandler, Transport, TransportOptions, WebSocketLike } from "./transport.ts" export { doCollectionOptions, WriteOutsideSubError } from "./do-collection.ts" -export type { DoCollectionOptions } from "./do-collection.ts" +export type { DoCollectionOptions, DoSyncMeta } from "./do-collection.ts" +// SSR (experimental — tracks TanStack DB draft PR #1564; 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..d4e16ca --- /dev/null +++ b/src/client/ssr-transport.ts @@ -0,0 +1,103 @@ +// 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 { + /** 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) + } + + 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 091f2a8..667aaf2 100644 --- a/src/client/transport.ts +++ b/src/client/transport.ts @@ -31,7 +31,11 @@ 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 } @@ -45,6 +49,28 @@ export class MutationRejectedError extends Error { } } +/** The transport surface `doCollectionOptions` consumes — structural, so the + * WebSocket transport and the SSR snapshot transport are interchangeable + * (ADR-0011 D2). */ +export interface Transport { + 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 + seedCursor(cursor: string): void +} + export interface TransportOptions { url: string /** Returns a CONNECTED socket. Default opens `new WebSocket(url)` and resolves @@ -93,6 +119,12 @@ export class WebSocketTransport { private intentionallyClosed = false /** True while reconnecting, so connect() resubscribes on success. */ private reconnecting = false + /** True between a live cursor REGRESS (late hydration) and the reconnect + * that replays from it. The old socket's already-queued boundary frames + * would otherwise re-advance the cursor past the repair window — their + * data still applies (idempotent), but the claim must hold at the seed + * until the fresh socket's replay owns it. */ + private suppressAdvance = false private readonly reconnectDelayMs: number constructor(opts: TransportOptions) { @@ -114,6 +146,70 @@ export class WebSocketTransport { return String(this.appliedSeq) } + /** + * 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's queued frames + // stop counting (advance suppressed; their data still applies, + // idempotently), and the FRESH socket resubscribes from the seed — + // clean ordering, replay guaranteed. + this.suppressAdvance = true + this.forceReconnect() + } + } + + /** Abandon the current socket and reconnect. Teardown is explicit — a + * locally-initiated close does not reliably fire our own close event in + * every runtime, and the close handler ignores abandoned sockets. */ + private forceReconnect(): void { + const old = this.ws + this.ws = null + this.connectPromise = null + try { + old?.close() + } catch { + /* already dead; the reconnect proceeds regardless */ + } + this.scheduleReconnect() + } + + private scheduleReconnect(): void { + // The flag is set at SCHEDULING time, not in the timer: a demand-driven + // connect() (a mutation inside the reconnect window) may establish the + // fresh socket first, and it must run the resubscribe path too — or + // every subscription is silently dead on the new socket and the late + // timer wedges the flag (pre-existing bug, found in the ADR-0011 grill). + this.reconnecting = true + setTimeout(() => { + void this.connect().catch(() => { + /* next attempt retries on the following close */ + }) + }, this.reconnectDelayMs) + } + async connect(): Promise { if (this.ws) return if (this.connectPromise) return this.connectPromise @@ -129,29 +225,20 @@ export class WebSocketTransport { } ws.addEventListener("message", (ev) => this.onMessage(ev.data)) ws.addEventListener("close", () => { + // A close for a socket we already abandoned (forceReconnect tore it + // down, or a newer connection is live) must not double-schedule. + if (this.ws !== ws) return this.ws = null this.connectPromise = null // Auto-reconnect on an unexpected drop while subscriptions are active. - if (!this.intentionallyClosed && this.handlers.size > 0) { - // The flag is set at SCHEDULING time, not in the timer: a demand- - // driven connect() (a mutation inside the reconnect window) may - // establish the fresh socket first, and it must run the resubscribe - // path too — or every subscription is silently dead on the new - // socket and the late timer wedges the flag (pre-existing bug, found - // in the ADR-0011 grill). - this.reconnecting = true - setTimeout(() => { - void this.connect().catch(() => { - /* next attempt retries on the following close */ - }) - }, this.reconnectDelayMs) - } + if (!this.intentionallyClosed && this.handlers.size > 0) this.scheduleReconnect() }) this.ws = ws // On a reconnect, re-establish every subscription from our single applied // cursor so the server serves a windowed catch-up rather than a snapshot. if (this.reconnecting) { this.reconnecting = false + this.suppressAdvance = false // the fresh socket's frames own the cursor again this.resubscribeAll() } })() @@ -161,14 +248,7 @@ export class WebSocketTransport { // live — otherwise one unreachable attempt wedges the transport forever. this.connectPromise.catch(() => { this.connectPromise = null - if (!this.intentionallyClosed && this.handlers.size > 0) { - this.reconnecting = true - setTimeout(() => { - void this.connect().catch(() => { - /* next attempt retries on the following close */ - }) - }, this.reconnectDelayMs) - } + if (!this.intentionallyClosed && this.handlers.size > 0) this.scheduleReconnect() }) return this.connectPromise } @@ -221,10 +301,13 @@ 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 }) + this.sendFrame({ t: "sub", subId, collection, where, orderBy, limit, since }) } unsubscribe(subId: string): void { @@ -303,7 +386,10 @@ export class WebSocketTransport { 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() + // 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": { @@ -342,6 +428,7 @@ export class WebSocketTransport { } private advance(seq: string): void { + if (this.suppressAdvance) return // stale pre-regress boundaries don't count const s = BigInt(seq) if (s > this.appliedSeq) this.appliedSeq = s if (this.seqWaiters.length === 0) return diff --git a/src/server/changes.ts b/src/server/changes.ts index 7b21ca6..b12770a 100644 --- a/src/server/changes.ts +++ b/src/server/changes.ts @@ -172,6 +172,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/sync-do.ts b/src/server/sync-do.ts index 0cee7ca..84e94e5 100644 --- a/src/server/sync-do.ts +++ b/src/server/sync-do.ts @@ -21,6 +21,7 @@ import { currentSeq, ensureTriggers, getDrainCursor, + highWaterSeq, hydrateRows, initSchema, minChangeSeq, @@ -121,6 +122,44 @@ export abstract class SyncDurableObject extends return undefined as TUser } + /** + * 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: + * `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. + */ + async readSyncSnapshot( + req: { collection: string; where?: unknown; orderBy?: unknown; limit?: number }, + request: Request, + ): Promise<{ + rows: Array> + cursor: string + }> { + await this.parseAttachment(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)) as Array> + return { rows, cursor: String(highWaterSeq(this.sql)) } + } + override async fetch(req: Request): Promise { if (req.headers.get("Upgrade") !== "websocket") { return new Response("expected websocket upgrade", { status: 426 }) @@ -661,7 +700,9 @@ export abstract class SyncDurableObject extends 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. */ 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 462ca41..0b09dad 100644 --- a/tests/do-collection.test.ts +++ b/tests/do-collection.test.ts @@ -48,7 +48,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"]), @@ -93,7 +93,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 cc1810e..6027e9c 100644 --- a/tests/filtered-client.test.ts +++ b/tests/filtered-client.test.ts @@ -51,7 +51,7 @@ function startFiltered(transport: WebSocketTransport, where: unknown): { calls: 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..5e74b86 --- /dev/null +++ b/tests/read-sync-snapshot.test.ts @@ -0,0 +1,102 @@ +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("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/reconnect-window.test.ts b/tests/reconnect-window.test.ts index 70d8892..d3db254 100644 --- a/tests/reconnect-window.test.ts +++ b/tests/reconnect-window.test.ts @@ -4,15 +4,15 @@ import { createFrameCodec } from "../src/wire/frame-codec.ts" import type { ClientFrame, ServerFrame } from "../src/wire/frames.ts" // WHY: PRE-EXISTING bug found while grilling ADR-0011's forced-reconnect -// design (the bug itself is in the plain reconnect path, present on this -// branch; the forced-reconnect machinery is not). The `reconnecting` flag was -// set inside the reconnect TIMER, so a connect() triggered on demand — a -// mutation fired within reconnectDelayMs of a drop — established the fresh -// socket with the flag still false: NO resubscribeAll, every subscription -// silently dead (the server has no subs for the new socket), and the late -// timer's connect() early-returned, wedging the flag. The flag must be set -// when the reconnect is SCHEDULED, so whichever connect() establishes — -// timer-driven or demand-driven — runs the resubscribe path. +// design. The `reconnecting` flag was set inside the reconnect TIMER, so a +// connect() triggered on demand — a mutation fired within reconnectDelayMs of +// a drop — established the fresh socket with the flag still false: NO +// resubscribeAll, every subscription silently dead (the server has no subs +// for the new socket), and the late timer's connect() early-returned, wedging +// the flag. On the forced-reconnect path the same race also left +// suppressAdvance set: a frozen cursor. The flag must be set when the +// reconnect is SCHEDULED, so whichever connect() establishes — timer-driven +// or demand-driven — runs the resubscribe path. const codec = createFrameCodec() diff --git a/tests/ssr-adapter.test.ts b/tests/ssr-adapter.test.ts new file mode 100644 index 0000000..02a7faa --- /dev/null +++ b/tests/ssr-adapter.test.ts @@ -0,0 +1,194 @@ +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 +} + +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", + 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") + }) +}) diff --git a/tests/ssr-cursor.test.ts b/tests/ssr-cursor.test.ts new file mode 100644 index 0000000..4ff4048 --- /dev/null +++ b/tests/ssr-cursor.test.ts @@ -0,0 +1,223 @@ +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}`, + reconnectDelayMs: 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", + reconnectDelayMs: 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() + }) +}) diff --git a/tests/ssr-hydration.test.ts b/tests/ssr-hydration.test.ts new file mode 100644 index 0000000..f809ebb --- /dev/null +++ b/tests/ssr-hydration.test.ts @@ -0,0 +1,283 @@ +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 +// (vendored PR #1564 build) 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 +} + +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}`, + reconnectDelayMs: 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..f67049f --- /dev/null +++ b/tests/ssr-transport.test.ts @@ -0,0 +1,87 @@ +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 +} + +/** 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 b629804..e00c1b9 100644 --- a/tests/test-worker.ts +++ b/tests/test-worker.ts @@ -100,7 +100,11 @@ 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 gate tests: parseAttachment is the ONE auth gate for both + // the WS upgrade and the readSyncSnapshot RPC (ADR-0011 D1). + if (userId === "forbidden") throw new Response("forbidden", { status: 403 }) + return { userId } } } diff --git a/vendor/README.md b/vendor/README.md new file mode 100644 index 0000000..057cfeb --- /dev/null +++ b/vendor/README.md @@ -0,0 +1,39 @@ +# vendor/ — branch-only packed builds of TanStack DB draft PR #1564 + +These tarballs exist **only on `feat/ssr`** and are removed when upstream +ships (npm canary or merged release). They are devDependency / example +inputs — the published package never depends on them. + +## Provenance + +Built from [TanStack/db PR #1564](https://github.com/TanStack/db/pull/1564) +("Add SSR DbClient and live query identity", draft, author @tannerlinsley): + +| field | value | +| --- | --- | +| upstream head | `132d53a9f03e9d0df442b2d15c74e5931925b77b` | +| upstream commit date | 2026-05-30 10:40:24 -0600 | +| fetched via | `git fetch origin pull/1564/head` | +| built | 2026-06-10, `pnpm@11.1.0`, `pnpm --filter "@tanstack/react-db..." build` | +| packages | `@tanstack/db@0.6.7` → `tanstack-db-0.6.7-pr1564.tgz` · `@tanstack/react-db@0.1.85` → `tanstack-react-db-0.1.85-pr1564.tgz` | + +Everything `tests/ssr-*.test.ts` and `examples/ssr` validate is validated +**against exactly this upstream commit**. When the PR is force-pushed or +revised, rebuild the tarballs, update this table, and re-run the suite — +green tests against stale tarballs prove nothing about the current draft. + +## Consumption gotcha + +The public npm registry has a REAL `@tanstack/db@0.6.7`, and the react-db +tarball pins it as a regular dependency. Anything installing these tarballs +MUST force resolution to the vendored file (root `package.json` does this +implicitly via the `file:` devDependency; `examples/ssr` needs explicit npm +`overrides` plus vite `resolve.dedupe`) — two copies of `@tanstack/db` break +the Symbol-branded `collectionOptions`. + +## Exit plan + +When upstream publishes: delete this directory, point the devDependency and +example at the published version, drop the example's overrides, and rebase +`feat/ssr` to remove the tarball commits from history (they are large blobs; +the branch is rebased-not-merged until then anyway). diff --git a/vendor/tanstack-db-0.6.7-pr1564.tgz b/vendor/tanstack-db-0.6.7-pr1564.tgz new file mode 100644 index 0000000..fe43882 Binary files /dev/null and b/vendor/tanstack-db-0.6.7-pr1564.tgz differ diff --git a/vendor/tanstack-react-db-0.1.85-pr1564.tgz b/vendor/tanstack-react-db-0.1.85-pr1564.tgz new file mode 100644 index 0000000..2c75c43 Binary files /dev/null and b/vendor/tanstack-react-db-0.1.85-pr1564.tgz differ