Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ Write the domain once, compose it into one `Service` or several, then
generate the client. Each stage below uses real code from
[`tests/e2e-ui`](tests/e2e-ui).

Unsigned command fields preserve their Rust range in generated client validation
and optimistic projections; see [unsigned command inputs](docs/unsigned-command-inputs.md).

```mermaid
sequenceDiagram
actor Author
Expand Down Expand Up @@ -1054,13 +1057,27 @@ fn upcast_initialized_v1_v2((id, task): InitV1) -> InitV2 {
}

aggregate!(Todo, entity {
"initialized"(id, task, priority) => initialize,
"initialized"(id, task, priority), version = 2 => initialize,
"completed"() => complete(),
} upcasters [
("initialized", 1 => 2, InitV1 => InitV2, upcast_initialized_v1_v2),
]);
```

Replay requires each event's schema version to match its registered handler
after upcasting. `#[sourced]` reads this version from `#[event(..., version = N)]`;
`aggregate!` declares it on the registration as above and must match the
corresponding `#[digest(..., version = N)]`. Omitted versions default to 1.
Older events need an explicit upcaster chain to the registered version; future
versions are rejected. Matching payload layouts do not bypass this check.
The check runs before payload decoding or handler invocation, including when
loading only the event tail after a snapshot in a native repository or cell.

**Breaking change:** an `aggregate!` registration for a versioned handler must
now declare its current version. Histories with unsupported versions fail replay
instead of being interpreted using the current payload shape. Stored events are
unchanged; add the appropriate upcasters to read supported older versions.

## Event Metadata

Metadata lets you attach cross-cutting context — correlation IDs, causation IDs, user context, trace spans — to events without changing your domain model.
Expand Down
6 changes: 6 additions & 0 deletions distributed_cli/src/client_compiler/command_manifest/shape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,12 @@ fn validate_codec(
) -> Result<(), ClientCompileError> {
match scalar_codecs.get(scalar) {
Some(expected) if expected == codec => Ok(()),
Some(_)
if scalar == "BigInt"
&& matches!(codec, "uint8" | "uint16" | "uint32" | "uint64_safe_integer") =>
{
Ok(())
}
Some(expected) => Err(invalid(
"client.manifest.command_type_codec",
format!("{label} codec `{codec}` does not match `{scalar}` codec `{expected}`"),
Expand Down
2 changes: 1 addition & 1 deletion distributed_cli/src/client_compiler/manifest/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub(crate) use util::{validate_hash, validate_nonempty};
const MANIFEST_VERSION: u64 = 2;
const PROTOCOL_VERSION: u64 = 1;
const PROTOCOL_FINGERPRINT: &str =
"sha256:00fb342f3acb4dc1c1716a43cc3001c748d5f6c500ff831690d820e9e43e2782";
"sha256:0dfa8a3f49e17d8d99c5c095c1ed14f528cae3e55fbc2f2b852975a50936ec5b";

pub(crate) const CLIENT_PROJECTION_PROGRAM_VERSION: u32 = 2;
pub(crate) const CLIENT_PROJECTION_BINDING_VERSION: u32 = 1;
Expand Down
10 changes: 6 additions & 4 deletions distributed_cli/src/client_compiler/manifest/projections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -985,9 +985,7 @@ fn input_codec_compatible(
(type_name, codec),
("Int", "int32") | ("BigInt", "json_number_precision_limited")
),
// Both numeric command codecs admit negative values. Without a
// non-negative refinement in the frozen manifest, neither proves U64.
ManifestProjectionValueType::U64 => false,
ManifestProjectionValueType::U64 => type_name == "BigInt" && unsigned_codec(codec),
ManifestProjectionValueType::F64 => type_name == "Float" && codec == "float64",
ManifestProjectionValueType::String => matches!(
(type_name, codec),
Expand All @@ -1004,7 +1002,7 @@ fn codec_compatible(codec: &str, expected: &ManifestProjectionValueType) -> bool
ManifestProjectionValueType::I64 => {
matches!(codec, "int32" | "json_number_precision_limited")
}
ManifestProjectionValueType::U64 => false,
ManifestProjectionValueType::U64 => unsigned_codec(codec),
ManifestProjectionValueType::F64 => codec == "float64",
ManifestProjectionValueType::String | ManifestProjectionValueType::Enum(_) => {
codec == "string"
Expand All @@ -1013,6 +1011,10 @@ fn codec_compatible(codec: &str, expected: &ManifestProjectionValueType) -> bool
}
}

fn unsigned_codec(codec: &str) -> bool {
matches!(codec, "uint8" | "uint16" | "uint32" | "uint64_safe_integer")
}

fn constant_compatible(
value: &ManifestProjectionValue,
expected: &ManifestProjectionValueType,
Expand Down
10 changes: 9 additions & 1 deletion distributed_cli/src/client_compiler/render/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,15 @@ fn render_command_field_type(
}
match field.codec.as_deref() {
Some("boolean") => Ok("boolean".into()),
Some("float64" | "int32" | "json_number_precision_limited") => Ok("number".into()),
Some(
"float64"
| "int32"
| "json_number_precision_limited"
| "uint8"
| "uint16"
| "uint32"
| "uint64_safe_integer",
) => Ok("number".into()),
Some("string" | "base64" | "string_unvalidated_timestamp") => Ok("string".into()),
Some("json") => Ok("ReplicaValue".into()),
Some(codec) => Err(ClientCompileError::manifest(
Expand Down
54 changes: 53 additions & 1 deletion distributed_cli/src/client_compiler/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ pub(super) fn manifest() -> JsonValue {
"service_id": "todos-service",
"surface": {"kind": "role", "name": "user"},
"schema_fingerprint": fingerprint("schema"),
"protocol_fingerprint": "sha256:00fb342f3acb4dc1c1716a43cc3001c748d5f6c500ff831690d820e9e43e2782",
"protocol_fingerprint": "sha256:0dfa8a3f49e17d8d99c5c095c1ed14f528cae3e55fbc2f2b852975a50936ec5b",
"execution": {
"max_depth": 8,
"max_complexity": 500,
Expand Down Expand Up @@ -3166,6 +3166,58 @@ fn command_protocol_and_extensions_are_preserved_exactly() {
invalid_u64_source["projection_programs"][0]["arms"][0]["operations"][0]["fields"][1]
["assignment"]["expression"]["value_type"] = json!({"type": "u64"});
refresh_schema_fingerprint(&mut invalid_u64_source);
for codec in ["uint8", "uint16", "uint32", "uint64_safe_integer"] {
let mut unsigned = invalid_u64_source.clone();
unsigned["commands"][0]["input"]["definition"]["fields"][1]["type_name"] = json!("BigInt");
unsigned["commands"][0]["input"]["definition"]["fields"][1]["codec"] = json!(codec);
unsigned["models"][0]["fields"][2]["scalar"] = json!("BigInt");
unsigned["models"][0]["fields"][2]["codec"] = json!("json_number_precision_limited");
refresh_schema_fingerprint(&mut unsigned);
let compiled = compile_client(input_with_manifest(
unsigned.clone(),
"query Todos { todos { id priority } }",
))
.expect("unsigned refined inputs prove U64 slots");
let source = file(&compiled, "commands.ts");
assert!(source.contains("readonly \"priority\": number;"));
let body = source
.split("export const Command_createTodo:")
.nth(1)
.unwrap()
.split_once(" = ")
.unwrap()
.1
.split_once("\n};")
.unwrap()
.0;
let artifact: JsonValue = serde_json::from_str(&format!("{body}\n}}")).unwrap();
assert_eq!(artifact["input"]["definition"]["fields"][1]["codec"], codec);
assert_eq!(
artifact["projection"]["preview"]["operations"][0]["mutation"]["fields"][1]["value"]
["kind"],
"input"
);
if codec == "uint64_safe_integer" {
let fixture: JsonValue = serde_json::from_str(include_str!(
"../../tests/fixtures/generated-unsigned-command.json"
))
.unwrap();
assert_eq!(
artifact, fixture,
"unsigned JS bridge must match generated output"
);
}
unsigned["commands"][0]["extensions"]["trusted_presets"] =
json!([{"name": "priority", "codec": codec}]);
unsigned["commands"][0]["extensions"]["projection"]["preview_occurrences"][0]["values"]
[2]["source"] = json!({"kind": "trusted_preset", "name": "priority", "codec": codec});
refresh_schema_fingerprint(&mut unsigned);
compile_client(input_with_manifest(
unsigned,
"query Todos { todos { id priority } }",
))
.expect("unsigned trusted preset codec proves U64 slot");
}
let mut invalid_constant_source = value.clone();
invalid_constant_source["commands"][0]["extensions"]["projection"]["preview_occurrences"][0]
["values"][2]["source"] =
Expand Down
2 changes: 1 addition & 1 deletion distributed_cli/tests/cli_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const ROLE_MANIFEST: &str = r#"{
"name": "user"
},
"schema_fingerprint": "sha256:758a97e4f7e1e538e8be86d24abd3d50a8da2d5813d29abd7a04bfa092d05189",
"protocol_fingerprint": "sha256:00fb342f3acb4dc1c1716a43cc3001c748d5f6c500ff831690d820e9e43e2782",
"protocol_fingerprint": "sha256:0dfa8a3f49e17d8d99c5c095c1ed14f528cae3e55fbc2f2b852975a50936ec5b",
"execution": {
"max_depth": 8,
"max_complexity": 500,
Expand Down
2 changes: 1 addition & 1 deletion distributed_cli/tests/cli_manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ fn client_manifest_uses_service_surface_export() {
);
assert_eq!(
manifest["protocol_fingerprint"],
"sha256:00fb342f3acb4dc1c1716a43cc3001c748d5f6c500ff831690d820e9e43e2782"
"sha256:0dfa8a3f49e17d8d99c5c095c1ed14f528cae3e55fbc2f2b852975a50936ec5b"
);
assert_eq!(manifest["models"][0]["id"], "OrderView");
assert_eq!(manifest["models"][0]["record_revisions"], true);
Expand Down
6 changes: 3 additions & 3 deletions distributed_cli/tests/fixtures/generated-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export const Command_importTodos: ReplicaCommandArtifact<Command_importTodos_Inp
},
"protocol": {
"operation": "sha256:e8e54238fd7618fa94e90ae60b1dfac8833943027d04e71be84cb03702f1cebf",
"protocolHash": "sha256:00fb342f3acb4dc1c1716a43cc3001c748d5f6c500ff831690d820e9e43e2782",
"protocolHash": "sha256:0dfa8a3f49e17d8d99c5c095c1ed14f528cae3e55fbc2f2b852975a50936ec5b",
"schemaHash": "sha256:238a646c368324e2576125f2b0af380d42dfe6f48149b0b36b2e1bbfd7976757",
"surface": {
"kind": "role",
Expand Down Expand Up @@ -131,7 +131,7 @@ export const Command_pingTodos: ReplicaCommandArtifact<Command_pingTodos_Input,
},
"protocol": {
"operation": "sha256:3cb3c1e96331b4e98191cc725ab6b01c0e9b04cc7cc0f37f4fa0ef394fee9acf",
"protocolHash": "sha256:00fb342f3acb4dc1c1716a43cc3001c748d5f6c500ff831690d820e9e43e2782",
"protocolHash": "sha256:0dfa8a3f49e17d8d99c5c095c1ed14f528cae3e55fbc2f2b852975a50936ec5b",
"schemaHash": "sha256:238a646c368324e2576125f2b0af380d42dfe6f48149b0b36b2e1bbfd7976757",
"surface": {
"kind": "role",
Expand Down Expand Up @@ -434,7 +434,7 @@ export const Command_projectTodo: ReplicaCommandArtifact<Command_projectTodo_Inp
},
"protocol": {
"operation": "sha256:f986d060555cdedfe94621914116306af704d8bb90e75289722a3b7119211d32",
"protocolHash": "sha256:00fb342f3acb4dc1c1716a43cc3001c748d5f6c500ff831690d820e9e43e2782",
"protocolHash": "sha256:0dfa8a3f49e17d8d99c5c095c1ed14f528cae3e55fbc2f2b852975a50936ec5b",
"schemaHash": "sha256:238a646c368324e2576125f2b0af380d42dfe6f48149b0b36b2e1bbfd7976757",
"surface": {
"kind": "role",
Expand Down
2 changes: 1 addition & 1 deletion distributed_cli/tests/fixtures/generated-operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -828,7 +828,7 @@ export const Operation_ScalarInputs: ReplicaOperationArtifact<Operation_ScalarIn
"protocol": {
"version": 1,
"schemaHash": "sha256:1b203cdad2827a6b52ac96e6d4d9114e160312cf93196a3d489dde2d5e331526",
"protocolHash": "sha256:00fb342f3acb4dc1c1716a43cc3001c748d5f6c500ff831690d820e9e43e2782",
"protocolHash": "sha256:0dfa8a3f49e17d8d99c5c095c1ed14f528cae3e55fbc2f2b852975a50936ec5b",
"surface": {
"kind": "role",
"name": "user"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@
},
"protocol": {
"operation": "sha256:9b2ba78faeaad3b384d74519aff78252c2f7f0305db1ac0dda9833ba25af58e5",
"protocolHash": "sha256:00fb342f3acb4dc1c1716a43cc3001c748d5f6c500ff831690d820e9e43e2782",
"protocolHash": "sha256:0dfa8a3f49e17d8d99c5c095c1ed14f528cae3e55fbc2f2b852975a50936ec5b",
"schemaHash": "sha256:574ba7862d1814821cb9de30c2494f298031dd1d25ea615317aac1062af08b60",
"surface": {
"kind": "role",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export const Operation_RustRuntimeBridge: ReplicaOperationArtifact<Operation_Rus
"protocol": {
"version": 1,
"schemaHash": "sha256:787216f76fd2c62f867b9d9553710c888f325c6cc5598c4681fb34c1a655e3be",
"protocolHash": "sha256:00fb342f3acb4dc1c1716a43cc3001c748d5f6c500ff831690d820e9e43e2782",
"protocolHash": "sha256:0dfa8a3f49e17d8d99c5c095c1ed14f528cae3e55fbc2f2b852975a50936ec5b",
"surface": {
"kind": "role",
"name": "user"
Expand Down
Loading
Loading