├── .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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------