Skip to content

feat(domain): View.snapshot gives state and cursor together; list takes atLeast - #2567

Open
artile wants to merge 1 commit into
alchemy-run:mainfrom
artile:feat/domain-view-snapshot
Open

artile wants to merge 1 commit into
alchemy-run:mainfrom
artile:feat/domain-view-snapshot

Conversation

@artile

@artile artile commented Oct 25, 2026

Copy link
Copy Markdown
Contributor

A page that shows a view at first paint and then follows it live has no read that gives the state and the place to resume from together. get gives the state without a cursor. list(key, { order: "desc", take: 1 }) gives the last entry's state and seq, but list has no atLeast: right after a write, it can read before the view folded it, so the page starts from an old cursor and replays entries it already shows.

Found while building a Foldkit front on Domain in a production app: the server renders the board into the page, and the browser's feed must start where that board ends, with no spinner, no gap and no duplicate.

// before
const page = yield* api.Totals.list("c-1", { order: "desc", take: 1 }); // no atLeast
const last = page.entries[0];
const cursor = last === undefined ? "0" : String(last.envelope.seq);

// after
const { state, cursor } = yield* api.Totals.snapshot("c-1", { atLeast: receipt });
render(state);
yield* api.Totals.events("c-1", { after: cursor }).pipe(Stream.runForEach((e) => render(e.state)));

What changed

  • ViewClient.snapshot(key, { atLeast?, where? }) gives { state, cursor }. Both come from one stored view snapshot (ViewSnapshot.state and .seq), so the cursor always matches the state. cursor is a string, as events takes it in after; "0" for a key with no events.
  • validate judges snapshot like get (mode "get"), and where narrows the state like get.
  • ViewClient.list takes atLeast, as get does.
  • View.Host.snapshot (the hosted view inside a model) and the View.Snapshot<S> type. The view host's query and snapshot share one atLeast wait.
  • A new <View>.snapshot RPC in the view's read RPCs, so it works over protocolHttp and protocolWebSocket (a plain call).
  • Docs: "Render a snapshot, then follow it" on the View page; notes in the skill's building-blocks.md and frontend.md.

Tests. New test/Domain/ViewSnapshot.test.ts, 4 tests: a snapshot of an empty key is { state: null, cursor: "0" }, and the cursor equals the last entry's seq; two writes between the snapshot and events({ after: cursor }) arrive once each (seq 2 and 3), and snapshot plus feed equals get; the same over a real HTTP server with protocolHttp; list with atLeast sees a write made just before it (the test fails without atLeast). pnpm test:integration test/Domain: 69 passed, 0 failed (65 before). format:check, tsrs -b, lint, docs:check-jsdoc, exports:check and docs:check pass locally.

Limits. Views only; streams keep peek (it has atLeast) and list (no atLeast). One key per call: a board plus one card per row is still N+1 reads.

@alchemy-version-bot

Copy link
Copy Markdown
Contributor

Install the packages built from this commit:

alchemy

pnpm install https://pkg.alchemy.run/alchemy/pr:2567:ff18dc6
@alchemy.run (16)
pnpm install https://pkg.alchemy.run/@alchemy.run/better-auth/pr:2567:ff18dc6
pnpm install https://pkg.alchemy.run/@alchemy.run/git/pr:2567:ff18dc6
pnpm install https://pkg.alchemy.run/@alchemy.run/drizzle/pr:2567:ff18dc6
pnpm install https://pkg.alchemy.run/@alchemy.run/acme/pr:2567:ff18dc6
pnpm install https://pkg.alchemy.run/@alchemy.run/planetscale/pr:2567:ff18dc6
pnpm install https://pkg.alchemy.run/@alchemy.run/neon/pr:2567:ff18dc6
pnpm install https://pkg.alchemy.run/@alchemy.run/cloudflare/pr:2567:ff18dc6
pnpm install https://pkg.alchemy.run/@alchemy.run/aws/pr:2567:ff18dc6
pnpm install https://pkg.alchemy.run/@alchemy.run/domain/pr:2567:ff18dc6
pnpm install https://pkg.alchemy.run/@alchemy.run/kubernetes/pr:2567:ff18dc6
pnpm install https://pkg.alchemy.run/@alchemy.run/docker/pr:2567:ff18dc6
pnpm install https://pkg.alchemy.run/@alchemy.run/sql/pr:2567:ff18dc6
pnpm install https://pkg.alchemy.run/@alchemy.run/cloudflare-runtime/pr:2567:ff18dc6
pnpm install https://pkg.alchemy.run/@alchemy.run/frontend-frameworks/pr:2567:ff18dc6
pnpm install https://pkg.alchemy.run/@alchemy.run/node-utils/pr:2567:ff18dc6
pnpm install https://pkg.alchemy.run/@alchemy.run/pkg/pr:2567:ff18dc6
Transitive Dependencies (9)
pnpm install https://pkg.alchemy.run/@distilled.cloud/aws/pr:2567:ff18dc6
pnpm install https://pkg.alchemy.run/@distilled.cloud/acme/pr:2567:ff18dc6
pnpm install https://pkg.alchemy.run/@distilled.cloud/core/pr:2567:ff18dc6
pnpm install https://pkg.alchemy.run/@distilled.cloud/planetscale/pr:2567:ff18dc6
pnpm install https://pkg.alchemy.run/@distilled.cloud/neon/pr:2567:ff18dc6
pnpm install https://pkg.alchemy.run/@distilled.cloud/aws4signer/pr:2567:ff18dc6
pnpm install https://pkg.alchemy.run/@distilled.cloud/cloudflare/pr:2567:ff18dc6
pnpm install https://pkg.alchemy.run/@distilled.cloud/iceberg/pr:2567:ff18dc6
pnpm install https://pkg.alchemy.run/@distilled.cloud/github/pr:2567:ff18dc6

Published Oct 25, 2026, 12:28 PM UTC. Expires Nov 1, 2026, 12:28 PM UTC, extended while this pull request is open.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant