use std::collections::BTreeSet;
use serde_json::Value as JsonValue;
use super::super::manifest::{
canonical_json_value, ClientManifest, ManifestCommand, ManifestCommandShape,
ManifestConsistencyKind, ManifestDirectProjection, ManifestEffectExpression, ManifestTypeDef,
ManifestTypeField,
};
use super::super::projection_delta::{compile_command_preview, CompiledCommandProjection};
use super::super::ClientCompileError;
use super::common::quoted_property;
const COMMAND_ARTIFACT_VERSION: u32 = 2;
pub(super) fn render_commands(manifest: &ClientManifest) -> Result<String, ClientCompileError> {
validate_command_namespaces(&manifest.commands)?;
let projectors = serde_json::to_string_pretty(&manifest.projectors).map_err(|error| {
ClientCompileError::manifest(
"client.render.projectors",
format!("failed to render projector artifacts: {error}"),
)
})?;
let mut sections = vec!["/** GENERATED by distributed client. Do not edit. */".to_string()];
let pure_inventory = pure_function_inventory(manifest)?;
if !manifest.commands.is_empty() {
sections.push(
"import {\n createReplicaCommandRuntime,\n prepareReplicaCommand\n} from '@hops-ops/distributed/replica';"
.into(),
);
sections.push(
"import type {\n DistributedReplica,\n PrepareReplicaCommandOptions,\n ReplicaCommandArtifact,\n ReplicaCommandRuntime,\n ReplicaCommandRuntimeOptions,\n ReplicaCommandTransport,\n ReplicaPreparedCommand,\n ReplicaValue\n} from '@hops-ops/distributed/replica';"
.into(),
);
sections.push("import { COMMAND_STATUS } from './protocol.js';".into());
if !pure_inventory.is_empty() {
sections.push("import { PURE_FUNCTIONS } from './pures.js';".into());
}
}
for command in &manifest.commands {
sections.push(render_command(command, manifest)?);
}
let artifact_names = manifest
.commands
.iter()
.map(|command| format!("Command_{}", command.mutation_field))
.collect::<Vec<_>>()
.join(", ");
sections.push(format!(
"export const COMMAND_ARTIFACTS = [{artifact_names}] as const;"
));
let command_entries = manifest
.commands
.iter()
.map(|command| {
format!(
" {}: Command_{}",
quoted_property(&command.name),
command.mutation_field
)
})
.collect::<Vec<_>>()
.join(",\n");
sections.push(format!(
"/** Inspectable command inventory consumed by the generated binding factory. */\nexport const COMMANDS = {{\n{command_entries}\n}} as const;"
));
if !manifest.commands.is_empty() {
sections.push(
[
"/** Runtime owning the generated callable command surface and its causal lifecycle. */",
"export type GeneratedCommandRuntime = ReplicaCommandRuntime<typeof COMMANDS>;",
"",
"/** Callable `commands.x(input)` surface exposed by GeneratedCommandRuntime. */",
"export type GeneratedCommands = GeneratedCommandRuntime['commands'];",
"",
"/** Runtime options excluding compiler-owned protocol authority. */",
"export type GeneratedCommandRuntimeOptions = Omit<ReplicaCommandRuntimeOptions, 'status'>;",
"",
"/** Bind this generated command inventory to a replica and transport. */",
"export function createCommands(",
" replica: DistributedReplica,",
" transport: ReplicaCommandTransport,",
" options?: GeneratedCommandRuntimeOptions",
"): GeneratedCommandRuntime {",
" return createReplicaCommandRuntime(replica, transport, COMMANDS, {",
" ...options,",
if pure_inventory.is_empty() {
" status: COMMAND_STATUS"
} else {
" pureFunctions: {\n ...PURE_FUNCTIONS,\n ...(options?.pureFunctions ?? {})\n },\n status: COMMAND_STATUS"
},
" });",
"}",
]
.join("\n"),
);
}
sections.push(format!(
"/** Projector topology retained for inspection and causal diagnostics. */\nexport const PROJECTOR_ARTIFACTS = {projectors} as const;"
));
sections
.push("export type GeneratedCommandArtifact = (typeof COMMAND_ARTIFACTS)[number];".into());
Ok(format!("{}\n", sections.join("\n\n")))
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum PureDelivery {
ClientModule { module: String, export: String },
WasmPackage { package: String, export: String },
}
fn pure_function_inventory(
manifest: &ClientManifest,
) -> Result<Vec<(String, PureDelivery)>, ClientCompileError> {
let mut seen = BTreeSet::new();
let mut out = Vec::new();
for command in &manifest.commands {
let Some(projection) = &command.extensions.projection else {
continue;
};
for reduce in &projection.pure_reduces {
if !seen.insert(reduce.fn_name.clone()) {
continue;
}
let hand = !reduce.client_module.is_empty() || !reduce.client_export.is_empty();
let wasm = !reduce.wasm_package.is_empty() || !reduce.wasm_export.is_empty();
let delivery = if hand && !wasm {
PureDelivery::ClientModule {
module: reduce.client_module.clone(),
export: reduce.client_export.clone(),
}
} else if wasm && !hand {
PureDelivery::WasmPackage {
package: reduce.wasm_package.clone(),
export: reduce.wasm_export.clone(),
}
} else {
return Err(ClientCompileError::manifest(
"client.projection_pure_reduce",
format!(
"pure `{}` must declare either client_module+client_export or wasm_package+wasm_export",
reduce.fn_name
),
));
};
out.push((reduce.fn_name.clone(), delivery));
}
}
out.sort_by(|a, b| a.0.cmp(&b.0));
Ok(out)
}
pub(super) fn render_pures(manifest: &ClientManifest) -> Result<Option<String>, ClientCompileError> {
let inventory = pure_function_inventory(manifest)?;
if inventory.is_empty() {
return Ok(None);
}
let mut sections = vec![
"/** GENERATED by distributed client. Pure functions for projection.pureReduces. */"
.to_string(),
];
let needs_wasm = inventory
.iter()
.any(|(_, d)| matches!(d, PureDelivery::WasmPackage { .. }));
if needs_wasm {
sections.push(
"import { createWasmJsonPure } from '@hops-ops/distributed/replica';".into(),
);
}
let mut entries = Vec::new();
let mut ready_calls = Vec::new();
for (index, (fn_name, delivery)) in inventory.iter().enumerate() {
match delivery {
PureDelivery::ClientModule { module, export } => {
let alias = format!("pure_{index}");
let rel = format!("../../{module}.js");
sections.push(format!("import {{ {export} as {alias} }} from '{rel}';"));
entries.push(format!(" {}: {alias}", quoted_property(fn_name)));
}
PureDelivery::WasmPackage { package, export } => {
let host = format!("pureHost_{index}");
let rel = format!("../../{package}.js");
sections.push(format!(
"const {host} = createWasmJsonPure({{\n load: () => import('{rel}'),\n exportName: {export_lit}\n}});",
export_lit = serde_json::to_string(export).unwrap_or_else(|_| "\"\"".into()),
));
entries.push(format!(" {}: {host}.pure", quoted_property(fn_name)));
ready_calls.push(format!(" await {host}.ensureReady();"));
}
}
}
sections.push(format!(
"export const PURE_FUNCTIONS = {{\n{}\n}} as const;",
entries.join(",\n")
));
if !ready_calls.is_empty() {
sections.push(format!(
"/** Instantiate WASM pure hosts (no-op when none / already ready). */\nexport async function ensurePureFunctionsReady(): Promise<void> {{\n{}\n}}",
ready_calls.join("\n")
));
}
Ok(Some(format!("{}\n", sections.join("\n\n"))))
}
fn validate_command_namespaces(commands: &[ManifestCommand]) -> Result<(), ClientCompileError> {
const RESERVED_SEGMENTS: [&str; 3] = ["__proto__", "constructor", "prototype"];
let mut paths = Vec::with_capacity(commands.len());
for command in commands {
let segments = command.name.split('.').collect::<Vec<_>>();
if command.name.len() > 512 || segments.len() > 64 {
return Err(ClientCompileError::manifest(
"client.command.namespace_segment",
format!(
"command `{}` cannot generate a safe nested command namespace: paths are limited to 512 bytes and 64 segments",
command.name
),
));
}
if let Some(segment) = segments.iter().copied().find(|segment| {
segment.is_empty()
|| segment.len() > 128
|| segment.trim() != *segment
|| segment.chars().any(char::is_control)
|| RESERVED_SEGMENTS.contains(segment)
}) {
return Err(ClientCompileError::manifest(
"client.command.namespace_segment",
format!(
"command `{}` cannot generate a safe nested command namespace: segment `{segment}` is empty, reserved, oversized, padded, or contains control characters",
command.name
),
));
}
paths.push((command.name.as_str(), segments));
}
for left_index in 0..paths.len() {
for right_index in (left_index + 1)..paths.len() {
let (left_name, left) = &paths[left_index];
let (right_name, right) = &paths[right_index];
let (prefix_name, prefix, descendant_name, descendant) = if left.len() <= right.len() {
(left_name, left, right_name, right)
} else {
(right_name, right, left_name, left)
};
if descendant.starts_with(prefix) {
return Err(ClientCompileError::manifest(
"client.command.namespace_collision",
format!(
"commands `{prefix_name}` and `{descendant_name}` collide in the generated nested command namespace; rename one command so neither dotted path prefixes the other"
),
));
}
}
}
Ok(())
}
fn render_command(
command: &ManifestCommand,
manifest: &ClientManifest,
) -> Result<String, ClientCompileError> {
let identifier = &command.mutation_field;
let input_name = format!("Command_{identifier}_Input");
let output_name = format!("Command_{identifier}_Output");
let defaults = command
.extensions
.input_defaults
.as_ref()
.map(|defaults| {
defaults
.defaults
.iter()
.map(|default| default.path.clone())
.collect::<BTreeSet<_>>()
})
.unwrap_or_default();
let input_type = render_command_shape_type(&command.input, true, &defaults)?;
let output_type = render_command_shape_type(&command.output, false, &BTreeSet::new())?;
let artifact = canonical_json_value(command_artifact_json(command, manifest)?);
let artifact = serde_json::to_string_pretty(&artifact).map_err(|error| {
ClientCompileError::manifest(
"client.render.command",
format!(
"failed to render executable command `{}`: {error}",
command.name
),
)
})?;
let prepare = match command.input {
ManifestCommandShape::None => format!(
"export function prepareCommand_{}(\n options?: PrepareReplicaCommandOptions\n): ReplicaPreparedCommand<{}, {}> {{\n return prepareReplicaCommand(Command_{}, undefined, options);\n}}",
identifier, input_name, output_name, identifier
),
_ => format!(
"export function prepareCommand_{}(\n input: {},\n options?: PrepareReplicaCommandOptions\n): ReplicaPreparedCommand<{}, {}> {{\n return prepareReplicaCommand(Command_{}, input, options);\n}}",
identifier, input_name, input_name, output_name, identifier
),
};
Ok(format!(
"export type {input_name} = {input_type};\n\n\
export type {output_name} = {output_type};\n\n\
/** Exact typed causal command descriptor and full mutation bytes. */\n\
export const Command_{}: ReplicaCommandArtifact<{}, {}> = {};\n\n\
{}",
identifier, input_name, output_name, artifact, prepare
))
}
fn render_command_shape_type(
shape: &ManifestCommandShape,
input: bool,
defaults: &BTreeSet<Vec<String>>,
) -> Result<String, ClientCompileError> {
match shape {
ManifestCommandShape::None => Ok("void".into()),
ManifestCommandShape::Object { definition } => {
render_command_type_definition(definition, input, defaults, &[], 0)
}
}
}
fn render_command_type_definition(
definition: &ManifestTypeDef,
input: bool,
defaults: &BTreeSet<Vec<String>>,
prefix: &[String],
indent: usize,
) -> Result<String, ClientCompileError> {
let member_padding = " ".repeat(indent + 2);
let closing_padding = " ".repeat(indent);
let mut lines = vec!["{".to_string()];
for field in &definition.fields {
let mut path = prefix.to_vec();
path.push(field.name.clone());
let optional = input && (field.nullable || defaults.contains(&path));
let mut value = render_command_field_type(field, input, defaults, &path, indent + 2)?;
if field.list {
if field.item_nullable {
value = format!("({value} | null)");
}
value = format!("readonly {value}[]");
}
if field.nullable {
value = format!("{value} | null");
}
lines.push(format!(
"{member_padding}readonly {}{}: {value};",
quoted_property(&field.name),
if optional { "?" } else { "" }
));
}
lines.push(format!("{closing_padding}}}"));
Ok(lines.join("\n"))
}
fn render_command_field_type(
field: &ManifestTypeField,
input: bool,
defaults: &BTreeSet<Vec<String>>,
path: &[String],
indent: usize,
) -> Result<String, ClientCompileError> {
if let Some(nested) = &field.nested {
return render_command_type_definition(nested, input, defaults, path, indent);
}
match field.codec.as_deref() {
Some("boolean") => Ok("boolean".into()),
Some("float64" | "int32" | "json_number_precision_limited") => Ok("number".into()),
Some("string" | "base64" | "string_unvalidated_timestamp") => Ok("string".into()),
Some("json") => Ok("ReplicaValue".into()),
Some(codec) => Err(ClientCompileError::manifest(
"client.scalar.codec_unsupported",
format!(
"command field `{}` uses unsupported TypeScript codec `{codec}`",
field.name
),
)),
None => Err(ClientCompileError::manifest(
"client.render.command_shape",
format!(
"command field `{}` has neither a scalar codec nor a nested definition",
field.name
),
)),
}
}
fn command_artifact_json(
command: &ManifestCommand,
manifest: &ClientManifest,
) -> Result<JsonValue, ClientCompileError> {
let consistency = &command.extensions.consistency;
let mut artifact = serde_json::Map::new();
artifact.insert(
"version".into(),
serde_json::json!(COMMAND_ARTIFACT_VERSION),
);
artifact.insert("name".into(), serde_json::json!(command.name));
artifact.insert(
"mutationField".into(),
serde_json::json!(command.mutation_field),
);
artifact.insert("document".into(), serde_json::json!(command.operation));
artifact.insert(
"operationHash".into(),
serde_json::json!(command.operation_hash),
);
artifact.insert(
"protocol".into(),
serde_json::json!({
"version": 1,
"schemaHash": manifest.schema_fingerprint,
"protocolHash": manifest.protocol_fingerprint,
"surface": &manifest.surface,
"operation": command.operation_hash,
"trustedPresets": &manifest.trusted_presets,
}),
);
artifact.insert("input".into(), command_shape_json(&command.input));
artifact.insert("output".into(), command_shape_json(&command.output));
if let Some(defaults) = &command.extensions.input_defaults {
artifact.insert(
"inputDefaults".into(),
serde_json::json!({
"version": defaults.version,
"defaults": defaults.defaults,
}),
);
}
artifact.insert(
"consistency".into(),
serde_json::json!(consistency_label(consistency.kind)),
);
let projection = compile_command_preview(command, manifest)?;
if let Some(projection) = &projection {
artifact.insert(
"projection".into(),
serde_json::to_value(projection).map_err(|error| {
ClientCompileError::manifest(
"client.render.command_projection",
format!(
"failed to render command projection `{}`: {error}",
command.name
),
)
})?,
);
}
if let Some(direct) = &command.extensions.direct_projection {
artifact.insert(
"directProjection".into(),
direct_projection_json(direct, manifest)?,
);
}
if !command.extensions.trusted_presets.is_empty() {
artifact.insert(
"trustedPresets".into(),
serde_json::json!(command.extensions.trusted_presets),
);
}
artifact.insert(
"revalidation".into(),
command_revalidation_json(command, manifest, projection.as_ref()),
);
Ok(JsonValue::Object(artifact))
}
fn command_shape_json(shape: &ManifestCommandShape) -> JsonValue {
match shape {
ManifestCommandShape::None => serde_json::json!({"kind": "none"}),
ManifestCommandShape::Object { definition } => serde_json::json!({
"kind": "object",
"definition": command_type_definition_json(definition),
}),
}
}
fn command_type_definition_json(definition: &ManifestTypeDef) -> JsonValue {
serde_json::json!({
"name": definition.name,
"fields": definition.fields.iter().map(command_type_field_json).collect::<Vec<_>>(),
})
}
fn command_type_field_json(field: &ManifestTypeField) -> JsonValue {
let mut result = serde_json::Map::new();
result.insert("name".into(), serde_json::json!(field.name));
result.insert("typeName".into(), serde_json::json!(field.type_name));
result.insert("nullable".into(), serde_json::json!(field.nullable));
result.insert("list".into(), serde_json::json!(field.list));
result.insert(
"itemNullable".into(),
serde_json::json!(field.item_nullable),
);
if let Some(codec) = &field.codec {
result.insert("codec".into(), serde_json::json!(codec));
}
if let Some(nested) = &field.nested {
result.insert("nested".into(), command_type_definition_json(nested));
}
JsonValue::Object(result)
}
fn consistency_label(kind: ManifestConsistencyKind) -> &'static str {
match kind {
ManifestConsistencyKind::Succeeded => "succeeded",
ManifestConsistencyKind::Eventual => "eventual",
ManifestConsistencyKind::Atomic => "atomic",
}
}
fn effect_expression_json(expression: &ManifestEffectExpression) -> JsonValue {
match expression {
ManifestEffectExpression::Input { path } => {
serde_json::json!({"kind": "input", "path": path})
}
ManifestEffectExpression::TrustedPreset { name } => {
serde_json::json!({"kind": "trusted_preset", "name": name})
}
ManifestEffectExpression::Constant { value } => {
serde_json::json!({"kind": "constant", "value": value})
}
ManifestEffectExpression::Null => serde_json::json!({"kind": "null"}),
}
}
fn direct_projection_json(
direct: &ManifestDirectProjection,
manifest: &ClientManifest,
) -> Result<JsonValue, ClientCompileError> {
let identity = manifest
.models
.get(&direct.model)
.and_then(|model| model.identity())
.filter(|fields| !fields.is_empty())
.ok_or_else(|| {
ClientCompileError::manifest(
"client.render.direct_projection_identity",
format!(
"direct projection model `{}` has no complete normalized identity",
direct.model
),
)
})?;
let mut result = serde_json::Map::new();
result.insert(
"topology".into(),
serde_json::json!({
"version": direct.topology.version,
"name": direct.topology.name,
"digest": direct.topology.digest,
}),
);
result.insert("model".into(), serde_json::json!(direct.model));
result.insert(
"identityFields".into(),
serde_json::json!(identity
.iter()
.map(|field| field.name.as_str())
.collect::<Vec<_>>()),
);
if let Some(partition) = &direct.partition {
result.insert("partition".into(), effect_expression_json(partition));
}
result.insert("changeEpoch".into(), serde_json::json!(direct.change_epoch));
Ok(JsonValue::Object(result))
}
fn command_revalidation_json(
command: &ManifestCommand,
manifest: &ClientManifest,
projection: Option<&CompiledCommandProjection>,
) -> JsonValue {
let mut required = manifest
.commands_requiring_revalidation
.contains(&command.name);
let mut models = BTreeSet::new();
let mut relationships = BTreeSet::new();
let mut dependencies = BTreeSet::new();
if let Some(projection) = projection {
models.extend(projection.affected_models());
relationships.extend(projection.affected_relationships(manifest));
required |= projection.requires_revalidation();
}
if let Some(direct) = &command.extensions.direct_projection {
models.insert(direct.model.clone());
if let Some(projector) = manifest
.projectors
.iter()
.find(|projector| projector.name == direct.topology.name)
{
dependencies.extend(projector.dependencies.iter().cloned());
}
}
if required && models.is_empty() {
if let Some(projection) = projection {
models.extend(projection.selected_models().iter().cloned());
} else {
models.extend(manifest.models.keys().cloned());
}
}
for model in &models {
if let Some(model) = manifest.models.get(model) {
dependencies.extend(model.dependencies.iter().cloned());
}
}
let relationship_values = relationships
.into_iter()
.map(|(source_model, field, target_model)| {
serde_json::json!({
"sourceModel": source_model,
"field": field,
"targetModel": target_model,
})
})
.collect::<Vec<_>>();
serde_json::json!({
"version": 1,
"required": required,
"dependencies": dependencies.into_iter().collect::<Vec<_>>(),
"models": models.into_iter().collect::<Vec<_>>(),
"relationships": relationship_values,
})
}