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

@dicabrio/durable-sdk

v0.4.2

Published

TypeScript SDK for the Durable execution engine: define durable functions, sync them, send events, and serve the replay callback.

Readme

@dicabrio/durable-sdk

TypeScript SDK for the self-hosted Durable execution engine. The service keeps queue and replay state; your application hosts a signed callback that executes at most one new checkpoint per request.

Install

npm install @dicabrio/durable-sdk

The package contains ESM, CommonJS and self-contained declarations. Node.js 18+ is supported; the Durable CLI requires Node.js 20+.

Define a function

import {
  createFunction,
  NonRetriableError,
} from "@dicabrio/durable-sdk";

export const onboarding = createFunction({
  id: "onboarding",
  trigger: { event: "user.created" },
  retries: 3,                 // retries after the initial attempt
  timeout: "20s",            // one callback/checkpoint invocation
  handler: async ({ event, step }) => {
    const data = event.data as { id: string };
    const user = await step.run("load-user", async () => {
      if (!data.id) throw new NonRetriableError("user id required", {
        code: "USER_ID_REQUIRED",
        details: { source: event.name },
      });
      return { id: data.id, email: `${data.id}@example.com` };
    });

    await step.sleep("cooldown", "5s");
    await step.run("send-email", () => sendWelcomeEmail(user));
    return { ok: true };
  },
  onFailure: async ({ event, error, step, runId }) => {
    // Separate durable run. event is the durable/function.failed system event.
    await step.run("mark-failed", () =>
      markRecipeFailed(event.data.event.data, error.message),
    );
  },
});

NonRetriableError skips remaining retries. Other errors are retried according to the function’s retries setting; if omitted, the service-wide default is used. Failure handling follows Inngest: a terminal failure emits durable/function.failed; onFailure is shorthand for a separate durable handler of that system event and receives { event, error, step, runId }. The original event is event.data.event, and event.data.run_id identifies the failed run. Cancellations do not invoke onFailure.

Host the callback

Express / Node HTTP

Mount without a JSON parser on this route; HMAC verification requires the exact raw bytes.

import express from "express";
import { serve } from "@dicabrio/durable-sdk";

const app = express();
app.all("/api/durable", serve([onboarding], {
  signingKey: process.env.DURABLE_APP_KEY!,
  baseUrl: process.env.DURABLE_BASE_URL,
  appId: process.env.DURABLE_APP_ID,
  appName: "billing",
}));

baseUrl and appId enable the best-effort live running-step marker. Durable execution itself does not depend on that marker. serve/serveNext accept maxBodyBytes (1 MiB by default); DurableClient similarly supports requestTimeoutMs, maxRequestBytes and maxResponseBytes.

Next.js App Router

// app/api/durable/route.ts
import { serveNext } from "@dicabrio/durable-sdk/next";
import { onboarding } from "@/durable/functions";

export const runtime = "nodejs";
export const maxDuration = 30;

export const { GET, POST } = serveNext([onboarding], {
  signingKey: process.env.DURABLE_APP_KEY!,
  baseUrl: process.env.DURABLE_BASE_URL,
  appId: process.env.DURABLE_APP_ID,
  appName: "billing",
});

In development, durable dev discovers this route at http://localhost:3000/api/durable; appName controls the workspace name. Discovery is enabled only when NODE_ENV=development (or dev: true is set explicitly).

The adapter uses standard Request/Response, has no Next.js dependency and is Node-runtime only because HMAC currently uses node:crypto.

Provision and connect

Create production workspaces in the authenticated dashboard. For controlled CI/local provisioning before app credentials exist:

import { DurableClient, provisionApp } from "@dicabrio/durable-sdk";

const credentials = await provisionApp({
  baseUrl: "https://app.durable.dicabrio.com",
  appUrl: "https://my-app.example.com/api/durable", // stored once on the app record
  name: "billing",
  environment: "prod",
  adminToken: process.env.DURABLE_ADMIN_TOKEN,
});

const client = new DurableClient({
  baseUrl: "https://app.durable.dicabrio.com",
  appId: credentials.id,
  signingKey: credentials.key,
});

The callback URL is supplied only when the workspace is created and is then stored by Durable. It is not part of DurableClient or /fn/sync; only an authenticated admin operation can change it. The first pull check may fail until the generated key is configured in the app; a subsequent sync() or dashboard Refresh completes registration.

Registration

await client.sync([onboarding]);

sync() pushes the complete manifest. The same endpoint also answers a signed GET, allowing the service to pull the manifest on startup, nightly and through Refresh. Missing functions are disabled rather than deleted, preserving run history and allowing explicitly retained old versions to finish.

Deploy the service and all workers before publishing/running an SDK that emits new protocol features.

Reliable events

const receipt = await client.send({
  id: `import:${importId}`,       // stable idempotency key within this app
  name: "import.requested",
  data: { importId },
});

If id is omitted the SDK generates one before its first attempt. Network, 408, 429 and 5xx failures are retried with the same signed body. If all attempts fail, EventSendError.eventId exposes that generated ID for a safe later retry. Reusing an ID with the same event returns the immutable original receipt; reusing it with a different name/payload returns HTTP 409. Idempotency receipts survive run-history cleanup and are removed only with the app.

For imports, client.sendMany(events) sends 1-100 events in one idempotent transaction. IDs omitted by the caller are generated before the first network attempt and exposed through BulkEventSendError.eventIds on failure.

Inside a workflow use step.sendEvent, never an uncheckpointed client.send:

const [sent] = await step.sendEvent("start-import", {
  id: `recipe:${recipeId}:import`,
  name: "import.requested",
  data: { recipeId },
});

The checkpoint and event fan-out commit in one service transaction.

Step primitives

| Primitive | Behavior | |---|---| | step.run(id, fn) | execute one checkpoint and memoize its JSON result | | step.parallel(id, tasks) | execute 1-20 callbacks concurrently as one composite checkpoint | | step.sleep(id, duration) | durable delay without holding a worker | | step.waitForEvent(id, options) | resume with a matching event or null on timeout | | step.sendEvent(id, event) | atomically checkpoint and dispatch idempotent events |

Step IDs must be stable and unique within a run. Inputs, outputs and event data must be JSON-serializable. Multiple new step.run calls in Promise.all are rejected before their callbacks start; use step.parallel. Its branches share one checkpoint and can all repeat if the callback result is lost.

Multiple and shared concurrency rules

createFunction({
  id: "domain-import",
  trigger: { event: "import.requested" },
  concurrency: [
    { limit: 2, key: "domain" },          // two per event.data.domain
    { limit: 20, scope: "all-imports" },  // shared across functions in this app
  ],
  handler: async ({ event, step }) => { /* ... */ },
});

All rules must have capacity before a callback starts. Shared scope names are isolated per app. key is a direct top-level field in event.data, not an expression; precompute derived values before sending the event. Every function participating in one shared scope must use the same limit and either all keyed or all unkeyed rules. Concurrency counts active callbacks, not sleeping/waiting runs.

Programmatic run management

All operations are signed and scoped to the client app:

const page = await client.searchRuns({ functionId: "recipe-import@v1", status: "running" });
const run = await client.getRun(page.runs[0].id);
await client.cancelRun(page.runs[0].id);
await client.rerun(page.runs[0].id, { idempotencyKey: "incident:42" });
await client.rerunFromStep(page.runs[0].id, "fetch", {
  idempotencyKey: "incident:42:fetch",
});
const failures = await client.listFailures();
await client.retryFailure(failures[0].id); // explicit DLQ replay

Always persist or derive rerun idempotency keys; retrying the same action key returns the original target run.

Workflow versions and deployments

Existing runs call the latest deployed code for their registered runtime ID. Use explicit versions for incompatible workflow changes:

const v1 = createFunction({
  id: "recipe-import", version: "v1", enabled: false,
  trigger: { event: "recipe.import" },
  handler: oldHandler,
});
const v2 = createFunction({
  id: "recipe-import", version: "v2",
  trigger: { event: "recipe.import" },
  handler: newHandler,
});

await client.sync([v1, v2]);

Only one version of a base ID may be enabled. Runs are pinned to IDs such as recipe-import@v1; keep the old handler deployed until its active, sleeping, waiting and retrying runs have drained. Treat a published version as immutable. For compatible edits, preserve step IDs and deterministic control flow. Use client.getVersionStatus("recipe-import@v1"); remove old code only when the version is disabled and safeToRemove is true.

Timeouts, payloads and long work

Defaults:

  • service API/callback request: 1 MiB;
  • callback/manifest response: 1 MiB;
  • app callback: 30 seconds unless overridden per function;
  • manifest pull: 10 seconds;
  • error details: 64 KiB;
  • total memoized invocation state: below the API request limit.

The request limit covers the triggering event, rich failure context and all memoized steps together. Keep normal state below roughly 750 KiB; store large objects externally and checkpoint references. The service records state_bytes and terminally fails with STATE_LIMIT_EXCEEDED instead of sending an oversized next callback.

A timeout cannot forcibly stop JavaScript already executing on Vercel or another platform. A step that times out before its result is persisted can execute again. External side effects must therefore use provider idempotency keys. Durable provides at-least-once checkpoint execution, not transactional exactly-once delivery to third parties.

Never put a complete crawl, newsletter send or unbounded batch loop in one step.run. Split it into bounded checkpoints that comfortably fit below both the Durable callback timeout and the hosting platform limit:

for (const item of chunk) {
  await step.run(`process-${item.id}`, () => processItem(item));
}

For large collections, fan out stable-ID events and process bounded chunks so the cumulative memoized invocation payload also stays below the request limit.

Security

Every app→service body uses x-durable-app plus an HMAC-SHA256 signature. Every service→app invocation and app response is signed with the workspace key. Manifest pulls sign a five-minute timestamp. Keep signing keys and admin tokens server-side; never expose them to browser code.

License

MIT