Skip to content

Commit 0de3c0a

Browse files
committed
Add tests for iterator close semantics of Array.fromAsync
The existing fromAsync tests cover that an abrupt mapper completion closes the iterator, but not the surrounding close semantics: - a normal completion (done: true) must not call return() - the close is awaited: the promise from return() settles before the promise returned by Array.fromAsync is rejected - when both the mapper and return() fail, the promise rejects with the mapper's error (AsyncIteratorClose returns the original completion) These distinguish implementations today: quickjs-ng closed exhausted iterators without awaiting, and V8 never settles the returned promise when return() throws or rejects during an abrupt close.
1 parent 5f1f06f commit 0de3c0a

4 files changed

Lines changed: 185 additions & 0 deletions
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright (C) 2026 Divy Srivastava. All rights reserved.
2+
// This code is governed by the BSD license found in the LICENSE file.
3+
4+
/*---
5+
esid: sec-array.fromasync
6+
description: >
7+
The iterator of an asynchronous iterable is not closed when the iterator
8+
reports it is done.
9+
info: |
10+
3.j.ii.4.a. If _next_ is *false*, then
11+
...
12+
iii. Return Completion Record { [[Type]]: ~return~, [[Value]]: _A_,
13+
[[Target]]: ~empty~ }.
14+
15+
A normal completion of the iteration never calls AsyncIteratorClose, so
16+
the iterator's return method is not invoked.
17+
flags: [async]
18+
includes: [asyncHelpers.js]
19+
features: [Array.fromAsync]
20+
---*/
21+
22+
let returnCalled = false;
23+
const iterator = {
24+
i: 0,
25+
next() {
26+
return Promise.resolve(
27+
this.i < 2 ? { value: this.i++, done: false } : { value: undefined, done: true }
28+
);
29+
},
30+
return() {
31+
returnCalled = true;
32+
return Promise.resolve({ done: true });
33+
},
34+
[Symbol.asyncIterator]() {
35+
return this;
36+
}
37+
};
38+
39+
asyncTest(async () => {
40+
const result = await Array.fromAsync(iterator);
41+
assert.compareArray(result, [0, 1]);
42+
assert.sameValue(
43+
returnCalled,
44+
false,
45+
"return() must not be called when the iterator is exhausted"
46+
);
47+
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (C) 2026 Divy Srivastava. All rights reserved.
2+
// This code is governed by the BSD license found in the LICENSE file.
3+
4+
/*---
5+
esid: sec-array.fromasync
6+
description: >
7+
The iterator of a synchronous iterable is not closed when the iterator
8+
reports it is done.
9+
info: |
10+
3.j.ii.4.a. If _next_ is *false*, then
11+
...
12+
iii. Return Completion Record { [[Type]]: ~return~, [[Value]]: _A_,
13+
[[Target]]: ~empty~ }.
14+
15+
A normal completion of the iteration never calls AsyncIteratorClose, so
16+
the iterator's return method is not invoked.
17+
flags: [async]
18+
includes: [asyncHelpers.js]
19+
features: [Array.fromAsync]
20+
---*/
21+
22+
let returnCalled = false;
23+
const iterator = {
24+
i: 0,
25+
next() {
26+
return this.i < 2 ? { value: this.i++, done: false } : { value: undefined, done: true };
27+
},
28+
return() {
29+
returnCalled = true;
30+
return { done: true };
31+
},
32+
[Symbol.iterator]() {
33+
return this;
34+
}
35+
};
36+
37+
asyncTest(async () => {
38+
const result = await Array.fromAsync(iterator);
39+
assert.compareArray(result, [0, 1]);
40+
assert.sameValue(
41+
returnCalled,
42+
false,
43+
"return() must not be called when the iterator is exhausted"
44+
);
45+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (C) 2026 Divy Srivastava. All rights reserved.
2+
// This code is governed by the BSD license found in the LICENSE file.
3+
4+
/*---
5+
esid: sec-array.fromasync
6+
description: >
7+
When the mapping function throws, the promise returned by the asynchronous
8+
iterator's return method is awaited before the promise returned by
9+
Array.fromAsync is rejected.
10+
info: |
11+
3.j.ii.6. If _mapping_ is *true*, then
12+
a. Let _mappedValue_ be Call(_mapfn_, _thisArg_, « _nextValue_, 𝔽(_k_) »).
13+
b. IfAbruptCloseAsyncIterator(_mappedValue_, _iteratorRecord_).
14+
15+
AsyncIteratorClose ( iteratorRecord, completion )
16+
5.d. If _innerResult_ is a normal completion, set _innerResult_ to
17+
Completion(Await(_innerResult_.[[Value]])).
18+
flags: [async]
19+
includes: [asyncHelpers.js]
20+
features: [Array.fromAsync]
21+
---*/
22+
23+
const order = [];
24+
const iterator = {
25+
next() {
26+
return Promise.resolve({ value: 1, done: false });
27+
},
28+
return() {
29+
order.push("return called");
30+
return new Promise((resolve) => {
31+
Promise.resolve().then(() => {
32+
order.push("return settled");
33+
resolve({ done: true });
34+
});
35+
});
36+
},
37+
[Symbol.asyncIterator]() {
38+
return this;
39+
}
40+
};
41+
42+
asyncTest(async () => {
43+
await assert.throwsAsync(
44+
Test262Error,
45+
() => Array.fromAsync(iterator, () => { throw new Test262Error("mapfn throws"); }),
46+
"mapfn throwing should cause fromAsync to reject"
47+
);
48+
order.push("fromAsync rejected");
49+
assert.compareArray(order, ["return called", "return settled", "fromAsync rejected"]);
50+
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (C) 2026 Divy Srivastava. All rights reserved.
2+
// This code is governed by the BSD license found in the LICENSE file.
3+
4+
/*---
5+
esid: sec-array.fromasync
6+
description: >
7+
When the mapping function throws and closing the asynchronous iterator
8+
also fails, the promise returned by Array.fromAsync is rejected with the
9+
mapping function's error.
10+
info: |
11+
3.j.ii.6. If _mapping_ is *true*, then
12+
a. Let _mappedValue_ be Call(_mapfn_, _thisArg_, « _nextValue_, 𝔽(_k_) »).
13+
b. IfAbruptCloseAsyncIterator(_mappedValue_, _iteratorRecord_).
14+
15+
AsyncIteratorClose ( iteratorRecord, completion )
16+
6. If _completion_ is a throw completion, return ? _completion_.
17+
flags: [async]
18+
includes: [asyncHelpers.js]
19+
features: [Array.fromAsync]
20+
---*/
21+
22+
let returnCalled = false;
23+
const iterator = {
24+
next() {
25+
return Promise.resolve({ value: 1, done: false });
26+
},
27+
return() {
28+
returnCalled = true;
29+
return Promise.reject(new Error("return rejects"));
30+
},
31+
[Symbol.asyncIterator]() {
32+
return this;
33+
}
34+
};
35+
36+
asyncTest(async () => {
37+
await assert.throwsAsync(
38+
Test262Error,
39+
() => Array.fromAsync(iterator, () => { throw new Test262Error("mapfn throws"); }),
40+
"fromAsync must reject with the mapfn error, not the return() rejection"
41+
);
42+
assert(returnCalled, "return() should have been called");
43+
});

0 commit comments

Comments
 (0)