Skip to content

Commit c31a8df

Browse files
committed
fix: ensure catchError/skip functions always return() source iterator
1 parent b8890f1 commit c31a8df

File tree

14 files changed

+205
-80
lines changed

14 files changed

+205
-80
lines changed

docs/asynciterable/creating.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ let value, done;
4646

4747
## Brief Interlude - `AsyncSink`
4848

49-
Very rarely will we ever need to create these async-iterables by hand, however, if you need a collection that you can add to as well as iterate, we have the `AsyncSink` class. This class serves as a basis for some of our operators such as binding to events and DOM and Node.js streams.
49+
Very rarely will we ever need to create these async-iterables by hand, however, if you need a collection that you can add to as well as iterate, we have the `AsyncSink` class. This class serves as a basis for some of our operators such as binding to events and DOM and Node.js streams.
5050

5151
```typescript
5252
import { AsyncSink } from 'ix/asynciterable';

docs/asynciterable/transforming.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,5 @@ await subscription.pipe(
1111
.forEach(handleBatch)
1212
```
1313

14-
Using this operator makes sure that if messages slow down you'll still
15-
handle them in a reasonable time whereas using `buffer` would leave you stuck until you get
14+
Using this operator makes sure that if messages slow down you'll still handle them in a reasonable time whereas using `buffer` would leave you stuck until you get
1615
the right amount of messages.

spec/asynciterable-operators/catcherror-spec.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1+
import { jest } from '@jest/globals';
12
import '../asynciterablehelpers.js';
2-
import { of, range, sequenceEqual, single, throwError } from 'ix/asynciterable/index.js';
3+
import {
4+
first,
5+
from,
6+
of,
7+
range,
8+
sequenceEqual,
9+
single,
10+
throwError,
11+
} from 'ix/asynciterable/index.js';
312
import { catchError } from 'ix/asynciterable/operators/index.js';
413

514
test('AsyncIterable#catchError error catches', async () => {
@@ -26,3 +35,14 @@ test('AsyncIterable#catchError source and handler types are composed', async ()
2635
const res = xs.pipe(catchError(async (_: Error) => of('foo')));
2736
await expect(sequenceEqual(res, xs)).resolves.toBeTruthy();
2837
});
38+
39+
test('AsyncIterable#catchError calls return() on source iterator when stopped early', async () => {
40+
const xs = range(0, 10)[Symbol.asyncIterator]();
41+
const returnSpy = jest.spyOn(xs, 'return');
42+
43+
const res = from(xs).pipe(catchError((_: Error) => from([])));
44+
45+
await first(res);
46+
47+
expect(returnSpy).toHaveBeenCalled();
48+
});

spec/asynciterable-operators/skip-spec.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { jest } from '@jest/globals';
12
import { hasNext, noNext } from '../asynciterablehelpers.js';
2-
import { of, throwError } from 'ix/asynciterable/index.js';
3+
import { as, first, of, range, throwError } from 'ix/asynciterable/index.js';
34
import { skip } from 'ix/asynciterable/operators/index.js';
45

56
test('AsyncIterable#skip skips some', async () => {
@@ -40,3 +41,14 @@ test('AsyncIterable#skip throws', async () => {
4041
const it = ys[Symbol.asyncIterator]();
4142
await expect(it.next()).rejects.toThrow(err);
4243
});
44+
45+
test('Iterable#skip calls return() on source iterator when stopped early', async () => {
46+
const xs = range(0, 10)[Symbol.asyncIterator]();
47+
const returnSpy = jest.spyOn(xs, 'return');
48+
49+
const res = as(xs).pipe(skip(2));
50+
51+
await first(res);
52+
53+
expect(returnSpy).toHaveBeenCalled();
54+
});

spec/asynciterable/catcherror-spec.ts

+29-5
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
1+
import { jest } from '@jest/globals';
2+
import { skip } from 'ix/asynciterable/operators.js';
13
import { hasNext } from '../asynciterablehelpers.js';
2-
import { catchError, concat, range, sequenceEqual, throwError } from 'ix/asynciterable/index.js';
3-
4-
test('AsyncIterable#catch with no errors', async () => {
4+
import {
5+
catchError,
6+
concat,
7+
first,
8+
from,
9+
range,
10+
sequenceEqual,
11+
throwError,
12+
} from 'ix/asynciterable/index.js';
13+
14+
test('AsyncIterable#catchError with no errors', async () => {
515
const res = catchError(range(0, 5), range(5, 5));
616
expect(await sequenceEqual(res, range(0, 5))).toBeTruthy();
717
});
818

9-
test('AsyncIterable#catch with concat error', async () => {
19+
test('AsyncIterable#catchError with concat error', async () => {
1020
const res = catchError(concat(range(0, 5), throwError(new Error())), range(5, 5));
1121

1222
expect(await sequenceEqual(res, range(0, 10))).toBeTruthy();
1323
});
1424

15-
test('AsyncIterable#catch still throws', async () => {
25+
test('AsyncIterable#catchError still throws', async () => {
1626
const e1 = new Error();
1727
const er1 = throwError(e1);
1828

@@ -31,3 +41,17 @@ test('AsyncIterable#catch still throws', async () => {
3141
await hasNext(it, 3);
3242
await expect(it.next()).rejects.toThrow();
3343
});
44+
45+
test('AsyncIterable#catchError calls return() on source iterator when stopped early', async () => {
46+
const e1 = new Error();
47+
const er1 = throwError(e1);
48+
49+
const xs2 = range(2, 2)[Symbol.asyncIterator]();
50+
const returnSpy = jest.spyOn(xs2, 'return');
51+
52+
const res = catchError(concat(range(0, 2), er1), from(xs2)).pipe(skip(2));
53+
54+
await first(res);
55+
56+
expect(returnSpy).toHaveBeenCalled();
57+
});

spec/iterable-operators/catcherror-spec.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { jest } from '@jest/globals';
12
import '../iterablehelpers';
2-
import { of, range, sequenceEqual, single, throwError } from 'ix/iterable/index.js';
3+
import { first, from, of, range, sequenceEqual, single, throwError } from 'ix/iterable/index.js';
34
import { catchError } from 'ix/iterable/operators/index.js';
45

56
test('Iterable#catchError error catches', () => {
@@ -26,3 +27,14 @@ test('Iterable#catchError source and handler types are composed', () => {
2627
const res = xs.pipe(catchError((_: Error) => of('foo')));
2728
expect(sequenceEqual(res, xs)).toBeTruthy();
2829
});
30+
31+
test('Iterable#catchError calls return() on source iterator when stopped early', () => {
32+
const xs = range(0, 10)[Symbol.iterator]();
33+
const returnSpy = jest.spyOn(xs, 'return');
34+
35+
const res = from(xs).pipe(catchError((_: Error) => from([])));
36+
37+
first(res);
38+
39+
expect(returnSpy).toHaveBeenCalled();
40+
});

spec/iterable-operators/skip-spec.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { jest } from '@jest/globals';
12
import { hasNext, noNext } from '../iterablehelpers.js';
2-
import { as, throwError } from 'ix/iterable/index.js';
3+
import { as, first, range, throwError } from 'ix/iterable/index.js';
34
import { skip } from 'ix/iterable/operators/index.js';
45

56
test('Iterable#skip skips some', () => {
@@ -39,3 +40,14 @@ test('Iterable#skip throws', () => {
3940
const it = ys[Symbol.iterator]();
4041
expect(() => it.next()).toThrow();
4142
});
43+
44+
test('Iterable#skip calls return() on source iterator when stopped early', () => {
45+
const xs = range(0, 10)[Symbol.iterator]();
46+
const returnSpy = jest.spyOn(xs, 'return');
47+
48+
const res = as(xs).pipe(skip(2));
49+
50+
first(res);
51+
52+
expect(returnSpy).toHaveBeenCalled();
53+
});

spec/iterable/catcherror-spec.ts

+25-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1+
import { jest } from '@jest/globals';
2+
import { skip } from 'ix/iterable/operators.js';
13
import { hasNext } from '../iterablehelpers.js';
2-
import { catchError, concat, range, sequenceEqual, throwError } from 'ix/iterable/index.js';
4+
import {
5+
from,
6+
catchError,
7+
concat,
8+
range,
9+
sequenceEqual,
10+
throwError,
11+
first,
12+
} from 'ix/iterable/index.js';
313

414
test('Iterable.catchError with no errors', () => {
515
const res = catchError(range(0, 5), range(5, 5));
@@ -31,3 +41,17 @@ test('Iterable.catchError still throws', () => {
3141
hasNext(it, 3);
3242
expect(() => it.next()).toThrow();
3343
});
44+
45+
test('Iterable.catchError calls return() on source iterator when stopped early', () => {
46+
const e1 = new Error();
47+
const er1 = throwError(e1);
48+
49+
const xs2 = range(2, 2)[Symbol.iterator]();
50+
const returnSpy = jest.spyOn(xs2, 'return');
51+
52+
const res = catchError(concat(range(0, 2), er1), from(xs2)).pipe(skip(2));
53+
54+
first(res);
55+
56+
expect(returnSpy).toHaveBeenCalled();
57+
});

src/asynciterable/catcherror.ts

+16-14
Original file line numberDiff line numberDiff line change
@@ -24,24 +24,26 @@ export class CatchAllAsyncIterable<TSource> extends AsyncIterableX<TSource> {
2424
error = null;
2525
hasError = false;
2626

27-
while (1) {
28-
let c = <TSource>{};
27+
try {
28+
while (1) {
29+
let c = <TSource>{};
2930

30-
try {
31-
const { done, value } = await it.next();
32-
if (done) {
33-
await returnAsyncIterator(it);
31+
try {
32+
const { done, value } = await it.next();
33+
if (done) {
34+
break;
35+
}
36+
c = value;
37+
} catch (e) {
38+
error = e;
39+
hasError = true;
3440
break;
3541
}
36-
c = value;
37-
} catch (e) {
38-
error = e;
39-
hasError = true;
40-
await returnAsyncIterator(it);
41-
break;
42-
}
4342

44-
yield c;
43+
yield c;
44+
}
45+
} finally {
46+
await returnAsyncIterator(it);
4547
}
4648

4749
if (!hasError) {

src/asynciterable/operators/catcherror.ts

+16-13
Original file line numberDiff line numberDiff line change
@@ -30,23 +30,26 @@ export class CatchWithAsyncIterable<TSource, TResult> extends AsyncIterableX<TSo
3030
let hasError = false;
3131
const source = wrapWithAbort(this._source, signal);
3232
const it = source[Symbol.asyncIterator]();
33-
while (1) {
34-
let c = <IteratorResult<TSource>>{};
3533

36-
try {
37-
c = await it.next();
38-
if (c.done) {
39-
await returnAsyncIterator(it);
34+
try {
35+
while (1) {
36+
let c = <IteratorResult<TSource>>{};
37+
38+
try {
39+
c = await it.next();
40+
if (c.done) {
41+
break;
42+
}
43+
} catch (e) {
44+
err = await this._handler(e, signal);
45+
hasError = true;
4046
break;
4147
}
42-
} catch (e) {
43-
err = await this._handler(e, signal);
44-
hasError = true;
45-
await returnAsyncIterator(it);
46-
break;
47-
}
4848

49-
yield c.value;
49+
yield c.value;
50+
}
51+
} finally {
52+
await returnAsyncIterator(it);
5053
}
5154

5255
if (hasError) {

src/asynciterable/operators/skip.ts

+12-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { AsyncIterableX } from '../asynciterablex.js';
22
import { MonoTypeOperatorAsyncFunction } from '../../interfaces.js';
33
import { wrapWithAbort } from './withabort.js';
44
import { throwIfAborted } from '../../aborterror.js';
5+
import { returnAsyncIterator } from '../../util/returniterator.js';
56

67
/** @ignore */
78
export class SkipAsyncIterable<TSource> extends AsyncIterableX<TSource> {
@@ -20,13 +21,18 @@ export class SkipAsyncIterable<TSource> extends AsyncIterableX<TSource> {
2021
const it = source[Symbol.asyncIterator]();
2122
let count = this._count;
2223
let next;
23-
while (count > 0 && !(next = await it.next()).done) {
24-
count--;
25-
}
26-
if (count <= 0) {
27-
while (!(next = await it.next()).done) {
28-
yield next.value;
24+
25+
try {
26+
while (count > 0 && !(next = await it.next()).done) {
27+
count--;
28+
}
29+
if (count <= 0) {
30+
while (!(next = await it.next()).done) {
31+
yield next.value;
32+
}
2933
}
34+
} finally {
35+
returnAsyncIterator(it);
3036
}
3137
}
3238
}

src/iterable/catcherror.ts

+16-14
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,26 @@ export class CatchIterable<TSource> extends IterableX<TSource> {
2020
error = null;
2121
hasError = false;
2222

23-
while (1) {
24-
let c = <TSource>{};
23+
try {
24+
while (1) {
25+
let c = <TSource>{};
2526

26-
try {
27-
const { done, value } = it.next();
28-
if (done) {
29-
returnIterator(it);
27+
try {
28+
const { done, value } = it.next();
29+
if (done) {
30+
break;
31+
}
32+
c = value;
33+
} catch (e) {
34+
error = e;
35+
hasError = true;
3036
break;
3137
}
32-
c = value;
33-
} catch (e) {
34-
error = e;
35-
hasError = true;
36-
returnIterator(it);
37-
break;
38-
}
3938

40-
yield c;
39+
yield c;
40+
}
41+
} finally {
42+
returnIterator(it);
4143
}
4244

4345
if (!hasError) {

0 commit comments

Comments
 (0)