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

durable-fn

v1.0.2

Published

Durable functions in under 300 bytes — pure JavaScript, zero dependencies, no infrastructure required.

Downloads

49

Readme

durable-fn

npm version npm downloads gzip size

Pause a function mid-run, resume it on another request or machine on-demand. Good for pipeline engines, IVRs, and checkout flows — all in under 300 bytes.

Write the flow as an ordinary function and mark where it waits with suspend; it resumes by replaying the results so far. The whole state is a string[] you keep wherever you like — a row, a cookie, a Redis key. No runtime, no storage engine, no dependencies.

Getting started

durable(fn) returns a function you call with the log so far. It replays your generator over it, fast-forwards the steps already resolved, and stops at the next suspend — handing you the value it's waiting on, and whether it's done.

import durable from 'durable-fn';

const call = durable(function* (suspend) {
  const dept = yield* suspend(() => 'Press 1 for sales, 2 for support.');

  if (dept === '1') {
    const size = yield* suspend(() => 'Press 1 if you have fewer than 50 seats, 2 otherwise.');
    return size === '2' ? 'Connecting you to enterprise sales…' : 'Connecting you to sales…';
  }

  const acct = yield* suspend(() => 'Enter your account number.');
  return `Pulling up account ${acct}…`;
});

// Call it with the log so far; it resumes by replaying it.
call([]).value; //             'Press 1 for sales, 2 for support.'
call(['1']).value; //          'Press 1 if you have fewer than 50 seats, 2 otherwise.'
call(['1', '2']).value; //     'Connecting you to enterprise sales…'
call(['2']).value; //          'Enter your account number.'
call(['2', '8842']).value; //  'Pulling up account 8842…'

dept is an ordinary local — the next step depends on it, and it's still in scope when the flow finishes, even though each call is a separate run. No state machine, no transition table, just branches you read top to bottom. suspend marks where it pauses; the reply becomes that step's result on the next call, and done tells you whether value is what it needs next or the final return.

It's fully typed — no annotations, no casts. Whatever your steps yield and return is inferred, and the result is a discriminated union on done: if (step.done) narrows value to the return type, otherwise to what a step yields. Those yielded values can be discriminated unions of their own that you switch on.

In a handler

The log is just a string[]. Persist it per session and hand it back next time. That's the only state; nothing lives in memory between calls.

app.post('/call/:id', (req, res) => {
  const events = store.load(req.params.id);          // the answers so far
  if (req.body.digit != null) events.push(String(req.body.digit));
  const step = call(events);
  store.save(req.params.id, events);
  res.json(step);                                    // { value, done }
});

Because the entire state is the replayed log, the same session resumes on any request, process, or machine — even after a crash or deploy.

Build an engine on it

durable-fn is the replay kernel, not the engine. It's the same trick Temporal and Azure Durable Functions run on — pause a function, replay a log to resume — with the engine left to you. So a pipeline or workflow engine is a thin layer on top: a step is a suspend your runner performs and logs; a sub-flow is just yield*; a job that runs for days on another machine suspends until its completion callback lands (a later request, a different process) and replay picks up from there. Concurrency, timers, retries, and distribution are yours to add — durable-fn only handles the pause and resume.

test/pipeline.test.ts is a worked example: a render pipeline that preps locally, dispatches multi-day jobs to a farm, suspends on each, and resumes across separate invocations.

Good to know

  • Keep replays deterministic. Reading Date.now(), Math.random(), or a mutating object inside the body would come out different on each replay and fall out of sync. Have the driver supply such values as answers instead — see test/determinism.test.ts for the pattern.

  • Sync or async. durable accepts a function* or an async function*. A sync body returns { value, done }; an async one returns Promise<{ value, done }> — so you can await a lookup between prompts.

  • The log is the position. The flow's place is nothing but the replay of events, so the array you persist is the entire cursor — editing it before a call decides where the flow resumes.

  • Version before reordering steps. The log is positional — answer #1, answer #2, … with no names attached — so inserting or reordering a suspend reinterprets an old log. Fine within a single run; for long-lived flows that outlive a deploy, version the body (or drain in-flight ones) before you change the order of steps.