{"version":3,"file":"index.mjs","names":[],"sources":["../../src/storage/memory.ts","../../src/test/index.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto'\nimport type {\n  ClaimedRun,\n  CreateRunResult,\n  RunStatus,\n  StepResult,\n  StorageAdapter,\n  WorkflowRun,\n} from '../core/types'\nimport { clonePersistedValue } from './codec'\n\ninterface StoredRun extends WorkflowRun {\n  leaseId: string | null\n}\n\nexport class MemoryStorage implements StorageAdapter {\n  private runs: Map<string, StoredRun> = new Map()\n  private steps: Map<string, StepResult[]> = new Map()\n\n  async initialize(): Promise<void> {}\n\n  async createRun(run: WorkflowRun): Promise<CreateRunResult> {\n    if (run.idempotencyKey) {\n      for (const existingRun of this.runs.values()) {\n        if (\n          existingRun.workflow === run.workflow\n          && existingRun.idempotencyKey === run.idempotencyKey\n        ) {\n          return {\n            run: cloneWorkflowRun(existingRun),\n            created: false,\n          }\n        }\n      }\n    }\n\n    const storedRun: StoredRun = {\n      ...run,\n      input: clonePersistedValue(run.input, 'Workflow input'),\n      leaseId: null,\n    }\n\n    this.runs.set(run.id, storedRun)\n    return {\n      run: cloneWorkflowRun(storedRun),\n      created: true,\n    }\n  }\n\n  async claimNextRun(workflowNames: readonly string[], staleBefore?: number): Promise<ClaimedRun | null> {\n    if (workflowNames.length === 0) {\n      return null\n    }\n\n    const candidates = Array.from(this.runs.values())\n      .filter((run) => {\n        if (!workflowNames.includes(run.workflow)) {\n          return false\n        }\n\n        if (run.status === 'pending') {\n          return true\n        }\n\n        return staleBefore !== undefined && run.status === 'running' && run.updatedAt <= staleBefore\n      })\n      .sort((left, right) => {\n        if (left.status !== right.status) {\n          return left.status === 'pending' ? -1 : 1\n        }\n        return left.createdAt - right.createdAt\n      })\n\n    const run = candidates[0]\n    if (!run) {\n      return null\n    }\n\n    run.status = 'running'\n    run.updatedAt = Date.now()\n    run.leaseId = randomUUID()\n\n    return cloneClaimedRun(run)\n  }\n\n  async heartbeatRun(runId: string, leaseId: string): Promise<boolean> {\n    const run = this.runs.get(runId)\n    if (!run || run.status !== 'running' || run.leaseId !== leaseId) {\n      return false\n    }\n\n    run.updatedAt = Date.now()\n    return true\n  }\n\n  async getRun(runId: string): Promise<WorkflowRun | null> {\n    const run = this.runs.get(runId)\n    return run ? cloneWorkflowRun(run) : null\n  }\n\n  async getStepResults(runId: string): Promise<StepResult[]> {\n    return (this.steps.get(runId) ?? []).map((step) => ({\n      ...step,\n      output: clonePersistedValue(step.output, 'Step output'),\n    }))\n  }\n\n  async saveStepResult(result: StepResult, leaseId?: string): Promise<boolean> {\n    if (leaseId) {\n      const run = this.runs.get(result.runId)\n      if (!run || run.status !== 'running' || run.leaseId !== leaseId) {\n        return false\n      }\n    }\n\n    const existing = this.steps.get(result.runId) ?? []\n    const idx = existing.findIndex((step) => step.id === result.id)\n    const cloned = { ...result, output: clonePersistedValue(result.output, 'Step output') }\n\n    if (idx >= 0) {\n      existing[idx] = cloned\n    } else {\n      existing.push(cloned)\n    }\n\n    this.steps.set(result.runId, existing)\n    return true\n  }\n\n  async updateRunStatus(runId: string, status: RunStatus): Promise<boolean> {\n    const run = this.runs.get(runId)\n    if (!run) {\n      return false\n    }\n\n    run.status = status\n    run.updatedAt = Date.now()\n    run.leaseId = null\n\n    return true\n  }\n\n  async updateClaimedRunStatus(runId: string, leaseId: string, status: RunStatus): Promise<boolean> {\n    const run = this.runs.get(runId)\n    if (!run || run.status !== 'running' || run.leaseId !== leaseId) {\n      return false\n    }\n\n    run.status = status\n    run.updatedAt = Date.now()\n\n    if (status !== 'running') {\n      run.leaseId = null\n    }\n\n    return true\n  }\n\n  close(): void {}\n}\n\nfunction cloneWorkflowRun(run: StoredRun): WorkflowRun {\n  return {\n    id: run.id,\n    workflow: run.workflow,\n    input: clonePersistedValue(run.input, 'Workflow input'),\n    idempotencyKey: run.idempotencyKey,\n    status: run.status,\n    createdAt: run.createdAt,\n    updatedAt: run.updatedAt,\n  }\n}\n\nfunction cloneClaimedRun(run: StoredRun): ClaimedRun {\n  if (!run.leaseId) {\n    throw new Error('Claimed run is missing a lease id')\n  }\n\n  return {\n    ...cloneWorkflowRun(run),\n    leaseId: run.leaseId,\n  }\n}\n","import { createEngine } from '../core/engine'\nimport type { PersistedValue } from '../core/types'\nimport { MemoryStorage } from '../storage/memory'\nimport type {\n  AnyWorkflow,\n  WorkflowInputMap,\n  WorkflowStepsMap,\n} from '../core/workflow'\n\n/** The result of a single step, discriminated by status. */\nexport type StepOutput<T extends PersistedValue = PersistedValue> =\n  | { status: 'completed'; output: T; error: null }\n  | { status: 'failed'; output: null; error: string }\n\n/** The result of running a workflow to completion via `testEngine.run()`. */\nexport interface RunResult<TSteps extends Record<string, PersistedValue> = Record<string, PersistedValue>> {\n  status: 'completed' | 'failed'\n  /** Step results keyed by step name, with typed outputs. */\n  steps: { [K in keyof TSteps]: StepOutput<TSteps[K]> }\n}\n\n/** A test-only engine that runs workflows synchronously with in-memory storage. */\nexport interface TestEngine<\n  TInputMap extends Record<string, PersistedValue> = Record<string, PersistedValue>,\n  TStepsMap extends Record<string, Record<string, PersistedValue>> = Record<string, Record<string, PersistedValue>>,\n> {\n  run<TName extends string & keyof TInputMap>(\n    workflowName: TName,\n    input: TInputMap[TName],\n  ): Promise<\n    RunResult<TName extends keyof TStepsMap ? TStepsMap[TName] : Record<string, PersistedValue>>\n  >\n}\n\n/**\n * Create a test engine with in-memory storage. Runs a workflow to completion in a single `tick()`\n * and returns typed step results.\n *\n * @example\n * ```ts\n * const te = testEngine({ workflows: [myWorkflow] })\n * const result = await te.run('my-workflow', { id: '123' })\n * expect(result.status).toBe('completed')\n * ```\n */\nexport function testEngine<const TWorkflows extends readonly AnyWorkflow[]>(config: {\n  workflows: TWorkflows\n}): TestEngine<WorkflowInputMap<TWorkflows>, WorkflowStepsMap<TWorkflows>> {\n  return {\n    async run(workflowName, input) {\n      const storage = new MemoryStorage()\n      await storage.initialize()\n\n      const engine = createEngine({\n        storage,\n        workflows: config.workflows,\n      })\n\n      const run = await engine.enqueue(workflowName, input)\n      await engine.tick()\n\n      const runInfo = await storage.getRun(run.id)\n      if (!runInfo) {\n        throw new Error(`Run \"${run.id}\" not found after tick()`)\n      }\n\n      if (runInfo.status !== 'completed' && runInfo.status !== 'failed') {\n        throw new Error(`Run \"${run.id}\" ended with unexpected status \"${runInfo.status}\"`)\n      }\n\n      const stepResults = await storage.getStepResults(run.id)\n      const steps: Record<string, StepOutput> = {}\n\n      for (const step of stepResults) {\n        steps[step.name] = step.status === 'completed' || step.status === 'completed-early'\n          ? {\n              status: 'completed',\n              output: step.output,\n              error: null,\n            }\n          : {\n              status: 'failed',\n              output: null,\n              error: step.error ?? 'Unknown error',\n            }\n      }\n\n      return {\n        status: runInfo.status,\n        steps,\n      } as never\n    },\n  }\n}\n"],"mappings":"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"}