Local Runtime (SQLite)

The local runtime executes workflows in Bun subprocesses with SQLite-backed persistence. It consists of three layers: a shared module (router + database), a worker process, and an application entry point.

Shared module

The shared module defines workflows, creates the database, and exports the router type:

shared.ts
import { workflow, createWorkflowRouter } from "yieldstar";
import { SqliteEventLoop, createSqliteDb } from "@yieldstar/bun-sqlite-runtime";

export const myWorkflow = workflow(async function* (step) {
  const n = yield* step.run(() => 1);
  yield* step.delay(1000);
  return yield* step.run(() => n * 2);
});

export const router = createWorkflowRouter({ "my-workflow": myWorkflow });
export type Router = typeof router;

export const db = createSqliteDb({ path: "./.db/local.sqlite" });
export const eventLoop = new SqliteEventLoop(db);

Worker process

The worker runs in a Bun subprocess spawned by the invoker. It wires the WorkflowRunner to the heap and scheduler, then listens for IPC messages:

worker.ts
import pino from "pino";
import { WorkflowRunner } from "@yieldstar/core";
import { createWorkflowWorker } from "@yieldstar/bun-worker-invoker";
import {
  SqliteHeapClient,
  SqliteSchedulerClient,
  SqliteTaskQueueClient,
  SqliteTimersClient,
} from "@yieldstar/bun-sqlite-runtime";
import { router, db } from "./shared";

const logger = pino();

const runner = new WorkflowRunner({
  router,
  heapClient: new SqliteHeapClient(db),
  schedulerClient: new SqliteSchedulerClient({
    taskQueueClient: new SqliteTaskQueueClient(db),
    timersClient: new SqliteTimersClient(db),
  }),
  logger,
});

createWorkflowWorker(runner, logger).listen();

Application entry point

The app creates the invoker, starts the event loop, and triggers workflows through the local SDK:

app.ts
import pino from "pino";
import { createWorkflowInvoker } from "@yieldstar/bun-worker-invoker";
import { createLocalSdk } from "yieldstar";
import { eventLoop } from "./shared";
import type { Router } from "./shared";

const logger = pino();
const workerPath = new URL("./worker.ts", import.meta.url).href;
const invoker = createWorkflowInvoker({ workerPath, logger });

eventLoop.start({ onNewEvent: invoker.execute, logger });

const sdk = createLocalSdk<Router>(invoker);
const result = await sdk.triggerAndWait({ workflowId: "my-workflow" });

console.log(result); // 2
eventLoop.stop();

How it works

  1. createWorkflowInvoker spawns a new Bun subprocess for each workflow execution.
  2. The subprocess receives the execution event via IPC and runs the workflow generator.
  3. The generator replays cached steps from the SQLite heap and executes new ones.
  4. If the generator yields a delay or retry, the WorkflowRunner calls schedulerClient.requestWakeUp, which writes a timer to SQLite.
  5. The SqliteEventLoop polls the task queue every 10ms. When a timer fires, it enqueues the event and the invoker spawns a new subprocess to resume.
  6. When the generator returns a WorkflowResult, the subprocess sends it back via IPC, and the workflowEndEmitter resolves the SDK promise.