콘텐츠로 이동

Command / CommandExecutor

import { CommandExecutor } from '@hurum/core'

CommandExecutor<TInput, TDeps?, TState?>(executorFn)

섹션 제목: “CommandExecutor<TInput, TDeps?, TState?>(executorFn)”

Command/Executor 쌍을 생성해요. Command는 작업을 식별하는 토큰이고, Executor는 Command가 디스패치될 때 실행되는 함수예요.

function CommandExecutor<
TInput,
TDeps = Record<string, never>,
TState = unknown,
>(fn: ExecutorFn<TInput, TDeps, TState>): [Command<TInput>, Executor<TInput, TDeps, TState>]
매개변수기본값설명
TInputExecutor가 받는 페이로드 타입.
TDepsRecord<string, never>context.deps로 주입되는 의존성.
TStateunknowncontext.getState()로 사용 가능한 상태 형태.
매개변수타입설명
fn(command: TInput, context: ExecutorContext) => void | Promise<void>Executor 함수. 첫 번째 인수는 Command 페이로드. 동기 또는 비동기일 수 있어요.

[Command<TInput>, Executor<TInput, TDeps, TState>] 튜플. 두 값을 모두 구조분해하세요:

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 })),
)
})

모든 Executor 함수의 두 번째 인수로 전달되는 컨텍스트 객체예요.

속성타입설명
depsTDeps주입된 의존성 (API 클라이언트, 레포지토리 등). Store.create({ deps })에서 제공돼요.
emit(event: EventInstance) => voidEvent를 발행해요. Store에 동기적으로 Event를 적용해서 on 핸들러를 트리거해요.
getState() => TStateStore의 현재 결합된 상태 (raw + computed)를 읽어요.
signalAbortSignal취소 시그널. store.cancel(ref) 또는 store.cancelAll() 호출 시 중단돼요.
scopeRecord<string, unknown>Nested 자식 Store 인스턴스에 대한 접근. 키는 Store 상태에 선언된 Nested 필드 이름과 일치해요.
  • emit은 동기적이에요. Event가 즉시 적용되며 — on 핸들러가 실행되고, 상태가 업데이트되고, Computed가 재계산되고, 구독자가 호출된 후에 emit이 반환돼요.
  • store.dispose() 이후의 emit 호출은 조용히 무시돼요.
  • Executor는 emit을 여러 번 호출할 수 있어요. 각 호출은 독립적인 상태 전환이에요.
  • signalfetch(), AbortController 호환 API에 전달하거나 루프에서 signal.aborted를 확인하세요.
  • 취소되면 Executor는 작업을 중단하고 반환해야 해요. 특별한 정리 작업은 필요하지 않아요.

CommandExecutor.passthrough(eventCreator, transform?)

섹션 제목: “CommandExecutor.passthrough(eventCreator, transform?)”

단일 Event를 발행하는 Command/Executor 쌍을 생성해요. 페이로드가 Event에 직접 매핑되는 동기적이고 사이드 이펙트 없는 Command에 사용하세요.

CommandExecutor.passthrough<TType extends string, TPayload, TInput = TPayload>(
eventCreator: EventCreator<TType, TPayload>,
transform?: (input: TInput) => TPayload,
): [Command<TInput>, Executor<TInput, Record<string, never>, unknown>]
매개변수타입설명
eventCreatorEventCreator<TType, TPayload>발행할 Event.
transform(input: TInput) => TPayloadCommand 입력을 Event 페이로드로 변환하는 선택적 함수.

[Command<TInput>, Executor] 튜플. Executor는 deps, getState, signal에 접근하지 않으며 — 하나의 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() })
)

Command를 식별하는 불투명 브랜드 타입이에요. Command에는 애플리케이션 코드에서 접근 가능한 메서드나 속성이 없어요. Intent를 Executor에 연결하는 토큰 역할을 해요.


Executor 함수와 연관된 Command를 보유하는 불투명 브랜드 타입이에요. Store 정의 시 Store.executors()에 전달해요.


  • Command는 명령형이에요 (“이것을 해라”). Event는 선언형이에요 (“이것이 일어났다”). CommandExecutor는 이 둘을 연결해요: Command를 받아 Event를 생성해요.
  • 사이드 이펙트당 하나의 CommandExecutor. Executor는 집중적으로 유지하세요 — 하나의 Executor는 하나의 관심사를 처리해야 해요 (예: 서버 저장, 데이터 로딩).
  • passthrough Executor는 의존성이나 비동기 작업이 필요 없는 동기적 상태 전환에 적합한 선택이에요.