Skip to content

feat: celld - #206

Merged
patrickleet merged 37 commits into
tasks--outbox-immediate-nonblocking-1from
tasks--portable-command-hosts-celld
Aug 24, 2026
Merged

feat: celld#206
patrickleet merged 37 commits into
tasks--outbox-immediate-nonblocking-1from
tasks--portable-command-hosts-celld

Conversation

@patrickleet

Copy link
Copy Markdown
Collaborator

Summary

Second command host for portable domain commands: a celld Durable Object runs the same todo-domain create/complete handle as SOA Routes::mount. Local compose uses Azurite (az://celld). The worker is workers-rs wasm around AggregateCell<Todo>. The event log and repository snapshot cache persist in Durable Object SQLite (cell_events, cell_snapshots) and survive a celld restart.

Stacked on #205 (Todo/Chat/Blob domain mounts). No celld Cargo feature (PCH-DEC-005). GraphQL/projectors stay off the cell.

Implements [[tasks/portable-command-hosts-4]] [[tasks/portable-command-hosts-5]] [[tasks/portable-command-hosts-6]] [[tasks/portable-command-hosts-7]] [[tasks/portable-command-hosts-8]] [[tasks/portable-command-hosts-9]]

Test plan

  • cargo test --lib cell_host (8 tests: dispatch, parent-shard, snapshot restore)
  • cd tests/e2e-ui && cargo test -p todo-domain (18 tests)
  • cargo test --test celld fixture path (no CELLD_URL)
  • Live: Azurite compose + worker-build + celld deploy; CELLD_URL=http://127.0.0.1:18880 cargo test --test celld — PUT/POST/GET/isolation
  • GET after docker compose restart celld still hydrates the sourced Todo (events + snapshot cache)
  • CI on this PR

Second portable-command host: CausalWorkspace talks to a per-shard
CellStreamStore (in-process stand-in for private SQLite, not sqlx and
not a celld Cargo feature). AggregateCell mounts the same PortableCommand
declarations as SOA Routes and dispatches them without GraphQL or
projectors.

Implements [[tasks/portable-command-hosts-4]]
CellStreamStore::for_parent_shard holds sibling streams (map, player,
bomb) in one cell SQLite and one CommitBatch. Bomberman tick shards by
game id (`game:{game_id}`), not player/bomb. Blob cells stay
`blob:{game_id}`. There is no two-cell transaction API.

Implements [[tasks/portable-command-hosts-5]]
One SQLite Durable Object class per todo id, official celld image via
Docker Compose. Fixture tests always run; live HTTP create/complete is
gated on CELLD_URL. No MinIO, no celld Cargo feature, no secrets.

Implements [[tasks/portable-command-hosts-6]]
Azurite is the documented local bucket (az://celld). Docker Desktop
injects extra_hosts, so celld cannot share Azurite's network namespace;
socat forwards 127.0.0.1:10000 to the azurite service.

Implements [[tasks/portable-command-hosts-6]]
Replace the JS TodoCell with a workers-rs Durable Object that mounts
todo-domain create/complete through AggregateCell. wasm32 uses a JS
Date wall clock because SystemTime::now panics on unknown-unknown.

Implements [[tasks/portable-command-hosts-7]]
CellStreamStore dumps EventRecords into the DO cell_events table and
restores them on each request. GET after celld restart still hydrates
the event-sourced Todo.

Implements [[tasks/portable-command-hosts-8]]
AggregateCell can use with_snapshots; CellStreamStore implements
SnapshotStore and get_stream_tail. Todo is Snapshottable. The worker
persists cell_snapshots next to cell_events so load after restart is
snapshot plus event tail.

Implements [[tasks/portable-command-hosts-9]]
@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0f3a9874-ea68-4bcf-b6f9-86e907e294fc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Make Service::dispatch_causal_with_receipt callable outside crate::microsvc
and add an integration test that asserts payload plus receipt.

Implements [[tasks/portable-command-hosts-10]]
POST /{command} and gRPC Dispatch accept { commandId, input } and return
payload plus receipt. Identity comes from transport headers/metadata.
Bus::send stays fire-and-forget.

Implements [[tasks/distributed-command-surfaces-2]]
Mutations and status resolve via LocalCommandHost or HttpCommandHost.
HTTP/WebSocket request data no longer carries Arc<Service>.

Implements [[tasks/distributed-command-surfaces-3]]
graphql_router_with_dispatcher is a CommandHost; GraphQL-only
engines wait-dispatch to HTTP writers. Task 20 mTLS stays the
CMP envelope; wait-path remote is HttpCommandHost.

Implements [[tasks/distributed-command-surfaces-3]]
Persist GET sealed JSON next to events/snapshots. Todo cell POST
/{command} with { commandId, input }. GET queues behind POST on
the same isolate.

Implements [[tasks/distributed-command-surfaces-4]]
Mount store per model on the engine, not the ReadModel type.
Cell-by-key compiles PK/by-id only and rejects list/filter/join.

Implements [[tasks/distributed-command-surfaces-5]]
Named profile under tests/e2e-ui/celld-nats-profile. Default
one-process host.rs / make run is unchanged.

Implements [[tasks/distributed-command-surfaces-6]]
@patrickleet patrickleet changed the title feat: celld host for portable Todo aggregates feat: celld Aug 23, 2026
authorized_unknown_status_returns_only_public_state no longer
puts Arc<Service> in request data.

Implements [[tasks/distributed-command-surfaces-3]]
make run stays the one-process playground. Bring-up, smoke, and
teardown of celld+NATS are named targets.

Implements [[tasks/distributed-command-surfaces-6]]
Reuse a running compose NATS; if 14222 is taken by something
else, print the listener and how to override NATS_PORT.
down-celld-nats also removes a stray docker-run container.

Implements [[tasks/distributed-command-surfaces-6]]
CommandHost routers need /graphql/ws for live chat. Export
ProtocolResponseAccumulator so out-of-crate hosts can implement
CommandHost, and let wait-path clients remap payload JSON.

Implements [[tasks/distributed-command-surfaces-7]]
Sibling example of e2e-ui (not make run). New todo/chat/blob/graphql
service crates reuse the e2e-ui domain crates. Todo create/complete
go through HttpCommandHost to {CELLD_URL}/todo/{id}/{command}; SQL
lists dual-write locally so the playground UI can render.

Implements [[tasks/distributed-command-surfaces-7]]
Navbar shows a CELLD badge when PUBLIC_E2E_PROFILE=celld-nats.
make run stays the one-process playground.

Implements [[tasks/distributed-command-surfaces-7]]
PCH-DEC-001 asked for a macro beside the Routes builder. #[command]
already exists, so the function-like form is portable_command!. Todo
thin commands (complete, rename, reopen, archive, purge) expand to
shard + invoke + Eventual. create and force_archive keep handle:.

Implements [[tasks/portable-command-hosts-2]]
Chat is lobby posts only. Identity owns ingress, scrape, and the
AuthUsers projector on its own outbox leaf — not the chat aggregate.

Implements [[tasks/distributed-command-surfaces-7]]
Stack badges, portable_command! walkthrough, and both make run recipes.
Domain declarations stay the same; the celld host wait-dispatches Todo.

Implements [[tasks/distributed-command-surfaces-7]]
Wait-path returns events+outbox from the cell SQLite. GraphQL publishes
via MessagePublisher (NATS here), fire-and-forgets outbox.complete, and
seals Eventual projection metadata from those occurrences without a
second command write. Chat @LiVe stays on the GraphQL process.
e2e-ui make run is unchanged.

Implements [[chat-celld-wait-path-keeps-graphql-live]]
Implements [[tasks/distributed-command-surfaces-7]]
CelldCommandHost and cell outbox drain live in distributed::cell_host.
Aggregate crates only supply CelldRoute. GraphQL is the user OIDC edge
(engine OidcBearer); the Tower JWT-to-header layer is gone. make run
cargo-watches GraphQL and the worker.

Implements [[chat-celld-wait-path-keeps-graphql-live]]
Implements [[tasks/distributed-command-surfaces-7]]
The example host no longer falls back to sqlite:./e2e-celld.db. DATABASE_URL
comes from e2e-ui.env (make -C tests/e2e-ui up). Cells still keep private
SQLite per Durable Object.

Implements [[chat-celld-wait-path-keeps-graphql-live]]
Fence Eventual projection-delta rows so a later complete @LiVe snapshot
cannot drop them after Delivered. Skip GraphQL SSR seeds on SvelteKit
isDataRequest so client navigations use the replica; hover prefetches
the route operation in the browser.

Implements [[specs/e2e-ui/sveltekit-dx]]
Add integration-celld.yaml: e2e-celld workspace tests plus live
Azurite+celld+NATS (`make test-celld`). Wire it into the PR and main
gates so live HTTP no longer skips without CELLD_URL.

Implements [[tasks/portable-command-hosts-11]]
Pack-smoke now lists matchDistributedRoute. Snapshot tail loads clamp
prefix to the durable stream so a planted-ahead cache misses and
replays. CausalDispatchResult/OutboxMessage implement PartialEq so
graphql lib tests compile. Chat Send no longer stays disabled while
Eventual projected is still catching up.

Implements [[tasks/portable-command-hosts-11]]
Keep snapshot-only SQLite loads when event rows were deleted; clamp
prefix only when a stream version exists so planted-ahead cache still
misses. Tail-only hydrate keeps post-snapshot events in memory.

Eventual `projected` settles when a committed result frame names the
command (or has no command payload), even if membership fences keep the
list overlay. Chat Send is disabled only while busy so it re-enables
after projected with an empty composer.

Implements [[tasks/portable-command-hosts-11]]
Settling projected whenever a live frame named the command made
status-regression tests miss their rejection. Query/@LiVe frames have
no command payload; those still settle so chat Send can clear busy
while membership fences keep the overlay.

Implements [[tasks/portable-command-hosts-11]]
Atomic direct projection was taking Eventual list membership fences.
Blob, new games, and client-side navigations never get @LiVe, so those
fences rejected later complete snapshots and the UI waited seconds.

Eventual chat still fences list membership so stale complete @LiVe cannot
drop a posted row. Chat Send stays disabled while busy or empty; tests
wait for enabled after fill so Playwright binds the draft.

Implements [[chat-celld-wait-path-keeps-graphql-live]]
Implements [[tasks/portable-command-hosts-11]]
Postgres listen/subscribe drained to idle and rebuilt Service on every
quiet stretch, so chat projection lagged several seconds. Long-running
hosts now idle-poll instead of exiting the consumer.

Zitadel scrape treated duplicate outbox ids as done and skipped the
directory upsert. Those events never reached bus_log, so auth_users
stayed empty and chat author names disappeared. Scrape now writes
auth_users from the Management API profile even when outbox already
has the delivery.

Implements [[chat-celld-wait-path-keeps-graphql-live]]
Implements [[tasks/portable-command-hosts-11]]
Keep long-running consumers alive across idle polls, route every Todo transition to one cell, persist fenced cell command replays, enforce CellByKey row policies, and run the real browser lifecycle in celld CI.

Refs [[incidents/pr-206-e2e-ui-command-latency-1]]
Trust exact authenticated projection deltas instead of refetching solely because they have no obligations. Preserve conservative revalidation for unconditional recovery cases, cover rapid Todo transitions, and keep newly-created Todo controls pending until the durable receipt arrives.
Install the anonymous public Chat client during client-side route entry even when SvelteKit omits data-request hydration. Atomically seal locally provable collection membership from authoritative direct command rows so Blob start is visible without refresh, while leaving unprovable membership stale.
@patrickleet
patrickleet merged commit 84c1576 into tasks--outbox-immediate-nonblocking-1 Aug 24, 2026
14 checks passed
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