Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

conclure

Package Overview
Dependencies
Maintainers
1
Versions
12
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

conclure

Generator runner

  • 1.0.0
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
11
decreased by-35.29%
Maintainers
1
Weekly downloads
 
Created
Source

⤕ Conclure JS

Brings cancellation and testability to your async flows.

It is a tiny (core is < 200 LOC), 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 - 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. 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:

CombinatorFlow k produces resultFlow k fails with errorAll flows conclude
all([])continueFail with errorReturn all results
all({})continueFail with {[k]: error}Return { [k in payload]: results[k] }
any([])Return resultcontinueFail with all errors
any({})Return {[k]: result}continueFail with { [k in payload]: errors[k] }
race([])Return resultFail with errornoop
race({})Return {[k]: result}Fail with {[k]: error}noop
allSettled([])continuecontinueReturn [{ result: results[k], error: errors[k] }] for all k
allSettled({})continuecontinueReturn { [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 }));

Keywords

FAQs

Package last updated on 06 Mar 2021

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc