Skip to content

Commit ba54b28

Browse files
committed
feat: reconcile abort-signal with type-safety and efficiency refactors
Layers the type-safety helpers from copilot/harden-implementation-type-safety and the perf cleanup from copilot/improve-code-efficiency on top of claude/add-abort-signal-support-j2Mmx (now the abort-signal feature commits already on this branch). - Use isType from @voxpelli/typed-utils for callback/bufferSize/result guards - Use guardedArrayIncludes for isPartOfArray (lib/type-checks.js) - Extract isValueObject helper used by isIterable/isAsyncIterable - Extract normalizeError helper (lib/misc.js); use it in fillQueue catches and the fail-fast / capturedErrors path - Replace [...subIterators, asyncIterator] spread with yieldArrayWithItem generator in fillQueue's unordered branch - Add bounds check to ordered-insertion while loop in fillQueue - Add @voxpelli/typed-utils as runtime dependency
1 parent 11fbb41 commit ba54b28

4 files changed

Lines changed: 50 additions & 14 deletions

File tree

index.js

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55
// TODO: See "iteratorKind" in https://tc39.es/ecma262/#sec-runtime-semantics-forin-div-ofbodyevaluation-lhs-stmt-iterator-lhskind-labelset – see how it loops and validates the returned values
66
// TODO: THERE'S ACTUALLY A "throw" method MENTION IN https://tc39.es/ecma262/#sec-generator-function-definitions-runtime-semantics-evaluation: "NOTE: Exceptions from the inner iterator throw method are propagated. Normal completions from an inner throw method are processed similarly to an inner next." THOUGH NOT SURE HOW TO TRIGGER IT IN PRACTICE, SEE yield.spec.js
77

8+
import { isType } from '@voxpelli/typed-utils';
89
import { findLeastTargeted } from './lib/find-least-targeted.js';
9-
import { arrayDeleteInPlace, makeIterableAsync } from './lib/misc.js';
10+
import { arrayDeleteInPlace, makeIterableAsync, normalizeError } from './lib/misc.js';
1011
import { isAsyncIterable, isIterable, isPartOfArray } from './lib/type-checks.js';
1112

1213
/**
@@ -18,6 +19,17 @@ async function * yieldIterable (item) {
1819
yield * item;
1920
}
2021

22+
/**
23+
* @template T, U
24+
* @param {T[]} array
25+
* @param {U} item
26+
* @returns {Iterable<T | U>}
27+
*/
28+
function * yieldArrayWithItem (array, item) {
29+
yield * array;
30+
yield item;
31+
}
32+
2133
/**
2234
* @template T
2335
* @param {Array<AsyncIterable<T> | Iterable<T> | T[]>} input
@@ -52,8 +64,8 @@ export function bufferedAsyncMap (input, callback, options) {
5264

5365
if (!input) throw new TypeError('Expected input to be provided');
5466
if (!isAsyncIterable(asyncIterable)) throw new TypeError('Expected asyncIterable to have a Symbol.asyncIterator function');
55-
if (typeof callback !== 'function') throw new TypeError('Expected callback to be a function');
56-
if (typeof bufferSize !== 'number') throw new TypeError('Expected bufferSize to be a number');
67+
if (!isType(callback, 'function')) throw new TypeError('Expected callback to be a function');
68+
if (!isType(bufferSize, 'number')) throw new TypeError('Expected bufferSize to be a number');
5769
if (externalSignal !== undefined && !(externalSignal instanceof AbortSignal)) throw new TypeError('Expected signal to be an AbortSignal');
5870
if (errorsMode !== 'fail-eventually' && errorsMode !== 'fail-fast') throw new TypeError("Expected errors to be 'fail-eventually' or 'fail-fast'");
5971

@@ -146,8 +158,12 @@ export function bufferedAsyncMap (input, callback, options) {
146158
if (ordered) {
147159
currentSubIterator = subIterators[0];
148160
} else {
161+
const targets = mainReturnedDone
162+
? subIterators
163+
: yieldArrayWithItem(subIterators, asyncIterator);
164+
149165
const iterator = findLeastTargeted(
150-
mainReturnedDone ? subIterators : [...subIterators, asyncIterator],
166+
targets,
151167
bufferedPromises,
152168
promisesToSourceIteratorMap
153169
);
@@ -159,10 +175,10 @@ export function bufferedAsyncMap (input, callback, options) {
159175
const bufferPromise = currentSubIterator
160176
? Promise.resolve(currentSubIterator.next())
161177
.catch(err => ({
162-
err: err instanceof Error ? err : new Error('Unknown subiterator error'),
178+
err: normalizeError(err, 'Unknown subiterator error'),
163179
}))
164180
.then(async result => {
165-
if (typeof result !== 'object') {
181+
if (!isType(result, 'object')) {
166182
throw new TypeError('Expected an object value');
167183
}
168184
if ('err' in result || result.done) {
@@ -184,10 +200,10 @@ export function bufferedAsyncMap (input, callback, options) {
184200
})
185201
: Promise.resolve(asyncIterator.next())
186202
.catch(err => ({
187-
err: err instanceof Error ? err : new Error('Unknown iterator error'),
203+
err: normalizeError(err, 'Unknown iterator error'),
188204
}))
189205
.then(async result => {
190-
if (typeof result !== 'object') {
206+
if (!isType(result, 'object')) {
191207
throw new TypeError('Expected an object value');
192208
}
193209
if ('err' in result || result.done) {
@@ -221,7 +237,7 @@ export function bufferedAsyncMap (input, callback, options) {
221237
promiseValue = {
222238
bufferPromise,
223239
done: true,
224-
err: err instanceof Error ? err : new Error('Unknown callback error'),
240+
err: normalizeError(err, 'Unknown callback error'),
225241
value: undefined,
226242
};
227243
}
@@ -234,7 +250,7 @@ export function bufferedAsyncMap (input, callback, options) {
234250
if (ordered && currentSubIterator) {
235251
let i = 0;
236252

237-
while (promisesToSourceIteratorMap.get(/** @type {BufferPromise} */ (bufferedPromises[i])) === currentSubIterator) {
253+
while (i < bufferedPromises.length && promisesToSourceIteratorMap.get(/** @type {BufferPromise} */ (bufferedPromises[i])) === currentSubIterator) {
238254
i += 1;
239255
}
240256

@@ -319,7 +335,7 @@ export function bufferedAsyncMap (input, callback, options) {
319335
return { done: true, value: undefined };
320336
} else if (err || done) {
321337
if (err) {
322-
const normalisedErr = err instanceof Error ? err : new Error('Unknown error');
338+
const normalisedErr = normalizeError(err, 'Unknown error');
323339

324340
// In fail-fast mode the first captured error short-circuits iteration:
325341
// route it through the same abort machinery so the next .next() rejects

lib/misc.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,12 @@ export function arrayDeleteInPlace (list, value) {
2222
list.splice(index, 1);
2323
}
2424
}
25+
26+
/**
27+
* @param {unknown} err
28+
* @param {string} defaultMessage
29+
* @returns {Error}
30+
*/
31+
export function normalizeError (err, defaultMessage) {
32+
return err instanceof Error ? err : new Error(defaultMessage);
33+
}

lib/type-checks.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
1+
import { guardedArrayIncludes } from '@voxpelli/typed-utils';
2+
3+
/**
4+
* @param {unknown} value
5+
* @returns {value is object}
6+
*/
7+
const isValueObject = (value) => Boolean(value && typeof value === 'object');
8+
19
/**
210
* @param {unknown} value
311
* @returns {value is Iterable<unknown>}
412
*/
5-
export const isIterable = (value) => Boolean(value && typeof value === 'object' && Symbol.iterator in value);
13+
export const isIterable = (value) => isValueObject(value) && Symbol.iterator in value;
614

715
/**
816
* @param {unknown} value
917
* @returns {value is AsyncIterable<unknown>}
1018
*/
11-
export const isAsyncIterable = (value) => Boolean(value && typeof value === 'object' && Symbol.asyncIterator in value);
19+
export const isAsyncIterable = (value) => isValueObject(value) && Symbol.asyncIterator in value;
1220

1321
/**
1422
* @template Values
1523
* @param {unknown} value
1624
* @param {Values[]} list
1725
* @returns {value is Values}
1826
*/
19-
export const isPartOfArray = (value, list) => list.includes(/** @type {Values} */ (value));
27+
export const isPartOfArray = (value, list) => guardedArrayIncludes(list, value);

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,8 @@
7070
"type-coverage": "^2.29.1",
7171
"typescript": "~5.5.3",
7272
"validate-conventional-commit": "^1.0.4"
73+
},
74+
"dependencies": {
75+
"@voxpelli/typed-utils": "^2.6.0"
7376
}
7477
}

0 commit comments

Comments
 (0)