Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
@watchable/unpromise
Advanced tools
Promise with unsubscribe feature that minimises memory leaks
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.
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.
npm install @watchable/unpromise
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 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));
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
.
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)
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.
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();
FAQs
Promise with unsubscribe feature that minimises memory leaks
We found that @watchable/unpromise demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
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.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.