Command execution scopes
Some work belongs around a command rather than inside it: opening and committing a unit of work, or measuring how long the handler took. An execution scope runs begin before provide() and handle(), and complete after them, with the result so far.
Add a scope
Section titled “Add a scope”Scopes are declared on low-level definitions with scopes, a list of factories. Arc creates new scope objects for every execution:
import { defineCommand, type CommandExecutionScope } from '@cratis/arc.core';import { z } from 'zod';
const timing = (): CommandExecutionScope => { let started = 0; return { begin: () => { started = performance.now(); }, complete: (context, result) => { console.log(context.correlationId, result.isSuccess, performance.now() - started); } };};
export const archive = defineCommand({ name: 'Archive', namespace: 'Tasks', schema: z.object({ id: z.string() }), scopes: [timing], handle: ({ id }) => ({ id })});How scopes run
Section titled “How scopes run”- Arc creates each scope and records it before calling its
begin, in list order. If abeginthrows, no later scope begins,provideandhandledo not run, and the command fails with a 500. provideandhandlerun.- Every recorded scope completes exactly once, in reverse order, with the result so far. That includes a scope whose
beginthrew, socompletemust cope with a partly started scope. - If any
completethrows, the command fails with a 500 and the response is removed from the result, even whenhandlesucceeded.
Scopes do not run for the validation-only route or when authorization or validation fails.
Arc does not make a scope transactional: whether complete commits or rolls back is your code’s decision, and it can read result.isSuccess. When the command returns operations, a scope that commits business changes must also report explicit commit facts before Arc considers compensation; see Implementing operations.
Model-bound commands
Section titled “Model-bound commands”@command() classes do not declare scopes. To wrap every validated model-bound command execution, register a runner with builder.addCommandExecutionRunner((context, execute) => ...) or the commandExecutionRunner option; it receives the CommandContext and a function that runs the rest of the command and returns its CommandResult. Integrations use this to set up ambient state for the whole execution. A runner is not a commit participant for operations.