DO PoC#86
Open
ryanrasti wants to merge 12 commits into
Open
Conversation
A Cloudflare Durable Object chat app whose data layer is typegres running inside the DO's SQLite, driven end-to-end over Cap'n Web (see plan). Phase 0 scaffolds the worker/DO + vitest-pool-workers (workerd runs locally here) and pins the SqlStorage binding/result contract as a living artifact. Key findings that shape the coming DoSqliteDriver: - numbers bind as REAL (7/2 = 3.5), same as better-sqlite3 -> integer affinity comes from the dialect's CAST, which works in workerd too; - SqlStorage REJECTS BigInt (opposite of better-sqlite3) -> the driver must map typegres's boolean 0n/1n to plain 0/1; - blobs bind from Uint8Array, return as ArrayBuffer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SqlStorage always marshals INTEGER results to a JS number (never bigint), so values above 2^53 come back lossy (9223372036854775807 -> ...776000) -- worse than better-sqlite3, which can return bigint. It stores int64 correctly, so CAST(col AS TEXT) reads the exact value. Chat ids are small, but the limitation is now on record with its workaround. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A typegres Driver over Cloudflare's SqlStorage. Compiles typegres queries to
{text, ?-values}, down-converts BigInt→Number (SqlStorage rejects BigInt),
runs ctx.storage.sql.exec, and normalizes native results (number/string/
ArrayBuffer/null) to the string form deserialize expects (blobs as \x-hex).
Single-connection; no-op close. Built/tested in the example; promote to core
once proven.
Test: a real typegres select (.upper()/.orderBy()) runs against the DO SQLite
inside workerd and deserializes correctly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 4-table schema (users, rooms, room_members, messages) as module-level typegres tables bound to a driverless Database; each DO attaches its own DoSqliteDriver. The DO runs the migration in blockConcurrencyWhile on init. Relation methods (room.messages(), message.author(), ...) use the codegen's scope propagation. Test: inserts across all four tables + a messages⋈users join run through the DO's SQLite in workerd. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
User (the main cap) and Room, with @expose gating as the authorization story: reads return client-refinable query builders (createRoom/joinRoom/room(id) hand back Room caps; room(id) enforces membership at grant time), mutations (post) execute server-side. authenticate() is the auth entry. Test (in-process, no wire): createRoom + post + read, and a non-member is denied a Room cap until joinRoom — all against the DO's SQLite. Cap'n Web (Phase 4) gates the same graph over the wire. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ase 4) The DO serves its @expose capability graph to clients over a real WebSocket via Cap'n Web (newWorkersWebSocketRpcResponse + toRpc(user)); clients author typegres queries as record-replay closures that run server-side against the DO's SQLite. Auth is a ?user= token → the authenticated User is the main cap. typegres now exports the shim at `typegres/capnweb` (toRpc/fromRpc/doRpc/ ShimStub); tsdown builds it with swc (decorators) and capnweb external. Tests (real workerd, WebSocket transport): - client authors createRoom + a .rooms().select(...).execute(conn) query, replayed in the DO; - @expose gating holds over the wire: a non-member's room(id) rejects. Notes: schema columns/relations are @expose'd so client-authored selects can read them; isolated storage is off (a live WS keeps the DO's SQLite open), so each test uses its own DO id. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A React chat client (client/) over the Cap'n Web capability graph: login → create/join rooms → post + read messages, every call authoring a typegres query that runs in the DO. client/api.ts is the framework-agnostic doRpc glue. Made the example a real deployable Cloudflare app: - vite builds client/ → dist/client, served as static assets; - wrangler.jsonc gains an `assets` binding + the Worker routes /ws to the DO and everything else to ASSETS; - the node-only typegres drivers (pg/better-sqlite3/pglite) are aliased to an empty stub so their native deps stay out of the workerd bundle (the DO uses only DoSqliteDriver); - scripts: dev (vite watch + wrangler dev), build, deploy, preview. `wrangler deploy --dry-run` bundles clean; 14 tests still green; README added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Verified against a real workerd (wrangler dev) driven by a headless browser;
all three are browser/React-runtime bugs the node and in-process workerd tests
could not surface.
1. Login sent the capnweb stub through setState. connect() returns a capnweb
stub, which is a *callable* Proxy. `setClient(connect(name))` made React
treat it as a functional updater and invoke `stub(prevState)` -> the wire
showed a spurious `main(null)` call and every later query pipelined off its
broken result ("'' is not a function"). Store it via an initializer:
`setClient(() => c)`.
2. joinRoom was not idempotent. Opening a room you already belong to (every
room in your sidebar) ran a plain INSERT into room_members and hit a UNIQUE
constraint. Skip the insert when membership already exists (the DO is
single-threaded; typegres has no ON CONFLICT yet). Regression test added.
3. Feed auto-scroll effect used a concise arrow body, so useEffect returned the
value of scrollIntoView() as its "cleanup" -> React later called it and threw
"destroy is not a function" on every feed update. Use a block body.
Full create -> open -> post -> read round trip now works in the minified
production bundle with zero console errors; 15 workerd tests pass.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Previously the sidebar only listed rooms you'd already joined, and nothing surfaced the id of a room someone else created -- so there was no way to join one through the UI (only the plumbing, User.joinRoom, existed). Add a public directory: - User.allRooms() -- a client-refinable builder over every room. Any authenticated user may list it; a Room *capability* is still only granted once you're a member, so the @expose authorization story is unchanged. - Client lists allRooms() alongside rooms() and diffs them: rooms you're in are clickable, rooms you're not in show a [Join] button that joins then opens. Verified in a real browser: a second user sees another's room in the directory, joins it, reads the existing message, and both users exchange messages. Added a workerd regression test for discover -> join -> read. 16 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Promote DoSqliteDriver into typegres with typegres/core and typegres/do-sqlite entries so Workers avoid optional node peers (drop empty.ts aliases). Reshape the chat demo around ChatApi.userByName, client-authored doRpc queries (no api/feed wrappers), Tailwind, and tg generate from migrate DDL. Add LLM-oriented guidelines.md.
- tg generate emits runtime imports from typegres/core (worker-safe). - SQLite introspector only marks INTEGER PRIMARY KEY as generated when the PK is single-column (composite pk=1 is not a rowid alias); regression test. - Chat schema: created_by → created_by_user_id so relation naming has no clash; drop generate post-process rewrites.
Expand guidelines with the relation-naming rule (strip trailing _id). Replace generate-schema.mjs with TypeScript that imports MIGRATE_SQL from src/migrate.ts, matching examples/sqlite's node --experimental-strip-types pattern.
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.