fix(celld): make command lifecycle durable and fast - #207
Merged
patrickleet merged 4 commits intoAug 24, 2026
Merged
Conversation
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]]
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
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
merged commit Aug 24, 2026
880bea1
into
tasks--portable-command-hosts-celld
23 checks passed
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.
What this PR is
This is the hardening layer for the portable command host and celld support introduced in #206.
The core idea is that a domain command is declared once, then the application decides where to run it:
The domain crate does not import
sqlx,QueuedRepository, Durable Objects, celld, NATS, or GraphQL transport code. Deployment topology is host configuration, not domain behavior.This PR makes that model durable and responsive end to end: commands have fenced, restart-safe receipts; Eventual delivery no longer rebuilds the service graph after idle polls; exact command results settle client optimism without an unnecessary collection refetch; and authorization is preserved across the GraphQL-to-cell boundary.
Mental model
flowchart LR C[Generated client] --> G[GraphQL command field] G --> H{CommandHost route?} H -->|not selected| S[SOA / LocalCommandHost] H -->|selected| D[celld] S --> SQL[(application SQL)] D --> CELL[aggregate shard<br/>one writer + private SQLite] S --> O[outbox] CELL --> O O --> B[NATS / Kafka / RabbitMQ] B --> P[Eventual projectors] P --> RM[(SQL read models)] RM --> GA cell is a consistency boundary, not another SQL dialect. One cell instance owns one aggregate shard, such as
todo:{todo_id}. GraphQL, global queries,@live, projectors, and identity ingestors remain outside cells.1. Declare the command once in the domain
The Todo domain's real
completecommand is representative:That declaration carries everything both hosts must agree on:
nameis the stable command identity.aggregateandshardidentify the consistency boundary. SOA aggregate loading and the celld route must resolve the same shard key.rolesandfielddescribe the generated GraphQL command surface.outcomepreserves the application'sAtomic<T>orEventual<T>contract; choosing celld does not silently change the public consistency contract.invokeandpayloadare domain behavior and result mapping.Simple transitions use
load+invoke+payload. Commands needing custom creation, guards, or orchestration can use thehandleescape hatch; for example,todo.createuses an authenticated guard, a custom handler, and a generated UUIDv7 default while remaining portable.Handlers continue to receive
CausalCommandContext<'_, A>and usectx.repo(). There is deliberately noctx.cell(): a handler must not know whether SOA or celld is executing it.See the complete declarations in
todo-domain/src/commands.rs.2. Run the commands as a normal SOA service
Mount the portable declarations on ordinary aggregate routes:
With the normal local command host, dispatch stays in the service process. The host supplies the configured repository, transaction/lock implementation, outbox, and bus. This is the simplest deployment when one database is the desired consistency boundary.
The runnable example is
service/src/modules/todo.rs.3. Run selected commands in celld
The cell worker mounts the same declarations:
The GraphQL host then declares which command names should wait-dispatch to that cell class:
todo_shardreadstodo_id, so all transitions for one Todo reach the sametodo:{todo_id}instance. That instance has one writer and private SQLite for events, snapshots, its durable command ledger, and its outbox.The concrete route is in
todo-service/src/host.rs, and the workers-rs cell mounts are intests/celld/worker/src/lib.rs.4. Mix SOA and celld in one application
Routing is additive and opt-in:
CelldCommandHostchecks the registered routes for each command. A match goes to celld; a command with no celld route automatically falls back to the local SOA service.The e2e application is intentionally hybrid:
todo_idchat.postmessage_idAtomiccommands@liveThat means a team can move only a hot or contention-heavy aggregate to cells without rewriting its domain handlers, changing the GraphQL schema, or moving unrelated services. Adding or removing a
CelldRoutechanges placement; it does not fork domain behavior.See the complete hybrid host in
graphql-service/src/host.rs.How a celld command completes
CelldCommandHostselects a route and derives the cell shard from the command input.MessagePublisher; normal Eventual projectors consume those events and update global SQL read models.The receipt is scoped by the named service and verified principal partition. Replaying the same command ID with the same input returns the committed result; reusing it with different input is a conflict. Because the ledger is in cell SQLite, that behavior survives worker/celld restarts rather than depending on process memory.
Why this fixes the latency and race failures
The original failure was not localhost network latency. Idle Eventual consumers were completing, causing service/projector graphs to be rebuilt, while the client also treated accepted commands as a reason to rerun broad queries. A late collection response could then temporarily overwrite a newer optimistic transition on another aggregate.
This PR changes that lifecycle:
Atomiccollection membership (the Blob create case) is applied without requiring a page refresh.The intended UX is immediate local feedback followed by a small authoritative settlement, not a several-second lockout followed by wholesale query replacement.
When to choose each host
Prefer SOA/local when
Atomicprojection.Prefer celld when
Prefer a hybrid when
One important boundary: do not model a required atomic invariant as a distributed transaction across multiple cells. Put the operation on one parent shard (for example
game:{game_id}) or keep it in an SOA database transaction. Cells are deliberately local consistency units.What this PR hardens beyond #206
CellByKeyreads and fails closed on malformed or unsupported policy material.Atomiccreation without-refresh behavior.Verification
Stack
Stacked directly on #206 (
tasks--portable-command-hosts-celld). Merge #206 first, then this PR.Refs [[incidents/pr-206-e2e-ui-command-latency-1]]