Skip to content

Commit a401bf6

Browse files
authored
refactor(mapAsyncIterable): use withCleanup to simplify (#4499)
The test changes are limited to uses of `mapAsyncIterable` that are not actually used in the codebase, i.e. `.return()` and `.throw().
1 parent 1ea23dd commit a401bf6

File tree

2 files changed

+44
-100
lines changed

2 files changed

+44
-100
lines changed

src/execution/__tests__/mapAsyncIterable-test.ts

Lines changed: 22 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -91,15 +91,10 @@ describe('mapAsyncIterable', () => {
9191

9292
it('allows returning early from mapped async generator', async () => {
9393
async function* source() {
94-
try {
95-
yield 1;
96-
/* c8 ignore next 3 */
97-
yield 2;
98-
yield 3; // Shouldn't be reached.
99-
} finally {
100-
// eslint-disable-next-line no-unsafe-finally
101-
return 'The End';
102-
}
94+
yield 1;
95+
/* c8 ignore next 3 */
96+
yield 2;
97+
yield 3; // Shouldn't be reached.
10398
}
10499

105100
const doubles = mapAsyncIterable(source(), (x) => x + x);
@@ -108,8 +103,8 @@ describe('mapAsyncIterable', () => {
108103
expect(await doubles.next()).to.deep.equal({ value: 4, done: false });
109104

110105
// Early return
111-
expect(await doubles.return('')).to.deep.equal({
112-
value: 'The End',
106+
expect(await doubles.return()).to.deep.equal({
107+
value: undefined,
113108
done: true,
114109
});
115110

@@ -147,23 +142,18 @@ describe('mapAsyncIterable', () => {
147142
expect(await doubles.next()).to.deep.equal({ value: 4, done: false });
148143

149144
// Early return
150-
expect(await doubles.return(0)).to.deep.equal({
145+
expect(await doubles.return()).to.deep.equal({
151146
value: undefined,
152147
done: true,
153148
});
154149
});
155150

156151
it('passes through early return from async values', async () => {
157152
async function* source() {
158-
try {
159-
yield 'a';
160-
/* c8 ignore next 3 */
161-
yield 'b';
162-
yield 'c'; // Shouldn't be reached.
163-
} finally {
164-
yield 'Done';
165-
yield 'Last';
166-
}
153+
yield 'a';
154+
/* c8 ignore next 3 */
155+
yield 'b';
156+
yield 'c'; // Shouldn't be reached.
167157
}
168158

169159
const doubles = mapAsyncIterable(source(), (x) => x + x);
@@ -173,14 +163,14 @@ describe('mapAsyncIterable', () => {
173163

174164
// Early return
175165
expect(await doubles.return()).to.deep.equal({
176-
value: 'DoneDone',
177-
done: false,
166+
value: undefined,
167+
done: true,
178168
});
179169

180-
// Subsequent next calls may yield from finally block
170+
// Subsequent next calls
181171
expect(await doubles.next()).to.deep.equal({
182-
value: 'LastLast',
183-
done: false,
172+
value: undefined,
173+
done: true,
184174
});
185175
expect(await doubles.next()).to.deep.equal({
186176
value: undefined,
@@ -260,14 +250,10 @@ describe('mapAsyncIterable', () => {
260250

261251
it('passes through caught errors through async generators', async () => {
262252
async function* source() {
263-
try {
264-
yield 1;
265-
/* c8 ignore next 2 */
266-
yield 2;
267-
yield 3; // Shouldn't be reached.
268-
} catch (e) {
269-
yield e;
270-
}
253+
yield 1;
254+
/* c8 ignore next 2 */
255+
yield 2;
256+
yield 3; // Shouldn't be reached.
271257
}
272258

273259
const doubles = mapAsyncIterable(source(), (x) => x + x);
@@ -276,11 +262,9 @@ describe('mapAsyncIterable', () => {
276262
expect(await doubles.next()).to.deep.equal({ value: 4, done: false });
277263

278264
// Throw error
279-
expect(await doubles.throw('Ouch')).to.deep.equal({
280-
value: 'OuchOuch',
281-
done: false,
282-
});
265+
await expectPromise(doubles.throw(new Error('Ouch'))).toRejectWith('Ouch');
283266

267+
// Subsequent next calls
284268
expect(await doubles.next()).to.deep.equal({
285269
value: undefined,
286270
done: true,

src/execution/mapAsyncIterable.ts

Lines changed: 22 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,18 @@
1+
import { isPromise } from '../jsutils/isPromise.js';
12
import type { PromiseOrValue } from '../jsutils/PromiseOrValue.js';
23

4+
import { withCleanup } from './withCleanup.js';
5+
36
/**
47
* Given an AsyncIterable and a callback function, return an AsyncIterator
58
* which produces values mapped via calling the callback function.
69
*/
7-
export function mapAsyncIterable<T, U, R = undefined>(
8-
iterable: AsyncGenerator<T, R, void> | AsyncIterable<T>,
10+
export function mapAsyncIterable<T, U>(
11+
iterable: AsyncGenerator<T> | AsyncIterable<T>,
912
callback: (value: T) => PromiseOrValue<U>,
10-
): AsyncGenerator<U, R, void> {
11-
const iterator = iterable[Symbol.asyncIterator]();
12-
13-
async function mapResult(
14-
promise: Promise<IteratorResult<T, R>>,
15-
): Promise<IteratorResult<U, R>> {
16-
const result = await promise;
17-
if (result.done) {
18-
return result;
19-
}
20-
21-
const value = result.value;
22-
try {
23-
return { value: await callback(value), done: false };
24-
} catch (error) {
25-
await returnIgnoringErrors();
26-
throw error;
27-
}
28-
}
29-
30-
async function returnIgnoringErrors(): Promise<void> {
13+
): AsyncGenerator<U, void, void> {
14+
return withCleanup(mapAsyncIterableImpl(iterable, callback), async () => {
15+
const iterator = iterable[Symbol.asyncIterator]();
3116
if (typeof iterator.return === 'function') {
3217
try {
3318
await iterator.return(); /* c8 ignore start */
@@ -36,44 +21,19 @@ export function mapAsyncIterable<T, U, R = undefined>(
3621
/* ignore error */
3722
} /* c8 ignore stop */
3823
}
39-
}
40-
41-
const asyncDispose: typeof Symbol.asyncDispose =
42-
Symbol.asyncDispose /* c8 ignore start */ ??
43-
Symbol.for('Symbol.asyncDispose'); /* c8 ignore stop */
44-
45-
return {
46-
async next() {
47-
return mapResult(iterator.next());
48-
},
49-
async return(): Promise<IteratorResult<U, R>> {
50-
// If iterator.return() does not exist, then type R must be undefined.
51-
return typeof iterator.return === 'function'
52-
? mapResult(iterator.return())
53-
: { value: undefined as any, done: true };
54-
},
55-
async throw(error?: unknown) {
56-
if (typeof iterator.throw === 'function') {
57-
return mapResult(iterator.throw(error));
58-
}
59-
60-
if (typeof iterator.return === 'function') {
61-
await returnIgnoringErrors();
62-
}
24+
});
25+
}
6326

64-
throw error;
65-
},
66-
[Symbol.asyncIterator]() {
67-
return this;
68-
},
69-
async [asyncDispose]() {
70-
await this.return(undefined as R);
71-
if (
72-
typeof (iterable as AsyncGenerator<T, R, void>)[asyncDispose] ===
73-
'function'
74-
) {
75-
await (iterable as AsyncGenerator<T, R, void>)[asyncDispose]();
76-
}
77-
},
78-
};
27+
async function* mapAsyncIterableImpl<T, U, R = undefined>(
28+
iterable: AsyncGenerator<T, R, void> | AsyncIterable<T>,
29+
mapFn: (value: T) => PromiseOrValue<U>,
30+
): AsyncGenerator<U, void, void> {
31+
for await (const value of iterable) {
32+
const result = mapFn(value);
33+
if (isPromise(result)) {
34+
yield await result;
35+
continue;
36+
}
37+
yield result;
38+
}
7939
}

0 commit comments

Comments
 (0)