Promise with unsubscribe feature that minimises memory leaks
npm install @watchable/unpromiseJavascript'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 ofUnpromise.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.
Substitute Unpromise.race or Unpromise.any in place of Promise.race andPromise.any...
``ts
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.
`zsh`
npm install @watchable/unpromise
`javascript`
import { Unpromise } from "@watchable/unpromise"; // esm build
const { Unpromise } = require("@watchable/unpromise"); // commonjs build
The library manages a single lazy-created ProxyPromise for you that shadowsPromise
any . For every native Promise there is only one ProxyPromise. ItProxyPromise
remains cached in a WeakMap for the lifetime of the Promise itself. On creation,
the shadow adds handlers to the native Promise's .then() and.catch() just once. This eliminates memory leaks from adding multiple
handlers.
`ts`
const proxyPromise = Unpromise.proxy(promise);
As an alternative if you are constructing your own Promise, you can useUnpromise to create a ProxyPromise right from the beginning...
`ts`
const proxyPromise = new Unpromise((resolve) => setTimeout(resolve, 1000));
Once you have a ProxyPromise you can call proxyPromise.then()proxyPromise.catch() or proxyPromise.finally() in the normal way. A promiseSubscribedPromise
returned by these methods is a . It behaves like any normalPromise except it has an unsubscribe() method that will remove its handlersProxyPromise
from the .
Finally you must call subscribedPromise.unsubscribe() before you release the
promise reference. This eliminates memory leaks from subscription and
(therefore) from reference retention.
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) completesconst subscribedPromise = Unpromise.proxy(promise).subscribe()
both Step 1 and Step 2 for you (it's equivalent to ). Then latersubscribedPromise.unsubscribe()
you can call to tidy up.
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 createsinterruptPromise
an unbreakable reference chain from the to the taskPromise
(and its task result), and these references can never be garbage-collected,
leading to an out of memory error.
`js
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);Interrupted by user
}
console.log();
}
run();
``