Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
662dec6
docs(adr): ADR-0011 — SSR dehydrate/hydrate design
grrowl Jun 10, 2026
2c410f4
feat(server): readSnapshot RPC — socketless snapshot + durable high-w…
grrowl Jun 10, 2026
a6a2157
feat(client): cursor bootstrap — since on first sub, seedCursor on th…
grrowl Jun 10, 2026
4a29c97
feat(client): SsrSnapshotTransport — server-rendering reads through t…
grrowl Jun 10, 2026
cd38d15
feat(client)!: SSR hydration — syncMeta hooks, hydrated sync paths, s…
grrowl Jun 10, 2026
24f0bd4
docs(readme): SSR usage section (experimental)
grrowl Jun 10, 2026
b11a427
fix(client): close the second adversarial round's holes in SSR hydration
grrowl Jun 10, 2026
c6bcee1
feat(examples): ssr — TanStack Start on Cloudflare, dehydrate→hydrate…
grrowl Jun 10, 2026
9b153cb
docs(readme): list examples/ssr
grrowl Jun 10, 2026
070ce18
refactor(server)!: rename readSnapshot -> readSyncSnapshot
grrowl Jun 10, 2026
701e530
feat(examples): ssr — useLiveQuery and useLiveSuspenseQuery showcase …
grrowl Jun 10, 2026
fb93e05
feat(server)!: readSyncSnapshot runs the request through parseAttachment
grrowl Jun 10, 2026
c62aff8
fix(client): syncMeta hooks fail loud but SAFE; eager reconcile alway…
grrowl Jun 10, 2026
9ae2a2a
fix(client): reconnecting flag set at scheduling, not in the timer
grrowl Jun 10, 2026
f1e6139
docs(adr): grill-session notes — C1' forward pointer, ready semantics…
grrowl Jun 10, 2026
864ca8b
docs(vendor): provenance for the PR-1564 tarballs
grrowl Jun 10, 2026
8e157ef
chore(release): v0.4.0-dev.0
grrowl Jun 11, 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.1] — 2026-06-11

### 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