Skip to content

Commit 376d192

Browse files
committed
replace mapAsyncIterable with repeater-based implementation
1 parent e5408a8 commit 376d192

File tree

3 files changed

+85
-55
lines changed

3 files changed

+85
-55
lines changed

src/execution/__tests__/mapAsyncIterable-test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,8 @@ describe('mapAsyncIterable', () => {
276276
yield 3; // Shouldn't be reached.
277277
} finally {
278278
didVisitFinally = true;
279-
yield 1000;
280-
}
279+
yield 1000; /* c8 ignore start */
280+
} /* c8 ignore stop */
281281
}
282282

283283
const throwOver1 = mapAsyncIterable(source(), mapper);

src/execution/__tests__/subscribe-test.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -739,9 +739,8 @@ describe('Subscription Publish Phase', () => {
739739
},
740740
});
741741

742+
const returned = await subscription.return();
742743
payload = subscription.next();
743-
await subscription.return();
744-
745744
// A new email arrives!
746745
expect(
747746
pubsub.emit({
@@ -752,6 +751,10 @@ describe('Subscription Publish Phase', () => {
752751
}),
753752
).to.equal(false);
754753

754+
expect(returned).to.deep.equal({
755+
done: true,
756+
value: undefined,
757+
});
755758
expect(await payload).to.deep.equal({
756759
done: true,
757760
value: undefined,
@@ -793,17 +796,21 @@ describe('Subscription Publish Phase', () => {
793796
},
794797
});
795798

799+
const error = new Error('should not trigger when subscription is thrown');
800+
const caughtError = subscription.throw(error).catch((e) => e);
796801
payload = subscription.next();
797802

798-
// Throw error
799-
let caughtError;
800-
try {
801-
/* c8 ignore next 2 */
802-
await subscription.throw('ouch');
803-
} catch (e) {
804-
caughtError = e;
805-
}
806-
expect(caughtError).to.equal('ouch');
803+
// A new email arrives!
804+
expect(
805+
pubsub.emit({
806+
from: 'yuzhi@graphql.org',
807+
subject: 'Alright 2',
808+
message: 'Tests are good 2',
809+
unread: true,
810+
}),
811+
).to.equal(true);
812+
813+
expect(await caughtError).to.equal(error);
807814

808815
expect(await payload).to.deep.equal({
809816
done: true,

src/execution/mapAsyncIterable.ts

Lines changed: 65 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,80 @@
1+
/* eslint-disable no-await-in-loop */
2+
13
import type { PromiseOrValue } from '../jsutils/PromiseOrValue';
4+
import { Repeater, RepeaterClosureSignal } 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 iterator: AsyncIterator<T, R, void> =
16+
iterable[Symbol.asyncIterator]();
1217

13-
async function mapResult(
14-
result: IteratorResult<T, R>,
15-
): Promise<IteratorResult<U, R>> {
16-
if (result.done) {
17-
return result;
18-
}
18+
let next = iterator.next();
19+
// eslint-disable-next-line no-constant-condition
20+
while (true) {
21+
let iteration: IteratorResult<T, R>;
22+
try {
23+
iteration = await next;
24+
} catch (err) {
25+
await abruptClose(iterator);
26+
throw err;
27+
}
1928

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 */
29+
const { done, value } = iteration;
30+
31+
if (done) {
32+
stop();
33+
return value;
34+
}
35+
36+
let mapped: U;
37+
try {
38+
mapped = await fn(value);
39+
} catch (err) {
40+
await abruptClose(iterator);
41+
throw err;
42+
}
43+
44+
try {
45+
await push(mapped);
46+
} catch (err) {
47+
if (err instanceof RepeaterClosureSignal) {
48+
if (typeof iterator.return !== 'function') {
49+
stop();
50+
return undefined as unknown as R; // void :(
51+
}
52+
53+
next = iterator.return(err.returnValue);
54+
continue;
3055
}
56+
57+
if (typeof iterator.throw !== 'function') {
58+
await abruptClose(iterator);
59+
throw err;
60+
}
61+
62+
next = iterator.throw(err);
63+
continue;
3164
}
32-
throw error;
33-
/* c8 ignore stop */
65+
66+
next = iterator.next();
3467
}
35-
}
68+
});
69+
}
3670

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-
};
71+
async function abruptClose(iterator: AsyncIterator<unknown>): Promise<void> {
72+
if (typeof iterator.return === 'function') {
73+
try {
74+
await iterator.return(); /* c8 ignore start */
75+
} catch (_err) {
76+
// FIXME: add test case
77+
/* ignore error */
78+
} /* c8 ignore stop */
79+
}
5780
}

0 commit comments

Comments
 (0)