npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@synadia-ai/durable

v0.1.1

Published

The thin durable execution SDK: the replay core of @synadia-ai/durable in your process, durable-engine behind it.

Downloads

107

Readme

@synadia-ai/durable

Durable execution for TypeScript on NATS. Workflows are ordinary async functions over a ctx: every step is journaled, and after a crash a run resumes where it was, without repeating finished steps. Your workflow code runs in your process. The journal, the timers and the work queue are kept by durable-engine, which Synadia ships to you, or which the Synadia Agent Fabric runs for you.

Quickstart

A workflow on your laptop: NATS with JetStream, the engine next to it, and a workflow in a project of your own. You need nats-server ≥ 2.14 on PATH and Node ≥ 20, and from Synadia the engine for your platform (the archive durable-engine_<version>_<os>_<arch>.tar.gz, or the container image) and a licence file. On the Synadia Agent Fabric you need neither: the engine is already there (docs/FABRIC.md).

nats-server -js &                                                    # 1. NATS with JetStream, on 127.0.0.1:4222
tar xzf durable-engine_<version>_<os>_<arch>.tar.gz                  # 2. the engine Synadia sent you
./durable-engine_<version>_<os>_<arch>/durable-engine serve \
  --nats nats://127.0.0.1:4222 --licence durable.licence &           # 3. the engine, with your licence
mkdir order && cd order && npm init -y && npm pkg set type=module    # 4. a project of your own
npm install @synadia-ai/durable @nats-io/transport-node tsx
NATS_URL=nats://127.0.0.1:4222 npx tsx order.ts                      # 5. the file below

order.ts:

import { connect } from "@nats-io/transport-node";
import { createClient, createWorker, defineWorkflow } from "@synadia-ai/durable";

const order = defineWorkflow("order", async (ctx, cart: { cartId: string }) => {
  const orderId = await ctx.run("reserve", () => `ord-${cart.cartId}`); // journaled, never re-run
  const payment = await ctx.await<{ approved: boolean }>("payment"); // parks until a signal
  if (!payment.approved) return { status: "abandoned", orderId };
  await ctx.sleep("settle", 500); // a durable timer
  return { status: "paid", orderId };
});

const nc = await connect({ servers: process.env.NATS_URL ?? "nats://127.0.0.1:4222" });
const worker = createWorker({ nc, workflows: [order] });
try {
  await worker.start();
  const client = createClient({ nc });
  const handle = await client.start(order, { cartId: "c-42" });
  await client.signal(handle.runId, "payment", { approved: true });
  console.log(await handle.result(20_000)); // { status: 'paid', orderId: 'ord-c-42' }
} finally {
  await worker.stop(); // on any error too: its polls withdrawn, an item they were handed goes back
  await nc.drain();
}

It prints { status: 'paid', orderId: 'ord-c-42' }. The engine logs licence … licensee=<you> and serving at start; it creates its streams and buckets on the first start and answers on de.api.v1.* (nats req de.api.v1.info '{}' shows it, with the nats CLI). Without a valid licence it exits at once and says why. Stop the engine and the server with kill %2 %1.

With the container image instead of the archive, step 3 is docker run --rm --network host -v $PWD/durable.licence:/etc/durable-engine/licence:ro durable-engine:<version> (on Docker Desktop add --nats nats://host.docker.internal:4222 and drop --network host).

@nats-io/transport-node is yours to pick; the SDK takes any NatsConnection. On the fabric only the connection changes: your tenant's URL and credentials, never the workflow.

Which engine

How you get one: on the Synadia Agent Fabric the tenant's engine is already there, the operator's, reached through your tenant's import (docs/FABRIC.md); you run none. Standalone, Synadia hands you the engine's archive or image and a licence, and you run it next to your NATS, as in the Quickstart.

The examples

Each runs with npm run example:<name> from a clone of this repository, against the Quickstart's nats-server -js and engine. examples/README.md has each one's command line, and what changes on the fabric (the connection, never the workflow).

| Example | Shows | | -------------- | -------------------------------------------------------------------------------------------------------------------- | | charge | run, await, sleep, call; start, signal, result, status | | saga | compensations in reverse on failure | | crash-replay | a worker killed mid-run; a fresh one resumes without re-running finished steps (run the engine with --ack-wait 2s) | | fan-out | ctx.startCall + Promise.all, and ctx.race | | cancel | client.cancel and ctx.onCancel |

Errors

A refusal from the engine is a typed error, exported from the package:

| Error | When | Retried | | ------------------------ | -------------------------------------------------------------- | ------------- | | InvalidRequestError | the request was malformed | no | | RunNotFoundError | no run with that id | no | | PayloadTooLargeError | a value too large to store | no | | LimitError | your licence or tenant limit is reached (limit names which) | no | | LeaseExpiredError | a worker lost an item it held; the item goes to another worker | by the engine | | EngineUnavailableError | no engine answered in time | yes | | ConnectionClosedError | your NATS connection closed for good | no: restart | | UnsupportedEngineError | the engine is older than this SDK supports | no |

Running a worker

  • Stop it in a finally, as in the Quickstart: worker.stop() hands back what it holds, so a process may exit right after it.
  • Make a step's side effect idempotent, keyed by ctx.key(): a finished step never runs again, a step interrupted by a crash runs again on the next worker.
  • Connect with withReconnectDefaults({ servers, … }), so the connection never gives up reconnecting, and point a readiness probe at worker.health().
  • To keep values private, give payloadCodec: aesGcmPayloadCodec(key) (32 bytes, from your secret store) to every worker and client: the engine then sees a run's shape and never a value.

Contributing

How the SDK works with the engine, the test suites and the interop worker: docs/DEVELOPING.md.