Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ccfa932
docs(adr): ADR-0011 — SSR dehydrate/hydrate design
grrowl Jun 10, 2026
7d86bdf
feat(server): readSnapshot RPC — socketless snapshot + durable high-w…
grrowl Jun 10, 2026
b6ae0dc
feat(client): cursor bootstrap — since on first sub, seedCursor on th…
grrowl Jun 10, 2026
ec730f7
feat(client): SsrSnapshotTransport — server-rendering reads through t…
grrowl Jun 10, 2026
977deaf
feat(client)!: SSR hydration — syncMeta hooks, hydrated sync paths, s…
grrowl Jun 10, 2026
9d473c3
docs(readme): SSR usage section (experimental)
grrowl Jun 10, 2026
fe6f7dd
fix(client): close the second adversarial round's holes in SSR hydration
grrowl Jun 10, 2026
34f66ff
feat(examples): ssr — TanStack Start on Cloudflare, dehydrate→hydrate…
grrowl Jun 10, 2026
3eeaa24
docs(readme): list examples/ssr
grrowl Jun 10, 2026
236071f
refactor(server)!: rename readSnapshot -> readSyncSnapshot
grrowl Jun 10, 2026
ec7b1af
feat(examples): ssr — useLiveQuery and useLiveSuspenseQuery showcase …
grrowl Jun 10, 2026
6e39aa6
feat(server)!: readSyncSnapshot runs the request through parseAttachment
grrowl Jun 10, 2026
227ca47
fix(client): syncMeta hooks fail loud but SAFE; eager reconcile alway…
grrowl Jun 10, 2026
ca11b07
fix(client): reconnecting flag set at scheduling, not in the timer
grrowl Jun 10, 2026
9536020
docs(adr): grill-session notes — C1' forward pointer, ready semantics…
grrowl Jun 10, 2026
3b6cfbe
docs(vendor): provenance for the PR-1564 tarballs
grrowl Jun 10, 2026
beb0be9
chore(release): v0.4.0-dev.0
grrowl Jun 11, 2026
64bbf64
chore(client): reconcile feat/ssr with 0.3.2 after rebase onto main
grrowl Jun 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.3] — 2026-06-13

### Fixed
Expand Down
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Message>({ 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<Message>({ 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
Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion docs/adr/0002-adversarial-review-corrections.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading