Response value handlers
A command sometimes decides more than its client needs to see: an audit entry, a notification to queue, or an event to append. Performing those effects inside handle() mixes the decision with the plumbing. Instead, return them next to the response and let a response value handler process each one on the server.
Return a tuple
Section titled “Return a tuple”import { field } from '@cratis/fundamentals';import { command, commandResponseValueHandler, injectable, tuple, type CommandContext, type CommandResponseValueHandler} from '@cratis/arc.core';
export class AuditEntry { constructor(readonly text: string) {}}
export class AuditLog { readonly entries: string[] = []; record(correlationId: string, text: string): void { this.entries.push(`${correlationId}: ${text}`); }}
@commandResponseValueHandler()@injectable(AuditLog)export class AuditEntryHandler implements CommandResponseValueHandler { constructor(private readonly log: AuditLog) {}
canHandle(_context: CommandContext, value: unknown): boolean { return value instanceof AuditEntry; }
handle(context: CommandContext, value: unknown): void { this.log.record(context.correlationId, (value as AuditEntry).text); }}
@command()export class CloseTask { @field(String) id!: string;
handle() { return tuple({ id: this.id }, new AuditEntry(`Closed ${this.id}`)); }}Register AuditLog with builder.services.addSingleton(AuditLog) and add or discover the command and the handler. POST /api/close-task with { "id": "a1" } answers 200 with "response":{"id":"a1"}; the client never sees the audit entry, and AuditLog has recorded it.
How Arc sorts the values
Section titled “How Arc sorts the values”- Arc flattens branded nested tuples and
response(...)branches, in order. - For each value, it asks every registered handler’s
canHandle(context, value). Every matching handler runs, in deterministic name order. - A value no handler accepts is a candidate client response. At most one such value is allowed; two unhandled values fail the command instead of returning an array.
- Ordinary arrays are not flattened: an array is one value.
canHandle may return a boolean or a promise of one. A handler’s handle(context, value) may return rejected(...) or denied(...) to fail the command, but it must not return a client response. It runs in the command’s service scope and reads the CommandContext: the command, key, values, correlation ID, principal, tenant, allowed severity, and signal. Set incompatibleWithOperations: true on a handler whose effects cannot be combined with command operations; Arc then rejects the combination before any effect. Cancellation during handler resolution, classification, or handling waits for the active callback to settle, then stops before starting another handler or operation. Completed handler effects are not rolled back; command scopes still complete, owned services are disposed, and a prepared operation journal remains available for recovery.
Register a handler
Section titled “Register a handler”- Mark the class
@commandResponseValueHandler()and add it withbuilder.add(...)orbuilder.discover(...). It is registered as scoped. - Or register its token in
builder.servicesand callbuilder.addCommandResponseValueHandler(token). - For a low-level
ArcServer, list tokens in thecommandResponseValueHandlersoption.
The experimental Chronicle integration uses this mechanism to append returned events.