Skip to content
Open
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,30 @@ can deliver the same event ID again; consumers must remain idempotent. Event
history belongs in the event store, and command retry results belong in command
receipts. Neither depends on retaining delivered outbox rows.

Commands dispatched to another aggregate cell use the same command ledger, but
with an explicit external dispatch binding. The binding records the logical
route kind and shard alongside the command identity; it is immutable for that
reservation and survives lease reclaim and terminal replay. External completion
updates only the ledger after the returned attempt fence and binding match. It
does not append local events, write the outbox, or treat the cell's commit as
proof that an asynchronous read-model projection has completed. A local causal
commit cannot complete an externally bound reservation, and an external
completion cannot complete a local reservation. Retained cell-only receipts
without a gateway binding remain explicit unknowns until a matching gateway
reservation and trusted completion protocol can be established; they are never
retrofitted into a different route or silently replayed.

The celld command host reserves that gateway intent before making the cell
request. The reservation's causation ID and logical route binding must be
echoed by the trusted cell receipt; a changed command ID, input, causation, or
route is rejected before it can complete the gateway row. A terminal cell
receipt is durably recorded in the gateway ledger before the response is
returned. Status checks current authorization and re-evaluates the durable
receipt's projection evidence, so an in-memory completed-status cache is never
the source of truth. An ambiguous transport result remains retryable against
the same reservation and cell; it does not invoke the local domain handler or
re-publish the cell's events and outbox.

In v5, `OutboxStore::complete` and `complete_many` remove delivered rows instead
of retaining `Published` records. Repeating settlement of a removed row returns
`NotFound`; stale claims on existing rows still return `InvalidState`.
Expand Down
2 changes: 1 addition & 1 deletion distributed_cli/src/contracts/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1259,7 +1259,7 @@ fn migration_inventory_is_deterministic_and_preserves_runtime_order() {
.iter()
.map(|migration| migration.version)
.collect::<Vec<_>>();
assert_eq!(versions, vec![1, 2, 3, 4, 5, 6]);
assert_eq!(versions, vec![1, 2, 3, 4, 5, 6, 7, 8]);
assert_eq!(
inventory.canonical_bytes().expect("canonical inventory"),
inventory
Expand Down
8 changes: 8 additions & 0 deletions js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,14 @@ same replica and GraphQL transport. A command call:
(`confirmDirectProjection`) before the call settles. The server waited in
the command handler because it could; an event handler cannot.

An Eventual command may be terminal `succeeded` at a cell boundary while its
status envelope already carries every exact projection observation. Those
observations settle `receipt.projected` because they prove delivery of the
projection obligation; they do not retire the accepted optimistic layer. The
layer is retired only by a matching canonical query or live frame, so an
`@load` operation does not need to become `@live` merely to complete a causal
wait. A later query or navigation can still supply that canonical frame.

Applications do not provide list targets, merge functions, mutation update
callbacks, board simulators, or invalidation maps. If the compiler cannot prove
safe maintenance, the generated plan marks the affected projection stale and
Expand Down
32 changes: 32 additions & 0 deletions js/src/replica/command-runtime/create.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
parseGraphqlResponseExtensions,
type DistributedCommandMetadata,
type DistributedProtocolEnvelope
} from '../../protocol.js';
import {
Expand Down Expand Up @@ -34,6 +35,7 @@ import {
pendingProjection,
preparedDispatchKeys,
preparedSemanticChanges,
projectionExpectationFingerprint,
requireCommandEnvelope,
requireCommandRejectionEnvelope,
requireStatusEnvelope,
Expand Down Expand Up @@ -85,6 +87,22 @@ import {
type ReplicaCommandProjection
} from '../projection-delta/index.js';

function hasCompleteProjectionObservations(
metadata: DistributedCommandMetadata
): boolean {
if (metadata.expects.length === 0 || metadata.observations.length === 0) {
return false;
}
const observed = new Set(
metadata.observations
.filter((observation) => observation.causationId === metadata.causationId)
.map(projectionExpectationFingerprint)
);
return metadata.expects.every((expectation) =>
observed.has(projectionExpectationFingerprint(expectation))
);
}

function assertActualProjectionCapabilities(
contract: ReplicaCommandProjection,
delta: ProjectionDelta
Expand Down Expand Up @@ -1038,6 +1056,20 @@ export function createReplicaCommandRuntime<
if (tracker.pending !== undefined) {
settleTrackedProjection(tracker, pending);
}
} else if (
metadata.state === 'succeeded' &&
!prepared.revalidation.required &&
!statusRequiresRevalidation &&
hasCompleteProjectionObservations(metadata)
) {
/*
* A cell may keep a committed external receipt in the public
* `succeeded` state after its exact modeled observation is durable.
* That proof settles the projected delivery wait, but it does not
* retire the accepted layer: no canonical query/live frame has
* confirmed read-model membership yet.
*/
settleTrackedProjection(tracker, pending);
} else if (
metadata.state === 'atomic' &&
!prepared.revalidation.required &&
Expand Down
Loading
Loading