@watchable/unpromise
TypeScript icon, indicating that this package has built-in type declarations

1.0.1 • Public • Published

Unpromise: Proxy promises for unsubscription

Javascript's built-in implementation of Promise.race and Promise.any have a bug/feature that leads to uncontrollable memory leaks. See the Typical Problem Case below for reference.

The Memory leaks are fixed by using @watchable/unpromise.

Usage

Substitute Unpromise.race or Unpromise.any in place of Promise.race and Promise.any...

import { Unpromise } from "@watchable/unpromise";

const raceResult = await Unpromise.race([taskPromise, interruptPromise]);

const anyResult = await Unpromise.any([taskPromise, interruptPromise]);

Advanced users exploring other async/await patterns should consider Unpromise.proxy() or Unpromise.resolve(). Read more at the API docs.

Install

npm install @watchable/unpromise

Import OR Require

import { Unpromise } from "@watchable/unpromise"; // esm build
const { Unpromise } = require("@watchable/unpromise"); // commonjs build

Under the hood: Step 1 - proxy

The library manages a single lazy-created ProxyPromise for you that shadows any Promise. For every native Promise there is only one ProxyPromise. It remains cached in a WeakMap for the lifetime of the Promise itself. On creation, the shadow ProxyPromise adds handlers to the native Promise's .then() and .catch() just once. This eliminates memory leaks from adding multiple handlers.

const proxyPromise = Unpromise.proxy(promise);

As an alternative if you are constructing your own Promise, you can use Unpromise to create a ProxyPromise right from the beginning...

const proxyPromise = new Unpromise((resolve) => setTimeout(resolve, 1000));

Under the hood: Step 2 - subscribe

Once you have a ProxyPromise you can call proxyPromise.then() proxyPromise.catch() or proxyPromise.finally() in the normal way. A promise returned by these methods is a SubscribedPromise. It behaves like any normal Promise except it has an unsubscribe() method that will remove its handlers from the ProxyPromise.

Under the hood: Step 3 - unsubscribe

Finally you must call subscribedPromise.unsubscribe() before you release the promise reference. This eliminates memory leaks from subscription and (therefore) from reference retention.

Under the hood: Simple Shortcuts

Using Unpromise.race() or Unpromise.any() is recommended. Using these static methods, the proxying, subscribing and unsubscribing steps are handled behind the scenes for you automatically.

Alternatively const subscribedPromise = Unpromise.resolve(promise) completes both Step 1 and Step 2 for you (it's equivalent to const subscribedPromise = Unpromise.proxy(promise).subscribe() ). Then later you can call subscribedPromise.unsubscribe() to tidy up.

Typical Problem Case

In the example app below, we have a long-lived Promise that we await every time around the loop with Promise.race(...). We use race so that we can respond to either the task result or the keyboard interrupt.

Unfortunately this leads to a memory leak. Every call to Promise.race creates an unbreakable reference chain from the interruptPromise to the taskPromise (and its task result), and these references can never be garbage-collected, leading to an out of memory error.

const interruptPromise = new Promise((resolve) => {
  process.once("SIGINT", () => resolve("interrupted"));
});

async function run() {
  let count = 0;
  for (; ; count++) {
    const taskPromise = new Promise((resolve) => {
      // an imaginary task
      setImmediate(() => resolve("task_result"));
    });
    const result = await Promise.race([taskPromise, interruptPromise]);
    if (result === "interrupted") {
      break;
    }
    console.log(`Completed ${count} tasks`);
  }
  console.log(`Interrupted by user`);
}

run();

Package Sidebar

Install

npm i @watchable/unpromise

Weekly Downloads

452

Version

1.0.1

License

MIT

Unpacked Size

42.3 kB

Total Files

14

Last publish

Collaborators

  • cefn