From ed890e688c17c60a8eac318afdd7fb5e66886af3 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Mon, 31 Aug 2026 17:06:11 +0200 Subject: [PATCH 1/2] fix: CJS/ESM p-limit import race --- lib/asyncbox.ts | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/lib/asyncbox.ts b/lib/asyncbox.ts index b078811..252664e 100644 --- a/lib/asyncbox.ts +++ b/lib/asyncbox.ts @@ -1,4 +1,4 @@ -import {limitFunction} from 'p-limit'; +import type {limitFunction} from 'p-limit'; import type { CancellablePromise, @@ -11,6 +11,17 @@ import type { const LONG_SLEEP_THRESHOLD = 5000; // anything over 5000ms will turn into a spin +type PLimitModule = {limitFunction: typeof limitFunction}; + +// p-limit is ESM-only. tsc downlevels a plain `import()` in CommonJS output into a +// require()-in-a-microtask, which still hits Node's require(esm) interop and can race with a +// concurrent dynamic import() of the same module elsewhere in the process +// (ERR_REQUIRE_ESM_RACE_CONDITION). Route through `new Function` so tsc can't see or rewrite the +// import() call, keeping it a genuine native dynamic import in both the ESM and CJS builds. +const dynamicImport = new Function('specifier', 'return import(specifier)') as ( + specifier: string, +) => Promise; + /** Error thrown by {@link withTimeout} when the deadline is exceeded. */ export class TimeoutError extends Error { constructor(message?: string) { @@ -241,7 +252,9 @@ export async function asyncmap( ); } const adjustedMapper = - options === true ? mapperAsync : limitFunction(mapperAsync, {concurrency: options.concurrency}); + options === true + ? mapperAsync + : (await getLimitFunction())(mapperAsync, {concurrency: options.concurrency}); return Promise.all(coll.map(adjustedMapper)); } @@ -271,7 +284,9 @@ export async function asyncfilter( }, Promise.resolve([])); } const adjustedFilter = - options === true ? filterAsync : limitFunction(filterAsync, {concurrency: options.concurrency}); + options === true + ? filterAsync + : (await getLimitFunction())(filterAsync, {concurrency: options.concurrency}); const bools = await Promise.all(coll.map(adjustedFilter)); return coll.reduce((acc, item, i) => { if (bools[i]) { @@ -363,3 +378,9 @@ function parseSleepArg(arg: SleepArg): {ms: number; cancelError?: string | Error } throw new TypeError('sleep: expected a finite number or an object with ms'); } + +let limitFunctionPromise: Promise | undefined; +async function getLimitFunction() { + limitFunctionPromise ??= dynamicImport('p-limit'); + return (await limitFunctionPromise).limitFunction; +} From 115fc552cc6c1851dc7975e34d94f5422c2b71d1 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Mon, 31 Aug 2026 17:11:51 +0200 Subject: [PATCH 2/2] format --- lib/asyncbox.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/asyncbox.ts b/lib/asyncbox.ts index 252664e..61fb840 100644 --- a/lib/asyncbox.ts +++ b/lib/asyncbox.ts @@ -252,9 +252,7 @@ export async function asyncmap( ); } const adjustedMapper = - options === true - ? mapperAsync - : (await getLimitFunction())(mapperAsync, {concurrency: options.concurrency}); + options === true ? mapperAsync : (await getLimitFunction())(mapperAsync, {concurrency: options.concurrency}); return Promise.all(coll.map(adjustedMapper)); } @@ -284,9 +282,7 @@ export async function asyncfilter( }, Promise.resolve([])); } const adjustedFilter = - options === true - ? filterAsync - : (await getLimitFunction())(filterAsync, {concurrency: options.concurrency}); + options === true ? filterAsync : (await getLimitFunction())(filterAsync, {concurrency: options.concurrency}); const bools = await Promise.all(coll.map(adjustedFilter)); return coll.reduce((acc, item, i) => { if (bools[i]) {