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

@watchable/unpromise

v1.0.2

Published

Promise with unsubscribe feature that minimises memory leaks

Downloads

798,577

Readme

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.

In general the Promise API doesn't allow for an unsubscription model. The @watchable/unpromise package wraps individual promises to provide an unsubscribe method. It uses this approach to provide safe implementations of Unpromise.race and Unpromise.any. However, the ability to unsubscribe Promises may be useful for other cases where the Promise reference chains (and therefore memory leaks) are otherwise out of your control.

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();