examples/chat: typegres + Cap'n Web on a Durable Object#94
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a complete, runnable “typegres chat” example that demonstrates capability-scoped queries authored in the browser and replayed inside a Cloudflare Durable Object over Cap’n Web, with a Vite/React SPA, DO-backed SQLite schema, migrations/seed, and an end-to-end test suite.
Changes:
- Introduces a Durable Object Worker implementation (schema-as-API via facets), plus migration/seed and PBKDF2-based demo auth.
- Adds a React SPA with a wire log + console-style query prompt for client-authored queries and live subscriptions.
- Adds Wrangler/Vite/Vitest configuration and multiple DO + WebSocket-driven tests to validate confinement claims.
Reviewed changes
Copilot reviewed 22 out of 24 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| examples/chat/wrangler.jsonc | Wrangler configuration for SPA assets + /ws DO routing and DO binding/migration. |
| examples/chat/worker/migrate.ts | SQLite DDL plus seed data for an initial lobby experience. |
| examples/chat/worker/index.ts | Worker entrypoint that forwards /ws to the Chat durable object. |
| examples/chat/worker/chat.ts | Core “schema is the API” table/facet capability graph and exposed RPC surface. |
| examples/chat/worker/chat-do.ts | Durable Object wrapper that attaches DO SQLite storage and serves Cap’n Web RPC. |
| examples/chat/worker/auth.ts | Demo PBKDF2 password hashing/verification using WebCrypto. |
| examples/chat/vitest.config.ts | Vitest configuration to run in workerd via Cloudflare’s workers pool + SWC decorators. |
| examples/chat/vite.config.ts | Vite configuration with SWC decorator lowering and Cloudflare plugin integration. |
| examples/chat/tsconfig.json | TypeScript configuration for SPA + worker + tests. |
| examples/chat/tests/facet-spike.test.ts | In-DO spike test proving facet classes and RPC/hydration behavior end-to-end. |
| examples/chat/tests/env.d.ts | Test env typing to expose Wrangler bindings to cloudflare:test. |
| examples/chat/tests/do-roundtrip.test.ts | DO-local tests for migration/seed and typed insert/select round-trips. |
| examples/chat/tests/capabilities.test.ts | End-to-end confinement + live subscription behavior over real WebSocket route. |
| examples/chat/src/wire-log.ts | Client-side wire log store and prompt injection helpers. |
| examples/chat/src/styles.css | Tailwind import and stat flash animation for live-updating counts. |
| examples/chat/src/rpc.ts | Client RPC setup + logging wrappers + window.* exposure helpers. |
| examples/chat/src/main.tsx | SPA bootstrap without StrictMode to avoid duplicating network effects in the wire log. |
| examples/chat/src/components/WireLog.tsx | UI component for displaying RPC traffic and evaluating local prompt expressions. |
| examples/chat/src/App.tsx | Main app UI: login, directory, room view, live feed, and stats aggregation entrypoint. |
| examples/chat/README.md | Documentation explaining the model, run commands, and capability graph rationale. |
| examples/chat/package.json | Example app dependencies and scripts (dev/test/deploy/typecheck). |
| examples/chat/index.html | Minimal SPA HTML shell. |
| examples/chat/.gitignore | Ignores build artifacts, Wrangler output, and generated worker types. |
Comments suppressed due to low confidence (1)
examples/chat/src/App.tsx:483
- runStats() currently starts a live subscription but never stores the returned subscription handle (statsSub.current) and never updates
stats(the oldsetStats(rows)is commented out). As a result the “top posters” button doesn’t render anything and the subscription can’t be unsubscribed via stopStats() / unmount cleanup.
.select(({ users, messages }) => ({ author: users.name, posts: messages.id.count() }))
.orderBy(({ messages }) => [messages.id.count(), "desc"])
.live()
.observe({ onNext: setStatsByRef })
);
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+444
to
+447
| return () => { | ||
| cancelled = true; | ||
| void sub?.unsubscribe(); | ||
| }; |
Comment on lines
+138
to
+139
| await sub.unsubscribe(); | ||
| const pushesAfterStop = got.length; |
ryanrasti
force-pushed
the
ryan_5_example_chat
branch
from
July 24, 2026 01:54
af26a83 to
fb1c94a
Compare
Comment on lines
+526
to
+546
| const runStats = async () => { | ||
| // The server has no "top posters" endpoint. This aggregation is | ||
| // authored here, client-side, and replayed against the room's granted | ||
| // builder — it cannot reach past this room. | ||
| // | ||
| // One-shot: computes the standings once. To make them update live, swap | ||
| // `.execute()` for a subscription that pushes on every change: | ||
| // statsSub.current = await loggedDoRpc(current.stub, (r) => | ||
| // r.messages().groupBy(...).select(...).orderBy(...) | ||
| // .live().observe({ onNext: setStatsByRef })); | ||
| const rows = await loggedDoRpc(current.stub, (r) => | ||
| r | ||
| .messages() | ||
| .groupBy(({ users }) => [users.name]) | ||
| .select(({ users, messages }) => ({ author: users.name, posts: messages.id.count() })) | ||
| .orderBy(({ messages }) => [messages.id.count(), "desc"]) | ||
| .live() | ||
| .observe({ onNext: setStatsByRef }) | ||
| ); | ||
| //setStats(rows); | ||
| }; |
Comment on lines
+65
to
+70
| const onLogout = () => { | ||
| localStorage.removeItem(AUTH_KEY); | ||
| localStorage.removeItem(ROOM_KEY); | ||
| setUser(null); | ||
| setUserName(""); | ||
| }; |
| // Durable Object. Static assets (the SPA) are served by the assets binding; | ||
| // wrangler.jsonc routes /ws here first. | ||
| export default { | ||
| async fetch(request, env): Promise<Response> { |
Comment on lines
+28
to
+43
| export const verifyPassword = async (password: string, stored: string): Promise<boolean> => { | ||
| const [scheme, iterStr, saltB64, hashB64] = stored.split(":"); | ||
| if (scheme !== "pbkdf2" || !iterStr || !saltB64 || !hashB64) { | ||
| return false; | ||
| } | ||
| const expected = unb64(hashB64); | ||
| const actual = await derive(password, unb64(saltB64), Number(iterStr)); | ||
| if (expected.length !== actual.length) { | ||
| return false; | ||
| } | ||
| let diff = 0; | ||
| for (let i = 0; i < expected.length; i++) { | ||
| diff |= expected[i]! ^ actual[i]!; | ||
| } | ||
| return diff === 0; | ||
| }; |
Comment on lines
+19
to
+20
| this.conn = db.attach(new DoSqliteDriver(ctx.storage)); | ||
| ctx.blockConcurrencyWhile(() => migrate(this.conn)); |
ryanrasti
force-pushed
the
ryan_5_example_chat
branch
from
July 24, 2026 02:43
fb1c94a to
58e45a1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.