Skip to content

microsoft/duroxide-node

Repository files navigation

duroxide-node

Node.js/TypeScript SDK for the Duroxide durable execution runtime. Write reliable, long-running workflows in JavaScript using generator functions — backed by a Rust runtime that handles persistence, replay, and fault tolerance.

See CHANGELOG.md for release notes.

Features

  • Durable orchestrations — generator-based workflows that survive process restarts
  • Automatic replay — the Rust runtime replays history on restart, your code picks up where it left off
  • Activities — async functions for side effects (API calls, DB writes, etc.)
  • Timers — durable delays that persist across restarts
  • Sub-orchestrations — compose workflows from smaller workflows
  • External events — pause workflows and wait for signals
  • Fan-out/fan-in — run tasks in parallel with ctx.all() (supports all task types)
  • Race conditions — wait for the first of multiple tasks with ctx.race() (supports all task types)
  • Cooperative cancellation — activities detect when they're no longer needed via ctx.isCancelled()
  • Activity client access — activities can start new orchestrations via ctx.getClient()
  • Custom status — set orchestration progress visible to external clients via ctx.setCustomStatus()
  • Event queues — persistent FIFO message passing with ctx.dequeueEvent() and client.enqueueEvent()
  • Continue-as-new — restart orchestrations with fresh history for eternal workflows
  • Structured tracing — orchestration and activity logs route through Rust's tracing crate
  • Runtime metricsmetricsSnapshot() for orchestration/activity counters
  • Per-instance orchestration stats — inspect history size, pending queue carry-forward, and KV usage with client.getOrchestrationStats()
  • SQLite & PostgreSQL — pluggable storage backends
  • KV store — durable per-instance key-value state with snapshots and pruning via ctx.getKvAllValues() / client.getKvAllValues()

Quick Start

npm install duroxide

Prebuilt native packages are published for:

OS Architecture Native package
macOS arm64 duroxide-darwin-arm64
macOS x64 duroxide-darwin-x64
Linux glibc arm64 duroxide-linux-arm64-gnu
Linux glibc x64 duroxide-linux-x64-gnu
Windows x64 duroxide-windows-x64
const { SqliteProvider, Client, Runtime } = require('duroxide');

async function main() {
  // 1. Open a storage backend
  const provider = await SqliteProvider.open('sqlite:myapp.db');
  const client = new Client(provider);
  const runtime = new Runtime(provider);

  // 2. Register activities (async functions with side effects)
  runtime.registerActivity('Greet', async (ctx, name) => {
    ctx.traceInfo(`greeting ${name}`);
    return `Hello, ${name}!`;
  });

  // 3. Register orchestrations (generator functions)
  runtime.registerOrchestration('GreetWorkflow', function* (ctx, input) {
    const greeting = yield ctx.scheduleActivity('Greet', input.name);
    ctx.traceInfo(`got: ${greeting}`);
    return greeting;
  });

  // 4. Start the runtime
  await runtime.start();

  // 5. Start an orchestration and wait for it
  await client.startOrchestration('greet-1', 'GreetWorkflow', { name: 'World' });
  const result = await client.waitForOrchestration('greet-1');
  console.log(result.output); // "Hello, World!"

  await runtime.shutdown();
}

main();

Why Generators (not async/await)?

Duroxide uses function* generators instead of async function for orchestrations. This is a deliberate design choice — see Architecture for the full explanation. The short version: generators give Rust full control over when and how each step executes, which is essential for deterministic replay.

// ✅ Orchestrations use yield
runtime.registerOrchestration('MyWorkflow', function* (ctx, input) {
  const result = yield ctx.scheduleActivity('DoWork', input);
  return result;
});

// ✅ Activities use async/await (normal async functions)
runtime.registerActivity('DoWork', async (ctx, input) => {
  const data = await fetch(`https://api.example.com/${input}`);
  return data;
});

Orchestration Context API

All scheduling methods return descriptors that must be yielded:

Method Description
yield ctx.scheduleActivity(name, input) Run an activity
yield ctx.scheduleActivityWithRetry(name, input, retryPolicy) Run with retry
yield ctx.scheduleTimer(delayMs) Durable delay
yield ctx.waitForEvent(eventName) Wait for external signal
yield ctx.scheduleSubOrchestration(name, input) Run child workflow (await result)
yield ctx.scheduleSubOrchestrationWithId(name, id, input) Child with explicit ID
yield ctx.startOrchestration(name, id, input) Fire-and-forget orchestration
yield ctx.all([task1, task2, ...]) Parallel execution (like Promise.all)
yield ctx.race(task1, task2) First-to-complete (like Promise.race)
yield ctx.utcNow() Deterministic timestamp
yield ctx.newGuid() Deterministic GUID
yield ctx.dequeueEvent(queueName) Dequeue from persistent FIFO mailbox
yield ctx.scheduleActivityWithRetryOnSession(name, input, retry, sessionId) Retry with session affinity
yield ctx.continueAsNew(newInput) Restart with fresh history
ctx.setValue(key, value) Set a durable KV entry (no yield)
ctx.getValue(key) Read a KV entry for the current instance (no yield)
ctx.clearValue(key) Remove a single KV entry (no yield)
ctx.clearAllValues() Remove all KV entries (no yield)
ctx.getKvAllValues() Snapshot all KV entries for the current instance
ctx.getKvAllKeys() List all KV keys for the current instance
ctx.getKvLength() Count KV entries for the current instance
ctx.pruneKvValuesUpdatedBefore(cutoffMs) Remove persisted KV entries older than a cutoff
yield ctx.getValueFromInstance(instanceId, key) Read another instance's KV entry

Tracing methods are fire-and-forget (no yield needed):

Method Description
ctx.traceInfo(message) INFO log (suppressed during replay)
ctx.traceWarn(message) WARN log
ctx.traceError(message) ERROR log
ctx.traceDebug(message) DEBUG log
ctx.setCustomStatus(status) Set progress visible to clients
ctx.resetCustomStatus() Clear custom status

Storage Backends

SQLite

const provider = await SqliteProvider.open('sqlite:path/to/db.db');
// or in-memory:
const provider = await SqliteProvider.inMemory();

PostgreSQL

const provider = await PostgresProvider.connectWithSchema(
  'postgresql://user:pass@host:5432/db',
  'my_schema'
);

PostgreSQL with Microsoft Entra ID (Azure)

For Azure Database for PostgreSQL Flexible Server, use Entra ID token authentication instead of a password:

const provider = await PostgresProvider.connectWithEntra(
  'myserver.postgres.database.azure.com',
  5432,
  'mydb',
  'my-entra-user@contoso.onmicrosoft.com'
);

// With a custom schema and options:
const provider = await PostgresProvider.connectWithSchemaAndEntra(
  'myserver.postgres.database.azure.com',
  5432,
  'mydb',
  'my-entra-user@contoso.onmicrosoft.com',
  'my_schema',
  { maxConnections: 20, acquireTimeoutMs: 45_000 }
);

Credentials are resolved automatically via the default chain:

  • WorkloadIdentityCredential (AKS Workload Identity, if env vars present)
  • ManagedIdentityCredential (Azure VMs, Container Apps, etc.)
  • DeveloperToolsCredential (local development: az login)

All connections use TLS (PgSslMode::VerifyFull). See PostgresEntraOptions for tunable options.

Logging

Duroxide uses Rust's tracing crate. Control verbosity with RUST_LOG:

RUST_LOG=info node app.js              # INFO and above
RUST_LOG=duroxide=debug node app.js    # DEBUG for duroxide only
RUST_LOG=duroxide::activity=info node app.js  # Activity traces only

Management API

The Client class includes a management API for inspecting and managing orchestration instances:

const client = new Client(provider);

// Event queues
await client.enqueueEvent(instanceId, 'queueName', JSON.stringify(data));

// Custom status polling
const change = await client.waitForStatusChange(instanceId, 0, 100, 30000);
// change: { customStatus, customStatusVersion } or null on timeout

// Instance management
const instances = await client.listAllInstances();
const info = await client.getInstanceInfo(instanceId);
const tree = await client.getInstanceTree(instanceId);
await client.deleteInstance(instanceId, false);

// Execution history with full event data
const executions = await client.listExecutions(instanceId);
const events = await client.readExecutionHistory(instanceId, executions[0]);
for (const event of events) {
  console.log(event.kind, event.data);
  // event.kind: "OrchestrationStarted" | "ActivityScheduled" | "ActivityCompleted" | ...
  // event.data: JSON string with event-specific content (result, input, error, etc.)
}

// Metrics
const metrics = await client.getSystemMetrics();
const stats = await client.getOrchestrationStats(instanceId);
const depths = await client.getQueueDepths();

getOrchestrationStats() returns null for a missing instance, otherwise:

{
  historyEventCount: 2,
  historySizeBytes: 184,
  queuePendingCount: 0,
  kvUserKeyCount: 1,
  kvTotalValueBytes: 11
}

KV Store

// Read a KV entry from an orchestration instance
const value = await client.getValue(instanceId, 'myKey');

// Snapshot every KV entry for an orchestration instance
const allValues = await client.getKvAllValues(instanceId);

// Wait until a KV key is set (with timeout in ms)
const readyValue = await client.waitForValue(instanceId, 'myKey', 30000);

// Inside an orchestration, inspect or prune the local KV snapshot
const snapshot = ctx.getKvAllValues();
const keys = ctx.getKvAllKeys();
const size = ctx.getKvLength();
const removed = ctx.pruneKvValuesUpdatedBefore(cutoffMs);

MAX_KV_KEYS is now 150, and MAX_KV_VALUE_BYTES is now 65536.

Documentation

  • Architecture — how the Rust/JS interop works, yield vs await, limitations
  • User Guide — patterns, recipes, and best practices

Tests

Requires PostgreSQL (see .env.example):

npm test                 # e2e tests (25 PG + 1 SQLite smoketest)
npm run test:races       # Race/join composition tests (7 tests)
npm run test:admin       # Admin API tests (14 tests)
npm run test:scenarios   # Scenario tests (6 tests)
npm run test:all         # Everything (52 tests)

License

MIT

About

No description, website, or topics provided.

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages