JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 14
  • Score
    100M100P100Q33759F
  • License MIT

JavaScript promises are made to be broken. Speculations are cancellable promises.

Package Exports

  • speculation

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (speculation) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

Speculation

JavaScript promises are made to be broken. Speculations are cancellable promises.

Getting started

Install:

npm install --save speculation

Use:

Let's use it to create a cancellable wait() function. It'll take time in ms, and a promise to use as shouldCancel. When the time runs out, the promise will resolve. If the wait is cancelled, the returned promise will reject with a 'Cancelled' error.

import speculation from 'speculation';

const wait = (
  time,
  cancel = Promise.reject() // By default, don't cancel
) => speculation((resolve, reject, onCancel) => {
  const timer = setTimeout(resolve, time);

  // Use onCancel to clean up any lingering resources
  // and then call reject(). You can pass a custom reason.
  onCancel(() => {
    clearTimeout(timer);
    reject(new Error('Cancelled'));
  });
}, cancel); // remember to pass in cancel!

wait(200, wait(500)).then(
  () => console.log('Hello!'),
  (e) => console.log(e)
); // 'Hello!'

wait(200, wait(50)).then(
  () => console.log('Hello!'),
  (e) => console.log(e)
); // [Error: Cancelled]

What is a Speculation?

A speculation is exactly like a promise, except for these changes:

  • Speculations can be easily cancelled. Simply pass a shouldCancel promise into the speculation during creation.
  • new is not required when creating a speculation.
  • The signature is speculation((resolve, reject, handleCancel) => Void) => Promise

As you can see from the signature, speculations are promises, meaning they share exactly the same promise interface. Anything that understands promises can use speculations instead. There are no extra properties on speculation objects.

Why?

Promises don't have a built-in cancel mechanism. Many people hack it in in various ways. Here are some problems I've seen with those hacks:

Adding .cancel() to the promise

Adding .cancel() makes the promise non-standard, but it also violates another rule of promises: Only the function that creates the promise should be able to resolve, reject, or cancel the promise. Exposing it breaks that encapsulation, and encourages people to write code that manipulates the promise in places that shouldn't know about it. Avoid spaghetti and broken promises.

Forgetting to clean up

Some clever people have figured out that there's a way to use Promise.race() as a cancellation mechanism. The problem with that is that cancellation control is taken from the function that creates the promise, which is the only place that you can conduct proper cleanup activities, such as clearing timeouts or freeing up memory by clearing references to data, etc...

Forgetting to handle a rejected cancel promise

Did you know that Chrome throws warning messages all over the console when you forget to handle a promise rejection? Oops! speculation() handles that for you, so you can get on with building your app.

How does it work?

const speculation = (
  fn,
  cancel = new Promise.reject() // No cancel by default
) => new Promise((resolve, reject) => {
  const noop = () => {}; // Prevent unhandled rejections

  const handleCancel = (
    onCancel
  ) => cancel.then(onCancel, noop);

  return fn(resolve, reject, handleCancel);
});

Bonus

See that wait() utility in these docs? You can use it by importing it:

import { wait } from 'speculation';

wait(500).then(() => {
  console.log('OMG I can time things!');
});

// Of course, you can cancel it by resolving
// the `shouldReject` promise:
wait(200, wait(50)).then(
  () => {
    console.log('nothing to see here'); // never runs
  },
  (e) => console.log(e) // [Error: Cancelled]
);