├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── book.yml │ └── ci.yml ├── .gitignore ├── .poggit.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── await-generator ├── src │ └── SOFe │ │ └── AwaitGenerator │ │ ├── Await.php │ │ ├── AwaitChild.php │ │ ├── AwaitException.php │ │ ├── Channel.php │ │ ├── EmptyChannelState.php │ │ ├── GeneratorUtil.php │ │ ├── InterruptException.php │ │ ├── Loading.php │ │ ├── Mutex.php │ │ ├── PromiseState.php │ │ ├── PubSub.php │ │ ├── RaceLostException.php │ │ ├── ReceivingChannelState.php │ │ ├── SendingChannelState.php │ │ ├── Traverser.php │ │ └── UnawaitedCallbackException.php └── virion.yml ├── book ├── .gitignore ├── README.md ├── book.toml └── src │ ├── SUMMARY.md │ ├── all-race.md │ ├── async-iterators.md │ ├── async.md │ ├── await-gen.md │ ├── await-once.md │ ├── f2c-g2c.md │ ├── generators.md │ ├── intro.md │ ├── main.md │ └── semver.md ├── chs └── README.md ├── composer.json ├── fiber.jpeg ├── infection.json.dist ├── logic.md ├── phpstan-ignore.neon ├── phpstan.neon.dist ├── phpunit.xml ├── tests ├── SOFe │ └── AwaitGenerator │ │ ├── AwaitTest.php │ │ ├── ChannelTest.php │ │ ├── DummyException.php │ │ ├── LoadingTest.php │ │ ├── MockClock.php │ │ ├── MutexTest.php │ │ ├── PubSubTest.php │ │ └── TraverseTest.php └── autoload.php └── zho └── README.md /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.php] 4 | indent_style = tab 5 | indent_size = 4 6 | 7 | [*.md] 8 | indent_style = space 9 | indent_size = 4 10 | 11 | [*.yml] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | labels: 9 | - dependencies 10 | -------------------------------------------------------------------------------- /.github/workflows/book.yml: -------------------------------------------------------------------------------- 1 | name: Book 2 | on: 3 | push: 4 | branches: [master] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions-rs/toolchain@v1 11 | with: 12 | toolchain: stable 13 | - uses: actions/cache@v2 14 | with: 15 | path: | 16 | ~/.cargo/bin 17 | key: mdbook 18 | - run: test -f ~/.cargo/bin/mdbook || cargo install mdbook 19 | - run: mdbook build 20 | working-directory: book 21 | - run: cp -r book/book ../book 22 | - run: git checkout -- . && git clean -fd 23 | - run: git fetch && git checkout gh-pages 24 | - run: test ! -d $(echo ${{github.ref}} | cut -d/ -f3) || rm -r $(echo ${{github.ref}} | cut -d/ -f3) 25 | - run: cp -r ../book $(echo ${{github.ref}} | cut -d/ -f3) 26 | - run: git config --local user.name "github-actions[bot]" && git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" 27 | - run: git add $(echo ${{github.ref}} | cut -d/ -f3) && git commit --allow-empty -m "Docs build for SOF3/await-generator@${{github.sha}}" 28 | - run: git push 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | phpunit: 7 | name: Unit test 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | php-version: 12 | - "8.0" 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | - uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: ${{matrix.php-version}} 20 | tools: composer 21 | coverage: xdebug2 22 | - run: composer config --no-plugins allow-plugins.infection/extension-installer true 23 | - run: composer update --optimize-autoloader 24 | - name: phpunit test 25 | env: 26 | XDEBUG_MODE: coverage 27 | run: composer test -- --coverage-clover=coverage.xml 28 | - uses: codecov/codecov-action@v1 29 | with: 30 | token: ${{secrets.CODECOV_SECRET}} 31 | fail_ci_if_error: true 32 | phpstan: 33 | name: phpstan analyze 34 | runs-on: ubuntu-latest 35 | strategy: 36 | matrix: 37 | php-version: 38 | - "8.0" 39 | steps: 40 | - uses: actions/checkout@v2 41 | - uses: shivammathur/setup-php@v2 42 | with: 43 | php-version: ${{matrix.php-version}} 44 | tools: composer 45 | - run: composer config --no-plugins allow-plugins.infection/extension-installer true 46 | - run: composer update --optimize-autoloader 47 | - name: phpstan analyze 48 | run: composer analyze 49 | infection: 50 | name: Mutation test 51 | runs-on: ubuntu-latest 52 | strategy: 53 | matrix: 54 | php-version: 55 | - "8.0" 56 | steps: 57 | - uses: actions/checkout@v2 58 | - uses: shivammathur/setup-php@v2 59 | with: 60 | php-version: ${{matrix.php-version}} 61 | tools: composer 62 | coverage: xdebug2 63 | - run: composer config --no-plugins allow-plugins.infection/extension-installer true 64 | - run: composer update --optimize-autoloader 65 | - name: infection 66 | env: 67 | XDEBUG_MODE: coverage 68 | run: composer infection 69 | - uses: actions/upload-artifact@v1 70 | with: 71 | name: infection.log 72 | path: infection.log 73 | - uses: actions/upload-artifact@v1 74 | with: 75 | name: infection-summary.log 76 | path: infection-summary.log 77 | - uses: actions/upload-artifact@v1 78 | with: 79 | name: infection-per-mutator.md 80 | path: infection-per-mutator.md 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | /vendor/ 3 | *.log 4 | infection-per-mutator.md 5 | .phpunit.result.cache 6 | /composer.lock 7 | -------------------------------------------------------------------------------- /.poggit.yml: -------------------------------------------------------------------------------- 1 | projects: 2 | await-generator: 3 | path: await-generator 4 | type: library 5 | model: virion 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | === 3 | 4 | ## 3.6.1 5 | - Fixed syntax error 6 | 7 | ## 3.6.0 8 | - Added `Traverser::asGenerator()` 9 | 10 | ## 3.5.2 11 | - Fixed `Channel` crashing if there is only one pending sender/receiver and it gets canceled. 12 | 13 | ## 3.5.1 14 | - Added `PubSub` class 15 | 16 | ## 3.5.0 17 | - Added `Await::safeRace` 18 | 19 | ## 3.5.0 20 | - Added `Await::safeRace` 21 | 22 | ## 3.4.4 23 | - Support virion 3.0 spec 24 | 25 | ## 3.4.3 26 | - Fixed Traverser not passing resolved value to the inner generator (#184) 27 | 28 | ## 3.4.2 29 | - Updated phpstan hint to allow promise resolver to have no arguments 30 | - Deprecated `yield $generator`, use `yield from $generator` instead 31 | 32 | ## 3.4.1 33 | - Added `Loading::getSync` 34 | 35 | ## 3.4.0 36 | - Added `Channel` 37 | 38 | ## 3.3.0 39 | - Added `Await::promise` 40 | - Deprecated all constnat yields in favour of `Await::promise` 41 | 42 | ## 3.2.0 43 | - Added `Mutex` 44 | 45 | ## 3.1.1 46 | - Allow `Await::all([])` to simply return empty array 47 | 48 | ## 3.1.0 49 | - Added `Traverser` API 50 | - Added `Traverser::next()` 51 | - Added `Traverser::collect()` 52 | - Added `Traverser::interrupt()` 53 | 54 | ## 3.0.0 55 | - Added `Await::all()` and `Await::race()` for a generator interface 56 | - Fixed crash during double promise resolution, ignores second call instead 57 | - Marked some internal functions @internal or private 58 | 59 | ## 2.3.0 60 | - Debug backtrace includes objects. 61 | - Added `Await::RESOLVE_MULTI` 62 | 63 | ## 2.2.0 64 | - Added debug mode 65 | - Generator traces are appended to throwable traces under debug mode 66 | - Resolve function (result of `yield Await::RESOLVE`) no longer requires a parameter 67 | 68 | ## 2.1.0 69 | - Added `Await::RACE` 70 | - Fixed later-resolve/immediate-reject with `Await::ALL` 71 | 72 | ## 2.0.0 73 | Complete rewrite 74 | 75 | ## 1.0.0 76 | - Entry level 77 | - `Await::func` 78 | - `Await::closure` 79 | - Intermediate level 80 | - `Await::FROM` (currently equivalent to `yield from`) 81 | - Implementation level 82 | - `Await::ASYNC` 83 | - `Await::CALLBACK` 84 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Eng | [繁](zho) | [简](chs) 2 | # await-generator 3 | [![Build Status][ci-badge]][ci-page] 4 | [![Codecov][codecov-badge]][codecov-page] 5 | 6 | A library to use async/await pattern in PHP. 7 | 8 | ## Documentation 9 | Read the [await-generator tutorial][book] for an introduction 10 | from generators and traditional async callbacks to await-generator. 11 | 12 | ## Why await-generator? 13 | Traditional async programming requires callbacks, 14 | which leads to spaghetti code known as "callback hell": 15 |
16 | Click to reveal example callback hell 17 | 18 | ```php 19 | load_data(function($data) { 20 | $init = count($data) === 0 ? init_data(...) : fn($then) => $then($data); 21 | $init(function($data) { 22 | $output = []; 23 | foreach($data as $k => $datum) { 24 | processData($datum, function($result) use(&$output, $data) { 25 | $output[$k] = $result; 26 | if(count($output) === count($data)) { 27 | createQueries($output, function($queries) { 28 | $run = function($i) use($queries, &$run) { 29 | runQuery($queries[$i], function() use($i, $queries, $run) { 30 | if($i === count($queries)) { 31 | $done = false; 32 | commitBatch(function() use(&$done) { 33 | if(!$done) { 34 | $done = true; 35 | echo "Done!\n"; 36 | } 37 | }); 38 | onUserClose(function() use(&$done) { 39 | if(!$done) { 40 | $done = true; 41 | echo "User closed!\n"; 42 | } 43 | }); 44 | onTimeout(function() use(&$done) { 45 | if(!$done) { 46 | $done = true; 47 | echo "Timeout!\n"; 48 | } 49 | }); 50 | } else { 51 | $run($i + 1); 52 | } 53 | }); 54 | }; 55 | }); 56 | } 57 | }); 58 | } 59 | }); 60 | }); 61 | ``` 62 | 63 |
64 | With await-generator, this is simplified into: 65 | 66 | ```php 67 | $data = yield from load_data(); 68 | if(count($data) === 0) $data = yield from init_data(); 69 | $output = yield from Await::all(array_map(fn($datum) => processData($datum), $data)); 70 | $queries = yield from createQueries($output); 71 | foreach($queries as $query) yield from runQuery($query); 72 | [$which, ] = yield from Await::race([ 73 | 0 => commitBatch(), 74 | 1 => onUserClose(), 75 | 2 => onTimeout(), 76 | ]) 77 | echo match($which) { 78 | 0 => "Done!\n", 79 | 1 => "User closed!\n", 80 | 2 => "Timeout!\n", 81 | }; 82 | ``` 83 | 84 | ## Can I maintain backward compatibility? 85 | Yes, await-generator does not impose any restrictions on your existing API. 86 | You can wrap all await-generator calls as internal implementation detail, 87 | although you are strongly encouraged to expose the generator functions directly. 88 | 89 | await-generator starts an await context with the `Await::f2c` method, 90 | with which you can adapt into the usual callback syntax: 91 | 92 | ```php 93 | function oldApi($args, Closure $onSuccess) { 94 | Await::f2c(fn() => $onSuccess(yield from newApi($args))); 95 | } 96 | ``` 97 | 98 | Or if you want to handle errors too: 99 | 100 | ```php 101 | function newApi($args, Closure $onSuccess, Closure $onError) { 102 | Await::f2c(function() use($onSuccess, $onError) { 103 | try { 104 | $onSuccess(yield from newApi($args)); 105 | } catch(Exception $ex) { 106 | $onError($ex); 107 | } 108 | }); 109 | } 110 | ``` 111 | 112 | You can continue to call functions implemented as callback style 113 | using the `Await::promise` method (similar to `new Promise` in JS): 114 | 115 | ```php 116 | yield from Await::promise(fn($resolve, $reject) => oldFunction($args, $resolve, $reject)); 117 | ``` 118 | 119 | ## Why *not* await-generator 120 | await-generator has a few common pitfalls: 121 | 122 | - Forgetting to `yield from` a `Generator` method will end up doing nothing. 123 | - If you delete all `yield`s from a function, 124 | it automatically becomes a non-generator function thanks to PHP magic. 125 | This issue can be mitigated by always adding `: Generator` to the function signature. 126 | - `finally` blocks may never get executed if an async function never resolves 127 | (e.g. `Await::promise(fn($resolve) => null)`). 128 | 129 | While these pitfalls cause some trouble, 130 | await-generator style is still much less bug-prone than a callback hell. 131 | 132 | ## But what about fibers? 133 | This might be a subjective comment, 134 | but I do not prefer fibers for a few reasons: 135 | 136 | ### Explicit suspension in type signature 137 | ![fiber.jpg](./fiber.jpeg) 138 | 139 | For example, it is easy to tell from the type signature that 140 | `$channel->send($value): Generator` suspends until the value is sent 141 | and `$channel->sendBuffered($value): void` 142 | is a non-suspending method that returns immediately. 143 | Type signatures are often self-explanatory. 144 | 145 | Of course, users could call `sleep()` anyway, 146 | but it is quite obvious to everyone that `sleep()` blocks the whole runtime 147 | (if they didn't already know, they will find out when the whole world stops). 148 | 149 | ### Concurrent states 150 | When a function suspends, many other things can happen. 151 | Indeed, calling a function allows the implementation to call any other functions 152 | which could modify your states anyway, 153 | but a sane, genuine implementation of e.g. an HTTP request 154 | wouldn't call functions that modify the private states of your library. 155 | But this assumption does not hold with fibers 156 | because the fiber is preempted and other fibers can still modify the private states. 157 | This means you have to check for possible changes in private properties 158 | every time you call any function that *might* be suspending. 159 | 160 | On the other hand, using explicit await, 161 | it is obvious where exactly the suspension points are, 162 | and you only need to check for state mutations at the known suspension points. 163 | 164 | ### Trapping suspension points 165 | await-generator provides a feature called ["trapping"][trap-pr], 166 | which allows users to add pre-suspend and pre-resume hooks to a generator. 167 | This is simply achieved by adding an adapter to the generator, 168 | and does not even require explicit support from the await-generator runtime. 169 | This is currently not possible with fibers. 170 | 171 | [book]: https://sof3.github.io/await-generator/master/ 172 | [ci-badge]: https://github.com/SOF3/await-generator/workflows/CI/badge.svg 173 | [ci-page]: https://github.com/SOF3/await-generator/actions?query=workflow%3ACI 174 | [codecov-badge]: https://img.shields.io/codecov/c/github/codecov/example-python.svg 175 | [codecov-page]: https://codecov.io/gh/SOF3/await-generator 176 | [trap-pr]: https://github.com/SOF3/await-generator/pull/106 177 | -------------------------------------------------------------------------------- /await-generator/src/SOFe/AwaitGenerator/Await.php: -------------------------------------------------------------------------------- 1 | , mixed, T> 62 | * */ 63 | private $generator; 64 | /** 65 | * @var callable|null 66 | * @phpstan-var (callable(T): void)|null 67 | */ 68 | private $onComplete; 69 | /** 70 | * @var callable[] 71 | * @phpstan-var array 72 | */ 73 | private $catches = []; 74 | /** @var bool */ 75 | private $sleeping; 76 | /** @var PromiseState[] */ 77 | private $promiseQueue = []; 78 | /** @var AwaitChild|null */ 79 | private $lastResolveUnrejected = null; 80 | /** 81 | * @var string|string[]|null 82 | * @phpstan-var Await::RESOLVE|null|Await::RESOLVE_MULTI|Await::REJECT|Await::ONCE|Await::ALL|Await::RACE|Generator|null 83 | */ 84 | private $current = null; 85 | 86 | protected final function __construct(){ 87 | } 88 | 89 | /** 90 | * Converts a `Function` to a VoidCallback 91 | * 92 | * @param callable $closure 93 | * @phpstan-param callable(): Generator $closure 94 | * @param callable|null $onComplete 95 | * @phpstan-param (callable(T): void)|null $onComplete This argument has been deprecated. Append the call to the generator closure instead. 96 | * @param callable[]|callable $catches This argument has been deprecated. Use a try-catch block in the generator closure instead. 97 | * @phpstan-param array|callable(Throwable): void $catches 98 | * 99 | * @return Await 100 | */ 101 | public static function f2c(callable $closure, ?callable $onComplete = null, $catches = []) : Await{ 102 | return self::g2c($closure(), $onComplete, $catches); 103 | } 104 | 105 | /** 106 | * Converts an AwaitGenerator to a VoidCallback 107 | * 108 | * @param Generator $generator 109 | * @phpstan-param Generator $generator 110 | * @param callable|null $onComplete 111 | * @phpstan-param (callable(T): void)|null $onComplete This argument has been deprecated. Append the call to the generator closure instead. 112 | * @param callable[]|callable $catches This argument has been deprecated. Use a try-catch block in the generator instead. 113 | * @phpstan-param array|callable(Throwable): void $catches 114 | * 115 | * @return Await 116 | */ 117 | public static function g2c(Generator $generator, ?callable $onComplete = null, $catches = []) : Await{ 118 | /** @var Await $await */ 119 | $await = new Await(); 120 | $await->generator = $generator; 121 | $await->onComplete = $onComplete; 122 | if(is_callable($catches)){ 123 | $await->catches = ["" => $catches]; 124 | }else{ 125 | $await->catches = $catches; 126 | } 127 | $executor = [$generator, "rewind"]; 128 | while($executor !== null){ 129 | $executor = $await->wakeup($executor); 130 | } 131 | return $await; 132 | } 133 | 134 | /** 135 | * Given an array of generators, 136 | * executes them simultaneously, 137 | * and returns an array with each generator mapped to the value. 138 | * Throws exception as soon as any of the generators throws an exception. 139 | * 140 | * @template U 141 | * @param Generator[] $generators 142 | * @return Generator 143 | */ 144 | public static function all(array $generators) : Generator{ 145 | if(count($generators) === 0){ 146 | return []; 147 | } 148 | 149 | foreach($generators as $k => $generator){ 150 | $resolve = yield; 151 | $reject = yield self::REJECT; 152 | self::g2c($generator, static function($result) use($k, $resolve) : void{ 153 | $resolve([$k, $result]); 154 | }, $reject); 155 | } 156 | $all = yield self::ALL; 157 | $return = []; 158 | foreach($all as [$k, $result]) { 159 | $return[$k] = $result; 160 | } 161 | return $return; 162 | } 163 | 164 | /** 165 | * Given an array of generators, 166 | * executes them simultaneously, 167 | * and returns a single-element array `[$k, $v]` as soon as any of the generators returns, 168 | * with `$k` being the key of that generator in the array 169 | * and `$v` being the value returned by the generator. 170 | * Throws exception as soon as any of the generators throws an exception. 171 | * 172 | * Note that the not-yet-resolved generators will keep on running, 173 | * but their return values or exceptions thrown will be ignored. 174 | * 175 | * The return value uses `[$k, $v]` instead of `[$k => $v]`. 176 | * The user may use the format `[$k, $v] = yield Await::race(...);` 177 | * to obtain `$k` and `$v` conveniently. 178 | * 179 | * @template K 180 | * @template U 181 | * @param array> $generators 182 | * @return Generator 183 | * 184 | * @deprecated `Await::race` does not clean up losing generators. Use `safeRace` instead. 185 | * @see Await::safeRace 186 | */ 187 | public static function race(array $generators) : Generator{ 188 | if(count($generators) === 0){ 189 | throw new AwaitException("Cannot race an empty array of generators"); 190 | } 191 | 192 | foreach($generators as $k => $generator){ 193 | $resolve = yield; 194 | $reject = yield self::REJECT; 195 | self::g2c($generator, static function($result) use($k, $resolve) : void{ 196 | $resolve([$k, $result]); 197 | }, $reject); 198 | } 199 | [$k, $result] = yield self::RACE; 200 | return [$k, $result]; 201 | } 202 | 203 | /** 204 | * Ensures that only exactly one generator is complete after return. 205 | * All other generators are either unstarted or suspending. 206 | * 207 | * @template K 208 | * @template U 209 | * @param array> $inputs 210 | * @return array> 211 | */ 212 | private static function raceSemaphore(array $inputs) : array{ 213 | $wrapped = []; 214 | 215 | // This channel acts as a semaphore that only starts one input at a time. 216 | $ch = new Channel; 217 | $ch->sendWithoutWait(null); 218 | 219 | foreach($inputs as $k => $input){ 220 | /** @var Generator $input */ 221 | $wrapped[$k] = (function() use($input, $ch){ 222 | yield from $ch->receive(); 223 | 224 | $input->rewind(); 225 | if(!$input->valid()) { 226 | return $input->getReturn(); 227 | } 228 | 229 | $ch->sendWithoutWait(null); 230 | while($input->valid()) { 231 | try { 232 | $send = yield $input->key() => $input->current(); 233 | $input->send($send); 234 | } catch(Throwable $e) { 235 | $input->throw($e); 236 | } 237 | } 238 | return $input->getReturn(); 239 | })(); 240 | } 241 | 242 | return $wrapped; 243 | } 244 | 245 | /** 246 | * This function is similar to `Await::race`, 247 | * but additionally throws RaceLostException on the losing generators to allow cancellation. 248 | * The generator should clean up resources in a `finally` block. 249 | * 250 | * A losing generator may never be started the first time, thus never cathc a RaceLostException. 251 | * Thus, the `finally` block should only be relied on for cleaning up resources created inside the generator. 252 | * This behavior differs from `race` where multiple generators started even though one of them succeeded. 253 | * 254 | * @template K 255 | * @template U 256 | * @param array> $generators 257 | * @return Generator 258 | */ 259 | public static function safeRace(array $generators) : Generator{ 260 | $generators = self::raceSemaphore($generators); 261 | 262 | $firstException = null; 263 | $which = null; 264 | 265 | try { 266 | [$which, $result] = yield from self::race($generators); 267 | 268 | return [$which, $result]; 269 | } catch(Throwable $e) { 270 | $firstException = $e; 271 | throw $e; 272 | } finally { 273 | foreach($generators as $key => $generator) { 274 | if($which !== null && $key !== $which) { 275 | try { 276 | $generator->throw(new RaceLostException); 277 | } catch(RaceLostException $e) { 278 | // expected 279 | } catch (Throwable $e) { 280 | if($firstException === null) { 281 | $firstException = $e; 282 | } 283 | } 284 | } 285 | } 286 | } 287 | } 288 | 289 | /** 290 | * Executes a callback-style async function using JavaScript Promise-like API. 291 | * 292 | * This *differs* from JavaScript Promise in that $closure is NOT executed 293 | * until it is yielded and processed by an Await runtime. 294 | * 295 | * @template U 296 | * @param Closure(Closure(U=): void, Closure(Throwable): void): void $closure 297 | * @return Generator 298 | */ 299 | public static function promise(Closure $closure) : Generator{ 300 | $resolve = yield Await::RESOLVE; 301 | $reject = yield Await::REJECT; 302 | 303 | $closure($resolve, $reject); 304 | return yield Await::ONCE; 305 | } 306 | 307 | 308 | /** 309 | * A wrapper around wakeup() to convert deep recursion to tail recursion 310 | * 311 | * @param callable|null $executor 312 | * @phpstan-param (callable(): void)|null $executor 313 | * 314 | * @internal This is implementation detail. Existence, signature and behaviour are semver-exempt. 315 | */ 316 | public function wakeupFlat(?callable $executor) : void{ 317 | while($executor !== null){ 318 | $executor = $this->wakeup($executor); 319 | } 320 | } 321 | 322 | /** 323 | * Calls $executor and returns the next function to execute 324 | * 325 | * @param callable $executor a function that triggers the execution of the generator 326 | * @phpstan-param callable(): void $executor 327 | * 328 | * @return (callable(): void)|null 329 | */ 330 | private function wakeup(callable $executor) : ?callable{ 331 | try{ 332 | $this->sleeping = false; 333 | $executor(); 334 | }catch(Throwable $throwable){ 335 | $this->reject($throwable); 336 | return null; 337 | } 338 | 339 | if(!$this->generator->valid()){ 340 | $ret = $this->generator->getReturn(); 341 | $this->resolve($ret); 342 | return null; 343 | } 344 | 345 | // $key = $this->generator->key(); 346 | $this->current = $current = $this->generator->current() ?? self::RESOLVE; 347 | 348 | if($current === self::RESOLVE){ 349 | return function() : void{ 350 | $promise = new AwaitChild($this); 351 | $this->promiseQueue[] = $promise; 352 | $this->lastResolveUnrejected = $promise; 353 | $this->generator->send(Closure::fromCallable([$promise, "resolve"])); 354 | }; 355 | } 356 | 357 | if($current === self::RESOLVE_MULTI){ 358 | return function() : void{ 359 | $promise = new AwaitChild($this); 360 | $this->promiseQueue[] = $promise; 361 | $this->lastResolveUnrejected = $promise; 362 | $this->generator->send(static function(...$args) use($promise) : void{ 363 | $promise->resolve($args); 364 | }); 365 | }; 366 | } 367 | 368 | if($current === self::REJECT){ 369 | if($this->lastResolveUnrejected === null){ 370 | $this->reject(new AwaitException("Cannot yield Await::REJECT without yielding Await::RESOLVE first; they must be yielded in pairs")); 371 | return null; 372 | } 373 | return function() : void{ 374 | $promise = $this->lastResolveUnrejected; 375 | assert($promise !== null); 376 | $this->lastResolveUnrejected = null; 377 | $this->generator->send(Closure::fromCallable([$promise, "reject"])); 378 | }; 379 | } 380 | 381 | $this->lastResolveUnrejected = null; 382 | 383 | if($current === self::RACE){ 384 | if(count($this->promiseQueue) === 0){ 385 | $this->reject(new AwaitException("Yielded Await::RACE when there is nothing racing")); 386 | return null; 387 | } 388 | 389 | $hasResult = 0; // 0 = all pending, 1 = one resolved, 2 = one rejected 390 | foreach($this->promiseQueue as $promise){ 391 | if($promise->state === self::STATE_RESOLVED){ 392 | $hasResult = 1; 393 | $result = $promise->resolved; 394 | break; 395 | } 396 | if($promise->state === self::STATE_REJECTED){ 397 | $hasResult = 2; 398 | $result = $promise->rejected; 399 | break; 400 | } 401 | } 402 | 403 | if($hasResult !== 0){ 404 | foreach($this->promiseQueue as $p){ 405 | $p->cancelled = true; 406 | } 407 | $this->promiseQueue = []; 408 | assert(isset($result)); 409 | if($hasResult === 1){ 410 | return function() use ($result) : void{ 411 | $this->generator->send($result); 412 | }; 413 | } 414 | assert($hasResult === 2); 415 | return function() use ($result) : void{ 416 | $this->generator->throw($result); 417 | }; 418 | } 419 | 420 | $this->sleeping = true; 421 | return null; 422 | } 423 | 424 | if($current === self::ONCE || $current === self::ALL){ 425 | if($current === self::ONCE && count($this->promiseQueue) !== 1){ 426 | $this->reject(new AwaitException("Yielded Await::ONCE when the pending queue size is " . count($this->promiseQueue) . " != 1")); 427 | return null; 428 | } 429 | 430 | $results = []; 431 | 432 | // first check if nothing is immediately rejected 433 | foreach($this->promiseQueue as $promise){ 434 | if($promise->state === self::STATE_REJECTED){ 435 | foreach($this->promiseQueue as $p){ 436 | $p->cancelled = true; 437 | } 438 | $this->promiseQueue = []; 439 | $ex = $promise->rejected; 440 | return function() use ($ex) : void{ 441 | $this->generator->throw($ex); 442 | }; 443 | } 444 | } 445 | 446 | foreach($this->promiseQueue as $promise){ 447 | // if anything is pending, some others are pending and some others are resolved, but we will eventually get rejected/resolved from the pending promises 448 | if($promise->state === self::STATE_PENDING){ 449 | $this->sleeping = true; 450 | return null; 451 | } 452 | assert($promise->state === self::STATE_RESOLVED); 453 | $results[] = $promise->resolved; 454 | } 455 | 456 | // all resolved 457 | $this->promiseQueue = []; 458 | return function() use ($current, $results) : void{ 459 | $this->generator->send($current === self::ONCE ? $results[0] : $results); 460 | }; 461 | } 462 | 463 | if($current instanceof Generator){ 464 | if(!self::$warnedDeprecatedDirectYield) { 465 | echo "\n" . 'NOTICE: `yield $generator` has been deprecated, please use `yield from $generator` instead.' . "\n"; 466 | self::$warnedDeprecatedDirectYield = true; 467 | } 468 | 469 | if(!empty($this->promiseQueue)){ 470 | $this->reject(new UnawaitedCallbackException("Yielding a generator")); 471 | return null; 472 | } 473 | 474 | $child = new AwaitChild($this); 475 | $await = Await::g2c($current, [$child, "resolve"], [$child, "reject"]); 476 | 477 | if($await->state === self::STATE_RESOLVED){ 478 | $return = $await->resolved; 479 | return function() use ($return) : void{ 480 | $this->generator->send($return); 481 | }; 482 | } 483 | if($await->state === self::STATE_REJECTED){ 484 | $ex = $await->rejected; 485 | return function() use ($ex) : void{ 486 | $this->generator->throw($ex); 487 | }; 488 | } 489 | 490 | $this->sleeping = true; 491 | $this->current = self::ONCE; 492 | $this->promiseQueue = [$await]; 493 | return null; 494 | } 495 | 496 | $this->reject(new AwaitException("Unknown yield value")); 497 | return null; 498 | } 499 | 500 | /** 501 | * @phpstan-param AwaitChild $changed 502 | * 503 | * @internal This is implementation detail. Existence, signature and behaviour are semver-exempt. 504 | */ 505 | public function recheckPromiseQueue(AwaitChild $changed) : void{ 506 | assert($this->sleeping); 507 | if($this->current === self::ONCE){ 508 | assert(count($this->promiseQueue) === 1); 509 | } 510 | 511 | if($this->current === self::RACE){ 512 | foreach($this->promiseQueue as $p){ 513 | $p->cancelled = true; 514 | } 515 | $this->promiseQueue = []; 516 | 517 | if($changed->state === self::STATE_REJECTED){ 518 | $ex = $changed->rejected; 519 | $this->wakeupFlat(function() use ($ex) : void{ 520 | $this->generator->throw($ex); 521 | }); 522 | }else{ 523 | $value = $changed->resolved; 524 | $this->wakeupFlat(function() use ($value) : void{ 525 | $this->generator->send($value); 526 | }); 527 | } 528 | return; 529 | } 530 | 531 | $current = $this->current; 532 | $results = []; 533 | foreach($this->promiseQueue as $promise){ 534 | if($promise->state === self::STATE_PENDING){ 535 | return; 536 | } 537 | if($promise->state === self::STATE_REJECTED){ 538 | foreach($this->promiseQueue as $p){ 539 | $p->cancelled = true; 540 | } 541 | $this->promiseQueue = []; 542 | $ex = $promise->rejected; 543 | $this->wakeupFlat(function() use ($ex) : void{ 544 | $this->generator->throw($ex); 545 | }); 546 | return; 547 | } 548 | assert($promise->state === self::STATE_RESOLVED); 549 | $results[] = $promise->resolved; 550 | } 551 | // all resolved 552 | $this->promiseQueue = []; 553 | $this->wakeupFlat(function() use ($current, $results) : void{ 554 | $this->generator->send($current === self::ONCE ? $results[0] : $results); 555 | }); 556 | } 557 | 558 | /** 559 | * @param mixed $value 560 | * 561 | * @internal This is implementation detail. Existence, signature and behaviour are semver-exempt. 562 | */ 563 | public function resolve($value) : void{ 564 | if(!empty($this->promiseQueue)){ 565 | $this->reject(new UnawaitedCallbackException("Resolution of await generator")); 566 | return; 567 | } 568 | $this->sleeping = true; 569 | parent::resolve($value); 570 | if($this->onComplete){ 571 | ($this->onComplete)($this->resolved); 572 | } 573 | } 574 | 575 | /** 576 | * @internal This is implementation detail. Existence, signature and behaviour are semver-exempt. 577 | */ 578 | public function reject(Throwable $throwable) : void{ 579 | $this->sleeping = true; 580 | 581 | parent::reject($throwable); 582 | foreach($this->catches as $class => $onError){ 583 | if($class === "" || is_a($throwable, $class)){ 584 | $onError($throwable); 585 | return; 586 | } 587 | } 588 | throw new AwaitException("Unhandled async exception: {$throwable->getMessage()}", 0, $throwable); 589 | } 590 | 591 | /** 592 | * @internal This is implementation detail. Existence, signature and behaviour are semver-exempt. 593 | */ 594 | public function isSleeping() : bool{ 595 | return $this->sleeping; 596 | } 597 | } 598 | -------------------------------------------------------------------------------- /await-generator/src/SOFe/AwaitGenerator/AwaitChild.php: -------------------------------------------------------------------------------- 1 | */ 32 | protected $await; 33 | 34 | 35 | /** 36 | * @phpstan-param Await $await 37 | */ 38 | public function __construct(Await $await){ 39 | $this->await = $await; 40 | } 41 | 42 | /** 43 | * @param mixed $value 44 | */ 45 | public function resolve($value = null) : void{ 46 | if($this->state !== self::STATE_PENDING){ 47 | return; // nothing should happen if resolved/rejected multiple times 48 | } 49 | 50 | parent::resolve($value); 51 | if(!$this->cancelled && $this->await->isSleeping()){ 52 | $this->await->recheckPromiseQueue($this); 53 | } 54 | } 55 | 56 | public function reject(Throwable $value) : void{ 57 | if($this->state !== self::STATE_PENDING){ 58 | return; // nothing should happen if resolved/rejected multiple times 59 | } 60 | 61 | parent::reject($value); 62 | if(!$this->cancelled && $this->await->isSleeping()){ 63 | $this->await->recheckPromiseQueue($this); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /await-generator/src/SOFe/AwaitGenerator/AwaitException.php: -------------------------------------------------------------------------------- 1 | state = new EmptyChannelState; 38 | } 39 | 40 | /** 41 | * Sends a value to the channel, 42 | * and wait until the value is received by the other side. 43 | * 44 | * @param T $value 45 | */ 46 | public function sendAndWait($value) : Generator{ 47 | if($this->state instanceof ReceivingChannelState){ 48 | $receiver = array_shift($this->state->queue); 49 | if(count($this->state->queue) === 0){ 50 | $this->state = new EmptyChannelState; 51 | } 52 | $receiver($value); 53 | return; 54 | } 55 | 56 | if($this->state instanceof EmptyChannelState){ 57 | $this->state = new SendingChannelState; 58 | } 59 | 60 | try { 61 | // $key holds the object reference directly instead of the key to avoid GC causing spl_object_id duplicate 62 | $key = null; 63 | 64 | yield from Await::promise(function($resolve) use($value, &$key){ 65 | $key = $resolve; 66 | $this->state->queue[spl_object_id($key)] = [$value, $resolve]; 67 | }); 68 | } finally { 69 | if($key !== null) { 70 | if($this->state instanceof SendingChannelState) { 71 | // our key may still exist in the channel state 72 | 73 | unset($this->state->queue[spl_object_id($key)]); 74 | if(count($this->state->queue) === 0) { 75 | $this->state = new EmptyChannelState; 76 | } 77 | } 78 | // else, state already changed means our key has been shifted already. 79 | } 80 | } 81 | } 82 | 83 | /** 84 | * Send a value to the channel 85 | * without waiting for a receiver. 86 | * 87 | * This method always returns immediately. 88 | * It is equivalent to calling `Await::g2c($channel->sendAndWait($value))`. 89 | * 90 | * @param T $value 91 | */ 92 | public function sendWithoutWait($value) : void{ 93 | Await::g2c($this->sendAndWait($value)); 94 | } 95 | 96 | /** 97 | * Try to send a value to the channel if there is a receive waiting. 98 | * Returns whether the value successfully sent. 99 | * 100 | * @param T $value 101 | */ 102 | public function trySend($value) : bool { 103 | if($this->state instanceof ReceivingChannelState) { 104 | $receiver = array_shift($this->state->queue); 105 | if(count($this->state->queue) === 0){ 106 | $this->state = new EmptyChannelState; 107 | } 108 | $receiver($value); 109 | return true; 110 | } 111 | 112 | return false; 113 | } 114 | 115 | /** 116 | * Receive a value from the channel. 117 | * Waits for a sender if there is currently no sender waiting. 118 | * 119 | * @return Generator 120 | */ 121 | public function receive() : Generator{ 122 | if($this->state instanceof SendingChannelState){ 123 | [$value, $sender] = array_shift($this->state->queue); 124 | if(count($this->state->queue) === 0){ 125 | $this->state = new EmptyChannelState; 126 | } 127 | $sender(); 128 | return $value; 129 | } 130 | 131 | if($this->state instanceof EmptyChannelState){ 132 | $this->state = new ReceivingChannelState; 133 | } 134 | 135 | try { 136 | // $key holds the object reference directly instead of the key to avoid GC causing spl_object_id duplicate 137 | $key = null; 138 | 139 | return yield from Await::promise(function($resolve) use(&$key){ 140 | $key = $resolve; 141 | $this->state->queue[spl_object_id($key)] = $resolve; 142 | }); 143 | } finally { 144 | if($key !== null) { 145 | if($this->state instanceof ReceivingChannelState) { 146 | // our key may still exist in the channel state 147 | 148 | unset($this->state->queue[spl_object_id($key)]); 149 | if(count($this->state->queue) === 0) { 150 | $this->state = new EmptyChannelState; 151 | } 152 | } 153 | // else, state already changed means our key has been shifted already. 154 | } 155 | } 156 | } 157 | 158 | /** 159 | * Try to receive a value from the channel if there is a sender waiting. 160 | * Returns `$default` if there is no sender waiting. 161 | * 162 | * @template U 163 | * @param U $default 164 | * 165 | * @return T|U 166 | */ 167 | public function tryReceiveOr($default) { 168 | if($this->state instanceof SendingChannelState) { 169 | [$value, $sender] = array_shift($this->state->queue); 170 | if(count($this->state->queue) === 0){ 171 | $this->state = new EmptyChannelState; 172 | } 173 | $sender(); 174 | return $value; 175 | } 176 | 177 | return $default; 178 | } 179 | 180 | public function getSendQueueSize() : int { 181 | if($this->state instanceof SendingChannelState){ 182 | return count($this->state->queue); 183 | } 184 | 185 | return 0; 186 | } 187 | 188 | public function getReceiveQueueSize() : int { 189 | if($this->state instanceof ReceivingChannelState){ 190 | return count($this->state->queue); 191 | } 192 | 193 | return 0; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /await-generator/src/SOFe/AwaitGenerator/EmptyChannelState.php: -------------------------------------------------------------------------------- 1 | 39 | */ 40 | public static function empty($ret = null) : Generator{ 41 | false && yield; 42 | return $ret; 43 | } 44 | 45 | /** 46 | * Returns a generator that yields nothing and throws $throwable 47 | * 48 | * @template T of Throwable 49 | * @param Throwable $throwable 50 | * 51 | * @return Generator 52 | * @throws Throwable 53 | * 54 | * @phpstan-param T $throwable 55 | * @phpstan-return Generator 56 | * @throws T 57 | */ 58 | public static function throw(Throwable $throwable) : Generator{ 59 | false && yield; 60 | throw $throwable; 61 | } 62 | 63 | /** 64 | * Returns a generator that never returns. 65 | * 66 | * Since await-generator does not maintain a runtime, 67 | * calling `Await::g2c(GeneratorUtil::pending())` does not leak memory. 68 | * 69 | * @phpstan-return Generator 70 | */ 71 | public static function pending() : Generator{ 72 | yield from Await::promise(function() : void{}); 73 | throw new AssertionError("this line is unreachable"); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /await-generator/src/SOFe/AwaitGenerator/InterruptException.php: -------------------------------------------------------------------------------- 1 | |null */ 39 | private ?array $onLoaded = []; 40 | private $value; 41 | 42 | /** 43 | * @param Closure(): Generator $loader 44 | */ 45 | public function __construct(Closure $loader){ 46 | Await::f2c(function() use($loader) { 47 | $this->value = yield from $loader(); 48 | $onLoaded = $this->onLoaded; 49 | $this->onLoaded = null; 50 | 51 | if($onLoaded === null){ 52 | throw new AssertionError("loader is called twice on the same object"); 53 | } 54 | 55 | foreach($onLoaded as $closure){ 56 | $closure(); 57 | } 58 | }); 59 | } 60 | 61 | /** 62 | * @return array{Loading, Closure(T): void} 63 | */ 64 | public static function byCallback() : array{ 65 | $callback = null; 66 | $loading = new self(function() use(&$callback){ 67 | return yield from Await::promise(function($resolve) use(&$callback){ 68 | $callback = $resolve; 69 | }); 70 | }); 71 | return [$loading, $callback]; 72 | } 73 | 74 | /** 75 | * @return Generator 76 | */ 77 | public function get() : Generator{ 78 | if($this->onLoaded !== null){ 79 | try { 80 | // $key holds the object reference directly instead of the key to avoid GC causing spl_object_id duplicate 81 | $key = null; 82 | 83 | yield from Await::promise(function($resolve) use(&$key) { 84 | $key = $resolve; 85 | $this->onLoaded[spl_object_id($key)] = $resolve; 86 | }); 87 | } finally { 88 | if($key !== null) { 89 | unset($this->onLoaded[spl_object_id($key)]); 90 | } 91 | } 92 | } 93 | 94 | return $this->value; 95 | } 96 | 97 | /** 98 | * @template U 99 | * @param U $default 100 | * @return T|U 101 | */ 102 | public function getSync($default) { 103 | return $this->onLoaded === null ? $this->value : $default; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /await-generator/src/SOFe/AwaitGenerator/Mutex.php: -------------------------------------------------------------------------------- 1 | */ 50 | private array $queue = []; 51 | 52 | /** 53 | * Returns whether the mutex is idle, 54 | * i.e. not acquired by any coroutine. 55 | */ 56 | public function isIdle() : bool{ 57 | return !$this->acquired; 58 | } 59 | 60 | public function acquire() : Generator{ 61 | if(!$this->acquired){ 62 | // Mutex is idle, no extra work to do 63 | $this->acquired = true; 64 | return; 65 | } 66 | 67 | $this->queue[] = yield Await::RESOLVE; 68 | 69 | yield Await::ONCE; 70 | 71 | if(!$this->acquired) { 72 | throw new AssertionError("Mutex->acquired should remain true if queue is nonempty"); 73 | } 74 | } 75 | 76 | public function release() : void{ 77 | if(!$this->acquired){ 78 | throw new RuntimeException("Attempt to release a released mutex"); 79 | } 80 | 81 | if(count($this->queue) === 0){ 82 | // Mutex is now idle, just clean up. 83 | $this->acquired = false; 84 | return; 85 | } 86 | 87 | $next = array_shift($this->queue); 88 | 89 | // When this call completes, $next may or may not be complete, 90 | // and $this->queue may or may not be modified. 91 | // `release()` may also have been called within `$next()`. 92 | // Therefore, we must not do anything after this call, 93 | // and leave the changes like setting $this->acquired to false to the other release call. 94 | $next(); 95 | } 96 | 97 | /** 98 | * @template T 99 | * @param Closure(): Generator $generatorClosure 100 | * @return Generator 101 | */ 102 | public function runClosure(Closure $generatorClosure) : Generator{ 103 | return yield from $this->run($generatorClosure()); 104 | } 105 | 106 | /** 107 | * @template T 108 | * @param Generator $generator 109 | * @return Generator 110 | */ 111 | public function run(Generator $generator) : Generator{ 112 | yield from $this->acquire(); 113 | try{ 114 | return yield from $generator; 115 | }finally{ 116 | $this->release(); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /await-generator/src/SOFe/AwaitGenerator/PromiseState.php: -------------------------------------------------------------------------------- 1 | state === self::STATE_PENDING); 48 | 49 | $this->state = self::STATE_RESOLVED; 50 | $this->resolved = $value; 51 | } 52 | 53 | public function reject(Throwable $value) : void{ 54 | assert($this->state === self::STATE_PENDING); 55 | 56 | $this->state = self::STATE_REJECTED; 57 | $this->rejected = $value; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /await-generator/src/SOFe/AwaitGenerator/PubSub.php: -------------------------------------------------------------------------------- 1 | [] */ 33 | private array $subscribers = []; 34 | 35 | /** 36 | * If a subscriber is lagging for more than $maxLag items behind, 37 | * a RuntimeException is thrown to help detect memory leaks in advance. 38 | * Set it to `null` to disable the check. 39 | */ 40 | public function __construct( 41 | private ?int $maxLag = 10000, 42 | ) {} 43 | 44 | /** 45 | * Publishes a message and return. 46 | * 47 | * This method does not wait for the event to be actually received by subscribers. 48 | * 49 | * @phpstan-param T $item 50 | */ 51 | public function publish($item) : void{ 52 | foreach($this->subscribers as $subscriber) { 53 | $subscriber->sendWithoutWait($item); 54 | if($this->maxLag !== null && $subscriber->getSendQueueSize() > $this->maxLag) { 55 | throw new RuntimeException("A subscriber has been lagging for $this->maxLag items. Forgot to call \$traverser->interrupt()?"); 56 | } 57 | } 58 | } 59 | 60 | /** 61 | * Subscribes to the messages of this topic. 62 | * 63 | * The returned traverser does not return any messages published prior to calling `subscribe()`. 64 | * 65 | * Subscribers are tracked in the `PubSub`. 66 | * To avoid memory leak, 67 | * callers to this method must interrupt the traverser in a `finally` block: 68 | * WARNING: Otherwise, a RuntimeException will be thrown once `maxLag` is 69 | * reached! 70 | * 71 | * ``` 72 | * $sub = $pubsub->subscribe(); 73 | * try { 74 | * while($sub->next($item)) { 75 | * // do something with $item 76 | * } 77 | * } finally { 78 | * yield from $sub->interrupt(); 79 | * } 80 | * ``` 81 | * 82 | * @return Traverser 83 | */ 84 | public function subscribe() : Traverser{ 85 | $channel = new Channel; 86 | 87 | return Traverser::fromClosure(function() use($channel){ 88 | try{ 89 | $this->subscribers[spl_object_id($channel)] = $channel; 90 | 91 | while(true) { 92 | $item = yield from $channel->receive(); 93 | yield $item => Traverser::VALUE; 94 | } 95 | }finally{ 96 | unset($this->subscribers[spl_object_id($channel)]); 97 | } 98 | }); 99 | } 100 | 101 | public function isEmpty() : bool { 102 | return count($this->subscribers) === 0; 103 | } 104 | 105 | public function getSubscriberCount() : int { 106 | return count($this->subscribers); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /await-generator/src/SOFe/AwaitGenerator/RaceLostException.php: -------------------------------------------------------------------------------- 1 | */ 31 | public array $queue = []; 32 | } 33 | -------------------------------------------------------------------------------- /await-generator/src/SOFe/AwaitGenerator/SendingChannelState.php: -------------------------------------------------------------------------------- 1 | */ 31 | public array $queue = []; 32 | } 33 | -------------------------------------------------------------------------------- /await-generator/src/SOFe/AwaitGenerator/Traverser.php: -------------------------------------------------------------------------------- 1 | Await::VALUE;` 35 | * to stop and output the value. 36 | * 37 | * @template I 38 | */ 39 | final class Traverser{ 40 | public const VALUE = "traverse.value"; 41 | public const MAX_INTERRUPTS = 16; 42 | 43 | /** @var Generator */ 44 | private $inner; 45 | 46 | public function __construct(Generator $inner){ 47 | $this->inner = $inner; 48 | } 49 | 50 | /** 51 | * @phpstan-param Closure(): Generator $closure 52 | */ 53 | public static function fromClosure(Closure $closure) : self{ 54 | return new self($closure()); 55 | } 56 | 57 | /** 58 | * Creates a future that starts the next iteration of the underlying coroutine, 59 | * and assigns the next yielded value to `$valueRef` and returns true. 60 | * 61 | * Returns false if there are no more values. 62 | * 63 | * @param-out I $valueRef 64 | */ 65 | public function next(mixed &$valueRef) : Generator{ 66 | while($this->inner->valid()){ 67 | $k = $this->inner->key(); 68 | $v = $this->inner->current(); 69 | 70 | if($v === self::VALUE){ 71 | $this->inner->next(); 72 | $valueRef = $k; 73 | return true; 74 | }else{ 75 | // fallback to parent async context 76 | $this->inner->send(yield $k => $v); 77 | } 78 | } 79 | 80 | return false; 81 | } 82 | 83 | /** 84 | * Asynchronously waits for all remaining values in the underlying iterator 85 | * and collects them into a linear array. 86 | * 87 | * @return Generator> 88 | */ 89 | public function collect() : Generator{ 90 | $array = []; 91 | while(yield from $this->next($value)){ 92 | $array[] = $value; 93 | } 94 | return $array; 95 | } 96 | 97 | /** 98 | * Throw an exception into the underlying generator repeatedly 99 | * so that all `finally` blocks can get asynchronously executed. 100 | * 101 | * If the underlying generator throws an exception not identical to `$ex`, 102 | * this function will return the new exceptioin. 103 | * Returns null if the underlying generator successfully terminated or throws. 104 | * 105 | * Throws `AwaitException` if `$attempts` throws were performed 106 | * and the iterator is still executing. 107 | * 108 | * All values iterated during interruption are discarded. 109 | */ 110 | public function interrupt(Throwable $ex = null, int $attempts = self::MAX_INTERRUPTS) : Generator{ 111 | $ex = $ex ?? InterruptException::get(); 112 | for($i = 0; $i < $attempts; $i++){ 113 | try{ 114 | $this->inner->throw($ex); 115 | $hasMore = yield from $this->next($_); 116 | if(!$hasMore){ 117 | return null; 118 | } 119 | }catch(Throwable $caught){ 120 | if($caught === $ex){ 121 | $caught = null; 122 | } 123 | return $caught; 124 | } 125 | } 126 | throw new AwaitException("Generator did not terminate after $attempts interrupts"); 127 | } 128 | 129 | /** 130 | * Returns the inner generator. 131 | * 132 | * Used to provide a shading-agnostic object so that it can be reconstructed in another shading namespace, e.g. for 133 | * ``` 134 | * $namespace1Traverser = new Namespace1\AwaitGenerator\Traverser($this->iter()); 135 | * $namespace2Traverser = new Namespace2\AwaitGenerator\Traverser($namespace1Traverser->asGenerator()); 136 | * ``` 137 | * Then `$namespace1Traverser` and `$namespace2Traverser` are fully interchangeable wherever type check passes. 138 | */ 139 | public function asGenerator() : Generator { 140 | return $this->inner; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /await-generator/src/SOFe/AwaitGenerator/UnawaitedCallbackException.php: -------------------------------------------------------------------------------- 1 | . 4 | -------------------------------------------------------------------------------- /book/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["SOFe"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "await-generator tutorial" 7 | -------------------------------------------------------------------------------- /book/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](intro.md) 4 | - [Generators](generators.md) 5 | - [Asynchronous programming](async.md) 6 | - [Using await-generator](main.md) 7 | - [Awaiting generators](await-gen.md) 8 | - [Using callback-style from generators](await-once.md) 9 | - [Exposing a generator to normal API](f2c-g2c.md) 10 | - [Running generators concurrently](all-race.md) 11 | - [Async iterators](async-iterators.md) 12 | - [Versioning concerns](semver.md) 13 | -------------------------------------------------------------------------------- /book/src/all-race.md: -------------------------------------------------------------------------------- 1 | # Running generators concurrently 2 | In addition to calling multiple generators sequentially, 3 | you can also use `Await::all()` or `Await::race()` to run multiple generators. 4 | 5 | If you have a JavaScript background, you can think of `Generator` objects as promises 6 | and `Await::all()` and `Await::race()` are just `Promise.all()` and `Promise.race()`. 7 | 8 | ## `Await::all()` 9 | `Await::all()` allows you to run an array of generators at the same time. 10 | If you yield `Await::all($array)`, your function resumes when 11 | all generators in `$array` have finished executing. 12 | 13 | ```php 14 | function loadData(string $name): Generator { 15 | // some async logic 16 | return strlen($name); 17 | } 18 | 19 | $array = [ 20 | "SOFe" => $this->loadData("SOFe"), // don't yield it yet! 21 | "PEMapModder" => $this->loadData("PEMapModder"), 22 | ]; 23 | $results = yield from Await::all($array); 24 | var_dump($result); 25 | ``` 26 | 27 | Output: 28 | ``` 29 | array(2) { 30 | ["SOFe"]=> 31 | int(4) 32 | ["PEMapModder"]=> 33 | int(11) 34 | } 35 | ``` 36 | 37 | Yielding `Await::all()` will throw an exception 38 | as long as *any* of the generators throw. 39 | The error condition will not wait until all generators return. 40 | 41 | ## `Await::race()` 42 | `Await::race()` is like `Await::all()`, 43 | but it resumes as long as *any* of the generators return or throw. 44 | The returned value of `yield from` is a 2-element array containing the key and the value. 45 | 46 | ```php 47 | function sleep(int $time): Generator { 48 | // Let's say this is an await version of `scheduleDelayedTask` 49 | return $time; 50 | } 51 | 52 | function main(): Generator { 53 | [$k, $v] = yield from Await::race([ 54 | "two" => $this->sleep(2), 55 | "one" => $this->sleep(1), 56 | ]); 57 | var_dump($k); // string(3) "one" 58 | var_dump($v); // int(1) 59 | } 60 | ``` 61 | -------------------------------------------------------------------------------- /book/src/async-iterators.md: -------------------------------------------------------------------------------- 1 | # Async iterators 2 | In normal PHP functions, there is only a single return value. 3 | If we want to return data progressively, 4 | generators should have been used, 5 | where the user can iterate on the returned generator. 6 | However, if the user intends to perform async operations 7 | in every step of progressive data fetching, 8 | the `next()` method needs to be async too. 9 | In other languages, this is called "async generator" or "async iterator". 10 | However, since await-generator has hijacked the generator syntax, 11 | it is not possible to create such structures directly. 12 | 13 | Instead, await-generator exposes the `Traverser` class, 14 | which is an extension to the normal await-generator syntax, 15 | providing an additional yield mode `Traverser::VALUE`, 16 | which allows an async function to yield async iteration values. 17 | A key (the current traversed value) is passed with `Traverser::VALUE`. 18 | The resultant generator is wrapped with the `Traverser` class, 19 | which provides an asynchronous `next()` method that 20 | executes the generator asynchronously and returns the next traversed value, 21 | 22 | ## Example 23 | In normal PHP, we may have an line iterator on a file stream like this: 24 | 25 | ```php 26 | function lines(string $file) : Generator { 27 | $fh = fopen($file, "rt"); 28 | try { 29 | while(($line = fgets($fh)) !== false) { 30 | yield $line; 31 | } 32 | } finally { 33 | fclose($fh); 34 | } 35 | } 36 | 37 | function count_empty_lines(string $file) { 38 | $count = 0; 39 | foreach(lines($file) as $line) { 40 | if(trim($line) === "") $count++; 41 | } 42 | return $count; 43 | } 44 | ``` 45 | 46 | What if we have async versions of `fopen`, `fgets` and `fclose` 47 | and want to reimplement this `lines` function as async? 48 | 49 | We would use the `Traverser` class instead: 50 | 51 | ```php 52 | function async_lines(string $file) : Generator { 53 | $fh = yield from async_fopen($file, "rt"); 54 | try { 55 | while(true) { 56 | $line = yield from async_fgets($fh); 57 | if($line === false) { 58 | return; 59 | } 60 | yield $line => Traverser::VALUE; 61 | } 62 | } finally { 63 | yield from async_fclose($fh); 64 | } 65 | } 66 | 67 | function async_count_empty_lines(string $file) : Generator { 68 | $count = 0; 69 | 70 | $traverser = new Traverser(async_lines($file)); 71 | while(yield from $traverser->next($line)) { 72 | if(trim($line) === "") $count++; 73 | } 74 | 75 | return $count; 76 | } 77 | ``` 78 | 79 | ## Interrupting a generator 80 | Yielding inside `finally` may cause a crash 81 | if the generator is not yielded fully. 82 | If you perform async operations in the `finally` block, 83 | you **must** drain the traverser fully. 84 | If you don't want the iterator to continue executing, 85 | you may use the `yield $traverser->interrupt()` method, 86 | which keeps throwing the first parameter 87 | (`SOFe\AwaitGenerator\InterruptException` by default) 88 | into the async iterator until it stops executing. 89 | Beware that `interrupt` may throw an `AwaitException` 90 | if the underlying generator catches exceptions during `yield Traverser::VALUE`s 91 | (hence consuming the interrupts). 92 | 93 | It is not necessary to interrupt the traverser 94 | if there are no `finally` blocks containing `yield` statements. 95 | -------------------------------------------------------------------------------- /book/src/async.md: -------------------------------------------------------------------------------- 1 | # Asynchronous programming 2 | Traditionally, when you call a function, 3 | it performs the required actions and returns after they're done. 4 | In asynchronous programming, 5 | the program logic may be executed *after* a function returns. 6 | 7 | This leads to two problems. 8 | First, the function can't return you with any useful results, 9 | because the results are only available after the logic completes. 10 | Second, you may do something else assuming the logic is completed, 11 | which leads to a bug. 12 | For example: 13 | 14 | ```php 15 | private $data; 16 | 17 | function loadData($player) { 18 | // we will set $this->data[$player] some time later. 19 | } 20 | 21 | function main() { 22 | $this->loadData("SOFe"); 23 | echo $this->data["SOFe"]; // Undefined offset "SOFe" 24 | } 25 | ``` 26 | 27 | Here, `loadData` is the function that loads data asynchronously. 28 | `main` is implemented incorrectly, assuming that `loadData` is synchronous, 29 | i.e. it assumes that `$this->data["SOFe"]` is initialized. 30 | 31 | ## Using callbacks 32 | One of the simplest ways to solve this problem is to use callbacks. 33 | The caller can pass a closure to the async function, 34 | then the async function will run this closure when it has finished. 35 | An example function signature would be like this: 36 | 37 | ```php 38 | function loadData($player, Closure $callback) { 39 | // $callback will be called when player data have been loaded. 40 | } 41 | 42 | function main() { 43 | $this->loadData("SOFe", function() { 44 | echo $this->data["SOFe"]; // this is guaranteed to work now 45 | }); 46 | } 47 | ``` 48 | 49 | The `$callback` will be called when some other logic happens. 50 | This depends on the implementation of the `loadData` logic. 51 | This may be when a player sends a certain packet, 52 | or when a scheduled task gets run, 53 | or other scenarios. 54 | 55 | ### More complex callbacks 56 | (This section is deliberately complicated and hard to understand, 57 | because the purpose is to tell you that using callbacks is bad.) 58 | 59 | What if we want to call multiple async functions one by one? 60 | In synchronous code, it would be simple: 61 | ```php 62 | $a = a(); 63 | $b = b($a); 64 | $c = c($b); 65 | $d = d($c); 66 | var_dump($d); 67 | ``` 68 | 69 | In async code, we might need to do this (let's say `a`, `b`, `c`, `d` are async): 70 | 71 | ```php 72 | a(function($a) { 73 | b($a, function($b) { 74 | c($b, function($c) { 75 | d($c, function($d) { 76 | var_dump($d); 77 | }); 78 | }); 79 | }); 80 | }); 81 | ``` 82 | 83 | Looks ugly, but readable enough. 84 | It might look more confusing if we need to pass `$a` to `$d` though. 85 | 86 | But what if we want to do if/else? 87 | In synchronous code, it looks like this: 88 | ```php 89 | $a = a(); 90 | if($a !== null) { 91 | $output = b($a); 92 | } else { 93 | $output = c() + 1; 94 | } 95 | 96 | $d = very_complex_code($output); 97 | $e = that_deals_with($output); 98 | var_dump($d + $e + $a); 99 | ``` 100 | 101 | In async code, it is much more confusing: 102 | ```php 103 | a(function($a) { 104 | if($a !== null) { 105 | b($a, function($output) use($a) { 106 | $d = very_complex_code($output); 107 | $e = that_deals_with($output); 108 | var_dump($d + $e + $a); 109 | }); 110 | } else { 111 | c(function($output) use($a) { 112 | $output = $output + 1; 113 | $d = very_complex_code($output); 114 | $e = that_deals_with($output); 115 | var_dump($d + $e + $a); 116 | }); 117 | } 118 | }); 119 | ``` 120 | 121 | But we don't want to copy-paste the three lines of duplicated code. 122 | Maybe we can assign the whole closure to a variable: 123 | 124 | ```php 125 | a(function($a) { 126 | $closure = function($output) use($a) { 127 | $d = very_complex_code($output); 128 | $e = that_deals_with($output); 129 | var_dump($d + $e + $a); 130 | }; 131 | 132 | if($a !== null) { 133 | b($a, $closure); 134 | } else { 135 | c(function($output) use($closure) { 136 | $closure($output + 1); 137 | }); 138 | } 139 | }); 140 | ``` 141 | 142 | Oh no, this is getting out of control. 143 | Think about how complicated this would become when 144 | we want to use asynchronous functions in loops! 145 | 146 | The await-generator library allows users to write async code in synchronous style. 147 | As you might have guessed, the `yield` keyword is a replacement for callbacks. 148 | -------------------------------------------------------------------------------- /book/src/await-gen.md: -------------------------------------------------------------------------------- 1 | # Awaiting generators 2 | Since every async function is implemented as a generator function, 3 | simply calling it will not have any effects. 4 | Instead, you have to `yield from` the generator. 5 | 6 | ```php 7 | function a(): Generator { 8 | // some other async logic here 9 | return 1; 10 | } 11 | 12 | function main(): Generator { 13 | $a = yield from $this->a(); 14 | var_dump($a); 15 | } 16 | ``` 17 | 18 | It is easy to forget to `yield from` the generator. 19 | 20 | 21 | 22 | ## Handling errors 23 | `yield from` will throw an exception 24 | if the generator function you called threw an exception. 25 | 26 | ```php 27 | function err(): Generator { 28 | // some other async logic here 29 | throw new Exception("Test"); 30 | } 31 | 32 | function main(): Generator { 33 | try { 34 | yield from err(); 35 | } catch(Exception $e) { 36 | var_dump($e->getMessage()); // string(4) "Test" 37 | } 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /book/src/await-once.md: -------------------------------------------------------------------------------- 1 | # Using callback-style from generators 2 | Although it is easier to work with generator functions, 3 | ultimately, you will need to work with functions that do not use await-generator. 4 | In that case, callbacks are easier to use. 5 | A callback `$resolve` can be acquired using `Await::promise`. 6 | 7 | ```php 8 | function a(Closure $callback): void { 9 | // The other function that uses callbacks. 10 | // Let's assume this function will call $callback("foo") some time later. 11 | } 12 | 13 | function main(): Generator { 14 | return yield from Await::promise(fn($resolve) => a($resolve)); 15 | } 16 | ``` 17 | 18 | Some callback-style async functions may accept another callback for exception handling. This callback can be acquired by taking a second parameter `$reject`. 19 | 20 | ```php 21 | function a(Closure $callback, Closure $onError): void { 22 | // The other function that uses callbacks. 23 | // Let's assume this function will call $callback("foo") some time later. 24 | } 25 | 26 | function main(): Generator { 27 | return yield from Await::promise(fn($resolve, $reject) => a($resolve, $reject)); 28 | } 29 | ``` 30 | 31 | ## Example 32 | Let's say we want to make a function that sleeps for 20 server ticks, 33 | or throws an exception if the task is cancelled: 34 | 35 | ```php 36 | use pocketmine\scheduler\Task; 37 | 38 | public function sleep(): Generator { 39 | yield from Await::promise(function($resolve, $reject) { 40 | $task = new class($resolve, $reject) extends Task { 41 | private $resolve; 42 | private $reject; 43 | public function __construct($resolve, $reject) { 44 | $this->resolve = $resolve; 45 | $this->reject = $reject; 46 | } 47 | public function onRun(int $tick) { 48 | ($this->resolve)(); 49 | } 50 | public function onCancel() { 51 | ($this->reject)(new \Exception("Task cancelled")); 52 | } 53 | }; 54 | $this->getServer()->getScheduler()->scheduleDelayedTask($task, 20); 55 | }); 56 | } 57 | ``` 58 | 59 | This is a bit complex indeed, but it gets handy once we have this function defined! 60 | Let's see what we can do with a countdown: 61 | 62 | ```php 63 | function countdown($player) { 64 | for($i = 10; $i > 0; $i--) { 65 | $player->sendMessage("$i seconds left"); 66 | yield from $this->sleep(); 67 | } 68 | 69 | $player->sendMessage("Time's up!"); 70 | } 71 | ``` 72 | 73 | This is much simpler than using `ClosureTask` in a loop! 74 | -------------------------------------------------------------------------------- /book/src/f2c-g2c.md: -------------------------------------------------------------------------------- 1 | # Exposing a generator to normal API 2 | Recall that generator functions do not do anything when they get called. 3 | Eventually, we have to call the generator function from a non-await-generator context. 4 | We can use the `Await::g2c` function for this: 5 | 6 | ```php 7 | private function generateFunction(): Generator { 8 | // some async logic 9 | } 10 | 11 | Await::g2c($this->generatorFunction()); 12 | ``` 13 | 14 | Sometimes we want to write the generator function as a closure 15 | and pass it directly: 16 | 17 | ```php 18 | Await::f2c(function(): Generator { 19 | // some async logic 20 | }); 21 | ``` 22 | 23 | You can also use `Await::g2c`/`Await::f2c` 24 | to schedule a separate async function in the background. 25 | -------------------------------------------------------------------------------- /book/src/generators.md: -------------------------------------------------------------------------------- 1 | # Generators 2 | A PHP function that contains a `yield` keyword is called a "generator function". 3 | 4 | ```php 5 | function foo() { 6 | echo "hi!\n"; 7 | yield; 8 | echo "working hard.\n"; 9 | yield; 10 | echo "bye!\n"; 11 | } 12 | ``` 13 | 14 | When you call this function, it does not do anything 15 | (it doesn't even echo "hi"). 16 | Instead, you get a [`Generator`](https://php.net/class.generator) object, 17 | which lets you control the execution of the function. 18 | 19 | Let's tell PHP to start running this function: 20 | 21 | ```php 22 | $generator = foo(); 23 | echo "Let's start foo\n"; 24 | $generator->rewind(); 25 | echo "foo stopped\n"; 26 | ``` 27 | 28 | You will get this output: 29 | 30 | ``` 31 | Let's start foo 32 | hi! 33 | foo stopped 34 | ``` 35 | 36 | The function stops when there is a `yield` statement. 37 | We can tell the function to continue running using the `Generator` object: 38 | 39 | ```php 40 | $generator->send(null); 41 | ``` 42 | 43 | And this additional output: 44 | ``` 45 | working hard. 46 | ``` 47 | 48 | Now it stops again at the next `yield`. 49 | 50 | ## Sending data into/out of the `Generator` 51 | We can put a value behind the `yield` keyword to send data to the controller: 52 | 53 | ```php 54 | function bar() { 55 | yield 1; 56 | } 57 | $generator = bar(); 58 | $generator->rewind(); 59 | var_dump($generator->current()); 60 | ``` 61 | 62 | ``` 63 | int(1) 64 | ``` 65 | 66 | Similarly, we can send data back to the function. 67 | If you use `yield [value]` as an expression, 68 | it is resolved into the value passed in `$generator->send()`. 69 | 70 | ```php 71 | function bar() { 72 | $receive = yield; 73 | var_dump($receive); 74 | } 75 | $generator = bar(); 76 | $generator->rewind(); 77 | $generator->send(2); 78 | ``` 79 | 80 | ``` 81 | int(2) 82 | ``` 83 | 84 | Furthermore, the function can eventually "return" a value. 85 | This return value is not handled the same way as a `yield`; 86 | it is obtained using `$generator->getReturn()`. 87 | However, the return type hint must always be `Generator` 88 | no matter what you return, or if you don't return: 89 | 90 | ```php 91 | function qux(): Generator { 92 | yield 1; 93 | return 2; 94 | } 95 | ``` 96 | 97 | ## Calling another generator 98 | You can call another generator in a generator, 99 | which will pass through all the yielded values 100 | and send back all the sent values 101 | using the `yield from` syntax. 102 | The `yield from` expression resolves to the return value of the generator. 103 | 104 | ```php 105 | function test($value): Generator { 106 | $send = yield $value; 107 | return $send; 108 | } 109 | 110 | function main(): Generator { 111 | $a = yield from test(1); 112 | $b = yield from test(2); 113 | var_dump($a + $b); 114 | } 115 | 116 | $generator = main(); 117 | $generator->rewind(); 118 | var_dump($generator->current()); 119 | $generator->send(3); 120 | var_dump($generator->current()); 121 | $generator->send(4); 122 | ``` 123 | 124 | ``` 125 | int(1) 126 | int(2) 127 | int(7) 128 | ``` 129 | 130 | ## Hacking generators 131 | Sometimes we want to make a generator function that does not yield at all. 132 | In that case, you can write `0 && yield;` at the start of the function; 133 | this will make your function a generator function, but it will not yield anything. 134 | As of PHP 7.4.0, `0 && yield;` is a no-op, 135 | which means it will not affect your program performance 136 | even if you run this line many times. 137 | 138 | ```php 139 | function emptyGenerator(): Generator { 140 | 0 && yield; 141 | return 1; 142 | } 143 | 144 | $generator = emptyGenerator(); 145 | var_dump($generator->next()); 146 | var_dump($generator->getReturn()); 147 | ``` 148 | 149 | ``` 150 | NULL 151 | int(1) 152 | ``` 153 | -------------------------------------------------------------------------------- /book/src/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This is a step-by-step tutorial of await-generator 4 | for newcomers with only basic PHP knowledge. 5 | 6 | await-generator plays tricks on a PHP feature called "generator". 7 | It allows you to write code more easily in a style called "asynchronous". 8 | 9 | This tutorial involves concepts from PocketMine-MP, 10 | a server software for Minecraft written in PHP. 11 | The target audience is plugin developers for PocketMine-MP. 12 | -------------------------------------------------------------------------------- /book/src/main.md: -------------------------------------------------------------------------------- 1 | # Using await-generator 2 | 3 | await-generator provides an alternative approach to asynchronous programming. 4 | Functions that use async logic are written in generator functions. 5 | The main trick is that your function pauses (using `yield`) 6 | when you want to wait for a value, 7 | then await-generator resumes your function and 8 | sends you the return value from the async function via `$generator->send()`. 9 | -------------------------------------------------------------------------------- /book/src/semver.md: -------------------------------------------------------------------------------- 1 | # Versioning concerns 2 | await-generator is guaranteed to be 3 | shade-compatible, backward-compatible and partly forward-compatible. 4 | 5 | Await-generator uses generator objects for communication. 6 | The values passed through generators (such as `Await::ONCE`) 7 | are constant strings that are guaranteed to remain unchanged within a major version. 8 | Therefore, multiple shaded versions of await-generator can be used together. 9 | 10 | New constants may be added over minor versions. 11 | Older versions will crash when they receive constants from newer versions. 12 | 13 | Only `Await::f2c`/`Await::g2c` loads await-generator code. 14 | Functions that merely `yield` values from the `Await` class 15 | will not affect the execution logic. 16 | Therefore, the version of await-generator 17 | on which `Await::f2c`/`Await::g2c` is called 18 | determines the highest version to use. 19 | 20 | (For those who do not use virion framework and are confused: 21 | await-generator is versioned just like the normal semver for you.) 22 | -------------------------------------------------------------------------------- /chs/README.md: -------------------------------------------------------------------------------- 1 | [Eng](../README.md) | [繁](../zho) | 简 2 | # await-generator 3 | [![Build Status][ci-badge]][ci-page] 4 | [![Codecov][codecov-badge]][codecov-page] 5 | 6 | 给予 PHP 「async/await 等待式异步」(代码流控制)设计模式的程序库。 7 | 8 | ## 文档 9 | 建议先阅读 [await-generator 教学(中文版赶工中)](../book),它涵盖了生成器、传统「回调式异步」,再到 await-generator 等概念的介绍。 10 | 11 | 以下部分名词在 await-generator 教学中都更详细地讲解(「回调」等)。 12 | 13 | ## await-generator 的优势 14 | 传统的异步代码流需要靠回调(匿名函数)来实现。 15 | 每个异步函数都要开新的回调,然后把异步函数后面的代码整个搬进去,导致了代码变成「callback hell 回调地狱」,难以被阅读、管理。 16 |
17 | 点击以查看「回调地狱」例子 18 | 19 | ```php 20 | load_data(function($data) { 21 | $init = count($data) === 0 ? init_data(...) : fn($then) => $then($data); 22 | $init(function($data) { 23 | $output = []; 24 | foreach($data as $k => $datum) { 25 | processData($datum, function($result) use(&$output, $data) { 26 | $output[$k] = $result; 27 | if(count($output) === count($data)) { 28 | createQueries($output, function($queries) { 29 | $run = function($i) use($queries, &$run) { 30 | runQuery($queries[$i], function() use($i, $queries, $run) { 31 | if($i === count($queries)) { 32 | $done = false; 33 | commitBatch(function() use(&$done) { 34 | if(!$done) { 35 | $done = true; 36 | echo "Done!\n"; 37 | } 38 | }); 39 | onUserClose(function() use(&$done) { 40 | if(!$done) { 41 | $done = true; 42 | echo "User closed!\n"; 43 | } 44 | }); 45 | onTimeout(function() use(&$done) { 46 | if(!$done) { 47 | $done = true; 48 | echo "Timeout!\n"; 49 | } 50 | }); 51 | } else { 52 | $run($i + 1); 53 | } 54 | }); 55 | }; 56 | }); 57 | } 58 | }); 59 | } 60 | }); 61 | }); 62 | ``` 63 | 64 |
65 | 如果使用 await-generator ,以上代码就可以被简化为: 66 | 67 | ```php 68 | $data = yield from load_data(); 69 | if(count($data) === 0) $data = yield from init_data(); 70 | $output = yield from Await::all(array_map(fn($datum) => processData($datum), $data)); 71 | $queries = yield from createQueries($output); 72 | foreach($queries as $query) yield from runQuery($query); 73 | [$which, ] = yield from Await::race([ 74 | 0 => commitBatch(), 75 | 1 => onUserClose(), 76 | 2 => onTimeout(), 77 | ]) 78 | echo match($which) { 79 | 0 => "Done!\n", 80 | 1 => "User closed!\n", 81 | 2 => "Timeout!\n", 82 | }; 83 | ``` 84 | 85 | ## 使用后的代码可以维持回溯相容性吗? 86 | 是的, await-generator 不会对已有的接口造成任何限制。 87 | 你可以将所有涉及 await-generator 的代码封闭在程序的内部。 88 | 但你确实应该把生成器函数直接当作程序接口。 89 | 90 | await-generator 会在 `Await::f2c` 开始进行异步代码流控制,你可以将它视为「等待式」至「回调式」的转接头。 91 | 92 | ```php 93 | function oldApi($args, Closure $onSuccess) { 94 | Await::f2c(fn() => $onSuccess(yield from newApi($args))); 95 | } 96 | ``` 97 | 98 | 你也用它来处理错误: 99 | 100 | ```php 101 | function newApi($args, Closure $onSuccess, Closure $onError) { 102 | Await::f2c(function() use($onSuccess, $onError) { 103 | try { 104 | $onSuccess(yield from newApi($args)); 105 | } catch(Exception $ex) { 106 | $onError($ex); 107 | } 108 | }); 109 | } 110 | ``` 111 | 112 | 「回调式」同样可以被 `Await::promise` method 转化成「等待式」。 113 | 它跟 JavaScript 的 `new Promise` 很像: 114 | 115 | ```php 116 | yield from Await::promise(fn($resolve, $reject) => oldFunction($args, $resolve, $reject)); 117 | ``` 118 | 119 | ## await-generator 的*劣势* 120 | await-generator 也有很多经常坑人的地方: 121 | 122 | - 忘了 `yield from` 的代码会毫无作用; 123 | - 如果你的函数没有任何 `yield` 或者 `yield from` , PHP 就不会把它当成生成器函数(在所有应为生成器的函数类型注释中加上 `: Generator` 可减轻影响); 124 | - 如果异步代码没有全面结束, `finally` 里面的代码也不会被执行(例: `Await::promise(fn($resolve) => null)`); 125 | 126 | 尽管一些地方会导致问题, await-generator 的设计模式出 bug 的机会依然比「回调地狱」少 。 127 | 128 | ## 不是有纤程吗? 129 | 虽然这样说很主观,但本人因为以下纤程缺少的特色而相对地不喜欢它: 130 | 131 | ### 靠类型注释就能区分异步、非异步函数 132 | > 先生,你已在暂停的纤程待了三十秒。
133 | > 因为有人实现一个界面时调用了 `Fiber::suspend() ` 。 134 | 135 | ![../../fiber.jpg](https://github.com/SOF3/await-generator/raw/master/fiber.jpeg) 136 | 137 | > 好家伙,我都等不及要回应我的 HTTP 请求了。
138 | > 框架肯定还没把它给超时清除。 139 | 140 | 例如能直观地看出 `$channel->send($value): Generator` 会暂停代码流至有数值被送入生成器; `$channel->sendBuffered($value): void` 141 | 则不会暂停代码流,这个 method 的代码会在一次过执行后回传。 142 | 类型注释通常是不言自明的。 143 | 144 | 当然,用户可以直接调用 `sleep()` ,但大家都应清楚 `sleep()` 会卡住整个线程(就算他们不懂也会在整个「世界」停止时发现)。 145 | 146 | ### 并发状态 147 | 当一个函数被暂停时会发生许多其他的事情。 148 | 调用函数时固然给予了实现者调用可修改状态函数的可能性, 149 | 但是一个正常的、合理的实现,例如 HTTP 请求所调用的函数不应修改你程序库的内部状态。 150 | 但是这个假设对于纤程来说并不成立, 151 | 因为当一个纤程被暂停后,其他纤程仍然可以修改你的内部状态。 152 | 每次你调用任何*可能*会被暂停的函数时,你都必须检查内部状态的可能变化。 153 | 154 | await-generator 相比起纤程,异步、非异步代码能简单区分,且暂停点的确切位置显而易见。 155 | 因此你只需要在已知的暂停点检查状态的变化。 156 | 157 | ### 捕捉暂停点 158 | await-generator 提供了一个叫做「[捕捉][trap-pr]」的功能。 159 | 它允许用户拦截生成器的暂停点和恢复点,在它暂停或恢复前执行一段加的插代码。 160 | 这只需透过向生成器添加一个转接头来实现。甚至不需要 await-generator 引擎的额外支援。 161 | 这目前在纤程中无法做到。 162 | 163 | [book]: https://sof3.github.io/await-generator/master/ 164 | [ci-badge]: https://github.com/SOF3/await-generator/workflows/CI/badge.svg 165 | [ci-page]: https://github.com/SOF3/await-generator/actions?query=workflow%3ACI 166 | [codecov-badge]: https://img.shields.io/codecov/c/github/codecov/example-python.svg 167 | [codecov-page]: https://codecov.io/gh/SOF3/await-generator 168 | [trap-pr]: https://github.com/SOF3/await-generator/pull/106 169 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sof3/await-generator", 3 | "description": "Use async/await in PHP using generators", 4 | "type": "library", 5 | "readme": "README.md", 6 | "license": "apache-2.0", 7 | "authors": [ 8 | { 9 | "name": "SOFe", 10 | "email": "sofe2038@gmail.com" 11 | } 12 | ], 13 | "autoload": { 14 | "psr-0": { 15 | "SOFe\\AwaitGenerator\\": "await-generator/src/" 16 | } 17 | }, 18 | "autoload-dev": { 19 | "psr-0": { 20 | "virion_tests\\SOFe\\AwaitGenerator\\": "tests/" 21 | } 22 | }, 23 | "require": { 24 | "php": "^8.0", 25 | "ext-bcmath": "*" 26 | }, 27 | "require-dev": { 28 | "phpunit/phpunit": "^9", 29 | "phpstan/phpstan": "^0.12.84", 30 | "infection/infection": "^0.18.2 || ^0.20.2 || ^0.26.0", 31 | "composer/package-versions-deprecated": "1.11.99.1" 32 | }, 33 | "scripts": { 34 | "test": "vendor/bin/phpunit", 35 | "analyze": "vendor/bin/phpstan analyze", 36 | "infection": "vendor/bin/infection -s" 37 | }, 38 | "extra": { 39 | "virion":{ 40 | "spec": "3.0", 41 | "namespace-root": "SOFe\\AwaitGenerator" 42 | } 43 | }, 44 | "config": { 45 | "allow-plugins": { 46 | "infection/extension-installer": true 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /fiber.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SOF3/await-generator/7c65a3b1a58e85c008415e14c608cb90f152b9a8/fiber.jpeg -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "await-generator\/src" 5 | ] 6 | }, 7 | "logs": { 8 | "text": "infection.log", 9 | "summary": "infection-summary.log", 10 | "perMutator": "infection-per-mutator.md" 11 | }, 12 | "mutators": { 13 | "@default": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /logic.md: -------------------------------------------------------------------------------- 1 | The following diagrams explain the control flow of await-generator. 2 | 3 | ## Await::ONCE, immediate resolution/rejection + return 4 | 5 | ``` 6 | | Start Resolve 7 | | Await | |---| |-----------------| |-----------| | 8 | | |wakeup ^ \ rewind yield RESOLVE / \ send callable / queues internally \ yield COLLECT / clear queue \ send result return / 9 | | | | \______ _____________/ \_____________ / \ _____________/ \___________ ______/ 10 | | v sleep| \ / \ / \ / \ / 11 | | Generator |---| |---| / \ |---| |---| 12 | | \ resolves immediately / \ / 13 | | \ ____________________/ \ / 14 | | \ / \ / 15 | | VoidCallback |---| |---| 16 | | 17 | ``` 18 | -------------------------------------------------------------------------------- /phpstan-ignore.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: "#^Unreachable statement \\- code above always terminates\\.$#" 5 | count: 1 6 | path: await-generator/src/SOFe/AwaitGenerator/Await.php 7 | 8 | - # https://github.com/phpstan/phpstan/issues/6344 9 | message: "#^Negated boolean expression is always false\\.$#" 10 | count: 1 11 | path: await-generator/src/SOFe/AwaitGenerator/Mutex.php 12 | 13 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-ignore.neon 3 | parameters: 4 | level: 5 5 | ignoreErrors: 6 | - "#^(Left|Right) side of && is always false\\.$#" 7 | paths: 8 | - await-generator/src 9 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | await-generator 11 | 12 | 13 | 14 | tests/SOFe/AwaitGenerator 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/SOFe/AwaitGenerator/AwaitTest.php: -------------------------------------------------------------------------------- 1 | rewind(); 62 | self::assertFalse($generator->valid()); 63 | self::assertEquals($rand, $generator->getReturn()); 64 | } 65 | 66 | public function testThrowsGeneratorCreation() : void{ 67 | $exception = new DummyException(); 68 | $generator = GeneratorUtil::throw($exception); 69 | $this->expectExceptionObject($exception); 70 | $generator->rewind(); 71 | } 72 | 73 | public function testEmpty() : void{ 74 | $rand = 0xB16B00B5; 75 | $generator = GeneratorUtil::empty($rand); 76 | $resolveCalled = false; 77 | $rejectCalled = false; 78 | Await::g2c($generator, function($arg) use (&$resolveCalled, &$resolveValue) : void{ 79 | $resolveCalled = true; 80 | $resolveValue = $arg; 81 | }, function() use (&$rejectCalled) : void{ 82 | $rejectCalled = true; 83 | }); 84 | self::assertTrue($resolveCalled); 85 | self::assertFalse($rejectCalled); 86 | self::assertEquals($rand, $resolveValue); 87 | } 88 | 89 | public function testImmediateThrow() : void{ 90 | $exception = new DummyException(); 91 | $generator = GeneratorUtil::throw($exception); 92 | $resolveCalled = false; 93 | Await::g2c($generator, function() use (&$resolveCalled) : void{ 94 | $resolveCalled = true; 95 | }, function($ex) use (&$rejectCalled, &$rejectValue) : void{ 96 | $rejectCalled = true; 97 | $rejectValue = $ex; 98 | }); 99 | self::assertFalse($resolveCalled); 100 | self::assertTrue($rejectCalled); 101 | self::assertEquals($exception, $rejectValue); 102 | } 103 | 104 | public function testBadYield() : void{ 105 | Await::f2c(function() : Generator{ 106 | yield "(some invalid value)"; 107 | }, function(){ 108 | self::fail("unexpected resolve call"); 109 | }, function($ex) : void{ 110 | self::assertInstanceOf(AwaitException::class, $ex); 111 | /** @var AwaitException $ex */ 112 | self::assertEquals("Unknown yield value", $ex->getMessage()); 113 | }); 114 | } 115 | 116 | public function testOneUnresolved() : void{ 117 | Await::f2c(function() : Generator{ 118 | yield; 119 | }, function(){ 120 | self::fail("unexpected resolve call"); 121 | }, function($ex) : void{ 122 | self::assertInstanceOf(UnawaitedCallbackException::class, $ex); 123 | /** @var AwaitException $ex */ 124 | self::assertEquals("Resolution of await generator is disallowed when Await::RESOLVE or Await::REJECT was yielded but is not awaited through Await::ONCE, Await::ALL or Await::RACE", $ex->getMessage()); 125 | }); 126 | } 127 | 128 | public function testRejectOnly() : void{ 129 | Await::f2c(function() : Generator{ 130 | yield Await::REJECT; 131 | }, function(){ 132 | self::fail("unexpected resolve call"); 133 | }, function($ex) : void{ 134 | self::assertInstanceOf(AwaitException::class, $ex); 135 | /** @var AwaitException $ex */ 136 | self::assertEquals("Cannot yield Await::REJECT without yielding Await::RESOLVE first; they must be yielded in pairs", $ex->getMessage()); 137 | }); 138 | } 139 | 140 | public function testDoubleReject() : void{ 141 | $firstRejectOk = false; 142 | Await::f2c(function() use (&$firstRejectOk) : Generator{ 143 | yield; 144 | yield Await::REJECT; 145 | $firstRejectOk = true; 146 | yield Await::REJECT; 147 | }, function(){ 148 | self::fail("unexpected resolve call"); 149 | }, function($ex) : void{ 150 | self::assertInstanceOf(AwaitException::class, $ex); 151 | /** @var AwaitException $ex */ 152 | self::assertEquals("Cannot yield Await::REJECT without yielding Await::RESOLVE first; they must be yielded in pairs", $ex->getMessage()); 153 | }); 154 | 155 | self::assertTrue($firstRejectOk, "first paired rejection failed"); 156 | } 157 | 158 | public function testUnhandledImmediateReject() : void{ 159 | $ex = new DummyException(); 160 | $generator = GeneratorUtil::throw($ex); 161 | try{ 162 | Await::g2c($generator, function() : void{ 163 | self::fail("unexpected resolve call"); 164 | }); 165 | }catch(AwaitException $e){ 166 | self::assertEquals("Unhandled async exception: {$ex->getMessage()}", $e->getMessage()); 167 | self::assertEquals($ex, $e->getPrevious()); 168 | } 169 | } 170 | 171 | public function testOnceAtZero() : void{ 172 | Await::f2c(function() : Generator{ 173 | yield Await::ONCE; 174 | }, function(){ 175 | self::fail("unexpected resolve call"); 176 | }, function($ex) : void{ 177 | self::assertInstanceOf(AwaitException::class, $ex); 178 | /** @var AwaitException $ex */ 179 | self::assertEquals("Yielded Await::ONCE when the pending queue size is 0 != 1", $ex->getMessage()); 180 | }); 181 | } 182 | 183 | public function testOnceAtTwo() : void{ 184 | Await::f2c(function() : Generator{ 185 | yield; 186 | yield; 187 | yield Await::ONCE; 188 | }, function(){ 189 | self::fail("unexpected resolve call"); 190 | }, function($ex) : void{ 191 | self::assertInstanceOf(AwaitException::class, $ex); 192 | /** @var UnawaitedCallbackException $ex */ 193 | self::assertEquals("Yielded Await::ONCE when the pending queue size is 2 != 1", $ex->getMessage()); 194 | }); 195 | } 196 | 197 | public function testRaceAtZero() : void{ 198 | Await::f2c(function() : Generator{ 199 | yield Await::RACE; 200 | }, function(){ 201 | self::fail("unexpected resolve call"); 202 | }, function($ex) : void{ 203 | self::assertInstanceOf(AwaitException::class, $ex); 204 | /** @var UnawaitedCallbackException $ex */ 205 | self::assertEquals("Yielded Await::RACE when there is nothing racing", $ex->getMessage()); 206 | }); 207 | } 208 | 209 | 210 | public function testVoidImmediateResolveVoid() : void{ 211 | $rand = 0xCAFEF33D; 212 | self::assertImmediateResolve(function() use ($rand) : Generator{ 213 | yield self::voidCallbackImmediate($rand, yield) => Await::ONCE; 214 | }, null); 215 | } 216 | 217 | public function testVoidLaterResolveVoid() : void{ 218 | $rand = 0xCAFEF33D; 219 | self::assertLaterResolve(function() use ($rand) : Generator{ 220 | yield self::voidCallbackLater($rand, yield) => Await::ONCE; 221 | }, null); 222 | } 223 | 224 | public function testVoidImmediateResolveNull() : void{ 225 | $rand = 0xCAFEFEED; 226 | self::assertImmediateResolve(function() use ($rand) : Generator{ 227 | return yield self::voidCallbackImmediate($rand, yield) => Await::ONCE; 228 | }, $rand); 229 | } 230 | 231 | public function testVoidImmediateResolve() : void{ 232 | $rand = 0xDEADBEEF; 233 | self::assertImmediateResolve(function() use ($rand) : Generator{ 234 | return yield self::voidCallbackImmediate($rand, yield Await::RESOLVE) => Await::ONCE; 235 | }, $rand); 236 | } 237 | 238 | public function testVoidLaterResolve() : void{ 239 | $rand = 0xFEEDFACE; 240 | self::assertLaterResolve(function() use ($rand) : Generator{ 241 | return yield self::voidCallbackLater($rand, yield Await::RESOLVE) => Await::ONCE; 242 | }, $rand); 243 | } 244 | 245 | public function testVoidImmediateResolveMulti() : void{ 246 | $rand = [0xDEADBEEF, 0xFEEDFACE]; 247 | $resolveCalled = false; 248 | 249 | $async = function(callable $callback) use($rand) : void { 250 | $callback($rand[0], $rand[1]); 251 | }; 252 | 253 | Await::f2c(function() use($async) : Generator{ 254 | return yield $async(yield Await::RESOLVE_MULTI) => Await::ONCE; 255 | }, function($actual) use ($rand, &$resolveCalled) : void{ 256 | $resolveCalled = true; 257 | self::assertEquals($rand, $actual); 258 | }, function(Throwable $ex) : void{ 259 | self::fail("unexpected reject call: " . $ex->getMessage()); 260 | }); 261 | self::assertTrue($resolveCalled, "resolve was not called"); 262 | } 263 | 264 | public function testVoidImmediateReject() : void{ 265 | $exception = new DummyException(); 266 | self::assertImmediateReject(function() use ($exception) : Generator{ 267 | yield Await::RESOLVE; // unused 268 | yield self::voidCallbackImmediate($exception, yield Await::REJECT) => Await::ONCE; 269 | }, $exception); 270 | } 271 | 272 | public function testVoidLaterReject() : void{ 273 | $exception = new DummyException(); 274 | self::assertLaterReject(function() use ($exception) : Generator{ 275 | yield Await::RESOLVE; // unused 276 | yield self::voidCallbackLater($exception, yield Await::REJECT) => Await::ONCE; 277 | }, $exception); 278 | } 279 | 280 | 281 | public function testVoidOnceImmediateResolveImmediateResolve() : void{ 282 | $rand = [0x12345678, 0x4bcd3f96]; 283 | self::assertImmediateResolve(function() use ($rand) : Generator{ 284 | $first = yield self::voidCallbackImmediate($rand[0], yield Await::RESOLVE) => Await::ONCE; 285 | $second = yield self::voidCallbackImmediate($rand[1], yield Await::RESOLVE) => Await::ONCE; 286 | return [$first, $second]; 287 | }, $rand); 288 | } 289 | 290 | public function testVoidOnceImmediateResolveLaterResolve() : void{ 291 | $rand = [0x12345678, 0x4bcd3f96]; 292 | self::assertLaterResolve(function() use ($rand) : Generator{ 293 | $first = yield self::voidCallbackImmediate($rand[0], yield Await::RESOLVE) => Await::ONCE; 294 | $second = yield self::voidCallbackLater($rand[1], yield Await::RESOLVE) => Await::ONCE; 295 | return [$first, $second]; 296 | }, $rand); 297 | } 298 | 299 | public function testVoidOnceLaterResolveImmediateResolve() : void{ 300 | $rand = [0x12345678, 0x4bcd3f96]; 301 | self::assertLaterResolve(function() use ($rand) : Generator{ 302 | $first = yield self::voidCallbackLater($rand[0], yield Await::RESOLVE) => Await::ONCE; 303 | $second = yield self::voidCallbackImmediate($rand[1], yield Await::RESOLVE) => Await::ONCE; 304 | return [$first, $second]; 305 | }, $rand); 306 | } 307 | 308 | public function testVoidOnceLaterResolveLaterResolve() : void{ 309 | $rand = [0x12345678, 0x4bcd3f96]; 310 | self::assertLaterResolve(function() use ($rand) : Generator{ 311 | $first = yield self::voidCallbackLater($rand[0], yield Await::RESOLVE) => Await::ONCE; 312 | $second = yield self::voidCallbackLater($rand[1], yield Await::RESOLVE) => Await::ONCE; 313 | return [$first, $second]; 314 | }, $rand); 315 | } 316 | 317 | 318 | public function testVoidAllImmediateResolveImmediateResolve() : void{ 319 | $rand = [0x12345678, 0x4bcd3f96]; 320 | self::assertImmediateResolve(function() use ($rand) : Generator{ 321 | self::voidCallbackImmediate($rand[0], yield Await::RESOLVE); 322 | self::voidCallbackImmediate($rand[1], yield Await::RESOLVE); 323 | return yield Await::ALL; 324 | }, $rand); 325 | } 326 | 327 | public function testVoidAllImmediateResolveImmediateReject() : void{ 328 | $rand = 0x12345678; 329 | $ex = new DummyException(); 330 | self::assertImmediateReject(function() use ($rand, $ex) : Generator{ 331 | self::voidCallbackImmediate($rand, yield Await::RESOLVE); 332 | yield Await::RESOLVE; 333 | self::voidCallbackImmediate($ex, yield Await::REJECT); 334 | return yield Await::ALL; 335 | }, $ex); 336 | } 337 | 338 | public function testVoidAllImmediateResolveLaterResolve() : void{ 339 | $rand = [0x12345678, 0x4bcd3f96]; 340 | self::assertLaterResolve(function() use ($rand) : Generator{ 341 | self::voidCallbackImmediate($rand[0], yield Await::RESOLVE); 342 | self::voidCallbackLater($rand[1], yield Await::RESOLVE); 343 | return yield Await::ALL; 344 | }, $rand); 345 | } 346 | 347 | public function testVoidAllImmediateResolveLaterReject() : void{ 348 | $rand = 0x12345678; 349 | $ex = new DummyException(); 350 | self::assertLaterReject(function() use ($rand, $ex) : Generator{ 351 | self::voidCallbackImmediate($rand, yield Await::RESOLVE); 352 | yield Await::RESOLVE; 353 | self::voidCallbackLater($ex, yield Await::REJECT); 354 | return yield Await::ALL; 355 | }, $ex); 356 | } 357 | 358 | public function testVoidAllImmediateRejectImmediateResolve() : void{ 359 | $ex = new DummyException(); 360 | $rand = 0x1234567B; 361 | self::assertImmediateReject(function() use ($ex, $rand) : Generator{ 362 | yield Await::RESOLVE; 363 | self::voidCallbackImmediate($ex, yield Await::REJECT); 364 | self::voidCallbackImmediate($rand, yield Await::RESOLVE); 365 | return yield Await::ALL; 366 | }, $ex); 367 | } 368 | 369 | public function testVoidAllImmediateRejectImmediateReject() : void{ 370 | $ex = new DummyException(); 371 | $ex2 = new DummyException(); 372 | self::assertImmediateReject(function() use ($ex, $ex2) : Generator{ 373 | yield Await::RESOLVE; 374 | self::voidCallbackImmediate($ex, yield Await::REJECT); 375 | yield Await::RESOLVE; 376 | self::voidCallbackImmediate($ex2, yield Await::REJECT); 377 | return yield Await::ALL; 378 | }, $ex); 379 | } 380 | 381 | public function testVoidAllImmediateRejectLaterResolve() : void{ 382 | $ex = new DummyException(); 383 | $rand = 0x1234567B; 384 | self::assertImmediateReject(function() use ($ex, $rand) : Generator{ 385 | yield Await::RESOLVE; 386 | self::voidCallbackImmediate($ex, yield Await::REJECT); 387 | self::voidCallbackLater($rand, yield Await::RESOLVE); 388 | return yield Await::ALL; 389 | }, $ex); 390 | } 391 | 392 | public function testVoidAllImmediateRejectLaterReject() : void{ 393 | $ex = new DummyException(); 394 | $ex2 = new DummyException(); 395 | self::assertImmediateReject(function() use ($ex, $ex2) : Generator{ 396 | yield Await::RESOLVE; 397 | self::voidCallbackImmediate($ex, yield Await::REJECT); 398 | yield Await::RESOLVE; 399 | self::voidCallbackLater($ex2, yield Await::REJECT); 400 | return yield Await::ALL; 401 | }, $ex); 402 | } 403 | 404 | public function testVoidAllLaterResolveImmediateResolve() : void{ 405 | $rand = [0x12345678, 0x4bcd3f96]; 406 | self::assertLaterResolve(function() use ($rand) : Generator{ 407 | self::voidCallbackLater($rand[0], yield Await::RESOLVE); 408 | self::voidCallbackImmediate($rand[1], yield Await::RESOLVE); 409 | return yield Await::ALL; 410 | }, $rand); 411 | } 412 | 413 | public function testVoidAllLaterResolveImmediateReject() : void{ 414 | $rand = 0x12345678; 415 | $ex = new DummyException(); 416 | self::assertImmediateReject(function() use ($rand, $ex) : Generator{ 417 | self::voidCallbackLater($rand, yield Await::RESOLVE); 418 | yield Await::RESOLVE; 419 | self::voidCallbackImmediate($ex, yield Await::REJECT); 420 | return yield Await::ALL; 421 | }, $ex); 422 | } 423 | 424 | public function testVoidAllLaterResolveLaterResolve() : void{ 425 | $rand = [0x12345678, 0x4BCD3F96]; 426 | self::assertLaterResolve(function() use ($rand) : Generator{ 427 | self::voidCallbackLater($rand[0], yield Await::RESOLVE); 428 | self::voidCallbackLater($rand[1], yield Await::RESOLVE); 429 | return yield Await::ALL; 430 | }, $rand); 431 | } 432 | 433 | public function testVoidAllLaterResolveLaterReject() : void{ 434 | $rand = 0x12345678; 435 | $ex = new DummyException(); 436 | self::assertLaterReject(function() use ($rand, $ex) : Generator{ 437 | self::voidCallbackLater($rand, yield Await::RESOLVE); 438 | yield Await::RESOLVE; 439 | self::voidCallbackLater($ex, yield Await::REJECT); 440 | return yield Await::ALL; 441 | }, $ex); 442 | } 443 | 444 | public function testVoidAllLaterRejectImmediateReject() : void{ 445 | $ex = new DummyException(); 446 | $ex2 = new DummyException(); 447 | self::assertImmediateReject(function() use ($ex, $ex2) : Generator{ 448 | yield Await::RESOLVE; 449 | self::voidCallbackLater($ex, yield Await::REJECT); 450 | yield Await::RESOLVE; 451 | self::voidCallbackImmediate($ex2, yield Await::REJECT); 452 | return yield Await::ALL; 453 | }, $ex2); 454 | } 455 | 456 | public function testVoidAllLaterRejectImmediateResolve() : void{ 457 | $ex = new DummyException(); 458 | $rand = 0x1234567B; 459 | self::assertLaterReject(function() use ($ex, $rand) : Generator{ 460 | yield Await::RESOLVE; 461 | self::voidCallbackLater($ex, yield Await::REJECT); 462 | self::voidCallbackImmediate($rand, yield Await::RESOLVE); 463 | return yield Await::ALL; 464 | }, $ex); 465 | } 466 | 467 | public function testVoidAllLaterRejectLaterResolve() : void{ 468 | $ex = new DummyException(); 469 | $rand = 0x1234567B; 470 | self::assertLaterReject(function() use ($ex, $rand) : Generator{ 471 | yield Await::RESOLVE; 472 | self::voidCallbackLater($ex, yield Await::REJECT); 473 | self::voidCallbackLater($rand, yield Await::RESOLVE); 474 | return yield Await::ALL; 475 | }, $ex); 476 | } 477 | 478 | public function testVoidAllLaterRejectLaterReject() : void{ 479 | $ex = new DummyException(); 480 | $ex2 = new DummyException(); 481 | self::assertLaterReject(function() use ($ex, $ex2) : Generator{ 482 | yield Await::RESOLVE; 483 | self::voidCallbackLater($ex, yield Await::REJECT); 484 | yield Await::RESOLVE; 485 | self::voidCallbackLater($ex2, yield Await::REJECT); 486 | return yield Await::ALL; 487 | }, $ex); 488 | } 489 | 490 | 491 | public function testVoidRaceImmediateResolveImmediateResolve() : void{ 492 | $rand = [0x12345678, 0x4bcd3f96]; 493 | self::assertImmediateResolve(function() use ($rand) : Generator{ 494 | self::voidCallbackImmediate($rand[0], yield Await::RESOLVE); 495 | self::voidCallbackImmediate($rand[1], yield Await::RESOLVE); 496 | return yield Await::RACE; 497 | }, $rand[0]); 498 | } 499 | 500 | public function testVoidRaceImmediateResolveImmediateReject() : void{ 501 | $rand = 0x12345678; 502 | $ex = new DummyException(); 503 | self::assertImmediateResolve(function() use ($rand, $ex) : Generator{ 504 | self::voidCallbackImmediate($rand, yield Await::RESOLVE); 505 | yield Await::RESOLVE; // start a new promise 506 | self::voidCallbackImmediate($ex, yield Await::REJECT); 507 | return yield Await::RACE; 508 | }, $rand); 509 | } 510 | 511 | public function testVoidRaceImmediateResolveLaterResolve() : void{ 512 | $rand = [0x12345678, 0x4bcd3f96]; 513 | self::assertImmediateResolve(function() use ($rand) : Generator{ 514 | self::voidCallbackImmediate($rand[0], yield Await::RESOLVE); 515 | self::voidCallbackLater($rand[1], yield Await::RESOLVE); 516 | return yield Await::RACE; 517 | }, $rand[0]); 518 | } 519 | 520 | public function testVoidRaceImmediateResolveLaterReject() : void{ 521 | $rand = 0x12345678; 522 | $ex = new DummyException(); 523 | self::assertImmediateResolve(function() use ($rand, $ex) : Generator{ 524 | self::voidCallbackImmediate($rand, yield Await::RESOLVE); 525 | self::voidCallbackLater($ex, yield Await::REJECT); 526 | return yield Await::RACE; 527 | }, $rand); 528 | } 529 | 530 | public function testVoidRaceImmediateRejectImmediateResolve() : void{ 531 | $ex = new DummyException(); 532 | $rand = 0x1234567B; 533 | self::assertImmediateReject(function() use ($ex, $rand) : Generator{ 534 | yield Await::RESOLVE; 535 | self::voidCallbackImmediate($ex, yield Await::REJECT); 536 | self::voidCallbackImmediate($rand, yield Await::RESOLVE); 537 | return yield Await::RACE; 538 | }, $ex); 539 | } 540 | 541 | public function testVoidRaceImmediateRejectImmediateReject() : void{ 542 | $ex = new DummyException(); 543 | $ex2 = new DummyException(); 544 | self::assertImmediateReject(function() use ($ex, $ex2) : Generator{ 545 | yield Await::RESOLVE; 546 | self::voidCallbackImmediate($ex, yield Await::REJECT); 547 | yield Await::RESOLVE; 548 | self::voidCallbackImmediate($ex2, yield Await::REJECT); 549 | return yield Await::RACE; 550 | }, $ex); 551 | } 552 | 553 | public function testVoidRaceImmediateRejectLaterResolve() : void{ 554 | $ex = new DummyException(); 555 | $rand = 0x1234567B; 556 | self::assertImmediateReject(function() use ($ex, $rand) : Generator{ 557 | yield Await::RESOLVE; 558 | self::voidCallbackImmediate($ex, yield Await::REJECT); 559 | self::voidCallbackLater($rand, yield Await::RESOLVE); 560 | return yield Await::RACE; 561 | }, $ex); 562 | } 563 | 564 | public function testVoidRaceImmediateRejectLaterReject() : void{ 565 | $ex = new DummyException(); 566 | $ex2 = new DummyException(); 567 | self::assertImmediateReject(function() use ($ex, $ex2) : Generator{ 568 | yield Await::RESOLVE; 569 | self::voidCallbackImmediate($ex, yield Await::REJECT); 570 | yield Await::RESOLVE; 571 | self::voidCallbackLater($ex2, yield Await::REJECT); 572 | return yield Await::RACE; 573 | }, $ex); 574 | } 575 | 576 | public function testVoidRaceLaterResolveImmediateResolve() : void{ 577 | $rand = [0x12345678, 0x4bcd3f96]; 578 | self::assertImmediateResolve(function() use ($rand) : Generator{ 579 | self::voidCallbackLater($rand[0], yield Await::RESOLVE); 580 | self::voidCallbackImmediate($rand[1], yield Await::RESOLVE); 581 | return yield Await::RACE; 582 | }, $rand[1]); 583 | } 584 | 585 | public function testVoidRaceLaterResolveImmediateReject() : void{ 586 | $rand = 0x12345678; 587 | $ex = new DummyException(); 588 | self::assertImmediateReject(function() use ($rand, $ex) : Generator{ 589 | self::voidCallbackLater($rand, yield Await::RESOLVE); 590 | yield Await::RESOLVE; 591 | self::voidCallbackImmediate($ex, yield Await::REJECT); 592 | return yield Await::RACE; 593 | }, $ex); 594 | } 595 | 596 | public function testVoidRaceLaterResolveLaterResolve() : void{ 597 | $rand = [0x12345678, 0x4BCD3F96]; 598 | self::assertLaterResolve(function() use ($rand) : Generator{ 599 | self::voidCallbackLater($rand[0], yield Await::RESOLVE); 600 | self::voidCallbackLater($rand[1], yield Await::RESOLVE); 601 | return yield Await::RACE; 602 | }, $rand[0]); 603 | } 604 | 605 | public function testVoidRaceLaterResolveLaterReject() : void{ 606 | $rand = 0x12345678; 607 | $ex = new DummyException(); 608 | self::assertLaterResolve(function() use ($rand, $ex) : Generator{ 609 | self::voidCallbackLater($rand, yield Await::RESOLVE); 610 | self::voidCallbackLater($ex, yield Await::RESOLVE); 611 | return yield Await::RACE; 612 | }, $rand); 613 | } 614 | 615 | public function testVoidRaceLaterRejectImmediateResolve() : void{ 616 | $ex = new DummyException(); 617 | $rand = 0x1234567B; 618 | self::assertImmediateResolve(function() use ($ex, $rand) : Generator{ 619 | yield Await::RESOLVE; 620 | self::voidCallbackLater($ex, yield Await::REJECT); 621 | self::voidCallbackImmediate($rand, yield Await::RESOLVE); 622 | return yield Await::RACE; 623 | }, $rand); 624 | } 625 | 626 | public function testVoidRaceLaterRejectImmediateReject() : void{ 627 | $ex = new DummyException(); 628 | $ex2 = new DummyException(); 629 | self::assertImmediateReject(function() use ($ex, $ex2) : Generator{ 630 | yield Await::RESOLVE; 631 | self::voidCallbackLater($ex, yield Await::REJECT); 632 | yield Await::RESOLVE; 633 | self::voidCallbackImmediate($ex2, yield Await::REJECT); 634 | return yield Await::RACE; 635 | }, $ex2); 636 | } 637 | 638 | public function testVoidRaceLaterRejectLaterResolve() : void{ 639 | $ex = new DummyException(); 640 | $rand = 0x1234567B; 641 | self::assertLaterReject(function() use ($ex, $rand) : Generator{ 642 | yield Await::RESOLVE; 643 | self::voidCallbackLater($ex, yield Await::REJECT); 644 | self::voidCallbackLater($rand, yield Await::RESOLVE); 645 | return yield Await::RACE; 646 | }, $ex); 647 | } 648 | 649 | public function testVoidRaceLaterRejectLaterReject() : void{ 650 | $ex = new DummyException(); 651 | $ex2 = new DummyException(); 652 | self::assertLaterReject(function() use ($ex, $ex2) : Generator{ 653 | yield Await::RESOLVE; 654 | self::voidCallbackLater($ex, yield Await::REJECT); 655 | yield Await::RESOLVE; 656 | self::voidCallbackLater($ex2, yield Await::REJECT); 657 | return yield Await::RACE; 658 | }, $ex); 659 | } 660 | 661 | public function testVoidRaceImmediateResolveLaterResolveOnceLaterResolve() : void{ 662 | $rand = [0x12345678, 0x4bcd3f96, 0xdeadbeef]; 663 | self::assertLaterResolve(function() use ($rand) : Generator{ 664 | self::voidCallbackImmediate($rand[0], yield Await::RESOLVE); 665 | self::voidCallbackLater($rand[1], yield Await::RESOLVE); 666 | $race = yield Await::RACE; 667 | self::voidCallbackLater($rand[2], yield Await::RESOLVE); 668 | self::$later = array_reverse(self::$later); 669 | $once = yield Await::ONCE; 670 | return [$race, $once]; 671 | }, [$rand[0], $rand[2]]); 672 | } 673 | 674 | 675 | public function testGeneratorWithoutCollect() : void{ 676 | Await::f2c(function(){ 677 | yield; 678 | yield from self::generatorVoidImmediate(); 679 | }, function() : void{ 680 | self::fail("unexpected resolve call"); 681 | }, function($ex) : void{ 682 | self::assertInstanceOf(UnawaitedCallbackException::class, $ex); 683 | /** @var AwaitException $ex */ 684 | self::assertEquals("Resolution of await generator is disallowed when Await::RESOLVE or Await::REJECT was yielded but is not awaited through Await::ONCE, Await::ALL or Await::RACE", $ex->getMessage()); 685 | }); 686 | } 687 | 688 | public function testGeneratorImmediateResolve() : void{ 689 | $rand = 0xD3AD8EEF; 690 | self::assertImmediateResolve(function() use ($rand) : Generator{ 691 | return yield from GeneratorUtil::empty($rand); 692 | }, $rand); 693 | } 694 | 695 | public function testGeneratorLaterResolve() : void{ 696 | $rand = 0xD3AD8EEF; 697 | self::assertLaterResolve(function() use ($rand) : Generator{ 698 | return yield from self::generatorReturnLater($rand); 699 | }, $rand); 700 | } 701 | 702 | public function testGeneratorImmediateReject() : void{ 703 | $ex = new DummyException(); 704 | self::assertImmediateReject(function() use ($ex) : Generator{ 705 | yield from GeneratorUtil::throw($ex); 706 | }, $ex); 707 | } 708 | 709 | public function testGeneratorLaterReject() : void{ 710 | $ex = new DummyException(); 711 | self::assertLaterReject(function() use ($ex) : Generator{ 712 | yield from self::generatorThrowLater($ex); 713 | }, $ex); 714 | } 715 | 716 | public function testGeneratorImmediateResolveVoid() : void{ 717 | self::assertImmediateResolve(function() : Generator{ 718 | yield from self::generatorVoidImmediate(); 719 | }, null); 720 | } 721 | 722 | public function testGeneratorLaterResolveVoid() : void{ 723 | self::assertLaterResolve(function() : Generator{ 724 | yield from self::generatorVoidLater(); 725 | }, null); 726 | } 727 | 728 | public function testGeneratorAllResolve() : void{ 729 | self::assertLaterResolve(function() : Generator{ 730 | return yield from Await::all([ 731 | "a" => self::generatorReturnLater("b"), 732 | "c" => GeneratorUtil::empty("d"), 733 | "e" => self::generatorVoidLater(), 734 | ]); 735 | }, [ 736 | "a" => "b", 737 | "c" => "d", 738 | "e" => null, 739 | ]); 740 | } 741 | 742 | public function testGeneratorAllEmpty() : void{ 743 | self::assertImmediateResolve(function() : Generator{ 744 | return yield from Await::all([]); 745 | }, []); 746 | } 747 | 748 | public function testGeneratorRaceResolve() : void{ 749 | self::assertImmediateResolve(function() : Generator{ 750 | return yield from Await::race([ 751 | "a" => self::generatorReturnLater("b"), 752 | "c" => GeneratorUtil::empty("d"), 753 | "e" => self::generatorVoidLater(), 754 | ]); 755 | }, ["c", "d"]); 756 | } 757 | 758 | public function testGeneratorSafeRaceResolve() : void{ 759 | self::assertImmediateResolve(function() : Generator{ 760 | return yield from Await::safeRace([ 761 | "a" => self::generatorReturnLater("b"), 762 | "c" => GeneratorUtil::empty("d"), 763 | "e" => self::generatorVoidLater(), 764 | ]); 765 | }, ["c", "d"]); 766 | } 767 | 768 | public function testGeneratorRaceEmpty() : void{ 769 | try{ 770 | Await::f2c(function() : Generator{ 771 | yield from Await::race([]); 772 | }, function() : void{ 773 | self::fail("unexpected resolve call"); 774 | }); 775 | }catch(AwaitException $e){ 776 | self::assertEquals("Unhandled async exception: Cannot race an empty array of generators", $e->getMessage()); 777 | self::assertEquals("Cannot race an empty array of generators", $e->getPrevious()->getMessage()); 778 | } 779 | } 780 | 781 | public function testGeneratorSafeRaceEmpty() : void{ 782 | try{ 783 | Await::f2c(function() : Generator{ 784 | yield from Await::race([]); 785 | }, function() : void{ 786 | self::fail("unexpected resolve call"); 787 | }); 788 | }catch(AwaitException $e){ 789 | self::assertEquals("Unhandled async exception: Cannot race an empty array of generators", $e->getMessage()); 790 | self::assertEquals("Cannot race an empty array of generators", $e->getPrevious()->getMessage()); 791 | } 792 | } 793 | 794 | public function testSafeRaceCancel() : void{ 795 | $hasResolve = null; 796 | $hasFinally = false; 797 | 798 | $loser = function() use(&$hasFinally){ 799 | try { 800 | yield from Await::promise(function(){}); // never resolves 801 | } finally { 802 | $hasFinally = true; 803 | } 804 | }; 805 | 806 | $rand = 0x12345678; 807 | $winner = fn() => self::generatorReturnLater($rand); 808 | 809 | self::assertLaterResolve(function() use($loser, $winner, &$hasResolve) : Generator{ 810 | [$which, $_] = yield from Await::safeRace(["winner" => $winner(), "loser" => $loser()]); 811 | self::assertEquals("winner", $which); 812 | 813 | [$which2, $result2] = yield from Await::safeRace(["winner" => $winner(), "loser" => $loser()]); 814 | $hasResolve = $result2; 815 | return $which2; 816 | }, "winner"); 817 | 818 | self::assertEquals($rand, $hasResolve); 819 | self::assertTrue($hasFinally, "has finally"); 820 | } 821 | 822 | public function testSafeRaceCancelImmediateLaziness() : void{ 823 | $run = []; 824 | 825 | $gf = function(int $ret) use(&$run){ 826 | false && yield; 827 | $run[$ret] = true; 828 | return $ret; 829 | }; 830 | 831 | self::assertImmediateResolve(function() use($gf, &$run) : Generator{ 832 | [$which, $value] = yield from Await::safeRace([$gf(0), $gf(1)]); 833 | self::assertEquals(1, count($run), "only one generator should start"); 834 | self::assertArrayHasKey($which, $run, "returned \$which should be run"); 835 | self::assertEquals($which, $value, "returned value should be run"); 836 | 837 | return null; 838 | }, null); 839 | } 840 | 841 | public function testSafeRaceCancelAfterImmediateThrow() : void{ 842 | $cleanup = 0; 843 | 844 | $loser = function() use(&$cleanup){ 845 | $cleanup++; 846 | try { 847 | yield from Await::promise(function(){}); // never resolves 848 | } finally { 849 | $cleanup--; 850 | } 851 | }; 852 | 853 | $ex = new DummyException; 854 | $winner = function() use($ex){ 855 | false && yield; 856 | throw $ex; 857 | }; 858 | 859 | self::assertImmediateReject(function() use($loser, $winner, &$cleanup) : Generator{ 860 | [$which, $_] = yield from Await::safeRace(["winner" => GeneratorUtil::empty(), "loser" => $loser()]); 861 | self::assertEquals(0, $cleanup, "not cleaned up completely after immediate loss"); 862 | self::assertEquals("winner", $which); 863 | 864 | yield from Await::safeRace(["winner" => $winner(), "loser" => $loser()]); 865 | self::assertEquals(0, $cleanup, "not cleaned up completely after losing to throw"); 866 | }, $ex); 867 | } 868 | 869 | public function testSafeRaceCancelAfterAsyncThrow() : void{ 870 | $cleanup = 0; 871 | 872 | $loser = function() use(&$cleanup){ 873 | $cleanup++; 874 | try { 875 | yield from Await::promise(function(){}); // never resolves 876 | } finally { 877 | $cleanup--; 878 | } 879 | }; 880 | 881 | $ex = new DummyException; 882 | $winner = fn() => self::generatorThrowLater($ex); 883 | 884 | self::assertLaterReject(function() use($loser, $winner, &$cleanup) : Generator{ 885 | [$which, $_] = yield from Await::safeRace(["winner" => GeneratorUtil::empty(), "loser" => $loser()]); 886 | self::assertEquals(0, $cleanup, "not cleaned up completely after immediate loss"); 887 | self::assertEquals("winner", $which); 888 | 889 | [$which, $_] = yield from Await::safeRace(["loser" => $loser(), "winner" => GeneratorUtil::empty()]); 890 | self::assertEquals(0, $cleanup, "not cleaned up completely after immediate loss"); 891 | self::assertEquals("winner", $which); 892 | 893 | yield from Await::safeRace(["winner" => $winner(), "loser" => $loser()]); 894 | self::assertEquals(0, $cleanup, "not cleaned up completely after losing to throw"); 895 | 896 | yield from Await::safeRace(["loser" => $loser(), "winner" => $winner()]); 897 | self::assertEquals(0, $cleanup, "not cleaned up completely after losing to throw"); 898 | }, $ex); 899 | } 900 | 901 | public function testSameImmediateResolveImmediateResolve() : void{ 902 | $rand = [0x12345678, 0x4bcd3f96]; 903 | self::assertImmediateResolve(function() use ($rand) : Generator{ 904 | $cb = yield Await::RESOLVE; 905 | self::voidCallbackImmediate($rand[0], $cb); 906 | self::voidCallbackImmediate($rand[1], $cb); 907 | $once = yield Await::ONCE; 908 | return $once; 909 | }, $rand[0]); 910 | } 911 | 912 | public function testSameLaterResolveImmediateResolve() : void{ 913 | $rand = [0x12345678, 0x4bcd3f96]; 914 | self::assertImmediateResolve(function() use ($rand) : Generator{ 915 | $cb = yield Await::RESOLVE; 916 | self::voidCallbackLater($rand[0], $cb); 917 | self::voidCallbackImmediate($rand[1], $cb); 918 | $once = yield Await::ONCE; 919 | return $once; 920 | }, $rand[1]); 921 | } 922 | 923 | public function testSameLaterResolveLaterResolve() : void{ 924 | $rand = [0x12345678, 0x4bcd3f96]; 925 | self::assertLaterResolve(function() use ($rand) : Generator{ 926 | $cb = yield Await::RESOLVE; 927 | self::voidCallbackLater($rand[0], $cb); 928 | self::voidCallbackLater($rand[1], $cb); 929 | $once = yield Await::ONCE; 930 | return $once; 931 | }, $rand[0]); 932 | } 933 | 934 | public function testSameLaterRejectImmediateResolve() : void{ 935 | $rand = 0x12345678; 936 | $ex = new DummyException(); 937 | self::assertImmediateResolve(fn() => Await::promise(function($resolve, $reject) use($rand, $ex) { 938 | self::voidCallbackLater($ex, $reject); 939 | self::voidCallbackImmediate($rand, $resolve); 940 | }), $rand); 941 | self::callLater(); 942 | } 943 | 944 | 945 | protected function tearDown() : void{ 946 | try{ 947 | self::$later = []; 948 | }catch(Throwable $throwable){ 949 | echo "Suppressed " . get_class($throwable) . ": " . $throwable->getMessage(); 950 | } 951 | 952 | $assertions = self::getCount(); 953 | self::assertGreaterThan(0, $assertions, "Test does not assert anything"); 954 | } 955 | 956 | 957 | private static function assertImmediateResolve(Closure $closure, $expect) : void{ 958 | $resolveCalled = false; 959 | Await::f2c($closure, function($actual) use ($expect, &$resolveCalled) : void{ 960 | $resolveCalled = true; 961 | self::assertEquals($expect, $actual); 962 | }, function(Throwable $ex) : void{ 963 | self::fail("unexpected reject call: " . $ex->getMessage()); 964 | }); 965 | self::assertTrue($resolveCalled, "resolve was not called"); 966 | } 967 | 968 | private static function assertLaterResolve(Closure $closure, $expect) : void{ 969 | $laterCalled = false; 970 | $resolveCalled = false; 971 | Await::f2c($closure, function($actual) use ($expect, &$laterCalled, &$resolveCalled) : void{ 972 | self::assertTrue($laterCalled, "resolve called before callLater()"); 973 | $resolveCalled = true; 974 | self::assertEquals($expect, $actual); 975 | }, function(Throwable $ex) : void{ 976 | self::fail("unexpected reject call: " . $ex->getMessage()); 977 | }); 978 | 979 | $laterCalled = true; 980 | self::callLater(); 981 | self::assertTrue($resolveCalled, "resolve was not called"); 982 | } 983 | 984 | private static function assertImmediateReject(Closure $closure, Throwable $object) : void{ 985 | $rejectCalled = false; 986 | Await::f2c($closure, function() : void{ 987 | self::fail("unexpected resolve call"); 988 | }, function(Throwable $ex) use ($object, &$rejectCalled) : void{ 989 | $rejectCalled = true; 990 | self::assertEquals($object, $ex); 991 | }); 992 | self::assertTrue($rejectCalled, "reject was not called"); 993 | } 994 | 995 | private static function assertLaterReject(Closure $closure, Throwable $object) : void{ 996 | $laterCalled = false; 997 | $rejectCalled = false; 998 | Await::f2c($closure, function() : void{ 999 | self::fail("unexpected reject call"); 1000 | }, function(Throwable $ex) use ($object, &$laterCalled, &$rejectCalled) : void{ 1001 | self::assertTrue($laterCalled, "reject called before callLater(): " . $ex->getMessage()); 1002 | $rejectCalled = true; 1003 | self::assertEquals($object, $ex); 1004 | }); 1005 | 1006 | $laterCalled = true; 1007 | self::callLater(); 1008 | self::assertTrue($rejectCalled, "reject was not called"); 1009 | } 1010 | 1011 | private static function callLater() : void{ 1012 | while(($c = array_shift(self::$later)) !== null){ 1013 | $c(); 1014 | } 1015 | } 1016 | 1017 | private static function voidCallbackImmediate($ret, callable $callback) : void{ 1018 | $callback($ret); 1019 | } 1020 | 1021 | private static function voidCallbackLater($ret, callable $callback) : void{ 1022 | self::$later[] = function() use ($ret, $callback){ 1023 | $callback($ret); 1024 | }; 1025 | } 1026 | 1027 | private static function generatorReturnLater($ret) : Generator{ 1028 | return yield from Await::promise(fn($resolve) => self::voidCallbackLater($ret, $resolve)); 1029 | } 1030 | 1031 | private static function generatorThrowLater(Throwable $ex) : Generator{ 1032 | yield from Await::promise(fn($resolve) => self::voidCallbackLater(null, $resolve)); 1033 | throw $ex; 1034 | } 1035 | 1036 | private static function generatorVoidImmediate() : Generator{ 1037 | if(false){ 1038 | yield; 1039 | } 1040 | } 1041 | 1042 | private static function generatorVoidLater() : Generator{ 1043 | yield from Await::promise(fn($resolve) => self::voidCallbackLater(null, $resolve)); 1044 | } 1045 | } 1046 | -------------------------------------------------------------------------------- /tests/SOFe/AwaitGenerator/ChannelTest.php: -------------------------------------------------------------------------------- 1 | $channel */ 34 | $channel = new Channel; 35 | $clock = new MockClock; 36 | 37 | $eventCounter = 0; 38 | 39 | Await::f2c(function() use($channel, $clock, &$eventCounter){ 40 | yield from $clock->sleepUntil(1); 41 | yield from $channel->sendAndWait("a"); 42 | self::assertSame(3, $clock->currentTick()); 43 | $eventCounter += 1; 44 | }); 45 | 46 | Await::f2c(function() use($channel, $clock, &$eventCounter){ 47 | yield from $clock->sleepUntil(2); 48 | yield from $channel->sendAndWait("b"); 49 | self::assertSame(5, $clock->currentTick()); 50 | $eventCounter += 1; 51 | }); 52 | 53 | Await::f2c(function() use($channel, $clock, &$eventCounter){ 54 | yield from $clock->sleepUntil(3); 55 | $receive = yield from $channel->receive(); 56 | self::assertSame(3, $clock->currentTick()); 57 | self::assertSame("a", $receive); 58 | $eventCounter += 1; 59 | }); 60 | 61 | Await::f2c(function() use($channel, $clock, &$eventCounter){ 62 | yield from $clock->sleepUntil(4); 63 | yield from $channel->sendAndWait("c"); 64 | self::assertSame(6, $clock->currentTick()); 65 | $eventCounter += 1; 66 | }); 67 | 68 | Await::f2c(function() use($channel, $clock, &$eventCounter){ 69 | yield from $clock->sleepUntil(5); 70 | $receive = yield from $channel->receive(); 71 | self::assertSame(5, $clock->currentTick()); 72 | self::assertSame("b", $receive); 73 | $eventCounter += 1; 74 | }); 75 | 76 | Await::f2c(function() use($channel, $clock, &$eventCounter){ 77 | yield from $clock->sleepUntil(6); 78 | $receive = yield from $channel->receive(); 79 | self::assertSame(6, $clock->currentTick()); 80 | self::assertSame("c", $receive); 81 | $eventCounter += 1; 82 | }); 83 | 84 | Await::f2c(function() use($channel, $clock, &$eventCounter){ 85 | yield from $clock->sleepUntil(7); 86 | $receive = yield from $channel->receive(); 87 | self::assertSame(8, $clock->currentTick()); 88 | self::assertSame("d", $receive); 89 | $eventCounter += 1; 90 | }); 91 | 92 | Await::f2c(function() use($channel, $clock, &$eventCounter){ 93 | yield from $clock->sleepUntil(8); 94 | yield from $channel->sendAndWait("d"); 95 | self::assertSame(8, $clock->currentTick()); 96 | $eventCounter += 1; 97 | }); 98 | 99 | $clock->nextTick(1); 100 | self::assertSame(0, $eventCounter); 101 | self::assertSame(1, $channel->getSendQueueSize()); 102 | self::assertSame(0, $channel->getReceiveQueueSize()); 103 | 104 | $clock->nextTick(2); 105 | self::assertSame(0, $eventCounter); 106 | self::assertSame(2, $channel->getSendQueueSize()); 107 | self::assertSame(0, $channel->getReceiveQueueSize()); 108 | 109 | $clock->nextTick(3); 110 | self::assertSame(2, $eventCounter); 111 | self::assertSame(1, $channel->getSendQueueSize()); 112 | self::assertSame(0, $channel->getReceiveQueueSize()); 113 | 114 | $clock->nextTick(4); 115 | self::assertSame(2, $eventCounter); 116 | self::assertSame(2, $channel->getSendQueueSize()); 117 | self::assertSame(0, $channel->getReceiveQueueSize()); 118 | 119 | $clock->nextTick(5); 120 | self::assertSame(4, $eventCounter); 121 | self::assertSame(1, $channel->getSendQueueSize()); 122 | self::assertSame(0, $channel->getReceiveQueueSize()); 123 | 124 | $clock->nextTick(6); 125 | self::assertSame(6, $eventCounter); 126 | self::assertSame(0, $channel->getSendQueueSize()); 127 | self::assertSame(0, $channel->getReceiveQueueSize()); 128 | 129 | $clock->nextTick(7); 130 | self::assertSame(6, $eventCounter); 131 | self::assertSame(0, $channel->getSendQueueSize()); 132 | self::assertSame(1, $channel->getReceiveQueueSize()); 133 | 134 | $clock->nextTick(8); 135 | self::assertSame(8, $eventCounter); 136 | self::assertSame(0, $channel->getSendQueueSize()); 137 | self::assertSame(0, $channel->getReceiveQueueSize()); 138 | } 139 | 140 | public function testReceiveFirst() : void{ 141 | /** @var Channel $channel */ 142 | $channel = new Channel; 143 | $clock = new MockClock; 144 | 145 | $eventCounter = 0; 146 | 147 | Await::f2c(function() use($channel, $clock, &$eventCounter){ 148 | yield from $clock->sleepUntil(1); 149 | $receive = yield from $channel->receive(); 150 | self::assertSame(3, $clock->currentTick()); 151 | self::assertSame("a", $receive); 152 | $eventCounter += 1; 153 | }); 154 | 155 | Await::f2c(function() use($channel, $clock, &$eventCounter){ 156 | yield from $clock->sleepUntil(2); 157 | $receive = yield from $channel->receive(); 158 | self::assertSame(5, $clock->currentTick()); 159 | self::assertSame("b", $receive); 160 | $eventCounter += 1; 161 | }); 162 | 163 | Await::f2c(function() use($channel, $clock, &$eventCounter){ 164 | yield from $clock->sleepUntil(3); 165 | yield from $channel->sendAndWait("a"); 166 | self::assertSame(3, $clock->currentTick()); 167 | $eventCounter += 1; 168 | }); 169 | 170 | Await::f2c(function() use($channel, $clock, &$eventCounter){ 171 | yield from $clock->sleepUntil(4); 172 | $receive = yield from $channel->receive(); 173 | self::assertSame(6, $clock->currentTick()); 174 | self::assertSame("c", $receive); 175 | $eventCounter += 1; 176 | }); 177 | 178 | Await::f2c(function() use($channel, $clock, &$eventCounter){ 179 | yield from $clock->sleepUntil(5); 180 | yield from $channel->sendAndWait("b"); 181 | self::assertSame(5, $clock->currentTick()); 182 | $eventCounter += 1; 183 | }); 184 | 185 | Await::f2c(function() use($channel, $clock, &$eventCounter){ 186 | yield from $clock->sleepUntil(6); 187 | yield from $channel->sendAndWait("c"); 188 | self::assertSame(6, $clock->currentTick()); 189 | $eventCounter += 1; 190 | }); 191 | 192 | Await::f2c(function() use($channel, $clock, &$eventCounter){ 193 | yield from $clock->sleepUntil(7); 194 | yield from $channel->sendAndWait("d"); 195 | self::assertSame(8, $clock->currentTick()); 196 | $eventCounter += 1; 197 | }); 198 | 199 | Await::f2c(function() use($channel, $clock, &$eventCounter){ 200 | yield from $clock->sleepUntil(8); 201 | $receive = yield from $channel->receive(); 202 | self::assertSame(8, $clock->currentTick()); 203 | self::assertSame("d", $receive); 204 | $eventCounter += 1; 205 | }); 206 | 207 | $clock->nextTick(1); 208 | self::assertSame(0, $eventCounter); 209 | self::assertSame(0, $channel->getSendQueueSize()); 210 | self::assertSame(1, $channel->getReceiveQueueSize()); 211 | 212 | $clock->nextTick(2); 213 | self::assertSame(0, $eventCounter); 214 | self::assertSame(0, $channel->getSendQueueSize()); 215 | self::assertSame(2, $channel->getReceiveQueueSize()); 216 | 217 | $clock->nextTick(3); 218 | self::assertSame(2, $eventCounter); 219 | self::assertSame(0, $channel->getSendQueueSize()); 220 | self::assertSame(1, $channel->getReceiveQueueSize()); 221 | 222 | $clock->nextTick(4); 223 | self::assertSame(2, $eventCounter); 224 | self::assertSame(0, $channel->getSendQueueSize()); 225 | self::assertSame(2, $channel->getReceiveQueueSize()); 226 | 227 | $clock->nextTick(5); 228 | self::assertSame(4, $eventCounter); 229 | self::assertSame(0, $channel->getSendQueueSize()); 230 | self::assertSame(1, $channel->getReceiveQueueSize()); 231 | 232 | $clock->nextTick(6); 233 | self::assertSame(6, $eventCounter); 234 | self::assertSame(0, $channel->getSendQueueSize()); 235 | self::assertSame(0, $channel->getReceiveQueueSize()); 236 | 237 | $clock->nextTick(7); 238 | self::assertSame(6, $eventCounter); 239 | self::assertSame(1, $channel->getSendQueueSize()); 240 | self::assertSame(0, $channel->getReceiveQueueSize()); 241 | 242 | $clock->nextTick(8); 243 | self::assertSame(8, $eventCounter); 244 | self::assertSame(0, $channel->getSendQueueSize()); 245 | self::assertSame(0, $channel->getReceiveQueueSize()); 246 | } 247 | 248 | public function testNonBlockSend() : void{ 249 | /** @var Channel $channel */ 250 | $channel = new Channel; 251 | $clock = new MockClock; 252 | 253 | $eventCounter = 0; 254 | 255 | Await::f2c(function() use($channel, $clock, &$eventCounter){ 256 | yield from $clock->sleepUntil(1); 257 | $channel->sendWithoutWait("a"); 258 | $eventCounter += 1; 259 | }); 260 | 261 | Await::f2c(function() use($channel, $clock, &$eventCounter){ 262 | yield from $clock->sleepUntil(2); 263 | $receive = yield from $channel->receive(); 264 | self::assertSame(2, $clock->currentTick()); 265 | self::assertSame("a", $receive); 266 | $eventCounter += 1; 267 | }); 268 | 269 | 270 | Await::f2c(function() use($channel, $clock, &$eventCounter){ 271 | yield from $clock->sleepUntil(3); 272 | $receive = yield from $channel->receive(); 273 | self::assertSame(4, $clock->currentTick()); 274 | self::assertSame("a", $receive); 275 | $eventCounter += 1; 276 | }); 277 | 278 | Await::f2c(function() use($channel, $clock, &$eventCounter){ 279 | yield from $clock->sleepUntil(4); 280 | $channel->sendWithoutWait("a"); 281 | $eventCounter += 1; 282 | }); 283 | 284 | $clock->nextTick(1); 285 | self::assertSame(1, $eventCounter); 286 | self::assertSame(1, $channel->getSendQueueSize()); 287 | self::assertSame(0, $channel->getReceiveQueueSize()); 288 | 289 | $clock->nextTick(2); 290 | self::assertSame(2, $eventCounter); 291 | self::assertSame(0, $channel->getSendQueueSize()); 292 | self::assertSame(0, $channel->getReceiveQueueSize()); 293 | 294 | $clock->nextTick(3); 295 | self::assertSame(2, $eventCounter); 296 | self::assertSame(0, $channel->getSendQueueSize()); 297 | self::assertSame(1, $channel->getReceiveQueueSize()); 298 | 299 | $clock->nextTick(4); 300 | self::assertSame(4, $eventCounter); 301 | self::assertSame(0, $channel->getSendQueueSize()); 302 | self::assertSame(0, $channel->getReceiveQueueSize()); 303 | } 304 | 305 | public function testTrySend() : void{ 306 | /** @var Channel $channel */ 307 | $channel = new Channel; 308 | 309 | $received = false; 310 | 311 | self::assertFalse($channel->trySend("a")); 312 | 313 | Await::f2c(function() use($channel, &$received) { 314 | $value = yield from $channel->receive(); 315 | self::assertSame("b", $value); 316 | $received = true; 317 | }); 318 | 319 | self::assertTrue($channel->trySend("b")); 320 | } 321 | 322 | public function testTryReceive() : void{ 323 | /** @var Channel $channel */ 324 | $channel = new Channel; 325 | 326 | $receive = $channel->tryReceiveOr("b"); 327 | self::assertSame("b", $receive); 328 | 329 | $channel->sendWithoutWait("a"); 330 | $receive = $channel->tryReceiveOr("b"); 331 | 332 | self::assertSame("a", $receive); 333 | } 334 | 335 | public function testTryCancelSender() : void{ 336 | $ok = false; 337 | Await::f2c(function() use(&$ok){ 338 | /** @var Channel $channel */ 339 | $channel = new Channel; 340 | 341 | [$which, $_] = yield from Await::safeRace([ 342 | $channel->sendAndWait(null), 343 | GeneratorUtil::empty(null), 344 | ]); 345 | self::assertSame(1, $which); 346 | 347 | $ret = $channel->tryReceiveOr("no sender"); 348 | self::assertSame("no sender", $ret); 349 | $ok = true; 350 | }); 351 | 352 | self::assertTrue($ok, "test run complete"); 353 | } 354 | 355 | public function testTryCancelReceiver() : void{ 356 | $ok = false; 357 | Await::f2c(function() use(&$ok){ 358 | /** @var Channel $channel */ 359 | $channel = new Channel; 360 | 361 | [$which, $_] = yield from Await::safeRace([ 362 | $channel->receive(), 363 | GeneratorUtil::empty(null), 364 | ]); 365 | self::assertSame(1, $which); 366 | 367 | $channel->sendWithoutWait(null); 368 | $ok = true; 369 | }); 370 | 371 | self::assertTrue($ok, "test run complete"); 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /tests/SOFe/AwaitGenerator/DummyException.php: -------------------------------------------------------------------------------- 1 | GeneratorUtil::empty("a")); 33 | 34 | self::assertSame("a", $loading->getSync(1)); 35 | 36 | $done = false; 37 | Await::f2c(function() use($loading, &$done) { 38 | $value = yield from $loading->get(); 39 | self::assertSame("a", $value); 40 | self::assertSame("a", $loading->getSync(1)); 41 | 42 | $value = yield from $loading->get(); 43 | self::assertSame("a", $value, "Cannot get value the second time"); 44 | self::assertSame("a", $loading->getSync(1)); 45 | 46 | $done = true; 47 | }); 48 | 49 | self::assertTrue($done, "Cannot get value twice"); 50 | } 51 | 52 | public function testDeferred() : void{ 53 | $clock = new MockClock; 54 | 55 | $loading = new Loading(function() use($clock) { 56 | yield from $clock->sleepUntil(1); 57 | return "b"; 58 | }); 59 | 60 | self::assertSame(1, $loading->getSync(1)); 61 | 62 | $beforeDone = false; 63 | Await::f2c(function() use($loading, &$beforeDone) { 64 | $value = yield from $loading->get(); 65 | self::assertSame("b", $value); 66 | self::assertSame("b", $loading->getSync(1)); 67 | 68 | $value = yield from $loading->get(); 69 | self::assertSame("b", $value, "Cannot get value the second time"); 70 | self::assertSame("b", $loading->getSync(1)); 71 | 72 | $beforeDone = true; 73 | }); 74 | 75 | $afterDone = false; 76 | Await::f2c(function() use($loading, $clock, &$afterDone) { 77 | yield from $clock->sleepUntil(2); 78 | 79 | $value = yield from $loading->get(); 80 | self::assertSame("b", $value); 81 | self::assertSame("b", $loading->getSync(1)); 82 | 83 | $value = yield from $loading->get(); 84 | self::assertSame("b", $value, "Cannot get value the second time"); 85 | self::assertSame("b", $loading->getSync(1)); 86 | 87 | $afterDone = true; 88 | }); 89 | 90 | self::assertFalse($beforeDone); 91 | self::assertFalse($afterDone); 92 | 93 | self::assertSame(1, $loading->getSync(1)); 94 | 95 | $clock->nextTick(1); 96 | 97 | self::assertTrue($beforeDone); 98 | self::assertFalse($afterDone); 99 | 100 | $clock->nextTick(2); 101 | 102 | self::assertTrue($beforeDone); 103 | self::assertTrue($afterDone); 104 | } 105 | 106 | public function testSyncWinSyncCancel() : void{ 107 | $fast = new Loading(fn() => GeneratorUtil::empty("instant")); 108 | $slow = new Loading(fn() => GeneratorUtil::empty("later")); 109 | 110 | $done = false; 111 | $hasSlowReturn = false; 112 | 113 | Await::f2c(function() use($fast, $slow, &$done, &$hasSlowReturn) { 114 | [$which, $_] = yield from Await::safeRace([ 115 | "fast" => $fast->get(), 116 | "slow" => (function() use($slow, &$hasSlowReturn) { 117 | yield from $slow->get(); 118 | $hasSlowReturn = true; 119 | })(), 120 | ]); 121 | self::assertEquals("fast", $which); 122 | 123 | $done = true; 124 | }); 125 | 126 | self::assertTrue($done, "execution complete"); 127 | self::assertFalse($hasSlowReturn, "loser should not return after cancel"); 128 | } 129 | 130 | public function testAsyncWinAsyncCancel() : void{ 131 | $clock = new MockClock; 132 | 133 | $fast = new Loading(function() use($clock) { 134 | yield from $clock->sleepUntil(2); 135 | return "earlier"; 136 | }); 137 | $slow = new Loading(function() use($clock) { 138 | yield from $clock->sleepUntil(2); 139 | return "later"; 140 | }); 141 | 142 | $done = false; 143 | $hasSlowReturn = false; 144 | 145 | Await::f2c(function() use($fast, $slow, $clock, &$done, &$hasSlowReturn) { 146 | [$which, $_] = yield from Await::safeRace([ 147 | "fast" => $fast->get(), 148 | "slow" => (function() use($slow, &$hasSlowReturn) { 149 | yield from $slow->get(); 150 | $hasSlowReturn = true; 151 | })(), 152 | ]); 153 | self::assertEquals(2, $clock->currentTick()); 154 | self::assertEquals("fast", $which); 155 | 156 | $done = true; 157 | }); 158 | 159 | $clock->nextTick(1); 160 | self::assertFalse($done, "pending execution"); 161 | 162 | $clock->nextTick(2); 163 | self::assertTrue($done, "execution complete"); 164 | self::assertFalse($hasSlowReturn, "loser should not return after cancel"); 165 | } 166 | 167 | public function testCallback() : void{ 168 | [$loading, $resolve] = Loading::byCallback(); 169 | self::assertEquals(12345, $loading->getSync(12345)); 170 | $resolve(98765); 171 | self::assertEquals(98765, $loading->getSync(12345)); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /tests/SOFe/AwaitGenerator/MockClock.php: -------------------------------------------------------------------------------- 1 | > */ 32 | private array $ticks = []; 33 | private int $tick = 0; 34 | 35 | public function schedule(int $tick, Closure $closure) : void{ 36 | if($this->tick >= $tick){ 37 | throw new RuntimeException("Tick $tick is in the past"); 38 | } 39 | 40 | if(!isset($this->ticks[$tick])){ 41 | $this->ticks[$tick] = []; 42 | } 43 | 44 | $this->ticks[$tick][] = $closure; 45 | } 46 | 47 | public function nextTick(int $expectTick) : void{ 48 | $this->tick++; 49 | Assert::assertSame($expectTick, $this->tick, "Test case has wrong clock counting"); 50 | 51 | if(isset($this->ticks[$this->tick])){ 52 | foreach($this->ticks[$this->tick] as $closure){ 53 | $closure(); 54 | } 55 | } 56 | } 57 | 58 | public function sleepUntil(int $tick) : Generator{ 59 | $this->schedule($tick, yield Await::RESOLVE); 60 | yield Await::ONCE; 61 | } 62 | 63 | public function currentTick() : int{ 64 | return $this->tick; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/SOFe/AwaitGenerator/MutexTest.php: -------------------------------------------------------------------------------- 1 | isIdle(), "mutex should initialize is idle"); 37 | } 38 | 39 | public function testReturnAsIs() : void{ 40 | $mutex = new Mutex; 41 | $return = new stdClass; 42 | 43 | $done = 2; 44 | 45 | Await::f2c(function() use($mutex, $return, &$done) : Generator{ 46 | $value = yield from $mutex->runClosure(function() use($return) : Generator{ 47 | false && yield; 48 | return $return; 49 | }); 50 | 51 | self::assertSame($return, $value, "mutex should pass through returned values"); 52 | 53 | $done--; 54 | return $value; 55 | }, function($value) use($return, &$done) { 56 | self::assertSame($return, $value, "mutex should pass through returned values"); 57 | $done--; 58 | }); 59 | 60 | self::assertTrue($mutex->isIdle(), "mutex should initialize is idle"); 61 | self::assertSame(0, $done, "Await::f2c did not resolve"); 62 | } 63 | 64 | public function testNotIdleDuringLock() : void{ 65 | $done = 4; 66 | 67 | Await::f2c(function() use(&$done) : Generator{ 68 | $mutex = new Mutex; 69 | 70 | yield from $mutex->runClosure(function() use(&$done, $mutex) : Generator{ 71 | false && yield; 72 | self::assertFalse($mutex->isIdle(), "mutex should not be idle when locked closure is running"); 73 | $done--; 74 | }); 75 | 76 | self::assertTrue($mutex->isIdle(), "mutex should be idle after unlock"); 77 | $done--; 78 | 79 | yield from $mutex->runClosure(function() use(&$done, $mutex) : Generator{ 80 | false && yield; 81 | self::assertFalse($mutex->isIdle(), "mutex should not be idle when locked closure is running again"); 82 | $done--; 83 | }); 84 | 85 | self::assertTrue($mutex->isIdle(), "mutex should be idle after unlocking again"); 86 | $done--; 87 | }); 88 | 89 | self::assertSame(0, $done, "all branches should be executed"); 90 | } 91 | 92 | public function testMutualExclusion() : void{ 93 | $eventCounter = 0; 94 | 95 | $clock = new MockClock; 96 | 97 | $mutex = new Mutex; 98 | 99 | Await::f2c(function() use(&$eventCounter, $mutex, $clock) : Generator{ 100 | self::assertSame(0, $eventCounter++, "Await::f2c should start immediately"); 101 | 102 | yield from $mutex->runClosure(function() use(&$eventCounter, $mutex, $clock) : Generator{ 103 | self::assertSame(1, $eventCounter++, "mutex should start immediately"); 104 | self::assertFalse($mutex->isIdle(), "mutex should not be idle when locked closure is running"); 105 | 106 | yield from $clock->sleepUntil(2); 107 | 108 | self::assertSame(4, $eventCounter++, "mutex should run after clock ticks to 2"); 109 | self::assertSame(2, $clock->currentTick(), "mock clock implementation error"); 110 | self::assertFalse($mutex->isIdle(), "mutex should not be idle when locked closure is resumed"); 111 | }); 112 | }); 113 | 114 | self::assertSame(2, $eventCounter++, "mock clock should preempt coroutine"); 115 | 116 | Await::f2c(function() use(&$eventCounter, $mutex, $clock) : Generator{ 117 | yield from $mutex->runClosure(function() use(&$eventCounter, $mutex, $clock) : Generator{ 118 | self::assertSame(5, $eventCounter++, "mutex should start next task immediately"); 119 | self::assertSame(2, $clock->currentTick(), "mutex should start next task immediately"); 120 | self::assertFalse($mutex->isIdle(), "mutex should not be idle when lock is acquired concurrently"); 121 | 122 | yield from $clock->sleepUntil(4); 123 | 124 | self::assertSame(8, $eventCounter++, "mutex should run after clock ticks to 4"); 125 | self::assertSame(4, $clock->currentTick(), "mock clock implementation error"); 126 | self::assertFalse($mutex->isIdle(), "mutex should not be idle when locked closure is resumed again"); 127 | }); 128 | }); 129 | 130 | $clock->nextTick(1); 131 | 132 | self::assertSame(3, $eventCounter++, "mock clock implementation error"); 133 | self::assertFalse($mutex->isIdle(), "mutex should not be idle when locked closure is preempted"); 134 | 135 | $clock->nextTick(2); 136 | 137 | self::assertSame(6, $eventCounter++, "nextTick should resume coroutine"); 138 | self::assertFalse($mutex->isIdle(), "mutex should not be idle when lock is acquired concurrently"); 139 | 140 | $clock->nextTick(3); 141 | 142 | self::assertSame(7, $eventCounter++, "mock clock implementation error"); 143 | self::assertFalse($mutex->isIdle(), "mock clock implementation error"); 144 | 145 | $clock->nextTick(4); 146 | 147 | self::assertSame(9, $eventCounter++, "nextTick should resume coroutine"); 148 | self::assertTrue($mutex->isIdle(), "mutex should be idle when both locks are released"); 149 | } 150 | 151 | public function testSupportException() : void{ 152 | $mutex = new Mutex; 153 | 154 | $hasThrown = false; 155 | 156 | Await::f2c(function() use(&$hasThrown, $mutex) : Generator{ 157 | try{ 158 | yield from $mutex->runClosure(function() : Generator{ 159 | throw new DummyException; 160 | }); 161 | }catch(DummyException $e){ 162 | $hasThrown = true; 163 | } 164 | }); 165 | 166 | self::assertTrue($hasThrown, "Mutex does not pass through exception"); 167 | 168 | $hasRunClosure = 1; 169 | 170 | Await::f2c(function() use($mutex, &$hasRunClosure) : Generator{ 171 | yield from $mutex->runClosure(function() use(&$hasRunClosure) : Generator{ 172 | false && yield; 173 | $hasRunClosure = 0; 174 | }); 175 | }); 176 | 177 | self::assertSame(0, $hasRunClosure, "mutex should continue running subsequent closures despite throwing exceptions"); 178 | } 179 | 180 | public function testDoubleRelease() : void{ 181 | $done = 2; 182 | 183 | Await::f2c(function() use(&$done){ 184 | $mutex = new Mutex; 185 | yield from $mutex->acquire(); 186 | $mutex->release(); 187 | 188 | $done--; 189 | 190 | $mutex->release(); 191 | }, null, [ 192 | RuntimeException::class => function(RuntimeException $ex) use(&$done){ 193 | self::assertSame("Attempt to release a released mutex", $ex->getMessage()); 194 | 195 | $done--; 196 | }, 197 | ]); 198 | 199 | self::assertSame(0, $done, "Await::f2c did not reject"); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /tests/SOFe/AwaitGenerator/PubSubTest.php: -------------------------------------------------------------------------------- 1 | $pubsub */ 33 | $pubsub = new PubSub; 34 | 35 | $run = 0; 36 | for($i = 0; $i < 3; $i++) { 37 | Await::f2c(function() use($pubsub, &$run){ 38 | $sub = $pubsub->subscribe(); 39 | 40 | self::assertTrue(yield from $sub->next($item), "subscriber gets first item"); 41 | self::assertEquals(1, $item); 42 | 43 | self::assertTrue(yield from $sub->next($item), "subscriber gets second item"); 44 | self::assertEquals(2, $item); 45 | 46 | yield from $sub->interrupt(); 47 | 48 | $run += 1; 49 | }); 50 | } 51 | 52 | $pubsub->publish(1); 53 | $pubsub->publish(2); 54 | 55 | self::assertEquals(3, $run); 56 | 57 | self::assertEquals(0, $pubsub->getSubscriberCount()); 58 | self::assertTrue($pubsub->isEmpty()); 59 | } 60 | 61 | public function testPubFirst() : void{ 62 | /** @var PubSub $pubsub */ 63 | $pubsub = new PubSub; 64 | 65 | $pubsub->publish(1); 66 | $pubsub->publish(2); 67 | 68 | $run = 0; 69 | for($i = 0; $i < 3; $i++) { 70 | Await::f2c(function() use($pubsub, &$run){ 71 | $sub = $pubsub->subscribe(); 72 | $run++; 73 | yield from $sub->next($_); 74 | self::fail("subscriber should not receive items published before subscribe() call"); 75 | }); 76 | } 77 | self::assertEquals(3, $run); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tests/SOFe/AwaitGenerator/TraverseTest.php: -------------------------------------------------------------------------------- 1 | Traverser::VALUE; 37 | yield from GeneratorUtil::empty(); 38 | yield 3 => Traverser::VALUE; 39 | } 40 | 41 | public function testArrayCollect(){ 42 | Await::f2c(function() : Generator{ 43 | $trav = new Traverser(self::oneThree()); 44 | return yield from $trav->collect(); 45 | }, function(array $array){ 46 | self::assertSame([1, 3], $array); 47 | }); 48 | } 49 | 50 | public function testNormalInterrupt(){ 51 | Await::f2c(function() : Generator{ 52 | $trav = new Traverser(self::oneThree()); 53 | self::assertTrue(yield from $trav->next($value)); 54 | self::assertSame(1, $value); 55 | 56 | return yield from $trav->interrupt(); 57 | }, function($result) { 58 | self::assertSame(null, $result); 59 | }); 60 | } 61 | 62 | public function testCaughtInterruptFinalized(){ 63 | Await::f2c(function() : Generator{ 64 | $trav = Traverser::fromClosure(function() : Generator{ 65 | try{ 66 | yield from GeneratorUtil::empty(); 67 | yield 1 => Traverser::VALUE; 68 | yield from GeneratorUtil::empty(); 69 | yield 2 => Traverser::VALUE; 70 | }finally{ 71 | yield 3 => Traverser::VALUE; 72 | yield from GeneratorUtil::empty(); 73 | yield 4 => Traverser::VALUE; 74 | } 75 | }); 76 | self::assertTrue(yield from $trav->next($value)); 77 | self::assertSame(1, $value); 78 | 79 | return yield from $trav->interrupt(); 80 | }, function($result) { 81 | self::assertSame(null, $result); 82 | }); 83 | } 84 | 85 | public function testLoopingInterruptCatch(){ 86 | Await::f2c(function() : Generator{ 87 | $trav = Traverser::fromClosure(function() : Generator{ 88 | while(true){ 89 | try{ 90 | yield from GeneratorUtil::empty(); 91 | yield 1 => Traverser::VALUE; 92 | yield from GeneratorUtil::empty(); 93 | yield 2 => Traverser::VALUE; 94 | }catch(\Exception $ex){ 95 | yield from GeneratorUtil::empty(); 96 | yield 3 => Traverser::VALUE; 97 | } 98 | } 99 | }); 100 | self::assertTrue(yield from $trav->next($value)); 101 | self::assertSame(1, $value); 102 | 103 | return yield from $trav->interrupt(); 104 | }, function() { 105 | self::assertFalse("unreachable"); 106 | }, function(\Exception $ex) { 107 | self::assertEquals("Generator did not terminate after 16 interrupts", $ex->getMessage()); 108 | }); 109 | } 110 | 111 | /** 112 | * Test whether the inner-generator of a traverser can communicate with 113 | * the await-generator's runtime properly through `yield`. 114 | * 115 | * As a traverser should not handle any `yield` that 116 | * does not have {@link Traverser::VALUE} as its value. 117 | * 118 | * Otherwise, await-generator's core functionalities might not 119 | * work correctly, such as {@link Await::promise()}. 120 | */ 121 | public function testYieldBridging(){ 122 | Await::f2c(function() : Generator{ 123 | $trav = Traverser::fromClosure(function() : Generator{ 124 | for ($i = 0; $i < 2; $i++) { 125 | $got = yield from Await::promise(function ($resolve) use (&$i) : void { 126 | $resolve($i); 127 | }); 128 | yield $got => Traverser::VALUE; 129 | } 130 | }); 131 | 132 | for ($expect = 0; $expect < 2; $expect++) { 133 | self::assertTrue(yield from $trav->next($value)); 134 | self::assertSame($expect, $value); 135 | } 136 | self::assertFalse(yield from $trav->next($value)); 137 | }); 138 | 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /tests/autoload.php: -------------------------------------------------------------------------------- 1 | add('SOFe\AwaitGenerator', __DIR__, true); 11 | $classLoader->register(); 12 | -------------------------------------------------------------------------------- /zho/README.md: -------------------------------------------------------------------------------- 1 | [Eng](../README.md) | 繁 | [简](../chs) 2 | # await-generator 3 | [![Build Status][ci-badge]][ci-page] 4 | [![Codecov][codecov-badge]][codecov-page] 5 | 6 | 給予 PHP 「async/await 等待式異步」(代碼流控制)設計模式的程式庫。 7 | 8 | ## 文檔 9 | 建議先閱讀 [await-generator 教學(中文版趕工中)](../book),它涵蓋了生成器、傳統「回調式非同步」,再到 await-generator 等概念的介紹。 10 | 11 | 以下部分名詞在 await-generator 教學中都更詳細地講解(「回調」等)。 12 | 13 | ## await-generator 的優勢 14 | 傳統的異步代碼流需要靠回調(匿名函數)來實現。 15 | 每個異步函數都要開新的回調,然後把異步函數後面的代碼整個搬進去,導致了代碼變成「callback hell 回調地獄」,難以被閱讀、管理。 16 |
17 | 點擊以查看「回調地獄」例子 18 | 19 | ```php 20 | load_data(function($data) { 21 | $init = count($data) === 0 ? init_data(...) : fn($then) => $then($data); 22 | $init(function($data) { 23 | $output = []; 24 | foreach($data as $k => $datum) { 25 | processData($datum, function($result) use(&$output, $data) { 26 | $output[$k] = $result; 27 | if(count($output) === count($data)) { 28 | createQueries($output, function($queries) { 29 | $run = function($i) use($queries, &$run) { 30 | runQuery($queries[$i], function() use($i, $queries, $run) { 31 | if($i === count($queries)) { 32 | $done = false; 33 | commitBatch(function() use(&$done) { 34 | if(!$done) { 35 | $done = true; 36 | echo "Done!\n"; 37 | } 38 | }); 39 | onUserClose(function() use(&$done) { 40 | if(!$done) { 41 | $done = true; 42 | echo "User closed!\n"; 43 | } 44 | }); 45 | onTimeout(function() use(&$done) { 46 | if(!$done) { 47 | $done = true; 48 | echo "Timeout!\n"; 49 | } 50 | }); 51 | } else { 52 | $run($i + 1); 53 | } 54 | }); 55 | }; 56 | }); 57 | } 58 | }); 59 | } 60 | }); 61 | }); 62 | ``` 63 | 64 |
65 | 如果使用 await-generator ,以上代碼就可以被簡化為: 66 | 67 | ```php 68 | $data = yield from load_data(); 69 | if(count($data) === 0) $data = yield from init_data(); 70 | $output = yield from Await::all(array_map(fn($datum) => processData($datum), $data)); 71 | $queries = yield from createQueries($output); 72 | foreach($queries as $query) yield from runQuery($query); 73 | [$which, ] = yield from Await::race([ 74 | 0 => commitBatch(), 75 | 1 => onUserClose(), 76 | 2 => onTimeout(), 77 | ]) 78 | echo match($which) { 79 | 0 => "Done!\n", 80 | 1 => "User closed!\n", 81 | 2 => "Timeout!\n", 82 | }; 83 | ``` 84 | 85 | ## 使用後的代碼可以維持回溯相容性嗎? 86 | 是的, await-generator 不會對已有的接口造成任何限制。 87 | 你可以將所有涉及 await-generator 的代碼封閉在程式的內部。 88 | 但你確實應該把生成器函數直接當作程式接口。 89 | 90 | await-generator 會在 `Await::f2c` 開始進行異步代碼流控制,你可以將它視為「等待式」至「回調式」的轉接頭。 91 | 92 | ```php 93 | function oldApi($args, Closure $onSuccess) { 94 | Await::f2c(fn() => $onSuccess(yield from newApi($args))); 95 | } 96 | ``` 97 | 98 | 你也用它來處理錯誤: 99 | 100 | ```php 101 | function newApi($args, Closure $onSuccess, Closure $onError) { 102 | Await::f2c(function() use($onSuccess, $onError) { 103 | try { 104 | $onSuccess(yield from newApi($args)); 105 | } catch(Exception $ex) { 106 | $onError($ex); 107 | } 108 | }); 109 | } 110 | ``` 111 | 112 | 「回調式」同樣可以被 `Await::promise` method 轉化成「等待式」。 113 | 它跟 JavaScript 的 `new Promise` 很像: 114 | 115 | ```php 116 | yield from Await::promise(fn($resolve, $reject) => oldFunction($args, $resolve, $reject)); 117 | ``` 118 | 119 | ## await-generator 的*劣勢* 120 | await-generator 也有很多經常坑人的地方: 121 | 122 | - 忘了 `yield from` 的代碼會毫無作用; 123 | - 如果你的函數沒有任何 `yield` 或者 `yield from` , PHP 就不會把它當成生成器函數(在所有應為生成器的函數類型註釋中加上 `: Generator` 可減輕影響); 124 | - 如果異步代碼沒有全面結束, `finally` 裏面的代碼也不會被執行(例: `Await::promise(fn($resolve) => null)`); 125 | 126 | 儘管一些地方會導致問題, await-generator 的設計模式出 bug 的機會依然比「回調地獄」少 。 127 | 128 | ## 不是有纖程嗎? 129 | 雖然這樣說很主觀,但本人因為以下纖程缺少的特色而相對地不喜歡它: 130 | 131 | ### 靠類型註釋就能區分異步、非異步函數 132 | > 先生,你已在暫停的纖程待了三十秒。
133 | > 因為有人實現一個界面時調用了 `Fiber::suspend() ` 。 134 | 135 | ![../../fiber.jpg](https://github.com/SOF3/await-generator/raw/master/fiber.jpeg) 136 | 137 | > 好傢伙,我都等不及要回應我的 HTTP 請求了。
138 | > 框架肯定還沒把它給超時清除。 139 | 140 | 例如能直觀地看出 `$channel->send($value): Generator` 會暫停代碼流至有數值被送入生成器; `$channel->sendBuffered($value): void` 141 | 則不會暫停代碼流,這個 method 的代碼會在一次過執行後回傳。 142 | 類型註釋通常是不言自明的。 143 | 144 | 當然,用戶可以直接調用 `sleep()` ,但大家都應清楚 `sleep()` 會卡住整個線程(就算他們不懂也會在整個「世界」停止時發現)。 145 | 146 | ### 並發狀態 147 | 當一個函數被暫停時會發生許多其他的事情。 148 | 調用函數時固然給予了實現者調用可修改狀態函數的可能性, 149 | 但是一個正常的、合理的實現,例如 HTTP 請求所調用的函數不應修改你程式庫的內部狀態。 150 | 但是這個假設對於纖程來說並不成立, 151 | 因為當一個纖程被暫停後,其他纖程仍然可以修改你的內部狀態。 152 | 每次你調用任何*可能*會被暫停的函數時,你都必須檢查內部狀態的可能變化。 153 | 154 | await-generator 相比起纖程,異步、非異步代碼能簡單區分,且暫停點的確切位置顯而易見。 155 | 因此你只需要在已知的暫停點檢查狀態的變化。 156 | 157 | ### 捕捉暫停點 158 | await-generator 提供了一個叫做「[捕捉][trap-pr]」的功能。 159 | 它允許用戶攔截生成器的暫停點和恢復點,在它暫停或恢復前執行一段加的插代碼。 160 | 這只需透過向生成器添加一個轉接頭來實現。甚至不需要 await-generator 引擎的額外支援。 161 | 這目前在纖程中無法做到。 162 | 163 | [book]: https://sof3.github.io/await-generator/master/ 164 | [ci-badge]: https://github.com/SOF3/await-generator/workflows/CI/badge.svg 165 | [ci-page]: https://github.com/SOF3/await-generator/actions?query=workflow%3ACI 166 | [codecov-badge]: https://img.shields.io/codecov/c/github/codecov/example-python.svg 167 | [codecov-page]: https://codecov.io/gh/SOF3/await-generator 168 | [trap-pr]: https://github.com/SOF3/await-generator/pull/106 169 | --------------------------------------------------------------------------------