feat(promise-kit): Make leak-free race a fully compliant drop-in replacement
MODIFY
packages/promise-kit/NEWS.md
packages/promise-kit/NEWS.md
@@ -3,6 +3,7 @@ User-visible changes in `@endo/promise-kit`:
# Next release
Add a `racePromises` export which implements a non-leaky race algorithm.
+Add a `/shim.js` entrypoint which replaces `Promise.race` with the same non-leaky version.
# v0.2.31 (2022-01-25)

MODIFY
packages/promise-kit/index.js
packages/promise-kit/index.js
@@ -79,11 +79,11 @@ harden(isPromise);
* Unlike `Promise.race` it cleans up after itself so a non-resolved value doesn't hold onto
* the result promise.
*
- * @template {readonly unknown[] | []} T
- * @param {T} values An array of Promises.
- * @returns {Promise<Awaited<T[number]>>} A new Promise.
+ * @template T
+ * @param {Iterable<T>} values An iterable of Promises.
+ * @returns {Promise<Awaited<T>>} A new Promise.
*/
export function racePromises(values) {
- return harden(memoRace(values));
+ return harden(memoRace.call(BestPipelinablePromise, values));
}
harden(racePromises);

MODIFY
packages/promise-kit/package.json
packages/promise-kit/package.json
"module": "./index.js",
"exports": {
".": "./index.js",
+ "./shim.js": "./shim.js",
"./package.json": "./package.json"
},
"scripts": {

ADD
packages/promise-kit/shim.js
+import { memoRace } from './src/memo-race.js';
+
+// Unconditionally replace with a non-leaking version
+Promise.race = memoRace;

MODIFY
packages/promise-kit/src/memo-race.js
packages/promise-kit/src/memo-race.js
@@ -104,6 +104,7 @@ const getMemoRecord = value => {
return record;
};
+const { race } = {
/**
* Creates a Promise that is resolved or rejected when any of the provided Promises are resolved
* or rejected.
@@ -111,20 +112,26 @@ const getMemoRecord = value => {
* Unlike `Promise.race` it cleans up after itself so a non-resolved value doesn't hold onto
* the result promise.
*
- * @template {readonly unknown[] | []} T
- * @param {T} values An array of Promises.
- * @returns {Promise<Awaited<T[number]>>} A new Promise.
+ * @template T
+ * @template {PromiseConstructor} [P=PromiseConstructor]
+ * @this {P}
+ * @param {Iterable<T>} values An iterable of Promises.
+ * @returns {Promise<Awaited<T>>} A new Promise.
*/
-export const memoRace = values => {
+ race(values) {
let deferred;
- const result = new Promise((resolve, reject) => {
+ /** @type {T[]} */
+ const cachedValues = [];
+ const C = this;
+ const result = new C((resolve, reject) => {
deferred = { resolve, reject };
for (const value of values) {
+ cachedValues.push(value);
const { settled, deferreds } = getMemoRecord(value);
if (settled) {
// If the contender is settled (including primitives), it is safe
// to call `Promise.resolve(value).then` on it.
- Promise.resolve(value).then(resolve, reject);
+ C.resolve(value).then(resolve, reject);
} else {
deferreds.add(deferred);
}
@@ -134,11 +141,14 @@ export const memoRace = values => {
// The finally callback executes when any value settles, preventing any of
// the unresolved values from retaining a reference to the resolved value.
return result.finally(() => {
- for (const value of values) {
+ for (const value of cachedValues) {
const { deferreds } = getMemoRecord(value);
if (deferreds) {
deferreds.delete(deferred);
}
}
});
+ },
};
+
+export { race as memoRace };

feat(promise-kit): Make leak-free race a fully compliant drop-in replacement