Skip to content

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.

function CommandExecutor<
TInput,
TDeps = Record<string, never>,
TState = unknown,
>(fn: ExecutorFn<TInput, TDeps, TState>): [Command<TInput>, Executor<TInput, TDeps, TState>]
ParameterDefaultDescription
TInputThe payload type the executor receives.
TDepsRecord<string, never>Dependencies injected via context.deps.
TStateunknownState shape available via context.getState().
ParameterTypeDescription
fn(command: TInput, context: ExecutorContext) => void | Promise<void>The executor function. First argument is the command payload. May be sync or async.

A tuple [Command<TInput>, Executor<TInput, TDeps, TState>]. Destructure both values:

const [SaveCmd, SaveExec] = CommandExecutor<...>(async (command, ctx) => {
// ...
})
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 })),
)
})

The context object passed as the second argument to every executor function.

PropertyTypeDescription
depsTDepsInjected dependencies (API clients, repositories, etc.). Provided at Store.create({ deps }).
emit(event: EventInstance) => voidEmit an event. Synchronously applies the event to the store, triggering on handlers.
getState() => TStateRead the current combined state (raw + computed) of the store.
signalAbortSignalCancellation signal. Aborted when store.cancel(ref) or store.cancelAll() is called.
scopeRecord<string, unknown>Access to nested child store instances. Keys match the nested field names declared in the store state.
  • emit is synchronous. The event is applied immediately — the on handler runs, state updates, computed recalculates, and subscribers fire before emit returns.
  • After store.dispose(), emit calls are silently ignored.
  • Executors can call emit multiple times. Each call is an independent state transition.
  • Pass signal to fetch(), AbortController-aware APIs, or check signal.aborted in 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.

CommandExecutor.passthrough<TType extends string, TPayload, TInput = TPayload>(
eventCreator: EventCreator<TType, TPayload>,
transform?: (input: TInput) => TPayload,
): [Command<TInput>, Executor<TInput, Record<string, never>, unknown>]
ParameterTypeDescription
eventCreatorEventCreator<TType, TPayload>The event to emit.
transform(input: TInput) => TPayloadOptional function to transform the command input into the event payload.

A [Command<TInput>, Executor] tuple. The executor has no deps, getState, or signal access — it only emits one event.

// Direct passthrough: input type matches event payload type
const [IncrCmd, IncrExec] = CommandExecutor.passthrough(CounterEvent.incremented)
// With transform: reshape the input before emitting
const [SetCmd, SetExec] = CommandExecutor.passthrough(
UserEvent.nameChanged,
(cmd: { name: string }) => ({ name: cmd.name.trim(), changedAt: Date.now() })
)

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.


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 CommandExecutor bridges the two: it receives a command and produces events.
  • One CommandExecutor per 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.