Skip to content

Commit 9c0a6ea

Browse files
committed
replace mapAsyncIterator with repeater-based mapAsyncIterable
1 parent 60ee5c7 commit 9c0a6ea

File tree

2 files changed

+72
-49
lines changed

2 files changed

+72
-49
lines changed

src/execution/__tests__/mapAsyncIterable-test.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ describe('mapAsyncIterable', () => {
109109

110110
// Early return
111111
expect(await doubles.return('')).to.deep.equal({
112-
value: 'The End',
112+
value: '',
113113
done: true,
114114
});
115115

@@ -148,20 +148,24 @@ describe('mapAsyncIterable', () => {
148148

149149
// Early return
150150
expect(await doubles.return(0)).to.deep.equal({
151-
value: undefined,
151+
value: 0,
152152
done: true,
153153
});
154154
});
155155

156156
it('passes through early return from async values', async () => {
157+
let didVisitFinally = false;
158+
157159
async function* source() {
158160
try {
159161
yield 'a';
160162
/* c8 ignore next 3 */
161163
yield 'b';
162164
yield 'c'; // Shouldn't be reached.
163165
} finally {
166+
didVisitFinally = true;
164167
yield 'Done';
168+
/* c8 ignore next 2 */
165169
yield 'Last';
166170
}
167171
}
@@ -173,19 +177,21 @@ describe('mapAsyncIterable', () => {
173177

174178
// Early return
175179
expect(await doubles.return()).to.deep.equal({
176-
value: 'DoneDone',
177-
done: false,
180+
value: undefined,
181+
done: true,
178182
});
179183

180-
// Subsequent next calls may yield from finally block
184+
// Subsequent next calls
181185
expect(await doubles.next()).to.deep.equal({
182-
value: 'LastLast',
183-
done: false,
186+
value: undefined,
187+
done: true,
184188
});
185189
expect(await doubles.next()).to.deep.equal({
186190
value: undefined,
187191
done: true,
188192
});
193+
194+
expect(didVisitFinally).to.equal(true);
189195
});
190196

191197
it('allows throwing errors through async iterable', async () => {
@@ -277,6 +283,7 @@ describe('mapAsyncIterable', () => {
277283
} finally {
278284
didVisitFinally = true;
279285
yield 1000;
286+
/* c8 ignore next 1 */
280287
}
281288
}
282289

src/execution/mapAsyncIterable.ts

Lines changed: 58 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,73 @@
1+
import { isPromise } from '../jsutils/isPromise';
12
import type { PromiseOrValue } from '../jsutils/PromiseOrValue';
3+
import type { Push, Stop } from '../jsutils/Repeater';
4+
import { Repeater } from '../jsutils/Repeater';
25

36
/**
4-
* Given an AsyncIterable and a callback function, return an AsyncIterator
7+
* Given an AsyncIterable and a callback function, return an AsyncGenerator
58
* which produces values mapped via calling the callback function.
69
*/
710
export function mapAsyncIterable<T, U, R = undefined>(
811
iterable: AsyncGenerator<T, R, void> | AsyncIterable<T>,
9-
callback: (value: T) => PromiseOrValue<U>,
12+
fn: (value: T) => PromiseOrValue<U>,
1013
): AsyncGenerator<U, R, void> {
11-
const iterator = iterable[Symbol.asyncIterator]();
14+
return new Repeater<U, R, void>(async (push, stop) => {
15+
const iter: AsyncIterator<T, R, void> = iterable[Symbol.asyncIterator]();
16+
let finalIteration: PromiseOrValue<IteratorResult<T, R>> | undefined;
17+
// The stop promise never rejects.
18+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
19+
stop.then(() => {
20+
if (typeof iter.return === 'function') {
21+
finalIteration = iter.return();
22+
}
23+
});
1224

13-
async function mapResult(
14-
result: IteratorResult<T, R>,
15-
): Promise<IteratorResult<U, R>> {
16-
if (result.done) {
17-
return result;
25+
// eslint-disable-next-line no-unmodified-loop-condition
26+
while (!finalIteration) {
27+
// eslint-disable-next-line no-await-in-loop
28+
if (await handleNext(iter, iter.next(), fn, push, stop)) {
29+
break;
30+
}
1831
}
1932

20-
try {
21-
return { value: await callback(result.value), done: false };
22-
} catch (error) {
23-
/* c8 ignore start */
24-
// FIXME: add test case
25-
if (typeof iterator.return === 'function') {
26-
try {
27-
await iterator.return();
28-
} catch (_e) {
29-
/* ignore error */
30-
}
31-
}
32-
throw error;
33-
/* c8 ignore stop */
33+
if (isPromise(finalIteration)) {
34+
await finalIteration;
3435
}
36+
37+
return undefined as unknown as R; // void :();
38+
});
39+
}
40+
41+
async function handleNext<T, U, R>(
42+
iter: AsyncIterator<T, R, void>,
43+
next: Promise<IteratorResult<T, R>>,
44+
fn: (value: T) => PromiseOrValue<U>,
45+
push: Push<U>,
46+
stop: Stop,
47+
): Promise<boolean> {
48+
const { done, value } = await next;
49+
50+
if (done) {
51+
stop();
52+
return true;
3553
}
3654

37-
return {
38-
async next() {
39-
return mapResult(await iterator.next());
40-
},
41-
async return(): Promise<IteratorResult<U, R>> {
42-
// If iterator.return() does not exist, then type R must be undefined.
43-
return typeof iterator.return === 'function'
44-
? mapResult(await iterator.return())
45-
: { value: undefined as any, done: true };
46-
},
47-
async throw(error?: unknown) {
48-
if (typeof iterator.throw === 'function') {
49-
return mapResult(await iterator.throw(error));
50-
}
51-
throw error;
52-
},
53-
[Symbol.asyncIterator]() {
54-
return this;
55-
},
56-
};
55+
let mapped: U;
56+
try {
57+
mapped = await fn(value);
58+
} catch (err) {
59+
stop(err);
60+
return true;
61+
}
62+
63+
try {
64+
await push(mapped);
65+
} catch (err) {
66+
if (typeof iter.throw === 'function') {
67+
return handleNext(iter, iter.throw(err), fn, push, stop);
68+
}
69+
throw err;
70+
}
71+
72+
return false;
5773
}

0 commit comments

Comments
 (0)