├── .gitignore ├── .editorconfig ├── .github ├── workflows │ ├── lock.yml │ └── publish.yml └── pull_request_template.md ├── CODE_OF_CONDUCT.md ├── package.json ├── LICENSE.md ├── HISTORY.md ├── CONTRIBUTING.md ├── spec.html └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | npm-debug.log 5 | deploy_key 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | charset = utf-8 8 | indent_style = space 9 | indent_size = 2 10 | -------------------------------------------------------------------------------- /.github/workflows/lock.yml: -------------------------------------------------------------------------------- 1 | name: 'Lock Threads' 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | 7 | workflow_dispatch: 8 | 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | 13 | concurrency: 14 | group: lock 15 | 16 | jobs: 17 | action: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: dessant/lock-threads@v2 21 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build-and-deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 🛎️ 14 | uses: actions/checkout@v2 15 | with: 16 | persist-credentials: false 17 | 18 | - name: Setup Node 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: '18' 22 | 23 | - name: Install and Build 🔧 24 | run: | 25 | npm install 26 | npm run build 27 | 28 | - name: Deploy 🚀 29 | uses: peaceiris/actions-gh-pages@v3 30 | with: 31 | github_token: ${{ secrets.GITHUB_TOKEN }} 32 | publish_dir: ./dist 33 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of conduct 2 | This repository is a TC39 project, and it therefore subscribes to its [code of 3 | conduct][CoC]. It is available at . 4 | 5 | We all should strive here to be respectful, friendly and patient, inclusive, 6 | considerate, and careful in the words we choose. When we disagree, we should try 7 | to understand why. 8 | 9 | To ask a question or report an issue, please follow the [CoC]’s directions, 10 | e.g., emailing [tc39-conduct-reports@googlegroups.com][]. 11 | 12 | More information about contributing is also available in [CONTRIBUTING.md][]. 13 | 14 | [CoC]: https://tc39.es/code-of-conduct/ 15 | [tc39-conduct-reports@googlegroups.com]: mailto:tc39-conduct-reports@googlegroups.com 16 | [CONTRIBUTING.md]: https://github.com/tc39/proposal-pipeline-operator/blob/main/CONTRIBUTING.md 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "proposal-array-from-async", 3 | "private": true, 4 | "description": "A TC39 proposal and specification for an Array.fromAsync method.", 5 | "author": "J. S. Choi (https://jschoi.org/)", 6 | "license": "BSD-3-Clause", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/tc39/proposal-array-async-from.git" 10 | }, 11 | "keywords": [ 12 | "proposal", 13 | "tc39", 14 | "array", 15 | "async", 16 | "iterator", 17 | "iterator", 18 | "stream", 19 | "Array.fromAsync", 20 | "Array.asyncFrom" 21 | ], 22 | "scripts": { 23 | "prebuild": "mkdir -p dist", 24 | "build": "ecmarkup --load-biblio @tc39/ecma262-biblio --verbose spec.html dist/index.html --assets-dir dist/", 25 | "watch": "npm run build -- --watch" 26 | }, 27 | "devDependencies": { 28 | "@tc39/ecma262-biblio": "2.1.2869", 29 | "ecmarkup": "^21.2.0" 30 | }, 31 | "homepage": "https://github.com/tc39/proposal-array-async-from" 32 | } 33 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Thank you for taking the time to contribute to this proposal! 2 | We’re so happy you’re helping out. 3 | 4 | 1. Please take a look at the [contributing guidelines][] 5 | and the resources to which it links. 6 | 2. Please include the purpose of the pull request. For example: 7 | * “This adds…” 8 | * “This simplifies…” 9 | * “This fixes…” 10 | 3. Please be explicit about what feedback, if any, you want: 11 | a quick pair of eyes, discussion or critique of its approach, 12 | a review of its copywriting, and so on. 13 | 4. Please mark the pull request as a Draft if it is still unfinished. 14 | 15 | All text in this repository is under the 16 | [same BSD license as Ecma-262][LICENSE.md]. 17 | As is the norm in open source, by contributing to this GitHub repository, 18 | you are licensing your contribution under the same license, 19 | as per the 20 | [GitHub terms of service][ToS]. 21 | 22 | [contributing guidelines]: https://github.com/tc39/proposal-pipeline-operator/blob/main/CONTRIBUTING.md 23 | [LICENSE.md]: https://github.com/tc39/proposal-pipeline-operator/blob/main/LICENSE.md 24 | [ToS]: https://help.github.com/en/github/site-policy/github-terms-of-service 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2021 J. S. Choi 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | **This software is provided by the copyright holders and contributors 19 | “as is” and any express or implied warranties, including, but not 20 | limited to, the implied warranties of merchantability and fitness for a 21 | particular purpose are disclaimed. In no event shall the copyright 22 | holder or contributors be liable for any direct, indirect, incidental, 23 | special, exemplary, or consequential damages (including, but not limited 24 | to, procurement of substitute goods or services; loss of use, data, or 25 | profits; or business interruption) however caused and on any theory of 26 | liability, whether in contract, strict liability, or tort (including 27 | negligence or otherwise) arising in any way out of the use of this 28 | software, even if advised of the possibility of such damage.** 29 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # 2021-08 2 | Presented to [plenary meeting for Stage 2 on 2021-08-31][2021-08-31]. Was accepted. 3 | 4 | # 2021-10 5 | Presented to [plenary meeting as an update on 2021-10-26][2021-10-26]. 6 | 7 | # 2021-12 8 | Presented to [plenary meeting for Stage 2 on 2021-12-14][2021-12-14]. 9 | Was rejected due to need to clarify `await`ing semantics 10 | with and without mapping-function arguments. 11 | 12 | # 2022-07 13 | Discussion occurred about `await`ing semantics and mapping functions in [issue 14 | #19][]. It was eventually decided to match `for await` and the proposed 15 | [AsyncIterator.prototype.toArray][iterator-helpers] by `await`ing values from 16 | input sync iterators once, `await`ing values from input async iterators not at 17 | all, and `await`ing results returned by mapping functions once. 18 | 19 | # 2022-09 20 | The [plenary advances this proposal to Stage 3][2022-09], conditional on editor review. 21 | 22 | # 2023-05 23 | The [plenary discusses double construction of the `this` value][2023-05] ([pull request #41][]) and resolves to merge the pull request that fixes it. 24 | 25 | [2021-08-31]: https://github.com/tc39/notes/blob/HEAD/meetings/2021-08/aug-31.md 26 | [2021-10-26]: https://github.com/tc39/notes/blob/HEAD/meetings/2021-10/oct-26.md#arrayfromasync-update 27 | [2021-12-14]: https://github.com/tc39/notes/blob/HEAD/meetings/2021-12/dec-14.md#arrayfromasync-for-stage-2 28 | [issue #19]: https://github.com/tc39/proposal-array-from-async/issues/19 29 | [iterator-helpers]: https://github.com/tc39/proposal-iterator-helpers 30 | [iterator-helpers#168]: https://github.com/tc39/proposal-iterator-helpers/issues/168 31 | [2022-09]: https://github.com/tc39/notes/blob/HEAD/meetings/2022-09/sep-14.md#conclusionresolution-1 32 | [2023-05]: https://github.com/tc39/notes/blob/main/meetings/2023-05/may-15.md#arrayfromasync-41-avoid-double-construction-of-this-value 33 | [pull request #41]: https://github.com/tc39/proposal-array-from-async/pull/41 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this proposal 2 | First off, thank you for taking the time to contribute! 🎉 3 | 4 | Here are some suggestions to contributing to this proposal. 5 | 6 | 1. The general [TC39 Process][], which summarizes 7 | how TC39’s “consensus” and “Stages” work. 8 | 2. The guide on [contributing to TC39 proposals][contributing guide]. 9 | 3. The [TC39 Code of Conduct][CoC]: 10 | It has important information about how we’re all expected to act 11 | and what to do when we feel like someone’s conduct does not meet the Code. 12 | We all want to maintain a friendly, productive working environment! 13 | 4. The [TC39 How to Give Feedback][feedback] article. 14 | 5. The [proposal explainer][] to make sure that it is 15 | not already addressed there. 16 | 6. The [TC39 Matrix guide][] (if you want to chat with TC39 members on Matrix, 17 | which is a real-time chat platform). 18 | 7. If the explainer does not already explain your topic adequately, 19 | then please [search the GitHub repository’s issues][issues] 20 | to see if any issues match the topic you had in mind. 21 | This proposal is more than four years old, 22 | and it is likely that the topic has already been raised and thoroughly discussed. 23 | 24 | You can leave a comment on an [existing GitHub issue][issues], 25 | create a new issue (but do try to [find an existing GitHub issue][issues] first), 26 | or [participate on Matrix][TC39 Matrix guide]. 27 | 28 | Please try to keep any existing GitHub issues on their original topic. 29 | 30 | If you feel that someone’s conduct is not meeting the [TC39 Code of Conduct][CoC], 31 | whether in this GitHub repository or in a [TC39 Matrix room][TC39 Matrix guide], 32 | then please follow the [Code of Conduct][CoC]’s directions for reporting the violation, 33 | including emailing [tc39-conduct-reports@googlegroups.com][]. 34 | 35 | Thank you again for taking the time to contribute! 36 | 37 | [CoC]: https://tc39.es/code-of-conduct/ 38 | [TC39 process]: https://tc39.es/process-document/ 39 | [contributing guide]: https://github.com/tc39/ecma262/blob/master/CONTRIBUTING.md#new-feature-proposals 40 | [feedback]: https://github.com/tc39/how-we-work/blob/master/feedback.md 41 | [proposal explainer]: https://github.com/tc39/proposal-array-from-async/blob/main/README.md 42 | [TC39 Matrix guide]: https://github.com/tc39/how-we-work/blob/master/matrix-guide.md 43 | [issues]: https://github.com/tc39/proposal-array-from-async/issues?q=is%3Aissue+ 44 | [tc39-conduct-reports@googlegroups.com]: mailto:tc39-conduct-reports@googlegroups.com 45 | -------------------------------------------------------------------------------- /spec.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 |

Introduction

12 |

This is the formal specification for a proposed `Array.fromAsync` factory method 13 | in JavaScript. It modifies the original ECMAScript specification with 15 | several new or revised clauses. See the proposal's 17 | explainer for the proposal's background, motivation, and usage examples.

18 |

This proposal depends on the pull request tc39/ecma262#2942: “support built-in async functions”.

19 |
20 | 21 | 22 |

Abstract Operations

23 | 24 | 25 |

Operations on Iterator Objects

26 | 27 | 28 |

IfAbruptCloseAsyncIterator ( _value_, _iteratorRecord_ )

29 |

IfAbruptCloseAsyncIterator is a shorthand for a sequence of algorithm steps that use an Iterator Record. An algorithm step of the form:

30 | 31 | 1. IfAbruptCloseAsyncIterator(_value_, _iteratorRecord_). 32 | 33 |

means the same thing as:

34 | 35 | 1. Assert: _value_ is a Completion Record. 36 | 1. If _value_ is an abrupt completion, return ? AsyncIteratorClose(_iteratorRecord_, _value_). 37 | 1. Else, set _value_ to ! _value_. 38 | 39 |
40 |
41 |
42 | 43 | 44 |

Indexed Collections

45 | 46 | 47 |

Array Objects

48 | 49 | 50 |

Properties of the Array Constructor

51 | 52 | 53 |

Array.fromAsync ( _asyncItems_ [ , _mapper_ [ , _thisArg_ ] ] )

54 | 55 | 56 |

This section is a wholly new subsection of the original 58 | Properties of the Array Constructor clause, to be inserted before the Array.from 60 | clause.

61 |
62 | 63 |

This async method performs the following steps when called:

64 | 65 | 1. Let _C_ be the *this* value. 66 | 1. If _mapper_ is *undefined*, then 67 | 1. Let _mapping_ be *false*. 68 | 1. Else, 69 | 1. If IsCallable(_mapper_) is *false*, throw a *TypeError* exception. 70 | 1. Let _mapping_ be *true*. 71 | 1. Let _usingAsyncIterator_ be ? GetMethod(_asyncItems_, %Symbol.asyncIterator%). 72 | 1. If _usingAsyncIterator_ is *undefined*, then 73 | 1. Let _usingSyncIterator_ be ? GetMethod(_asyncItems_, %Symbol.iterator%). 74 | 1. Let _iteratorRecord_ be *undefined*. 75 | 1. If _usingAsyncIterator_ is not *undefined*, then 76 | 1. Set _iteratorRecord_ to ? GetIteratorFromMethod(_asyncItems_, _usingAsyncIterator_). 77 | 1. Else if _usingSyncIterator_ is not *undefined*, then 78 | 1. Set _iteratorRecord_ to CreateAsyncFromSyncIterator(? GetIteratorFromMethod(_asyncItems_, _usingSyncIterator_)). 79 | 1. If _iteratorRecord_ is not *undefined*, then 80 | 1. If IsConstructor(_C_) is *true*, then 81 | 1. Let _A_ be ? Construct(_C_). 82 | 1. Else, 83 | 1. Let _A_ be ! ArrayCreate(0). 84 | 1. Let _k_ be 0. 85 | 1. Repeat, 86 | 1. If _k_ ≥ 253 - 1, then 87 | 1. Let _error_ be ThrowCompletion(a newly created *TypeError* object). 88 | 1. Return ? AsyncIteratorClose(_iteratorRecord_, _error_). 89 | 1. Let _Pk_ be ! ToString(𝔽(_k_)). 90 | 1. Let _nextResult_ be ? Call(_iteratorRecord_.[[NextMethod]], _iteratorRecord_.[[Iterator]]). 91 | 1. Set _nextResult_ to ? Await(_nextResult_). 92 | 1. If _nextResult_ is not an Object, throw a *TypeError* exception. 93 | 1. Let _done_ be ? IteratorComplete(_nextResult_). 94 | 1. If _done_ is *true*, then 95 | 1. Perform ? Set(_A_, *"length"*, 𝔽(_k_), *true*). 96 | 1. Return _A_. 97 | 1. Let _nextValue_ be ? IteratorValue(_nextResult_). 98 | 1. If _mapping_ is *true*, then 99 | 1. Let _mappedValue_ be Completion(Call(_mapper_, _thisArg_, « _nextValue_, 𝔽(_k_) »)). 100 | 1. IfAbruptCloseAsyncIterator(_mappedValue_, _iteratorRecord_). 101 | 1. Set _mappedValue_ to Completion(Await(_mappedValue_)). 102 | 1. IfAbruptCloseAsyncIterator(_mappedValue_, _iteratorRecord_). 103 | 1. Else, 104 | 1. Let _mappedValue_ be _nextValue_. 105 | 1. Let _defineStatus_ be Completion(CreateDataPropertyOrThrow(_A_, _Pk_, _mappedValue_)). 106 | 1. IfAbruptCloseAsyncIterator(_defineStatus_, _iteratorRecord_). 107 | 1. Set _k_ to _k_ + 1. 108 | 1. Else, 109 | 1. NOTE: _asyncItems_ is neither an AsyncIterable nor an Iterable so assume it is an array-like object. 110 | 1. Let _arrayLike_ be ! ToObject(_asyncItems_). 111 | 1. Let _len_ be ? LengthOfArrayLike(_arrayLike_). 112 | 1. If IsConstructor(_C_) is *true*, then 113 | 1. Let _A_ be ? Construct(_C_, « 𝔽(_len_) »). 114 | 1. Else, 115 | 1. Let _A_ be ? ArrayCreate(_len_). 116 | 1. Let _k_ be 0. 117 | 1. Repeat, while _k_ < _len_, 118 | 1. Let _Pk_ be ! ToString(𝔽(_k_)). 119 | 1. Let _kValue_ be ? Get(_arrayLike_, _Pk_). 120 | 1. Set _kValue_ to ? Await(_kValue_). 121 | 1. If _mapping_ is *true*, then 122 | 1. Let _mappedValue_ be ? Call(_mapper_, _thisArg_, « _kValue_, 𝔽(_k_) »). 123 | 1. Set _mappedValue_ to ? Await(_mappedValue_). 124 | 1. Else, 125 | 1. Let _mappedValue_ be _kValue_. 126 | 1. Perform ? CreateDataPropertyOrThrow(_A_, _Pk_, _mappedValue_). 127 | 1. Set _k_ to _k_ + 1. 128 | 1. Perform ? Set(_A_, *"length"*, 𝔽(_len_), *true*). 129 | 1. Return _A_. 130 | 131 | 132 |

This method is an intentionally generic factory method; it does not require that its *this* value be the Array constructor. Therefore it can be transferred to or inherited by any other constructors that may be called with a single numeric argument.

133 |
134 |
135 |
136 |
137 |
138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Array.fromAsync for JavaScript 2 | ECMAScript Stage 4 (conditional on editor review) Proposal. J. S. Choi, 2021–2025. 3 | 4 | * **[Specification][]** available 5 | * **Experimental polyfills** (do **not** use in production code yet): 6 | * **[array-from-async][]** 7 | * **[core-js][]** 8 | 9 | [specification]: https://tc39.es/proposal-array-from-async/ 10 | [core-js]: https://github.com/zloirock/core-js#arrayfromasync 11 | [array-from-async]: https://www.npmjs.com/package/array-from-async 12 | [§ Errors]: #errors 13 | [§ Sync-iterable inputs]: #sync-iterable-inputs 14 | 15 | ## Why an Array.fromAsync method 16 | Since its standardization in JavaScript, **[Array.from][]** has become one of 17 | `Array`’s most frequently used built-in methods. However, no similar 18 | functionality exists for async iterators. 19 | 20 | ```js 21 | const arr = []; 22 | for (const v of iterable) { 23 | arr.push(v); 24 | } 25 | 26 | // This does the same thing. 27 | const arr = Array.from(iterable); 28 | ``` 29 | 30 | [Array.from]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from 31 | 32 | Such functionality would also be useful for **dumping** the entirety of an 33 | **async iterator** into a **single data structure**, especially in **unit 34 | tests** or in **command-line** interfaces. (Several real-world examples are 35 | included in a following section.) 36 | 37 | ```js 38 | const arr = []; 39 | for await (const v of asyncIterable) { 40 | arr.push(v); 41 | } 42 | 43 | // We should add something that does the same thing. 44 | const arr = await ??????????(asyncIterable); 45 | ``` 46 | 47 | There is an [it-all][] NPM library that performs only this task 48 | and which gets about 50,000 weekly downloads. 49 | This of course does **not** include any code 50 | that uses ad-hoc `for await`–`of` loops with empty arrays. 51 | Further demonstrating the demand for such functionality, 52 | several [Stack Overflow questions][Stack Overflow] have been asked 53 | by various developers, asking how to convert async iterators to arrays. 54 | 55 | There are several [real-world examples](#real-world-examples) listed 56 | later in this explainer. 57 | 58 | [it-all]: https://www.npmjs.com/package/it-all 59 | [Stack Overflow]: https://stackoverflow.com/questions/58668361/how-can-i-convert-an-async-iterator-to-an-array 60 | 61 | ## Description 62 | (A [formal draft specification][specification] is available.) 63 | 64 | **Array.fromAsync is to `for await`**\ 65 | as **Array.from is to `for`.** 66 | 67 | Similarly to [Array.from][], Array.fromAsync would be a static method of the 68 | `Array` built-in class, with one required argument and two optional arguments: 69 | `(items, mapfn, thisArg)`. 70 | 71 | ### Async-iterable inputs 72 | But, instead of converting a sync iterable to an array, Array.fromAsync can 73 | convert an async iterable to a **promise** that (if everything goes well) will 74 | resolve to a new array. Before the promise resolves, it will create an async 75 | iterator from the input, lazily iterate over it, and add each yielded value to 76 | the new array. (The promise is immediately returned after the Array.fromAsync 77 | function call, no matter what.) 78 | 79 | ```js 80 | async function * asyncGen (n) { 81 | for (let i = 0; i < n; i++) 82 | yield i * 2; 83 | } 84 | 85 | // `arr` will be `[0, 2, 4, 6]`. 86 | const arr = []; 87 | for await (const v of asyncGen(4)) { 88 | arr.push(v); 89 | } 90 | 91 | // This is equivalent. 92 | const arr = await Array.fromAsync(asyncGen(4)); 93 | ``` 94 | 95 | ### Sync-iterable inputs 96 | If the argument is a sync iterable (and not an async iterable), then the return 97 | value is still a promise that will resolve to an array. If the sync iterator 98 | yields promises, then each yielded promise is awaited before its value is added 99 | to the new array. (Values that are not promises are also awaited to 100 | [prevent Zalgo][Zalgo].) All of this matches the behavior of `for await`. 101 | 102 | [Zalgo]: https://blog.izs.me/2013/08/designing-apis-for-asynchrony/ 103 | 104 | ```js 105 | function * genPromises (n) { 106 | for (let i = 0; i < n; i++) 107 | yield Promise.resolve(i * 2); 108 | } 109 | 110 | // `arr` will be `[ 0, 2, 4, 6 ]`. 111 | const arr = []; 112 | for await (const v of genPromises(4)) { 113 | arr.push(v); 114 | } 115 | 116 | // This is equivalent. 117 | const arr = await Array.fromAsync(genPromises(4)); 118 | ``` 119 | 120 | Like `for await`, Array.fromAsync **lazily** iterates over a sync-but-not-async 121 | input. Whenever a developer needs to dump a synchronous input that yields 122 | promises into an array, the developer needs to choose carefully between 123 | Array.fromAsync and Promise.all, which have complementary control flows: 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 |
Parallel awaitingSequential awaiting
Lazy iterationImpossibleawait Array.fromAsync(input)
Eager iterationawait Promise.all(Array.from(input))Useless
146 | 147 | Also like `for await`, when given a sync-but-not-async iterable input, then 148 | Array.fromAsync will catch **only** the first rejection that its iteration 149 | reaches, and only if that rejection does **not** occur in a microtask before 150 | the iteration reaches and awaits for it. For more information, see 151 | [§ Errors][]. 152 | 153 | ```js 154 | // `arr` will be `[ 0, 2, 4, 6 ]`. 155 | // `genPromises(4)` is lazily iterated, 156 | // and its four yielded promises are awaited in sequence. 157 | const arr = await Array.fromAsync(genPromises(4)); 158 | 159 | // `arr` will also be `[ 0, 2, 4, 6 ]`. 160 | // However, `genPromises(4)` is eagerly iterated 161 | // (into an array of four promises), 162 | // and the four promises are awaited in parallel. 163 | const arr = await Promise.all(Array.from(genPromises(4))); 164 | ``` 165 | 166 | ### Non-iterable array-like inputs 167 | Array.fromAsync’s valid inputs are a superset of Array.from’s valid inputs. 168 | This includes non-iterable array-likes: objects that have a length property as 169 | well as indexed elements (similarly to Array.prototype.values). The return 170 | value is still a promise that will resolve to an array. If the array-like 171 | object’s elements are promises, then each accessed promise is awaited before 172 | its value is added to the new array. 173 | 174 | One [TC39 representative’s opinion][issue #7 comment]: “[Array-likes are] very 175 | much not obsolete, and it’s very nice that things aren’t forced to implement 176 | the iterator protocol to be transformable into an Array.” 177 | 178 | [issue #7 comment]: https://github.com/tc39/proposal-array-from-async/issues/7#issuecomment-920299880 179 | 180 | ```js 181 | const arrLike = { 182 | length: 4, 183 | 0: Promise.resolve(0), 184 | 1: Promise.resolve(2), 185 | 2: Promise.resolve(4), 186 | 3: Promise.resolve(6), 187 | } 188 | 189 | // `arr` will be `[ 0, 2, 4, 6 ]`. 190 | const arr = []; 191 | for await (const v of Array.from(arrLike)) { 192 | arr.push(v); 193 | } 194 | 195 | // This is equivalent. 196 | const arr = await Array.fromAsync(arrLike); 197 | ``` 198 | 199 | As it does with sync-but-not-async iterable inputs, Array.fromAsync lazily 200 | iterates over the values of array-like inputs, and it awaits each value. 201 | The developer must choose between using Array.fromAsync and Promise.all (see 202 | [§ Sync-iterable inputs](#sync-iterable-inputs) and [§ Errors][]). 203 | 204 | ### Generic factory method 205 | Array.fromAsync is a generic factory method. It does not require that its this 206 | receiver be the Array constructor. fromAsync can be transferred to or inherited 207 | by any other constructor. In that case, the final result will be the data 208 | structure created by that constructor (with no arguments), and with each value 209 | yielded by the input being assigned to the data structure’s numeric properties. 210 | (Symbol.species is not involved at all.) If the this receiver is not a 211 | constructor, then fromAsync creates an array as usual. This matches the 212 | behavior of Array.from. 213 | 214 | ```js 215 | async function * asyncGen (n) { 216 | for (let i = 0; i < n; i++) 217 | yield i * 2; 218 | } 219 | function Data (n) {} 220 | Data.from = Array.from; 221 | Data.fromAsync = Array.fromAsync; 222 | 223 | // d will be a `new Data(0)`, with its `0` property assigned to `0`, its `1` 224 | // property assigned to `2`, etc. 225 | const d = new Data(0); let i = 0; 226 | for await (const v of asyncGen(4)) { 227 | d[i++] = v; 228 | } 229 | 230 | // This is equivalent. 231 | const d = await Data.fromAsync(asyncGen(4)); 232 | ``` 233 | 234 | ### Optional parameters 235 | Array.fromAsync has two optional parameters: `mapfn` and `thisArg`. 236 | 237 | #### Mapping function 238 | `mapfn` is an optional mapping callback, which is called on each value yielded from the input, 239 | along with its index integer (starting from 0). 240 | Each result of the mapping callback is, in turn, awaited then added to the array. 241 | 242 | However, when `mapfn` is given and the input is a sync iterable (or non-iterable array-like), 243 | then each value from the input is awaited before being given to `mapfn`. 244 | (The values from the input are *not* awaited if the input is an async iterable.) 245 | This matches the behavior of `for await`. 246 | 247 | When `mapfn` is not given, each value yielded from asynchronous 248 | inputs is not awaited, and each value yielded from synchronous inputs is 249 | awaited only once, before the value is added to the result array. 250 | This also matches the behavior of `for await`. 251 | 252 | This means that: 253 | ```js 254 | Array.fromAsync(input) 255 | ``` 256 | …is not equivalent to: 257 | ```js 258 | Array.fromAsync(input, x => x) 259 | ``` 260 | …at least when `input` is an async iterable. 261 | 262 | This is because, whenever input is an async iterable that yields promise items, 263 | `Array.fromAsync(input)` will not resolve those promise items, 264 | but `Array.fromAsync(input, x => x)` will resolve them 265 | because the result of the `x => x` mapping function is awaited. 266 | 267 | For example: 268 | 269 | ```js 270 | function createAsyncIter () { 271 | let i = 0; 272 | return { 273 | [Symbol.asyncIterator]() { 274 | return { 275 | async next() { 276 | if (i > 2) return { done: true }; 277 | i++; 278 | return { value: Promise.resolve(i), done: false } 279 | } 280 | } 281 | } 282 | }; 283 | } 284 | 285 | // This prints `[Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)]`: 286 | console.log(await Array.fromAsync(createAsyncIter())); 287 | 288 | // This prints `[1, 2, 3]`: 289 | console.log(await Array.fromAsync(createAsyncIter(), x => x)); 290 | ``` 291 | 292 | See also [issue #19](https://github.com/tc39/proposal-array-from-async/issues/19). 293 | 294 | #### `this` argument 295 | `thisArg` is a `this`-binding receiver value for the mapping callback. By 296 | default, this is undefined. These optional parameters match the behavior of 297 | Array.from. Their exclusion would be surprising to developers who are already 298 | used to Array.from. 299 | 300 | ```js 301 | async function * asyncGen (n) { 302 | for (let i = 0; i < n; i++) 303 | yield i * 2; 304 | } 305 | 306 | // `arr` will be `[ 0, 4, 16, 36 ]`. 307 | const arr = []; 308 | for await (const v of asyncGen(4)) { 309 | arr.push(await (v ** 2)); 310 | } 311 | 312 | // This is equivalent. 313 | const arr = await Array.fromAsync(asyncGen(4), v => 314 | v ** 2); 315 | ``` 316 | 317 | ### Errors 318 | Like other promise-based APIs, Array.fromAsync will always immediately return a 319 | promise. Array.fromAsync will never synchronously throw an error and [summon 320 | Zalgo][Zalgo]. 321 | 322 | When Array.fromAsync’s input throws an error while creating its async or sync 323 | iterator, then Array.fromAsync’s returned promise will reject with that error. 324 | 325 | ```js 326 | const err = new Error; 327 | const badIterable = { [Symbol.iterator] () { throw err; } }; 328 | 329 | // This returns a promise that will reject with `err`. 330 | Array.fromAsync(badIterable); 331 | ``` 332 | 333 | When Array.fromAsync’s input is iterable but the input’s iterator throws while 334 | iterating, then Array.fromAsync’s returned promise will reject with that error. 335 | 336 | ```js 337 | const err = new Error; 338 | async function * genErrorAsync () { throw err; } 339 | 340 | // This returns a promise that will reject with `err`. 341 | Array.fromAsync(genErrorAsync()); 342 | ``` 343 | 344 | ```js 345 | const err = new Error; 346 | function * genError () { throw err; } 347 | 348 | // This returns a promise that will reject with `err`. 349 | Array.fromAsync(genError()); 350 | ``` 351 | 352 | When Array.fromAsync’s input is synchronous only (i.e., the input is not an 353 | async iterable), and when one of the input’s values is a promise that 354 | eventually rejects or has rejected, then iteration stops and Array.fromAsync’s 355 | returned promise will reject with the first such error. 356 | 357 | In this case, Array.fromAsync will catch and handle that first input rejection 358 | **only if** that rejection does **not** occur in a microtask before the 359 | iteration reaches and awaits for it. 360 | 361 | ```js 362 | const err = new Error; 363 | function * genRejection () { 364 | yield Promise.reject(err); 365 | } 366 | 367 | // This returns a promise that will reject with `err`. There is **no** 368 | // unhandled promise rejection, because the rejection occurs in the same 369 | // microtask. 370 | Array.fromAsync(genZeroThenRejection()); 371 | ``` 372 | 373 | Just like with `for await`, Array.fromAsync will **not** catch any rejections 374 | by the input’s promises whenever those rejections occur **before** the ticks in 375 | which Array.fromAsync’s iteration reaches those promises. 376 | 377 | This is because – like `for await` – Array.fromAsync **lazily** iterates over 378 | its input and **sequentially** awaits each yielded value. Whenever a developer 379 | needs to dump a synchronous input that yields promises into an array, the 380 | developer needs to choose carefully between Array.fromAsync and Promise.all, 381 | which have complementary control flows (see [§ Sync-iterable 382 | inputs](#sync-iterable-inputs)). 383 | 384 | For example, when a synchronous input contains two promises, the latter of 385 | which will reject before the former promise resolves, then Array.fromAsync will 386 | not catch that rejection, because it lazily reaches the rejecting promise only 387 | after it already has rejected. 388 | 389 | ```js 390 | const numOfMillisecondsPerSecond = 1000; 391 | const slowError = new Error; 392 | const fastError = new Error; 393 | 394 | function waitThenReject (value) { 395 | return new Promise((resolve, reject) => { 396 | setTimeout(() => reject(value), numOfMillisecondsPerSecond); 397 | }); 398 | } 399 | 400 | function * genRejections () { 401 | // Slow promise. 402 | yield waitAndReject(slowError); 403 | // Fast promise. 404 | yield Promise.reject(fastError); 405 | } 406 | 407 | // This returns a promise that will reject with `slowError`. There is **no** 408 | // unhandled promise rejection: the iteration is lazy and will stop early at the 409 | // slow promise, so the fast promise will never be created. 410 | Array.fromAsync(genSlowRejectThenFastReject()); 411 | 412 | // This returns a promise that will reject with `slowError`. There **is** an 413 | // unhandled promise rejection with `fastError`: the iteration eagerly creates 414 | // and dumps both promises into an array, but Array.fromAsync will 415 | // **sequentially** handle only the slow promise. 416 | Array.fromAsync([ ...genSlowRejectThenFastReject() ]); 417 | 418 | // This returns a promise that will reject with `fastError`. There is **no** 419 | // unhandled promise rejection: the iteration eagerly creates and dumps both 420 | // promises into an array, but Promise.all will handle both promises **in 421 | // parallel**. 422 | Promise.all([ ...genSlowRejectThenFastReject() ]); 423 | ``` 424 | 425 | When Array.fromAsync’s input has at least one value, and when Array.fromAsync’s 426 | mapping callback throws an error when given any of those values, then 427 | Array.fromAsync’s returned promise will reject with the first such error. 428 | 429 | ```js 430 | const err = new Error; 431 | function badCallback () { throw err; } 432 | 433 | // This returns a promise that will reject with `err`. 434 | Array.fromAsync([ 0 ], badCallback); 435 | ``` 436 | 437 | When Array.fromAsync’s input is null or undefined, or when Array.fromAsync’s 438 | mapping callback is neither undefined nor callable, then Array.fromAsync’s 439 | returned promise will reject with a TypeError. 440 | 441 | ```js 442 | // These return promises that will reject with TypeErrors. 443 | Array.fromAsync(null); 444 | Array.fromAsync([], 1); 445 | ``` 446 | 447 | ### Closing sync iterables? 448 | Array.fromAsync tries to match `for await`’s behavior as much as possible. 449 | 450 | Previously, `for await` did not close sync iterables when it 451 | yields a rejected promise. 452 | 453 |
454 | Old code example 455 | 456 | ```js 457 | function * createIter() { 458 | try { 459 | yield Promise.resolve(console.log("a")); 460 | yield Promise.reject("x"); 461 | } finally { 462 | console.log("finalized"); 463 | } 464 | } 465 | 466 | // Prints "a" and then prints "finalized". 467 | // There is an uncaught "x" rejection. 468 | for (const x of createIter()) { 469 | console.log(await x); 470 | } 471 | 472 | // Prints "a" and then prints "finalized". 473 | // There is an uncaught "x" rejection. 474 | Array.from(createIter()); 475 | 476 | // Prints "a" and does *not* print "finalized". 477 | // There is an uncaught "x" rejection. 478 | for await (const x of createIter()) { 479 | console.log(x); 480 | } 481 | 482 | // Prints "a" and does *not* print "finalized". 483 | // There is an uncaught "x" rejection. 484 | Array.fromAsync(createIter()); 485 | ``` 486 | 487 |
488 | 489 | TC39 has recently changed `for await`’s behavior here. 490 | In the latest version of the language, 491 | `for await` now will close sync iterators when async wrappers yield rejections (see [tc39/ecma262#2600][]). 492 | All of the JavaScript engines are already updating to this new behavior. 493 | 494 | [tc39/ecma262#2600]: https://github.com/tc39/ecma262/pull/2600 495 | 496 | `Array.fromAsync` matches this new behavior of `for await`. 497 | Both will close any given sync iterator 498 | when the sync iterator yields a rejected promise as its next value. 499 | 500 | ## Other proposals 501 | 502 | ### Relationship with iterator-helpers 503 | The [iterator-helpers][] and [async-iterator-helpers][] proposals define 504 | Iterator.toArray and AsyncIterator.toArray. The following pairs of lines are 505 | equivalent: 506 | 507 | [iterator-helpers]: https://github.com/tc39/proposal-iterator-helpers 508 | [async-iterator-helpers]: https://github.com/tc39/proposal-async-iterator-helpers 509 | 510 | ```js 511 | // Array.from 512 | 513 | Array.from(iterable) 514 | Iterator(iterable).toArray() 515 | 516 | Array.from(iterable, mapfn) 517 | Iterator(iterable).map(mapfn).toArray() 518 | 519 | // Array.fromAsync 520 | 521 | Array.fromAsync(asyncIterable) 522 | AsyncIterator(asyncIterable).toArray() 523 | 524 | Array.fromAsync(asyncIterable, mapfn) 525 | AsyncIterator(asyncIterable).map(mapfn).toArray() 526 | ``` 527 | 528 | Iterator.toArray overlaps with Array.from, and AsyncIterator.toArray overlaps 529 | with Array.fromAsync. This is okay: they all can coexist. 530 | 531 | A [co-champion of iterable-helpers agrees][tc39/proposal-iterator-helpers#156] 532 | that we should have both or that we should prefer Array.fromAsync: “I 533 | remembered why it’s better for a buildable structure to consume an iterable 534 | than for an iterable to consume a buildable protocol. Sometimes building 535 | something one element at a time is the same as building it [more than one] 536 | element at a time, but sometimes it could be slow to build that way or produce 537 | a structure with equivalent semantics but different performance properties.” 538 | 539 | [tc39/proposal-iterator-helpers#156]: https://github.com/tc39/proposal-iterator-helpers/issues/156. 540 | 541 | ### TypedArray.fromAsync, Set.fromAsync, Object.fromEntriesAsync, etc. 542 | The following built-ins also resemble Array.from: 543 | ```js 544 | TypedArray.from() 545 | new Set 546 | Object.fromEntries() 547 | new Map 548 | ``` 549 | We are deferring any async versions of these methods to future proposals. 550 | See [issue #8][] and [proposal-setmap-offrom][]. 551 | 552 | [issue #8]: https://github.com/tc39/proposal-array-from-async/issues/8 553 | [proposal-setmap-offrom]: https://github.com/tc39/proposal-setmap-offrom 554 | 555 | ### Async spread operator 556 | In the future, standardizing an async spread operator (like `[ 0, await ...v 557 | ]`) may be useful. This proposal leaves that idea to a **separate** proposal. 558 | 559 | ### Records and tuples 560 | The **[record/tuple] proposal** puts forward two new data types with APIs that 561 | respectively **resemble** those of **`Array` and `Object`**. The `Tuple` 562 | constructor, too, would probably need an `fromAsync` method. Whether the 563 | `Record` constructor gets a `fromEntriesAsync` method will depend on whether 564 | `Object.fromEntriesAsync` will also be added in a separate proposal. 565 | 566 | [record/tuple]: https://github.com/tc39/proposal-record-tuple 567 | 568 | ## Real-world examples 569 | Only minor formatting changes have been made to the status-quo examples. 570 | 571 | 572 | 573 | 574 | 578 | 579 | 629 | 659 |
Status quo 575 | With Array.fromAsync 576 | 577 |
580 | 581 | ```js 582 | const all = require('it-all'); 583 | 584 | // Add the default assets to the repo. 585 | const results = await all( 586 | addAll( 587 | globSource(initDocsPath, { 588 | recursive: true, 589 | }), 590 | { preload: false }, 591 | ), 592 | ); 593 | const dir = results 594 | .filter(file => 595 | file.path === 'init-docs') 596 | .pop() 597 | print('to get started, enter:\n'); 598 | print( 599 | `\tjsipfs cat` + 600 | `/ipfs/${dir.cid}/readme\n`, 601 | ); 602 | ``` 603 | From [ipfs-core/src/runtime/init-assets-nodejs.js][]. 604 | 605 | 606 | 607 | ```js 608 | // Add the default assets to the repo. 609 | const results = await Array.fromAsync( 610 | addAll( 611 | globSource(initDocsPath, { 612 | recursive: true, 613 | }), 614 | { preload: false }, 615 | ), 616 | ); 617 | const dir = results 618 | .filter(file => 619 | file.path === 'init-docs') 620 | .pop() 621 | print('to get started, enter:\n'); 622 | print( 623 | `\tjsipfs cat` + 624 | `/ipfs/${dir.cid}/readme\n`, 625 | ); 626 | ``` 627 | 628 |
630 | 631 | ```js 632 | const all = require('it-all'); 633 | 634 | const results = await all( 635 | node.contentRouting 636 | .findProviders('a cid'), 637 | ); 638 | expect(results) 639 | .to.be.an('array') 640 | .with.lengthOf(1) 641 | .that.deep.equals([result]); 642 | ``` 643 | From [js-libp2p/test/content-routing/content-routing.node.js][]. 644 | 645 | 646 | 647 | ```js 648 | const results = await Array.fromAsync( 649 | node.contentRouting 650 | .findProviders('a cid'), 651 | ); 652 | expect(results) 653 | .to.be.an('array') 654 | .with.lengthOf(1) 655 | .that.deep.equals([result]); 656 | ``` 657 | 658 |
660 | 661 | ```js 662 | async function toArray(items) { 663 | const result = []; 664 | for await (const item of items) { 665 | result.push(item); 666 | } 667 | return result; 668 | } 669 | 670 | it('empty-pipeline', async () => { 671 | const pipeline = new Pipeline(); 672 | const result = await toArray( 673 | pipeline.execute( 674 | [ 1, 2, 3, 4, 5 ])); 675 | assert.deepStrictEqual( 676 | result, 677 | [ 1, 2, 3, 4, 5 ], 678 | ); 679 | }); 680 | ``` 681 | 682 | From [node-httptransfer/test/generator/pipeline.test.js][]. 683 | 684 | 685 | 686 | ```js 687 | it('empty-pipeline', async () => { 688 | const pipeline = new Pipeline(); 689 | const result = await Array.fromAsync( 690 | pipeline.execute( 691 | [ 1, 2, 3, 4, 5 ])); 692 | assert.deepStrictEqual( 693 | result, 694 | [ 1, 2, 3, 4, 5 ], 695 | ); 696 | }); 697 | ``` 698 | 699 |
700 | 701 | [ipfs-core/src/runtime/init-assets-nodejs.js]: https://github.com/ipfs/js-ipfs/blob/release/v0.54.x/packages/ipfs-core/src/runtime/init-assets-nodejs.js 702 | [js-libp2p/test/content-routing/content-routing.node.js]: https://github.com/libp2p/js-libp2p/blob/13cf4761489d59b22924bb8ec2ec6dbe207b280c/test/content-routing/content-routing.node.js 703 | [node-httptransfer/test/generator/pipeline.test.js]: https://github.com/adobe/node-httptransfer/blob/22a32e72df89ce40e77a1dae5575a07654a0851f/test/generator/pipeline.test.js 704 | --------------------------------------------------------------------------------