feat(promise-kit): detach promise from resolvers after use
Avoids leaking the promise through stray resolvers
MODIFY
packages/promise-kit/index.js
packages/promise-kit/index.js
/// <reference types="ses"/>
+import { makeReleasingExecutorKit } from './src/promise-executor-kit.js';
import { memoRace } from './src/memo-race.js';
export * from './src/is-promise.js';
@@ -12,11 +13,6 @@ export * from './src/types.js';
/** @type {PromiseConstructor} */
const BestPipelinablePromise = globalThis.HandledPromise || Promise;
-/**
- * Needed to prevent type errors where functions are detected to be undefined.
- */
-const NOOP_INITIALIZER = harden(() => {});
-
/**
* makePromiseKit() builds a Promise object, and returns a record
* containing the promise itself, as well as separate facets for resolving
@@ -26,21 +22,18 @@ const NOOP_INITIALIZER = harden(() => {});
* @returns {import('./src/types.js').PromiseKit<T>}
*/
export function makePromiseKit() {
- /** @type {(value: import('./src/types.js').ERef<T>) => void} */
- let resolve = NOOP_INITIALIZER;
- /** @type {(reason: unknown) => void} */
- let reject = NOOP_INITIALIZER;
+ const { resolve, reject, executor } = makeReleasingExecutorKit();
- /** @type {Promise<T>} */
- const promise = new BestPipelinablePromise((res, rej) => {
- resolve = res;
- reject = rej;
- });
+ const promise = new BestPipelinablePromise(executor);
return harden({ promise, resolve, reject });
}
harden(makePromiseKit);
+// NB: Another implementation for Promise.race would be to use the releasing executor,
+// However while it would no longer leak the raced promise objects themselves, it would
+// still leak reactions on the non-resolved promises contending for the race.
+
/**
* Creates a Promise that is resolved or rejected when any of the provided Promises are resolved
* or rejected.

ADD
packages/promise-kit/src/promise-executor-kit.js
+// @ts-check
+
+/// <reference types="ses"/>
+
+/**
+ * @template T
+ * @callback PromiseExecutor The promise executor
+ * @param {(value: import('./types.js').ERef<T>) => void} resolve
+ * @param {(reason: any) => void} reject
+ */
+
+/**
+ * makeReleasingExecutorKit() builds resolve/reject functions which drop references
+ * to the resolve/reject functions gathered from an executor to be used with a
+ * promise constructor.
+ *
+ * @template T
+ * @returns {Pick<import('./types.js').PromiseKit<T>, 'resolve' | 'reject'> & { executor: PromiseExecutor<T>}}
+ */
+export const makeReleasingExecutorKit = () => {
+ /** @type {null | undefined | ((value: import('./types.js').ERef<T>) => void)} */
+ let internalResolve;
+ /** @type {null | undefined | ((reason: unknown) => void)} */
+ let internalReject;
+
+ /** @param {import('./types.js').ERef<T>} value */
+ const resolve = value => {
+ if (internalResolve) {
+ internalResolve(value);
+ internalResolve = null;
+ internalReject = null;
+ } else {
+ assert(internalResolve === null);
+ }
+ };
+
+ /** @param {unknown} reason */
+ const reject = reason => {
+ if (internalReject) {
+ internalReject(reason);
+ internalResolve = null;
+ internalReject = null;
+ } else {
+ assert(internalReject === null);
+ }
+ };
+
+ const executor = (res, rej) => {
+ assert(internalResolve === undefined && internalReject === undefined);
+ internalResolve = res;
+ internalReject = rej;
+ };
+
+ return { resolve, reject, executor };
+};
+harden(makeReleasingExecutorKit);

feat(promise-kit): detach promise from resolvers after use
Avoids leaking the promise through stray resolvers