├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json ├── test.js └── types.d.ts /LICENSE: -------------------------------------------------------------------------------- 1 | The Unlicense 2 | 3 | This is free and unencumbered software released into the public domain. 4 | 5 | Anyone is free to copy, modify, publish, use, compile, sell, or 6 | distribute this software, either in source code form or as a compiled 7 | binary, for any purpose, commercial or non-commercial, and by any 8 | means. 9 | 10 | In jurisdictions that recognize copyright laws, the author or authors 11 | of this software dedicate any and all copyright interest in the 12 | software to the public domain. We make this dedication for the benefit 13 | of the public at large and to the detriment of our heirs and 14 | successors. We intend this dedication to be an overt act of 15 | relinquishment in perpetuity of all present and future rights to this 16 | software under copyright law. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | 26 | For more information, please refer to 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # race-as-promised 2 | 3 | This module implements `Promise.race()` in a way that does not leak 4 | memory. 5 | 6 | ## Rationale 7 | 8 | The V8 Promise implementation does leak memory in many common 9 | `Promise.race([...])` call cases; see 10 | e.g. https://github.com/nodejs/node/issues/17469. 11 | 12 | The V8 Promise implementation is likely [not going to be 13 | fixed](https://github.com/nodejs/node/issues/17469#issuecomment-349794909). 14 | 15 | See also: https://bugs.chromium.org/p/v8/issues/detail?id=9858 16 | 17 | ## Installation 18 | 19 | ```bash 20 | npm install race-as-promised 21 | ``` 22 | 23 | ## Usage 24 | 25 | ```js 26 | const race = require ("race-as-promised"); 27 | 28 | // Use race([...]) instead of Promise.race([...]) 29 | ``` 30 | 31 | ## Author 32 | 33 | The source code and test core [have been made available under The 34 | Unlicense](https://github.com/nodejs/node/issues/17469#issuecomment-776343813) 35 | by [Brian Kim](https://github.com/brainkim), to whom we owe our gratitude. 36 | 37 | An additional issue in the original code has been found and fixed by 38 | [Dan Bornstein](https://github.com/danfuzz), whose efforts are 39 | likewise appreciated. 40 | 41 | ## License 42 | 43 | [The Unlicense](https://spdx.org/licenses/Unlicense.html) 44 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Authored by Brian Kim: 3 | https://github.com/nodejs/node/issues/17469#issuecomment-685216777 4 | 5 | Adapted to module structure. 6 | 7 | This is free and unencumbered software released into the public domain. 8 | 9 | Anyone is free to copy, modify, publish, use, compile, sell, or 10 | distribute this software, either in source code form or as a compiled 11 | binary, for any purpose, commercial or non-commercial, and by any 12 | means. 13 | 14 | In jurisdictions that recognize copyright laws, the author or authors 15 | of this software dedicate any and all copyright interest in the 16 | software to the public domain. We make this dedication for the benefit 17 | of the public at large and to the detriment of our heirs and 18 | successors. We intend this dedication to be an overt act of 19 | relinquishment in perpetuity of all present and future rights to this 20 | software under copyright law. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 23 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 24 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 25 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 26 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 27 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | OTHER DEALINGS IN THE SOFTWARE. 29 | 30 | For more information, please refer to 31 | */ 32 | 33 | function isPrimitive(value) { 34 | return ( 35 | value === null || 36 | (typeof value !== "object" && typeof value !== "function") 37 | ); 38 | } 39 | 40 | function addRaceContender(contender) { 41 | const deferreds = new Set(); 42 | const record = {deferreds, settled: false}; 43 | 44 | // This call to `then` happens once for the lifetime of the value. 45 | Promise.resolve(contender).then( 46 | (value) => { 47 | for (const {resolve} of deferreds) { 48 | resolve(value); 49 | } 50 | 51 | deferreds.clear(); 52 | record.settled = true; 53 | }, 54 | (err) => { 55 | for (const {reject} of deferreds) { 56 | reject(err); 57 | } 58 | 59 | deferreds.clear(); 60 | record.settled = true; 61 | }, 62 | ); 63 | return record 64 | } 65 | 66 | // Keys are the values passed to race, values are a record of data containing a 67 | // set of deferreds and whether the value has settled. 68 | /** @type {WeakMap, settled: boolean}>} */ 69 | const wm = new WeakMap(); 70 | function safeRace(contenders) { 71 | let deferred; 72 | const result = new Promise((resolve, reject) => { 73 | deferred = {resolve, reject}; 74 | for (const contender of contenders) { 75 | if (isPrimitive(contender)) { 76 | // If the contender is a primitive, attempting to use it as a key in the 77 | // weakmap would throw an error. Luckily, it is safe to call 78 | // `Promise.resolve(contender).then` on a primitive value multiple times 79 | // because the promise fulfills immediately. 80 | Promise.resolve(contender).then(resolve, reject); 81 | continue; 82 | } 83 | 84 | let record = wm.get(contender); 85 | if (record === undefined) { 86 | record = addRaceContender(contender); 87 | record.deferreds.add(deferred); 88 | wm.set(contender, record); 89 | } else if (record.settled) { 90 | // If the value has settled, it is safe to call 91 | // `Promise.resolve(contender).then` on it. 92 | Promise.resolve(contender).then(resolve, reject); 93 | } else { 94 | record.deferreds.add(deferred); 95 | } 96 | } 97 | }); 98 | 99 | // The finally callback executes when any value settles, preventing any of 100 | // the unresolved values from retaining a reference to the resolved value. 101 | return result.finally(() => { 102 | for (const contender of contenders) { 103 | if (!isPrimitive(contender)) { 104 | const record = wm.get(contender); 105 | record.deferreds.delete(deferred); 106 | } 107 | } 108 | }); 109 | } 110 | 111 | module.exports = safeRace 112 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "race-as-promised", 3 | "version": "0.0.3", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "race-as-promised", 3 | "version": "0.0.3", 4 | "description": "Alternate Promise.race() implementation which doesn't leak memory, courtesy Brian Kim (https://github.com/brainkim)", 5 | "main": "index.js", 6 | "types": "types.d.ts", 7 | "scripts": { 8 | "test": "node test.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/digitalloggers/race-as-promised.git" 13 | }, 14 | "keywords": [ 15 | "promise", 16 | "race", 17 | "leak" 18 | ], 19 | "license": "Unlicense", 20 | "bugs": { 21 | "url": "https://github.com/digitalloggers/race-as-promised/issues" 22 | }, 23 | "homepage": "https://github.com/digitalloggers/race-as-promised#readme" 24 | } 25 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Authored by Brian Kim: 3 | * https://github.com/nodejs/node/issues/17469#issuecomment-685216777 4 | * 5 | * Adapted to module structure. 6 | * 7 | * Adjusted to run for a finite time and perform explicit leak checks. 8 | */ 9 | 10 | const raceAsPromised = require("./"); 11 | const nativeRace = Promise.race.bind(Promise); 12 | 13 | async function randomString(length) { 14 | await new Promise((resolve) => setTimeout(resolve, 1)); 15 | let result = ""; 16 | const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 17 | for (let i = 0; i < length; i++) { 18 | result += characters.charAt(Math.floor(Math.random() * characters.length)); 19 | } 20 | return result; 21 | } 22 | 23 | const iterationCount = 1000; 24 | const stringSize = 10000; 25 | 26 | function usageMeaningfullyIncreasing(usages, key) { 27 | return usages[2][key] - usages[0][key] > 2 * iterationCount * stringSize && usages[2][key] - usages[1][key] > (usages[1][key] - usages[0][key]) / 2 28 | } 29 | 30 | function detectLeak(usages) { 31 | return usageMeaningfullyIncreasing(usages, 'rss') || usageMeaningfullyIncreasing(usages, 'heapUsed') 32 | } 33 | 34 | async function run(race) { 35 | const pending = new Promise(() => {}); 36 | for (let i = 0; i < iterationCount; i++) { 37 | // We use random strings to prevent string interning. 38 | // Pass a different length string to see effects on memory usage. 39 | await race([pending, randomString(stringSize)]); 40 | } 41 | } 42 | 43 | async function test(label, race, expectPass) { 44 | const usages = []; 45 | usages.push(process.memoryUsage()); 46 | await run(race); 47 | usages.push(process.memoryUsage()); 48 | await run(race); 49 | usages.push(process.memoryUsage()); 50 | const pass = !detectLeak(usages) 51 | const expectationMet = pass == expectPass 52 | console.log(`${expectationMet ? "ok" : "ERROR"}: ${label} ${pass ? "passed" : "failed"} the memory leak test`); 53 | return expectationMet 54 | } 55 | 56 | (async function main() { 57 | // NB: we run the test not expected to leak first 58 | process.exit( 59 | await test('race-as-promised', raceAsPromised, true) 60 | && 61 | await test('native Promise.race', nativeRace, false) 62 | ? 0 : 1 63 | ); 64 | })(); 65 | -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | export default function race(values: T): Promise>; --------------------------------------------------------------------------------