Command / CommandExecutor
import { CommandExecutor } from '@hurum/core'CommandExecutor<TInput, TDeps?, TState?>(executorFn)
Section titled “CommandExecutor<TInput, TDeps?, TState?>(executorFn)”Creates a Command/Executor pair. The command is a token that identifies the operation. The executor is the function that runs when the command is dispatched.
Signature
Section titled “Signature”function CommandExecutor< TInput, TDeps = Record<string, never>, TState = unknown,>(fn: ExecutorFn<TInput, TDeps, TState>): [Command<TInput>, Executor<TInput, TDeps, TState>]Type Parameters
Section titled “Type Parameters”| Parameter | Default | Description |
|---|---|---|
TInput | — | The payload type the executor receives. |
TDeps | Record<string, never> | Dependencies injected via context.deps. |
TState | unknown | State shape available via context.getState(). |
Parameters
Section titled “Parameters”| Parameter | Type | Description |
|---|---|---|
fn | (command: TInput, context: ExecutorContext) => void | Promise<void> | The executor function. First argument is the command payload. May be sync or async. |
Returns
Section titled “Returns”A tuple [Command<TInput>, Executor<TInput, TDeps, TState>]. Destructure both values:
const [SaveCmd, SaveExec] = CommandExecutor<...>(async (command, ctx) => { // ...})Example
Section titled “Example”const [SaveCmd, SaveExec] = CommandExecutor< { purchase: Purchase }, { repo: PurchaseRepo }, { purchase: Purchase | null }>(async (command, { deps, emit, getState, signal }) => { emit(PurchaseEvent.saveRequested({ id: command.purchase.id })) const result = await deps.repo.save(command.purchase) result.match( (saved) => emit(PurchaseEvent.saved({ purchase: saved })), (error) => emit(PurchaseEvent.saveFailed({ error })), )})ExecutorContext<TDeps, TState>
Section titled “ExecutorContext<TDeps, TState>”The context object passed as the second argument to every executor function.
Properties
Section titled “Properties”| Property | Type | Description |
|---|---|---|
deps | TDeps | Injected dependencies (API clients, repositories, etc.). Provided at Store.create({ deps }). |
emit | (event: EventInstance) => void | Emit an event. Synchronously applies the event to the store, triggering on handlers. |
getState | () => TState | Read the current combined state (raw + computed) of the store. |
signal | AbortSignal | Cancellation signal. Aborted when store.cancel(ref) or store.cancelAll() is called. |
scope | Record<string, unknown> | Access to nested child store instances. Keys match the nested field names declared in the store state. |
Notes on emit
Section titled “Notes on emit”emitis synchronous. The event is applied immediately — theonhandler runs, state updates, computed recalculates, and subscribers fire beforeemitreturns.- After
store.dispose(),emitcalls are silently ignored. - Executors can call
emitmultiple times. Each call is an independent state transition.
Notes on signal
Section titled “Notes on signal”- Pass
signaltofetch(),AbortController-aware APIs, or checksignal.abortedin loops. - When cancelled, the executor should stop work and return. No special cleanup is required.
CommandExecutor.passthrough(eventCreator, transform?)
Section titled “CommandExecutor.passthrough(eventCreator, transform?)”Creates a Command/Executor pair that simply emits a single event. Use this for synchronous, side-effect-free commands where the payload maps directly to an event.
Signature
Section titled “Signature”CommandExecutor.passthrough<TType extends string, TPayload, TInput = TPayload>( eventCreator: EventCreator<TType, TPayload>, transform?: (input: TInput) => TPayload,): [Command<TInput>, Executor<TInput, Record<string, never>, unknown>]Parameters
Section titled “Parameters”| Parameter | Type | Description |
|---|---|---|
eventCreator | EventCreator<TType, TPayload> | The event to emit. |
transform | (input: TInput) => TPayload | Optional function to transform the command input into the event payload. |
Returns
Section titled “Returns”A [Command<TInput>, Executor] tuple. The executor has no deps, getState, or signal access — it only emits one event.
Example
Section titled “Example”// Direct passthrough: input type matches event payload typeconst [IncrCmd, IncrExec] = CommandExecutor.passthrough(CounterEvent.incremented)
// With transform: reshape the input before emittingconst [SetCmd, SetExec] = CommandExecutor.passthrough( UserEvent.nameChanged, (cmd: { name: string }) => ({ name: cmd.name.trim(), changedAt: Date.now() }))Command<TInput>
Section titled “Command<TInput>”An opaque branded type that identifies a command. Commands have no methods or properties accessible to application code. They serve as tokens connecting Intents to Executors.
Executor<TInput, TDeps, TState>
Section titled “Executor<TInput, TDeps, TState>”An opaque branded type holding the executor function and its associated command. Passed to Store.executors() during store definition.
- Commands are imperative (“do this”). Events are declarative (“this happened”). A
CommandExecutorbridges the two: it receives a command and produces events. - One
CommandExecutorper side effect. Keep executors focused — a single executor should handle one concern (e.g. saving to the server, loading data). - Passthrough executors are the right choice for synchronous state transitions that need no dependencies or async work.