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 🙏

© 2024 – Pkg Stats / Ryan Hefner

conclure

v2.2.0

Published

Generator runner

Downloads

40

Readme

⤕ Conclure JS

Brings cancellation and testability to your async flows.

It is a tiny (core is < 200 lines of code), zero dependencies generator runner.

Just grep and replace:

  • async -> function*
  • await -> yield
  • Promise.(all|race|allSettled|any) -> Conclude.(all|race|allSettled|any)
import { conclude } from 'conclure';
import * as Conclude from 'conclure/combinators';

// An example of a multi-step async flow that a user might want to cancel at any time
function* fetchItem(item) {
  const { contentsUrl } = yield item.fetchMetaData();
  const res = yield fetch(contentsUrl);
  return res.text();
};

const loadAll = items => Conclude.all(items.map(fetchItem));

const cancel = conclude(loadAll(myDocs), (err, contents) => {
  if (err) console.error(err);
  else console.log(contents);
});

/// later...
cancel();

You can yield/conclude iterators, promises, and effects interchangeably, so you can gradually introduce cancellation and testability to your async flows.

Design concepts and rationale

You should avoid Promises for two major reasons:

  • Promises are greedy: once created, cannot be cancelled
  • await promise always inserts a tick into your async flow, even if the promise is already resolved or can be resolved synchronously.

You can see a Promise as a particular type of an iterator for which the JS VM provides a built-in runner, a quite poorly designed one nonetheless.

⤕ Conclure JS is a custom generator runner that

  • allows you to cancel your async flows
  • ensures that sync flows always resolve synchronously
  • delivers better testability through the use of effects as popularized by redux-saga.

Terminology and semantics

An async flow may be represented by any of the three base concepts:

  • a promise (e.g. a result of an async function)
  • an iterator (e.g. a result of a generator function)
  • an effect: a declarative (lazy) function call, redux-saga style

You can yield or return a flow from a generator function. Conclure's runner will conclude the flow that will either

  • produce a result: promise resolves / iterator returns / CPS callback is called with (null, result), or
  • fail with an error: promise rejects / iterator throws / CPS callback is called with (error)

The runner returns the concluded value to the generator function via .next(result) or .throw(error)

The return value of the generator function yielding the flow - an iterator - becomes the flow's parent.

A flow may have multiple parents - different generators yielding the same flow. Conclure ensures that in this case the flow only runs once, but the results are delivered to all parents once concluded.

The root flow may be concluded by calling conclude explicitly, which itself is a CPS function, in the same vein as you would attach a then handler to a Promise outside of an async function. You may have multiple root flows.

conclude returns a cancel function that cancels the top-level flow. A child flow would then be cancelled if all of its parents are cancelled.

Unlike redux-saga, Conclure does not call .return with some "magic" value on the iterator. It simply attempts to cancel the currently pending operation and stops iterating the iterator.

A flow is considered finished when it is either concluded (with a result or an error) or cancelled.

You can also attach weak watchers to a flow using whenFinished(flow, callback). The callback will be called with { cancelled, error, result } when the flow has finished.

In case the flow concludes with a result or an error, the weak watchers are called before the result is delivered to the flow's parents, so the callback passed to whenFinished is roughly equivalent to the finally block of a redux-saga generator. However, it can be attached to promises and effects as well, and enables perfectly valid edge cases, when a flow is cancelled synchronously while the generator is running.

Check out some examples in the Recipes section below.

Effects

import { call, cps, cps_no_cancel, delay } from 'conclure/effects';

An effect is simply an abstracted declarative (lazy) function call: it is a simple object { [TYPE], context, fn, args } which may come in two flavors: CALL or CPS.

  • A CALL effect, when concluded, will call fn.apply(context, args) and conclude the result. Create a CALL effect using call(fn, ...args). If fn requires this, you can pass the context as call([context, fn], ...args).

  • A CPS effect, when concluded, will call fn.call(context, ...args, callback), and resolve or reject when the callback is called. fn must return a cancellation. Create a CPS effect using cps(fn, ...args). If fn requires this, you can pass the context as cps([context, fn], ...args).

To call third-party CPS functions that do not return a cancellation, use the cps_no_cancel effect instead.

delay(ms)

delay is a CPS function. However, when called without the second callback argument it returns a cps effect on itself. When concluded, it introduces a delay of ms milliseconds into the flow.

Combinators

import * as Conclude from 'conclure/combinators';

Conclude.[all|any|race|allSettled] combinators would do the same thing as their Promise counterparts, except that they operate on all types of flows supported by ConclureJS: promises, iterators, or effects. All other values are concluded as themselves. The payload argument may be an Iterable or an object.

Combinator conclude behavior summary:

| Combinator | Flow k produces result | Flow k fails with error | All flows conclude |---|---|---|---| |all([])|continue|Fail with error|Return all results |all({})|continue|Fail with {[k]: error}|Return { [k in payload]: results[k] } |any([])|Return result|continue|Fail with all errors |any({})|Return {[k]: result}|continue|Fail with { [k in payload]: errors[k] } |race([])|Return result|Fail with error|noop |race({})|Return {[k]: result}|Fail with {[k]: error}|noop |allSettled([])|continue|continue|Return [{ result: results[k], error: errors[k] }] for all k |allSettled({})|continue|continue|Return { [k in payload]: { result: results[k], error: errors[k] } }

All the combinators are implemented as CPS functions. Same as delay, when called without the callback argument, each combinator returns a cps effect on itself.

IMPORTANT

  • If a combinator can conclude synchronously, it is guaranteed to do so!
  • If some of the flows are still running when a combinator concludes they will be automatically cancelled

Refer to the API reference for more details.

Typical use cases and recipes

  1. Abortable fetch
export function* abortableFetch(url, options) {
  const controller = new AbortController();

  const promise = fetch(url, { ...options, signal: controller.signal });
  whenFinished(promise, ({ cancelled }) => cancelled && controller.abort());

  const res = yield promise;
  if (!res.ok) throw new Error(res.statusText);

  const contentType = res.headers.get('Content-Type');

  return contentType && contentType.indexOf('application/json') !== -1
    ? res.json()
    : res.text();
}
  1. Caching flow results
const withCache = (fn, expiry = 0, cache = new Map()) => function(key, ...args) {
  if (cache.has(key)) {
    return cache.get(key);
  }

  const it = fn(key, ...args);
  cache.set(key, it);

  whenFinished(it, ({ cancelled, error, result }) => {
    if (cancelled || error || !expiry) cache.delete(key);
    else setTimeout(() => cache.delete(key), expiry);
  });

  return it;
}

const cachedFetch = withCache(abortableFetch, 10000);
  1. Show a spinner while a flow is running
function withSpinner(flow) {
  const it = call(() => {
    showSpinner();
    return flow;
  });
  whenFinished(it, () => hideSpinner());
  return it;
}

conclude(withSpinner(cachedFetch(FILE_URL)), (err, res) => console.log({ err, res }));