├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── cache │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ └── src │ │ ├── cache.rs │ │ └── main.rs └── ping │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ └── src │ ├── main.rs │ └── ping.rs ├── src ├── compat │ ├── mod.rs │ ├── native.rs │ └── wasm.rs ├── joint.rs ├── lib.rs ├── prelude.rs ├── scheduler.rs ├── sink.rs └── stream.rs └── tests ├── joint.rs ├── sink.rs └── stream.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "cargo" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Troupe CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | code_cleaniness_checks: 14 | name: Formatting Checks 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Setup Rust 18 | uses: hecrj/setup-rust-action@v2 19 | with: 20 | rust-version: nightly 21 | components: rustfmt 22 | - uses: actions/checkout@v4 23 | - run: cargo fmt --check 24 | wasm_build: 25 | needs: code_cleaniness_checks 26 | name: Wasm Tests 27 | runs-on: ubuntu-latest 28 | strategy: 29 | matrix: 30 | rust: [stable, beta, nightly] 31 | steps: 32 | - uses: hecrj/setup-rust-action@v2 33 | with: 34 | rust-version: ${{ matrix.rust }} 35 | targets: wasm32-unknown-unknown 36 | - uses: actions/checkout@v4 37 | - run: cargo build --target=wasm32-unknown-unknown --verbose 38 | - run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 39 | - run: wasm-pack test --headless --chrome --firefox --verbose 40 | - run: cargo doc --target wasm32-unknown-unknown --verbose 41 | native_builds: 42 | needs: code_cleaniness_checks 43 | name: Native Tests 44 | runs-on: ${{ matrix.os }} 45 | strategy: 46 | matrix: 47 | rust: [stable, beta, nightly] 48 | os: [ubuntu-latest, windows-latest, macOS-latest] 49 | steps: 50 | - uses: hecrj/setup-rust-action@v2 51 | with: 52 | rust-version: ${{ matrix.rust }} 53 | components: rustfmt 54 | - uses: actions/checkout@v4 55 | - run: cargo fmt --check 56 | - run: cargo build --verbose 57 | - run: cargo test --verbose 58 | - run: cargo test --no-default-features --features async-std --verbose 59 | - run: cargo doc --verbose 60 | coverage: 61 | needs: [native_builds, wasm_build] 62 | name: Coverage 63 | runs-on: ubuntu-latest 64 | steps: 65 | - name: Checkout repository 66 | uses: actions/checkout@v4 67 | - name: Setup Rust 68 | uses: hecrj/setup-rust-action@v2 69 | with: 70 | rust-version: stable 71 | - name: Fetch Tarpaulin 72 | uses: actions-rs/install@v0.1 73 | with: 74 | crate: cargo-tarpaulin 75 | version: 'latest' 76 | - name: Generate code coverage 77 | run: | 78 | cargo tarpaulin --verbose --workspace --timeout 120 --out Xml 79 | - name: Report coverage to Codecov 80 | uses: codecov/codecov-action@v5 81 | env: 82 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 83 | 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | examples/*/target 4 | /.cargo 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "troupe" 3 | version = "0.2.0" 4 | edition = "2021" 5 | rust-version = "1.74" 6 | authors = ["Tyler Bloom "] 7 | license = "LGPL-2.1" 8 | readme = "README.md" 9 | repository = "https://github.com/TylerBloom/troupe" 10 | description = "Library for modelling Rust applications with actors" 11 | categories = ["asynchronous"] 12 | keywords = ["async", "non-blocking", "futures"] 13 | 14 | [package.metadata.docs.rs] 15 | all-features = false 16 | targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] 17 | 18 | [features] 19 | default = ["tokio"] 20 | 21 | [dependencies] 22 | tokio = { version = "1.33", features = ["sync", "macros"] } 23 | tokio-stream = { version = "0.1", features = ["sync"] } 24 | futures = "0.3.19" 25 | instant = { version = "0.1" } 26 | pin-project = { version = "1.1" } 27 | anymap2 = "0.13" 28 | 29 | [target.'cfg(target_arch = "wasm32")'.dependencies] 30 | wasm-bindgen-futures = { version = "0.4.37" } 31 | instant = { version = "0.1", features = ["wasm-bindgen"] } 32 | gloo-timers = { version = "0.3", features = ["futures"] } 33 | send_wrapper = { version = "0.6", features = ["futures"] } 34 | 35 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 36 | tokio = { version = "1.33", features = ["sync", "rt"], optional = true } 37 | async-std = { version = "1.12.0", optional = true } 38 | 39 | [dev-dependencies] 40 | tokio = { version = "1.33", features = ["rt"] } 41 | 42 | [target.'cfg(target_arch = "wasm32")'.dev-dependencies] 43 | wasm-bindgen-test = "0.3" 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 489 | USA 490 | 491 | Also add information on how to contact you by electronic and paper mail. 492 | 493 | You should also get your employer (if you work as a programmer) or your 494 | school, if any, to sign a "copyright disclaimer" for the library, if 495 | necessary. Here is a sample; alter the names: 496 | 497 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 498 | library `Frob' (a library for tweaking knobs) written by James Random 499 | Hacker. 500 | 501 | , 1 April 1990 502 | Ty Coon, President of Vice 503 | 504 | That's all there is to it! 505 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Crates.io](https://img.shields.io/crates/v/troupe.svg)](https://crates.io/crates/troupe) 2 | [![Documentation](https://docs.rs/troupe/badge.svg)](https://docs.rs/troupe/) 3 | ![GitHub Workflows](https://github.com/TylerBloom/troupe/actions/workflows/ci.yml/badge.svg) 4 | [![Coverage Status](https://codecov.io/gh/TylerBloom/troupe/branch/main/graph/badge.svg)](https://codecov.io/gh/TylerBloom/troupe) 5 | ![Maintenance](https://img.shields.io/badge/Maintenance-Actively%20Developed-brightgreen.svg) 6 | 7 | # About 8 | Troupe is an experimental, high-level library for modeling your application using the [actor model](https://en.wikipedia.org/wiki/Actor_model). 9 | Actors model concurrent mutable state via isolated components and message passing. 10 | This approach enables you to both have clear seperation of concerns between different components of your program as well as plainly model how a component can change. 11 | An actor approach is strongest when: 12 | - there are multiple independent sources of mutation in your application 13 | - you want to model the data flow through part or all of your system 14 | - you want to want to build your system from a series of distinct components that can evolve independently 15 | 16 | Troupe was born while building full-stack Rust apps. 17 | There, the frontend client authenticates, opens a websocket connection, pulls data from the server, and presents a UI to the user. 18 | In the single-threaded envinorment of WASM, it is difficult to manage all of the sources of mutation: UI interactions, updates from the server, etc. 19 | There, actors can be used to manage the websocket connection, process data from the server, and propagate it to the rest of the app (including non-actor parts like the UI). 20 | The UI can then communicate with the actors to present the UI and process interactions. 21 | On the server side, actors can be used to manage websockets, buffer communicate between the database and cached data, and track changes in a user's session info. 22 | 23 | To achieve isolation, actors in `troupe` are contained within an async processes of existing async runtimes. 24 | Because of this, `troupe` is able to support several async runtimes, including `tokio`, `async-std`, and `wasm-bindgen-futures` (for WASM targets), which allows you to slowly incorporate it into your project's architecture. 25 | Do note that actor communication uses `tokio`'s channels regardless of the async runtime. 26 | 27 | # Model 28 | The heart of a `troupe` actor is a state type and a message type. 29 | Let's take a simple cache as an example: 30 | ```rust 31 | pub struct Cache(HashMap); 32 | 33 | pub enum CacheCommand { 34 | Insert(Uuid, YourData), 35 | Get(Uuid, OneshotSender>), 36 | Delete(Uuid), 37 | } 38 | ``` 39 | 40 | The message type encapsulates how the state can change and what data the actor has to process. These messages are sent from various source to be processed by the state. To finish the actor, `Cache` just has to implement the `ActorState` trait. 41 | ```rust 42 | impl ActorState for Cache { 43 | type Permanence = Permanent; 44 | type ActorType = SinkActor; 45 | 46 | type Message = CacheCommand; 47 | type Output = (); 48 | 49 | async fn process(&mut self, _: &mut Scheduler, msg: CacheCommand) { 50 | match msg { 51 | CacheCommand::Insert(key, val) => { 52 | self.inner.insert(key, val); 53 | } 54 | CacheCommand::Get(key, send) => { 55 | let _ = send.send(self.inner.get(&key).cloned()); 56 | } 57 | CacheCommand::Delete(key) => { 58 | self.inner.remove(&key); 59 | } 60 | } 61 | } 62 | } 63 | ``` 64 | 65 | `ActorState` has several other associated types beyond the message type. 66 | These types are mostly marker types and inform how other parts of your program should interact with the actor. 67 | This communication is done through a client, which is created when the actor is launched. 68 | The associated `ActorType` type tells the client if the actor expects messages to be sent to it or if messages will be broadcast from the actor (or both). 69 | The associated `Permanence` type informs the client if it should expect the actor to close at any point. 70 | Lastly, the associated `Output` type is only used for actor that broadcast messages, in which case the actor will broadcast messages of the `Output` type. 71 | 72 | Once running, `troupe` pairs every actor state with a scheduler. 73 | The scheduler is responsible for managing futures that the state queues and attached streams of message. 74 | The queued futures will be polled at the same time that the scheduler waits for inbound messages. 75 | Most actors have one attached stream by default, the channel used to communicate between the client and the actor. 76 | Client message streams use tokio MPSC-style channels, but actors can add any stream that yield messages that can be converted into the actor's message type (such as socket connections). 77 | Conceptually, the scheduler-actor relationship can be model as: 78 | ``` 79 | _____________________________________________ 80 | | Actor | 81 | | ___________ ___________ | 82 | | | Scheduler | --- message --> | State | | 83 | | | [streams] | <-- streams --- | | | 84 | | | [futures] | <-- futures --- | | | 85 | | |___________| |___________| | 86 | |_____________________________________________| 87 | ``` 88 | 89 | As stated before, every actor returns a client. 90 | Each actor defines how its clients behave. 91 | There are three types of clients: `SinkClient`, `StreamClient`, and `JointClient`. 92 | 93 | A `SinkClient` is the type of client you are most likely to use. 94 | It enables you to send messages directly to the actor. 95 | These messages can be either "fire-and-forget" or "request-response" style messages. 96 | Note that a `SinkClient` does not actually implement the [`Sink`](https://docs.rs/futures/latest/futures/sink/trait.Sink.html) trait, but, rather, serves the same conceptual purpose. 97 | A `StreamClient` listens for messages that are broadcast from an actor. 98 | Unlike a `SinkClient`, a `StreamClient` does not directly support any type of message passing into the actor. 99 | It does, however, implement the [`Stream`](https://docs.rs/futures/latest/futures/stream/trait.Stream.html) trait. 100 | Lastly, a `JointClient` is both a `SinkClient` and a `StreamClient` put together. 101 | A `JointClient` can be decomposed into one or both of the other clients. 102 | Note, the actor type defines what kind of clients can be constructed, so you can not, for example, construct a `StreamClient` for an actor that will never broadcast a message. 103 | 104 | The last major component of the `troupe` actor model is permanence. 105 | Some actors are designed to run forever. 106 | These are called `Permanent` actors. 107 | Other actors are designed to run for some time (perhaps a long time) and then close. 108 | These are called `Transient` actors. 109 | This distinction largely serves to help with the ergonomics of interacting with actors. 110 | If an actor is designed to never shutdown, then the oneshot channels used for request-response style messages can be safely unwrapped. 111 | The same is not true for `Transient` actors. 112 | 113 | Regardless of the permanence of the actor, all actors might exhaust their source of messages. 114 | This happens when all streams have ran dry and no message-yeilding futures are queued. 115 | When this happens, there is built-in "garbage collection" for troupe actors. 116 | The scheduler will mark an actor as "dead" and then shutdown the actor process if it ever reaches this state. 117 | For `SinkActors`, this can only occur if all message-sending clients have been dropped. 118 | 119 | # Backwards Compatibility 120 | Troupe is currently experimental and subject to potential breaking changes (with due consideration). 121 | Breaking changes might occur to improve API ergonomics, to tweak the actor model, or to use new Rust language features. 122 | In particular, there are several language features that will be used improve this crate upon stabilization: 123 | - [`async fn` in traits](https://rust-lang.github.io/async-book/07_workarounds/05_async_in_traits.html) 124 | - [specialization](https://rust-lang.github.io/rfcs/1210-impl-specialization.html) 125 | - [associate-type defaults](https://rust-lang.github.io/rfcs/2532-associated-type-defaults.html) 126 | 127 | The stabilization of the first two present garunteed breaking changes for this crate but will drastically improve usability and ergonomics. 128 | Specialization will enable the `ActorBuilder` to present identically-named methods for launching the actor while returning the appropiate client type. 129 | The stabilization of `async fn` in traits will allow for the loosening of constraints on `ActorState`s in WASM contexts, allowing them to just be `'static` instead of `'static + Send`. 130 | 131 | # Future Work 132 | Currently, `troupe` only provides a framework for building actors. 133 | If there are certain patterns that are commonplace, a general version of that pattern might find its way into this crate or a similar crate. 134 | One such example is something like a local-first buffered state. 135 | Here, you have some data that exists in two locations, say client and server, and you want update the state in one location then propagate those changes elsewhere. 136 | 137 | Another possible actor pattern in a "poll" actor. 138 | This would build upon a broadcast actor, but the broadcast message would contain a oneshot channel for communicating a "vote" with the actor. 139 | 140 | 141 | # Usage and Licensing 142 | This project is currently licensed under a LGPL-2.1 license. 143 | This allows you to use this crate to build other crates (library or application) under any license (open-source or otherwise). 144 | The primary intent of using this license is to require crates that modify to source of this crate to also be open-source licensed (LGPL or stronger). 145 | 146 | # Contributing 147 | If there is an aspect of the project that you wish to see changed, improved, or even removed, open a ticket or PR. 148 | If you have an interesting design pattern that uses actors and would like to see if in this crate, open a ticket!! 149 | -------------------------------------------------------------------------------- /examples/cache/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "async-trait" 22 | version = "0.1.74" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" 25 | dependencies = [ 26 | "proc-macro2", 27 | "quote", 28 | "syn 2.0.39", 29 | ] 30 | 31 | [[package]] 32 | name = "autocfg" 33 | version = "1.1.0" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 36 | 37 | [[package]] 38 | name = "backtrace" 39 | version = "0.3.69" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 42 | dependencies = [ 43 | "addr2line", 44 | "cc", 45 | "cfg-if", 46 | "libc", 47 | "miniz_oxide", 48 | "object", 49 | "rustc-demangle", 50 | ] 51 | 52 | [[package]] 53 | name = "bitflags" 54 | version = "1.3.2" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 57 | 58 | [[package]] 59 | name = "bumpalo" 60 | version = "3.14.0" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" 63 | 64 | [[package]] 65 | name = "bytes" 66 | version = "1.5.0" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" 69 | 70 | [[package]] 71 | name = "cc" 72 | version = "1.0.83" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 75 | dependencies = [ 76 | "libc", 77 | ] 78 | 79 | [[package]] 80 | name = "cfg-if" 81 | version = "1.0.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 84 | 85 | [[package]] 86 | name = "convert_case" 87 | version = "0.4.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" 90 | 91 | [[package]] 92 | name = "derive_more" 93 | version = "0.99.17" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" 96 | dependencies = [ 97 | "convert_case", 98 | "proc-macro2", 99 | "quote", 100 | "rustc_version", 101 | "syn 1.0.109", 102 | ] 103 | 104 | [[package]] 105 | name = "futures" 106 | version = "0.3.29" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" 109 | dependencies = [ 110 | "futures-channel", 111 | "futures-core", 112 | "futures-executor", 113 | "futures-io", 114 | "futures-sink", 115 | "futures-task", 116 | "futures-util", 117 | ] 118 | 119 | [[package]] 120 | name = "futures-channel" 121 | version = "0.3.29" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" 124 | dependencies = [ 125 | "futures-core", 126 | "futures-sink", 127 | ] 128 | 129 | [[package]] 130 | name = "futures-core" 131 | version = "0.3.29" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" 134 | 135 | [[package]] 136 | name = "futures-example-imperative" 137 | version = "0.1.0" 138 | dependencies = [ 139 | "derive_more", 140 | "tokio", 141 | "troupe", 142 | ] 143 | 144 | [[package]] 145 | name = "futures-executor" 146 | version = "0.3.29" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" 149 | dependencies = [ 150 | "futures-core", 151 | "futures-task", 152 | "futures-util", 153 | ] 154 | 155 | [[package]] 156 | name = "futures-io" 157 | version = "0.3.29" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" 160 | 161 | [[package]] 162 | name = "futures-macro" 163 | version = "0.3.29" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" 166 | dependencies = [ 167 | "proc-macro2", 168 | "quote", 169 | "syn 2.0.39", 170 | ] 171 | 172 | [[package]] 173 | name = "futures-sink" 174 | version = "0.3.29" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" 177 | 178 | [[package]] 179 | name = "futures-task" 180 | version = "0.3.29" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" 183 | 184 | [[package]] 185 | name = "futures-util" 186 | version = "0.3.29" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" 189 | dependencies = [ 190 | "futures-channel", 191 | "futures-core", 192 | "futures-io", 193 | "futures-macro", 194 | "futures-sink", 195 | "futures-task", 196 | "memchr", 197 | "pin-project-lite", 198 | "pin-utils", 199 | "slab", 200 | ] 201 | 202 | [[package]] 203 | name = "gimli" 204 | version = "0.28.0" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" 207 | 208 | [[package]] 209 | name = "gloo-timers" 210 | version = "0.3.0" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" 213 | dependencies = [ 214 | "futures-channel", 215 | "futures-core", 216 | "js-sys", 217 | "wasm-bindgen", 218 | ] 219 | 220 | [[package]] 221 | name = "hermit-abi" 222 | version = "0.3.3" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" 225 | 226 | [[package]] 227 | name = "instant" 228 | version = "0.1.12" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 231 | dependencies = [ 232 | "cfg-if", 233 | "js-sys", 234 | "wasm-bindgen", 235 | "web-sys", 236 | ] 237 | 238 | [[package]] 239 | name = "js-sys" 240 | version = "0.3.65" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" 243 | dependencies = [ 244 | "wasm-bindgen", 245 | ] 246 | 247 | [[package]] 248 | name = "libc" 249 | version = "0.2.150" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" 252 | 253 | [[package]] 254 | name = "lock_api" 255 | version = "0.4.11" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 258 | dependencies = [ 259 | "autocfg", 260 | "scopeguard", 261 | ] 262 | 263 | [[package]] 264 | name = "log" 265 | version = "0.4.20" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 268 | 269 | [[package]] 270 | name = "memchr" 271 | version = "2.6.4" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 274 | 275 | [[package]] 276 | name = "miniz_oxide" 277 | version = "0.7.1" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 280 | dependencies = [ 281 | "adler", 282 | ] 283 | 284 | [[package]] 285 | name = "mio" 286 | version = "0.8.9" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" 289 | dependencies = [ 290 | "libc", 291 | "wasi", 292 | "windows-sys", 293 | ] 294 | 295 | [[package]] 296 | name = "num_cpus" 297 | version = "1.16.0" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 300 | dependencies = [ 301 | "hermit-abi", 302 | "libc", 303 | ] 304 | 305 | [[package]] 306 | name = "object" 307 | version = "0.32.1" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" 310 | dependencies = [ 311 | "memchr", 312 | ] 313 | 314 | [[package]] 315 | name = "once_cell" 316 | version = "1.18.0" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 319 | 320 | [[package]] 321 | name = "parking_lot" 322 | version = "0.12.1" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 325 | dependencies = [ 326 | "lock_api", 327 | "parking_lot_core", 328 | ] 329 | 330 | [[package]] 331 | name = "parking_lot_core" 332 | version = "0.9.9" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 335 | dependencies = [ 336 | "cfg-if", 337 | "libc", 338 | "redox_syscall", 339 | "smallvec", 340 | "windows-targets", 341 | ] 342 | 343 | [[package]] 344 | name = "pin-project" 345 | version = "1.1.3" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" 348 | dependencies = [ 349 | "pin-project-internal", 350 | ] 351 | 352 | [[package]] 353 | name = "pin-project-internal" 354 | version = "1.1.3" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" 357 | dependencies = [ 358 | "proc-macro2", 359 | "quote", 360 | "syn 2.0.39", 361 | ] 362 | 363 | [[package]] 364 | name = "pin-project-lite" 365 | version = "0.2.13" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 368 | 369 | [[package]] 370 | name = "pin-utils" 371 | version = "0.1.0" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 374 | 375 | [[package]] 376 | name = "proc-macro2" 377 | version = "1.0.69" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" 380 | dependencies = [ 381 | "unicode-ident", 382 | ] 383 | 384 | [[package]] 385 | name = "quote" 386 | version = "1.0.33" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 389 | dependencies = [ 390 | "proc-macro2", 391 | ] 392 | 393 | [[package]] 394 | name = "redox_syscall" 395 | version = "0.4.1" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 398 | dependencies = [ 399 | "bitflags", 400 | ] 401 | 402 | [[package]] 403 | name = "rustc-demangle" 404 | version = "0.1.23" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 407 | 408 | [[package]] 409 | name = "rustc_version" 410 | version = "0.4.0" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 413 | dependencies = [ 414 | "semver", 415 | ] 416 | 417 | [[package]] 418 | name = "scopeguard" 419 | version = "1.2.0" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 422 | 423 | [[package]] 424 | name = "semver" 425 | version = "1.0.20" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" 428 | 429 | [[package]] 430 | name = "send_wrapper" 431 | version = "0.6.0" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" 434 | dependencies = [ 435 | "futures-core", 436 | ] 437 | 438 | [[package]] 439 | name = "signal-hook-registry" 440 | version = "1.4.1" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 443 | dependencies = [ 444 | "libc", 445 | ] 446 | 447 | [[package]] 448 | name = "slab" 449 | version = "0.4.9" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 452 | dependencies = [ 453 | "autocfg", 454 | ] 455 | 456 | [[package]] 457 | name = "smallvec" 458 | version = "1.11.2" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" 461 | 462 | [[package]] 463 | name = "socket2" 464 | version = "0.5.5" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" 467 | dependencies = [ 468 | "libc", 469 | "windows-sys", 470 | ] 471 | 472 | [[package]] 473 | name = "syn" 474 | version = "1.0.109" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 477 | dependencies = [ 478 | "proc-macro2", 479 | "quote", 480 | "unicode-ident", 481 | ] 482 | 483 | [[package]] 484 | name = "syn" 485 | version = "2.0.39" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" 488 | dependencies = [ 489 | "proc-macro2", 490 | "quote", 491 | "unicode-ident", 492 | ] 493 | 494 | [[package]] 495 | name = "tokio" 496 | version = "1.34.0" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" 499 | dependencies = [ 500 | "backtrace", 501 | "bytes", 502 | "libc", 503 | "mio", 504 | "num_cpus", 505 | "parking_lot", 506 | "pin-project-lite", 507 | "signal-hook-registry", 508 | "socket2", 509 | "tokio-macros", 510 | "windows-sys", 511 | ] 512 | 513 | [[package]] 514 | name = "tokio-macros" 515 | version = "2.2.0" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" 518 | dependencies = [ 519 | "proc-macro2", 520 | "quote", 521 | "syn 2.0.39", 522 | ] 523 | 524 | [[package]] 525 | name = "tokio-stream" 526 | version = "0.1.14" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" 529 | dependencies = [ 530 | "futures-core", 531 | "pin-project-lite", 532 | "tokio", 533 | "tokio-util", 534 | ] 535 | 536 | [[package]] 537 | name = "tokio-util" 538 | version = "0.7.10" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" 541 | dependencies = [ 542 | "bytes", 543 | "futures-core", 544 | "futures-sink", 545 | "pin-project-lite", 546 | "tokio", 547 | ] 548 | 549 | [[package]] 550 | name = "troupe" 551 | version = "0.1.0" 552 | dependencies = [ 553 | "async-trait", 554 | "futures", 555 | "gloo-timers", 556 | "instant", 557 | "pin-project", 558 | "send_wrapper", 559 | "tokio", 560 | "tokio-stream", 561 | "wasm-bindgen-futures", 562 | ] 563 | 564 | [[package]] 565 | name = "unicode-ident" 566 | version = "1.0.12" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 569 | 570 | [[package]] 571 | name = "wasi" 572 | version = "0.11.0+wasi-snapshot-preview1" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 575 | 576 | [[package]] 577 | name = "wasm-bindgen" 578 | version = "0.2.88" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" 581 | dependencies = [ 582 | "cfg-if", 583 | "wasm-bindgen-macro", 584 | ] 585 | 586 | [[package]] 587 | name = "wasm-bindgen-backend" 588 | version = "0.2.88" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" 591 | dependencies = [ 592 | "bumpalo", 593 | "log", 594 | "once_cell", 595 | "proc-macro2", 596 | "quote", 597 | "syn 2.0.39", 598 | "wasm-bindgen-shared", 599 | ] 600 | 601 | [[package]] 602 | name = "wasm-bindgen-futures" 603 | version = "0.4.38" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" 606 | dependencies = [ 607 | "cfg-if", 608 | "js-sys", 609 | "wasm-bindgen", 610 | "web-sys", 611 | ] 612 | 613 | [[package]] 614 | name = "wasm-bindgen-macro" 615 | version = "0.2.88" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" 618 | dependencies = [ 619 | "quote", 620 | "wasm-bindgen-macro-support", 621 | ] 622 | 623 | [[package]] 624 | name = "wasm-bindgen-macro-support" 625 | version = "0.2.88" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" 628 | dependencies = [ 629 | "proc-macro2", 630 | "quote", 631 | "syn 2.0.39", 632 | "wasm-bindgen-backend", 633 | "wasm-bindgen-shared", 634 | ] 635 | 636 | [[package]] 637 | name = "wasm-bindgen-shared" 638 | version = "0.2.88" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" 641 | 642 | [[package]] 643 | name = "web-sys" 644 | version = "0.3.65" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" 647 | dependencies = [ 648 | "js-sys", 649 | "wasm-bindgen", 650 | ] 651 | 652 | [[package]] 653 | name = "windows-sys" 654 | version = "0.48.0" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 657 | dependencies = [ 658 | "windows-targets", 659 | ] 660 | 661 | [[package]] 662 | name = "windows-targets" 663 | version = "0.48.5" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 666 | dependencies = [ 667 | "windows_aarch64_gnullvm", 668 | "windows_aarch64_msvc", 669 | "windows_i686_gnu", 670 | "windows_i686_msvc", 671 | "windows_x86_64_gnu", 672 | "windows_x86_64_gnullvm", 673 | "windows_x86_64_msvc", 674 | ] 675 | 676 | [[package]] 677 | name = "windows_aarch64_gnullvm" 678 | version = "0.48.5" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 681 | 682 | [[package]] 683 | name = "windows_aarch64_msvc" 684 | version = "0.48.5" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 687 | 688 | [[package]] 689 | name = "windows_i686_gnu" 690 | version = "0.48.5" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 693 | 694 | [[package]] 695 | name = "windows_i686_msvc" 696 | version = "0.48.5" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 699 | 700 | [[package]] 701 | name = "windows_x86_64_gnu" 702 | version = "0.48.5" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 705 | 706 | [[package]] 707 | name = "windows_x86_64_gnullvm" 708 | version = "0.48.5" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 711 | 712 | [[package]] 713 | name = "windows_x86_64_msvc" 714 | version = "0.48.5" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 717 | -------------------------------------------------------------------------------- /examples/cache/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "futures-example-imperative" 3 | edition = "2021" 4 | version = "0.1.0" 5 | publish = false 6 | 7 | [dependencies] 8 | troupe = { path = "../../" } 9 | derive_more = "0.99.17" 10 | tokio = { version = "1.33", features = ["full"] } 11 | -------------------------------------------------------------------------------- /examples/cache/README.md: -------------------------------------------------------------------------------- 1 | # About 2 | By wrapping a hashmap in an `ActorState`, you can create a simple cache implementation. This cache allows for inserting, retrieving, deleting items from the cache as well as performing arbitrary queries on a specific item as well as the whole cache. 3 | -------------------------------------------------------------------------------- /examples/cache/src/cache.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, hash::Hash}; 2 | 3 | use derive_more::From; 4 | use troupe::prelude::*; 5 | 6 | /// The wrapper for the hashmap that will be our actor. 7 | #[derive(Default)] 8 | pub struct Cache { 9 | inner: HashMap, 10 | } 11 | 12 | /// The type that the cache receives and processes. `derive_more`'s `From` macro is very handy 13 | /// here. 14 | #[derive(From)] 15 | pub enum CacheCommand { 16 | Insert(K, T), 17 | Get(K, OneshotSender>), 18 | Delete(K), 19 | #[allow(clippy::type_complexity)] 20 | #[from(ignore)] 21 | Query(K, Box)>), 22 | } 23 | 24 | #[async_trait] 25 | impl ActorState for Cache 26 | where 27 | K: 'static + Send + Hash + Eq, 28 | T: 'static + Send + Clone, 29 | { 30 | type Message = CacheCommand; 31 | 32 | /// This actor is a [`SinkActor`] as it does not broadcast anything 33 | type ActorType = SinkActor; 34 | 35 | /// Sink actors don't output anything, so we can use a unit here. Ideally, [`ActorState`] would 36 | /// have a type default for this. 37 | type Output = (); 38 | 39 | async fn process(&mut self, _: &mut Scheduler, msg: CacheCommand) { 40 | println!("Message received!!"); 41 | match msg { 42 | CacheCommand::Insert(key, val) => { 43 | self.inner.insert(key, val); 44 | } 45 | // The OneshotSender returns a returns a Result since the Receiver might have been 46 | // dropped. If this happens, then the call no longer wants the result of the query 47 | CacheCommand::Get(key, send) => drop(send.send(self.inner.get(&key).cloned())), 48 | CacheCommand::Delete(key) => { 49 | self.inner.remove(&key); 50 | } 51 | CacheCommand::Query(key, query) => { 52 | query(self.inner.get(&key)); 53 | } 54 | } 55 | } 56 | } 57 | 58 | /// In order to allow users to return arbitary types from inside the `process` method, we must box 59 | /// the query function and send the data back to the user from within the boxed function. 60 | impl From<((K, F), OneshotSender>)> for CacheCommand 61 | where 62 | F: 'static + Send + FnOnce(&T) -> M, 63 | M: 'static + Send, 64 | K: 'static + Send + Hash + Eq, 65 | T: 'static + Send + Clone, 66 | { 67 | fn from(((key, query), send): ((K, F), OneshotSender>)) -> Self { 68 | let query = Box::new(move |item: Option<&T>| { 69 | let _ = send.send(item.map(query)); 70 | }); 71 | Self::Query(key, query) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /examples/cache/src/main.rs: -------------------------------------------------------------------------------- 1 | use cache::Cache; 2 | use troupe::ActorBuilder; 3 | 4 | mod cache; 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | let state: Cache = Cache::default(); 9 | let client = ActorBuilder::new(state).launch_sink(); 10 | client.send((1, "one")); 11 | let val = client.track(1).await.unwrap(); 12 | assert_eq!(val, "one"); 13 | let answer = client.track((1, |s: &&str| s.to_string())).await.unwrap_or_default(); 14 | assert_eq!(answer, "one"); 15 | let answer = client.track((2, |s: &&str| s.to_string())).await.unwrap_or_default(); 16 | assert_eq!(answer, ""); 17 | client.send(1); 18 | let answer = client.track((1, |s: &&str| s.to_string())).await.unwrap_or_default(); 19 | assert_eq!(answer, ""); 20 | } 21 | -------------------------------------------------------------------------------- /examples/ping/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "async-trait" 22 | version = "0.1.74" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" 25 | dependencies = [ 26 | "proc-macro2", 27 | "quote", 28 | "syn 2.0.39", 29 | ] 30 | 31 | [[package]] 32 | name = "autocfg" 33 | version = "1.1.0" 34 | source = "registry+https://github.com/rust-lang/crates.io-index" 35 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 36 | 37 | [[package]] 38 | name = "backtrace" 39 | version = "0.3.69" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" 42 | dependencies = [ 43 | "addr2line", 44 | "cc", 45 | "cfg-if", 46 | "libc", 47 | "miniz_oxide", 48 | "object", 49 | "rustc-demangle", 50 | ] 51 | 52 | [[package]] 53 | name = "bitflags" 54 | version = "1.3.2" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 57 | 58 | [[package]] 59 | name = "bumpalo" 60 | version = "3.14.0" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" 63 | 64 | [[package]] 65 | name = "bytes" 66 | version = "1.5.0" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" 69 | 70 | [[package]] 71 | name = "cc" 72 | version = "1.0.83" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" 75 | dependencies = [ 76 | "libc", 77 | ] 78 | 79 | [[package]] 80 | name = "cfg-if" 81 | version = "1.0.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 84 | 85 | [[package]] 86 | name = "convert_case" 87 | version = "0.4.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" 90 | 91 | [[package]] 92 | name = "derive_more" 93 | version = "0.99.17" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" 96 | dependencies = [ 97 | "convert_case", 98 | "proc-macro2", 99 | "quote", 100 | "rustc_version", 101 | "syn 1.0.109", 102 | ] 103 | 104 | [[package]] 105 | name = "futures" 106 | version = "0.3.29" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" 109 | dependencies = [ 110 | "futures-channel", 111 | "futures-core", 112 | "futures-executor", 113 | "futures-io", 114 | "futures-sink", 115 | "futures-task", 116 | "futures-util", 117 | ] 118 | 119 | [[package]] 120 | name = "futures-channel" 121 | version = "0.3.29" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" 124 | dependencies = [ 125 | "futures-core", 126 | "futures-sink", 127 | ] 128 | 129 | [[package]] 130 | name = "futures-core" 131 | version = "0.3.29" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" 134 | 135 | [[package]] 136 | name = "futures-example-imperative" 137 | version = "0.1.0" 138 | dependencies = [ 139 | "derive_more", 140 | "tokio", 141 | "troupe", 142 | ] 143 | 144 | [[package]] 145 | name = "futures-executor" 146 | version = "0.3.29" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" 149 | dependencies = [ 150 | "futures-core", 151 | "futures-task", 152 | "futures-util", 153 | ] 154 | 155 | [[package]] 156 | name = "futures-io" 157 | version = "0.3.29" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" 160 | 161 | [[package]] 162 | name = "futures-macro" 163 | version = "0.3.29" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" 166 | dependencies = [ 167 | "proc-macro2", 168 | "quote", 169 | "syn 2.0.39", 170 | ] 171 | 172 | [[package]] 173 | name = "futures-sink" 174 | version = "0.3.29" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" 177 | 178 | [[package]] 179 | name = "futures-task" 180 | version = "0.3.29" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" 183 | 184 | [[package]] 185 | name = "futures-util" 186 | version = "0.3.29" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" 189 | dependencies = [ 190 | "futures-channel", 191 | "futures-core", 192 | "futures-io", 193 | "futures-macro", 194 | "futures-sink", 195 | "futures-task", 196 | "memchr", 197 | "pin-project-lite", 198 | "pin-utils", 199 | "slab", 200 | ] 201 | 202 | [[package]] 203 | name = "gimli" 204 | version = "0.28.0" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" 207 | 208 | [[package]] 209 | name = "gloo-timers" 210 | version = "0.3.0" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" 213 | dependencies = [ 214 | "futures-channel", 215 | "futures-core", 216 | "js-sys", 217 | "wasm-bindgen", 218 | ] 219 | 220 | [[package]] 221 | name = "hermit-abi" 222 | version = "0.3.3" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" 225 | 226 | [[package]] 227 | name = "instant" 228 | version = "0.1.12" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 231 | dependencies = [ 232 | "cfg-if", 233 | "js-sys", 234 | "wasm-bindgen", 235 | "web-sys", 236 | ] 237 | 238 | [[package]] 239 | name = "js-sys" 240 | version = "0.3.65" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" 243 | dependencies = [ 244 | "wasm-bindgen", 245 | ] 246 | 247 | [[package]] 248 | name = "libc" 249 | version = "0.2.150" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" 252 | 253 | [[package]] 254 | name = "lock_api" 255 | version = "0.4.11" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 258 | dependencies = [ 259 | "autocfg", 260 | "scopeguard", 261 | ] 262 | 263 | [[package]] 264 | name = "log" 265 | version = "0.4.20" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 268 | 269 | [[package]] 270 | name = "memchr" 271 | version = "2.6.4" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 274 | 275 | [[package]] 276 | name = "miniz_oxide" 277 | version = "0.7.1" 278 | source = "registry+https://github.com/rust-lang/crates.io-index" 279 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 280 | dependencies = [ 281 | "adler", 282 | ] 283 | 284 | [[package]] 285 | name = "mio" 286 | version = "0.8.9" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" 289 | dependencies = [ 290 | "libc", 291 | "wasi", 292 | "windows-sys", 293 | ] 294 | 295 | [[package]] 296 | name = "num_cpus" 297 | version = "1.16.0" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 300 | dependencies = [ 301 | "hermit-abi", 302 | "libc", 303 | ] 304 | 305 | [[package]] 306 | name = "object" 307 | version = "0.32.1" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" 310 | dependencies = [ 311 | "memchr", 312 | ] 313 | 314 | [[package]] 315 | name = "once_cell" 316 | version = "1.18.0" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" 319 | 320 | [[package]] 321 | name = "parking_lot" 322 | version = "0.12.1" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 325 | dependencies = [ 326 | "lock_api", 327 | "parking_lot_core", 328 | ] 329 | 330 | [[package]] 331 | name = "parking_lot_core" 332 | version = "0.9.9" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 335 | dependencies = [ 336 | "cfg-if", 337 | "libc", 338 | "redox_syscall", 339 | "smallvec", 340 | "windows-targets", 341 | ] 342 | 343 | [[package]] 344 | name = "pin-project" 345 | version = "1.1.3" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" 348 | dependencies = [ 349 | "pin-project-internal", 350 | ] 351 | 352 | [[package]] 353 | name = "pin-project-internal" 354 | version = "1.1.3" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" 357 | dependencies = [ 358 | "proc-macro2", 359 | "quote", 360 | "syn 2.0.39", 361 | ] 362 | 363 | [[package]] 364 | name = "pin-project-lite" 365 | version = "0.2.13" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 368 | 369 | [[package]] 370 | name = "pin-utils" 371 | version = "0.1.0" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 374 | 375 | [[package]] 376 | name = "proc-macro2" 377 | version = "1.0.69" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" 380 | dependencies = [ 381 | "unicode-ident", 382 | ] 383 | 384 | [[package]] 385 | name = "quote" 386 | version = "1.0.33" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 389 | dependencies = [ 390 | "proc-macro2", 391 | ] 392 | 393 | [[package]] 394 | name = "redox_syscall" 395 | version = "0.4.1" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 398 | dependencies = [ 399 | "bitflags", 400 | ] 401 | 402 | [[package]] 403 | name = "rustc-demangle" 404 | version = "0.1.23" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 407 | 408 | [[package]] 409 | name = "rustc_version" 410 | version = "0.4.0" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 413 | dependencies = [ 414 | "semver", 415 | ] 416 | 417 | [[package]] 418 | name = "scopeguard" 419 | version = "1.2.0" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 422 | 423 | [[package]] 424 | name = "semver" 425 | version = "1.0.20" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" 428 | 429 | [[package]] 430 | name = "send_wrapper" 431 | version = "0.6.0" 432 | source = "registry+https://github.com/rust-lang/crates.io-index" 433 | checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" 434 | dependencies = [ 435 | "futures-core", 436 | ] 437 | 438 | [[package]] 439 | name = "signal-hook-registry" 440 | version = "1.4.1" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 443 | dependencies = [ 444 | "libc", 445 | ] 446 | 447 | [[package]] 448 | name = "slab" 449 | version = "0.4.9" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 452 | dependencies = [ 453 | "autocfg", 454 | ] 455 | 456 | [[package]] 457 | name = "smallvec" 458 | version = "1.11.2" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" 461 | 462 | [[package]] 463 | name = "socket2" 464 | version = "0.5.5" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" 467 | dependencies = [ 468 | "libc", 469 | "windows-sys", 470 | ] 471 | 472 | [[package]] 473 | name = "syn" 474 | version = "1.0.109" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 477 | dependencies = [ 478 | "proc-macro2", 479 | "quote", 480 | "unicode-ident", 481 | ] 482 | 483 | [[package]] 484 | name = "syn" 485 | version = "2.0.39" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" 488 | dependencies = [ 489 | "proc-macro2", 490 | "quote", 491 | "unicode-ident", 492 | ] 493 | 494 | [[package]] 495 | name = "tokio" 496 | version = "1.34.0" 497 | source = "registry+https://github.com/rust-lang/crates.io-index" 498 | checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" 499 | dependencies = [ 500 | "backtrace", 501 | "bytes", 502 | "libc", 503 | "mio", 504 | "num_cpus", 505 | "parking_lot", 506 | "pin-project-lite", 507 | "signal-hook-registry", 508 | "socket2", 509 | "tokio-macros", 510 | "windows-sys", 511 | ] 512 | 513 | [[package]] 514 | name = "tokio-macros" 515 | version = "2.2.0" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" 518 | dependencies = [ 519 | "proc-macro2", 520 | "quote", 521 | "syn 2.0.39", 522 | ] 523 | 524 | [[package]] 525 | name = "tokio-stream" 526 | version = "0.1.14" 527 | source = "registry+https://github.com/rust-lang/crates.io-index" 528 | checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" 529 | dependencies = [ 530 | "futures-core", 531 | "pin-project-lite", 532 | "tokio", 533 | "tokio-util", 534 | ] 535 | 536 | [[package]] 537 | name = "tokio-util" 538 | version = "0.7.10" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" 541 | dependencies = [ 542 | "bytes", 543 | "futures-core", 544 | "futures-sink", 545 | "pin-project-lite", 546 | "tokio", 547 | ] 548 | 549 | [[package]] 550 | name = "troupe" 551 | version = "0.1.0" 552 | dependencies = [ 553 | "async-trait", 554 | "futures", 555 | "gloo-timers", 556 | "instant", 557 | "pin-project", 558 | "send_wrapper", 559 | "tokio", 560 | "tokio-stream", 561 | "wasm-bindgen-futures", 562 | ] 563 | 564 | [[package]] 565 | name = "unicode-ident" 566 | version = "1.0.12" 567 | source = "registry+https://github.com/rust-lang/crates.io-index" 568 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 569 | 570 | [[package]] 571 | name = "wasi" 572 | version = "0.11.0+wasi-snapshot-preview1" 573 | source = "registry+https://github.com/rust-lang/crates.io-index" 574 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 575 | 576 | [[package]] 577 | name = "wasm-bindgen" 578 | version = "0.2.88" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" 581 | dependencies = [ 582 | "cfg-if", 583 | "wasm-bindgen-macro", 584 | ] 585 | 586 | [[package]] 587 | name = "wasm-bindgen-backend" 588 | version = "0.2.88" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" 591 | dependencies = [ 592 | "bumpalo", 593 | "log", 594 | "once_cell", 595 | "proc-macro2", 596 | "quote", 597 | "syn 2.0.39", 598 | "wasm-bindgen-shared", 599 | ] 600 | 601 | [[package]] 602 | name = "wasm-bindgen-futures" 603 | version = "0.4.38" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" 606 | dependencies = [ 607 | "cfg-if", 608 | "js-sys", 609 | "wasm-bindgen", 610 | "web-sys", 611 | ] 612 | 613 | [[package]] 614 | name = "wasm-bindgen-macro" 615 | version = "0.2.88" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" 618 | dependencies = [ 619 | "quote", 620 | "wasm-bindgen-macro-support", 621 | ] 622 | 623 | [[package]] 624 | name = "wasm-bindgen-macro-support" 625 | version = "0.2.88" 626 | source = "registry+https://github.com/rust-lang/crates.io-index" 627 | checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" 628 | dependencies = [ 629 | "proc-macro2", 630 | "quote", 631 | "syn 2.0.39", 632 | "wasm-bindgen-backend", 633 | "wasm-bindgen-shared", 634 | ] 635 | 636 | [[package]] 637 | name = "wasm-bindgen-shared" 638 | version = "0.2.88" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" 641 | 642 | [[package]] 643 | name = "web-sys" 644 | version = "0.3.65" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" 647 | dependencies = [ 648 | "js-sys", 649 | "wasm-bindgen", 650 | ] 651 | 652 | [[package]] 653 | name = "windows-sys" 654 | version = "0.48.0" 655 | source = "registry+https://github.com/rust-lang/crates.io-index" 656 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 657 | dependencies = [ 658 | "windows-targets", 659 | ] 660 | 661 | [[package]] 662 | name = "windows-targets" 663 | version = "0.48.5" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 666 | dependencies = [ 667 | "windows_aarch64_gnullvm", 668 | "windows_aarch64_msvc", 669 | "windows_i686_gnu", 670 | "windows_i686_msvc", 671 | "windows_x86_64_gnu", 672 | "windows_x86_64_gnullvm", 673 | "windows_x86_64_msvc", 674 | ] 675 | 676 | [[package]] 677 | name = "windows_aarch64_gnullvm" 678 | version = "0.48.5" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 681 | 682 | [[package]] 683 | name = "windows_aarch64_msvc" 684 | version = "0.48.5" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 687 | 688 | [[package]] 689 | name = "windows_i686_gnu" 690 | version = "0.48.5" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 693 | 694 | [[package]] 695 | name = "windows_i686_msvc" 696 | version = "0.48.5" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 699 | 700 | [[package]] 701 | name = "windows_x86_64_gnu" 702 | version = "0.48.5" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 705 | 706 | [[package]] 707 | name = "windows_x86_64_gnullvm" 708 | version = "0.48.5" 709 | source = "registry+https://github.com/rust-lang/crates.io-index" 710 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 711 | 712 | [[package]] 713 | name = "windows_x86_64_msvc" 714 | version = "0.48.5" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 717 | -------------------------------------------------------------------------------- /examples/ping/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "futures-example-imperative" 3 | edition = "2021" 4 | version = "0.1.0" 5 | publish = false 6 | 7 | [dependencies] 8 | troupe = { path = "../../" } 9 | derive_more = "0.99.17" 10 | tokio = { version = "1.33", features = ["full"] } 11 | -------------------------------------------------------------------------------- /examples/ping/README.md: -------------------------------------------------------------------------------- 1 | # About 2 | An implementation of a simple ping "server" to demonstrate how to setup an actor and send messages to it from different parts of an application. The actor tracks a counter of the number of messages it has received. 3 | -------------------------------------------------------------------------------- /examples/ping/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use ping::Ping; 4 | use troupe::ActorBuilder; 5 | 6 | mod ping; 7 | 8 | #[tokio::main] 9 | async fn main() { 10 | // Construct the builder for the ping "server" 11 | let builder = ActorBuilder::new(Ping::default()); 12 | // We can get a client before launching the actor process. 13 | let client = builder.sink_client(); 14 | // We can send messages to the client but they will not be processed until we launch the actor. 15 | client.send(()); 16 | let tracker = client.track(()); 17 | tokio::spawn(async move { 18 | let resp = tracker.await; 19 | println!("Got response from ping server: {resp}"); 20 | // Messages are processed in order, so we know that the second message will yield 1 21 | assert_eq!(resp, 1); 22 | }); 23 | tokio::spawn(async move { 24 | let resp = client.track(()).await; 25 | println!("Got response from ping server: {resp}"); 26 | assert_eq!(resp, 2); 27 | }); 28 | // Spawn the actor process, which returns a client 29 | let client = builder.launch_sink(); 30 | tokio::time::sleep(Duration::from_millis(10)).await; 31 | let resp = client.track(()).await; 32 | println!("Got response from ping server: {resp}"); 33 | assert_eq!(resp, 3); 34 | } 35 | -------------------------------------------------------------------------------- /examples/ping/src/ping.rs: -------------------------------------------------------------------------------- 1 | use derive_more::{Display, From}; 2 | use troupe::prelude::*; 3 | 4 | /// The wrapper for the hashmap that will be our actor. 5 | #[derive(Default)] 6 | pub struct Ping { 7 | /// The number of requests that the actor as received 8 | count: usize, 9 | } 10 | 11 | /// The type that the cache receives and processes. `derive_more`'s `From` macro is very handy 12 | /// here. 13 | #[derive(From, Display)] 14 | pub enum PingCommand { 15 | /// Because of `derive_more`, `()` can be used to create this variant. 16 | Ping, 17 | #[display(fmt = "RequestAndRespond")] 18 | RequestAndRespond(OneshotSender), 19 | } 20 | 21 | #[async_trait] 22 | impl ActorState for Ping { 23 | /// A message of type `T` must be `T: Into` or `(T, OneshotSender): 24 | /// Into, msg: PingCommand) { 35 | println!("Ping message receive: {msg}"); 36 | match msg { 37 | // Simply increment the counter 38 | PingCommand::Ping => self.count += 1, 39 | // Get the current count, increment, and send back the orignal count 40 | PingCommand::RequestAndRespond(send) => { 41 | let resp = self.count; 42 | self.count += 1; 43 | // We ignore the result returned by `send` because it means that the calls as 44 | // stopped listening and we can't do anything about it. You can also explicitly 45 | // drop the result. 46 | let _ = send.send(resp); 47 | } 48 | } 49 | } 50 | } 51 | 52 | impl From<((), OneshotSender)> for PingCommand { 53 | fn from(((), send): ((), OneshotSender)) -> Self { 54 | send.into() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/compat/mod.rs: -------------------------------------------------------------------------------- 1 | //! The compatability layer between async runtimes as well as native vs WASM targets. 2 | 3 | use std::fmt::Debug; 4 | 5 | use futures::{stream::FusedStream, Future, Stream}; 6 | 7 | #[cfg(not(target_family = "wasm"))] 8 | mod native; 9 | #[cfg(not(target_family = "wasm"))] 10 | pub use native::*; 11 | 12 | #[cfg(target_family = "wasm")] 13 | mod wasm; 14 | #[cfg(target_family = "wasm")] 15 | pub use wasm::*; 16 | 17 | /// This trait abstracts over the requirements for spawning a task. In native async runtimes, a 18 | /// task might be ran in a different thread, so the future must be `'static + Send`. In WASM, you 19 | /// are always running in a single thread, so spawning a task only requires that the future that 20 | /// the future is `'static`. This concept is used throughout `troupe` to make writing actors in 21 | /// WASM as easy as possible. 22 | pub trait Sendable: 'static + MaybeSend {} 23 | 24 | impl Sendable for T where T: 'static + MaybeSend {} 25 | 26 | /// For native targets, troupe requires nearly every future to be `Send`. For WASM targets, nothing 27 | /// needs to be `Send` because nothing can be sent across threads. 28 | pub trait MaybeSendFuture: MaybeSend + Future {} 29 | 30 | impl MaybeSendFuture for T where T: MaybeSend + Future {} 31 | 32 | /// A trait to abstract over if a future can be processed in a seperate async process. 33 | pub trait SendableFuture: Sendable + Future {} 34 | 35 | impl SendableFuture for T where T: Sendable + Future {} 36 | 37 | /// A trait to abstract over if a stream can be managed by the [`Scheduler`](crate::Scheduler). 38 | pub trait SendableStream: Sendable + Unpin + Stream {} 39 | 40 | impl SendableStream for T where T: Sendable + Unpin + Stream {} 41 | 42 | /// A trait to abstract over if a fused stream can be managed by the 43 | /// [`Scheduler`](crate::Scheduler). 44 | pub trait SendableFusedStream: SendableStream + FusedStream {} 45 | 46 | impl SendableFusedStream for T where T: SendableStream + FusedStream {} 47 | 48 | impl Debug for Sleep { 49 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 50 | write!(f, "Sleep(..)") 51 | } 52 | } 53 | 54 | #[cfg(test)] 55 | mod test { 56 | // Import tests 57 | #[allow(unused_imports)] 58 | use super::{sleep_for, sleep_until, spawn_task, Sendable}; 59 | 60 | // Impl trait tests 61 | } 62 | -------------------------------------------------------------------------------- /src/compat/native.rs: -------------------------------------------------------------------------------- 1 | use anymap2::any::Any; 2 | 3 | /* ------ Send workarounds ------ */ 4 | 5 | /// This trait abstracts over put of the requirements for spawning an async task. In native async runtimes, a 6 | /// task might be ran in a different thread, so the future must be `'static + Send`. In WASM, you 7 | /// are always running in a single thread, so spawning a task only requires that the future that 8 | /// the future is `'static`. This concept is used throughout `troupe` to make writing actors in 9 | /// WASM as easy as possible. 10 | pub trait MaybeSend: Send {} 11 | 12 | impl MaybeSend for T where T: Send {} 13 | 14 | pub(crate) type SendableAnyMap = anymap2::Map; 15 | 16 | /* ------ General Utils ------ */ 17 | 18 | #[cfg(all(feature = "tokio", feature = "async-std"))] 19 | compile_error!("You can not enable both the 'tokio' and 'async-std' features. This leads to namespace collisions"); 20 | 21 | #[cfg(feature = "tokio")] 22 | pub use tokio::*; 23 | 24 | #[cfg(feature = "async-std")] 25 | pub use async_std::*; 26 | 27 | #[cfg(feature = "tokio")] 28 | mod tokio { 29 | use crate::compat::SendableFuture; 30 | use instant::{Duration, Instant}; 31 | use pin_project::pin_project; 32 | use std::{ 33 | future::Future, 34 | pin::Pin, 35 | task::{Context, Poll}, 36 | }; 37 | 38 | use super::super::Sendable; 39 | 40 | /// A wrapper around the async runtime which spawns a future that will execute in the 41 | /// background. 42 | pub fn spawn_task(fut: F) 43 | where 44 | F: SendableFuture, 45 | T: Sendable, 46 | { 47 | drop(tokio::spawn(fut)); 48 | } 49 | 50 | /// A future that will sleep for a period of time before waking up. Created by the 51 | /// [`sleep_for`] and [`sleep_until`] fuctions. 52 | #[pin_project] 53 | pub struct Sleep(#[pin] tokio::time::Sleep); 54 | 55 | impl Future for Sleep { 56 | type Output = (); 57 | 58 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 59 | self.project().0.poll(cx) 60 | } 61 | } 62 | 63 | /// Creates an instance of [`Sleep`] that will sleep for at least as long as the given duration. 64 | pub fn sleep_for(dur: Duration) -> Sleep { 65 | Sleep(tokio::time::sleep(dur)) 66 | } 67 | 68 | /// Creates an instance of [`Sleep`] that will sleep at least until the given point in time. 69 | pub fn sleep_until(deadline: Instant) -> Sleep { 70 | Sleep(tokio::time::sleep_until(deadline.into())) 71 | } 72 | } 73 | 74 | #[cfg(feature = "async-std")] 75 | mod async_std { 76 | use crate::compat::SendableFuture; 77 | use futures::FutureExt; 78 | use instant::{Duration, Instant}; 79 | use std::{ 80 | future::Future, 81 | pin::Pin, 82 | task::{Context, Poll}, 83 | }; 84 | 85 | use super::super::Sendable; 86 | 87 | /// A wrapper around the async runtime which spawns a future that will execute in the 88 | /// background. 89 | pub fn spawn_task(fut: F) 90 | where 91 | F: SendableFuture, 92 | T: Sendable, 93 | { 94 | drop(async_std::task::spawn(fut)); 95 | } 96 | 97 | /// A future that will sleep for a period of time before waking up. Created by the 98 | /// [`sleep_for`] and [`sleep_until`] fuctions. 99 | pub struct Sleep(Pin>>); 100 | 101 | impl Future for Sleep { 102 | type Output = (); 103 | 104 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 105 | self.0.poll_unpin(cx) 106 | } 107 | } 108 | 109 | /// Creates an instance of [`Sleep`] that will sleep for at least as long as the given duration. 110 | pub fn sleep_for(dur: Duration) -> Sleep { 111 | Sleep(Box::pin(async_std::task::sleep(dur))) 112 | } 113 | 114 | /// Creates an instance of [`Sleep`] that will sleep at least until the given point in time. 115 | pub fn sleep_until(deadline: Instant) -> Sleep { 116 | sleep_for(deadline - Instant::now()) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/compat/wasm.rs: -------------------------------------------------------------------------------- 1 | use anymap2::any::Any; 2 | use futures::FutureExt; 3 | use gloo_timers::future::{sleep, TimeoutFuture}; 4 | use instant::{Duration, Instant}; 5 | use std::{ 6 | future::Future, 7 | pin::Pin, 8 | task::{Context, Poll}, 9 | }; 10 | 11 | use super::{Sendable, SendableFuture}; 12 | 13 | /* ------ Send workarounds ------ */ 14 | 15 | /// This trait abstracts over the requirements for spawning a task. In native async runtimes, a 16 | /// task might be ran in a different thread, so the future must be `'static + Send`. In WASM, you 17 | /// are always running in a single thread, so spawning a task only requires that the future that 18 | /// the future is `'static`. This concept is used throughout `troupe` to make writing actors in 19 | /// WASM as easy as possible. 20 | pub trait MaybeSend {} 21 | 22 | impl MaybeSend for T {} 23 | 24 | pub(crate) type SendableAnyMap = anymap2::Map; 25 | 26 | /* ------ General Utils ------ */ 27 | 28 | /// A wrapper around `wasm-bindgen-future`'s `spawn_local` function, which spawns a future tha 29 | /// will execute in the background. 30 | pub(crate) fn spawn_task(fut: F) 31 | where 32 | F: SendableFuture, 33 | T: Sendable, 34 | { 35 | wasm_bindgen_futures::spawn_local(fut.map(drop)); 36 | } 37 | 38 | /// A future that will sleep for a period of time before waking up. Created by the 39 | /// [`sleep_for`] and [`sleep_until`] fuctions. 40 | pub struct Sleep(TimeoutFuture); 41 | 42 | impl Future for Sleep { 43 | type Output = (); 44 | 45 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 46 | self.0.poll_unpin(cx) 47 | } 48 | } 49 | 50 | /// Creates an instance of [`Sleep`] that will sleep for at least as long as the given duration. 51 | pub fn sleep_for(dur: Duration) -> Sleep { 52 | Sleep(sleep(dur)) 53 | } 54 | 55 | /// Creates an instance of [`Sleep`] that will sleep at least until the given point in time. 56 | pub fn sleep_until(deadline: Instant) -> Sleep { 57 | sleep_for(deadline - Instant::now()) 58 | } 59 | -------------------------------------------------------------------------------- /src/joint.rs: -------------------------------------------------------------------------------- 1 | //! Actors that both can be sent messages and broadcast messages. 2 | 3 | use std::{ 4 | pin::Pin, 5 | task::{Context, Poll}, 6 | }; 7 | 8 | use futures::Stream; 9 | use pin_project::pin_project; 10 | 11 | use crate::{ 12 | compat::Sendable, 13 | sink::{self, SinkClient}, 14 | stream::StreamClient, 15 | Permanent, Transient, 16 | }; 17 | 18 | use crate::OneshotSender; 19 | 20 | /// A marker type used by the [`ActorBuilder`](crate::ActorBuilder) to know what kind of 21 | /// [`ActorState`](crate::ActorState) it is dealing with. A joint actor is one that acts as both a 22 | /// [`SinkActor`](crate::sink::SinkActor) and a [`StreamActor`](crate::stream::StreamActor). Its 23 | /// clients, [`JointClient`]s, can both send messages into the actor and recieve messages forwarded 24 | /// by the actor. 25 | #[derive(Debug)] 26 | pub struct JointActor; 27 | 28 | /// A client to an actor. This client is a combination of the [`SinkClient`] and the 29 | /// [`StreamClient`]. 30 | #[pin_project] 31 | #[derive(Debug)] 32 | pub struct JointClient { 33 | send: SinkClient, 34 | #[pin] 35 | recv: StreamClient, 36 | } 37 | 38 | impl JointClient { 39 | /// A constuctor for the client. 40 | pub(crate) fn new(send: SinkClient, recv: StreamClient) -> Self { 41 | Self { send, recv } 42 | } 43 | 44 | /// Consumes the client and return the constituent sink and stream clients. 45 | pub fn split(self) -> (SinkClient, StreamClient) { 46 | let Self { send, recv } = self; 47 | (send, recv) 48 | } 49 | 50 | /// Returns a clone of this client's sink client. 51 | pub fn sink(&self) -> SinkClient { 52 | self.send.clone() 53 | } 54 | 55 | /// Returns a clone of this client's stream client. 56 | pub fn stream(&self) -> StreamClient 57 | where 58 | O: Clone, 59 | { 60 | self.recv.clone() 61 | } 62 | 63 | /// Returns if the actor that the client is connected to is dead or not. 64 | pub fn is_closed(&self) -> bool { 65 | self.send.is_closed() 66 | } 67 | 68 | /// Sends a fire-and-forget style message to the actor and returns if the message was sent 69 | /// successfully. 70 | pub fn send(&self, msg: impl Into) -> bool { 71 | self.send.send(msg) 72 | } 73 | } 74 | 75 | impl JointClient { 76 | /// Sends a request-response style message to a [`Permanent`] actor. The given data is paired 77 | /// with a one-time use channel and sent to the actor. A 78 | /// [`Tracker`](crate::sink::permanent::Tracker) that will receive a response from the actor is 79 | /// returned. 80 | /// 81 | /// Note: Since this client is one for a permanent actor, there is an implicit unwrap once the 82 | /// tracker receives a message from the actor. If the actor drops the other half of the channel 83 | /// or has died somehow (likely from a panic), the returned tracker will panic too. So, it is 84 | /// important that the actor always sends back a message 85 | pub fn track(&self, msg: M) -> sink::permanent::Tracker 86 | where 87 | I: From<(M, OneshotSender)>, 88 | { 89 | self.send.track(msg) 90 | } 91 | } 92 | 93 | impl JointClient { 94 | /// Sends a request-response style message to a [`Transient`] actor. The given data is paired 95 | /// with a one-time use channel and sent to the actor. A 96 | /// [`Tracker`](crate::sink::transient::Tracker) that will receive a response from the actor is 97 | /// returned. 98 | pub fn track(&self, msg: M) -> sink::transient::Tracker 99 | where 100 | I: From<(M, OneshotSender)>, 101 | { 102 | self.send.track(msg) 103 | } 104 | } 105 | 106 | impl Clone for JointClient 107 | where 108 | O: Sendable + Clone, 109 | { 110 | fn clone(&self) -> Self { 111 | Self { 112 | send: self.send.clone(), 113 | recv: self.recv.clone(), 114 | } 115 | } 116 | } 117 | 118 | impl Stream for JointClient 119 | where 120 | O: Sendable + Clone, 121 | { 122 | type Item = Result; 123 | 124 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 125 | self.project().recv.poll_next(cx) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Troupe provides a high-level toolset for modelling crates with actors. Troupe actors are built 2 | //! on top of async process, like those created from `tokio::spawn`, and help you model and control 3 | //! the flow of information in and out of them. The main goals of `troupe` are to provide: 4 | //! - An easy to conceptualize data flow 5 | //! - Simple access to concurrently processing of futures within actors 6 | //! - A model that can be adopted into an existing project all at once or over time 7 | //! - An ergonomic API devoid of magic 8 | //! 9 | //! At the core of every actor is an [`ActorState`]. These are the building blocks used to model 10 | //! your program with `troupe`. This state is fully isolated from the rest of your application and 11 | //! can only be reached by attaching a stream of messages. For many actors, a stream is provided by 12 | //! `troupe` in the form of an [`mpsc-style`](tokio::sync::mpsc) tokio channel. All attached 13 | //! streams are managed by a [`Scheduler`]. The state can attach new streams, queue futures that 14 | //! yield message, or hand off futures that yield nothing to the scheduler. 15 | //! 16 | //! Communication to and from an actor is managed by a client. Each actor state defines how its 17 | //! clients should function via the [`ActorState`]'s `ActorType`. Conceptually, every actor is 18 | //! either something that consumes messages, i.e. a `Sink`, or something to broadcasts messages, 19 | //! i.e. a `Stream`, or both. An actor that largely receive messages from other parts of our 20 | //! program is a [`SinkActor`], which use [`SinkClient`]s. An actor that processes messages from a 21 | //! source (for example a Websocket) and then broadcast these messages is a [`StreamActor`], which 22 | //! use [`StreamClient`]s. If an actor does both of these, it is a [`JointActor`] and uses 23 | //! [`JointClient`]s. 24 | //! 25 | //! Troupe currently supports three async runtimes: `tokio`, `async-std`, and the runtime provided by 26 | //! the browser (via wasm-bindgen-futures). Do note that even if you are using the `async-std` 27 | //! runtime, client-actor communication is still done via tokio channels. 28 | 29 | #![warn(rust_2018_idioms)] 30 | #![deny( 31 | missing_docs, 32 | rustdoc::broken_intra_doc_links, 33 | rustdoc::invalid_rust_codeblocks, 34 | missing_debug_implementations, 35 | unreachable_pub, 36 | unreachable_patterns, 37 | unused, 38 | unused_results, 39 | unused_qualifications, 40 | while_true, 41 | trivial_casts, 42 | trivial_bounds, 43 | trivial_numeric_casts, 44 | unconditional_panic, 45 | unsafe_code, 46 | clippy::all 47 | )] 48 | 49 | pub mod compat; 50 | pub mod joint; 51 | pub mod prelude; 52 | pub(crate) mod scheduler; 53 | pub mod sink; 54 | pub mod stream; 55 | 56 | use compat::{MaybeSendFuture, Sendable, SendableAnyMap, SendableFusedStream}; 57 | use joint::{JointActor, JointClient}; 58 | pub use scheduler::Scheduler; 59 | use scheduler::{ActorRunner, ActorStream}; 60 | #[cfg(target_family = "wasm")] 61 | use send_wrapper::SendWrapper; 62 | use sink::{SinkActor, SinkClient}; 63 | use stream::{StreamActor, StreamClient}; 64 | pub use tokio::sync::oneshot::{ 65 | channel as oneshot_channel, Receiver as OneshotReceiver, Sender as OneshotSender, 66 | }; 67 | 68 | use std::marker::PhantomData; 69 | 70 | use futures::StreamExt; 71 | use tokio::sync::{ 72 | broadcast, 73 | mpsc::{unbounded_channel, UnboundedSender}, 74 | }; 75 | 76 | /// The core abstraction of the actor model. An [`ActorState`] sits at the heart of every actor. It 77 | /// processes messages, queues futures, and attaches streams in the [`Scheduler`], and it can 78 | /// forward messages. Actors serves two roles. They can act similarly to a 79 | /// [`Sink`](futures::Sink) where other parts of your application (including other actors) since 80 | /// messages into the actor. They can also act as a [`Stream`](futures::Stream) that generate 81 | /// messages to be sent throughout your application. This role is denoted by the actor's 82 | /// `ActorType`, which informs the [`ActorBuilder`] what kind of actor it is working with. For 83 | /// sink-like actors, use the [`SinkActor`] type. For stream-like actors, use the [`StreamActor`] 84 | /// type. For actors that function as both, use the [`JointActor`] type. 85 | /// 86 | /// Note: When implementing this trait, all of the methods are `async` and you can use the `async 87 | /// fn` to implement them. Their current bounds of `MaybeSendFuture` are there to abstract over the 88 | /// different requirements for native and WASM targets. 89 | pub trait ActorState: Sendable + Sized { 90 | /// This type should either be [`SinkActor`], [`StreamActor`], or [`JointActor`]. This type is 91 | /// mostly a marker to inform the [`ActorBuilder`]. 92 | type ActorType; 93 | 94 | /// This type should either be [`Permanent`] or [`Transient`]. This type is mostly a marker 95 | /// type to inform the actor's client(s) if it should expect the actor to shutdown at any 96 | /// point. 97 | type Permanence; 98 | 99 | /// Inbound messages to the actor must be this type. Clients will send the actor messages of 100 | /// this type and any queued futures or streams must yield this type. 101 | type Message: Sendable; 102 | 103 | /// For [`SinkActor`]s and [`JointActor`]s, this is the message type which is broadcasts. 104 | /// For [`StreamActor`]s, this can be `()` (unfortunately, default associated types are 105 | /// unstable). 106 | type Output: Sendable + Clone; 107 | 108 | /// Before starting the main loop of running the actor, this method is called to finalize any 109 | /// setup of the actor state, such as pulling data from a database or from over the network. No 110 | /// inbound messages will be processed until this method is completed. 111 | /// 112 | /// Note: When implementing this method, you can use `async fn` instead of `impl 113 | /// MaybeSendFuture`. 114 | #[allow(unused_variables)] 115 | fn start_up(&mut self, scheduler: &mut Scheduler) -> impl MaybeSendFuture { 116 | std::future::ready(()) 117 | } 118 | 119 | /// The heart of the actor. This method consumes messages attached streams and queued futures 120 | /// and streams. For [`SinkActor`]s and [`JointActor`]s, the state can "respond" to messages 121 | /// containing a [`OneshotChannel`](tokio::sync::oneshot::channel) sender. The state can also 122 | /// queue futures and attach streams in the [`Scheduler`]. Finally, for [`StreamActor`]s and 123 | /// [`JointActor`]s, the state can broadcast messages via [`Scheduler`]. 124 | /// 125 | /// Note: When implementing this method, you can use `async fn` instead of `impl 126 | /// MaybeSendFuture`. 127 | fn process( 128 | &mut self, 129 | scheduler: &mut Scheduler, 130 | msg: Self::Message, 131 | ) -> impl MaybeSendFuture; 132 | 133 | /// Once the actor has died, this method is called to allow the actor to clean up anything that 134 | /// remains. Note that this method is also called even for [`Permanent`] actors that have 135 | /// expired. 136 | /// 137 | /// Note: When implementing this method, you can use `async fn` instead of `impl 138 | /// MaybeSendFuture`. 139 | #[allow(unused_variables)] 140 | fn finalize(self, scheduler: &mut Scheduler) -> impl MaybeSendFuture { 141 | std::future::ready(()) 142 | } 143 | } 144 | 145 | /// A marker type used in the [`ActorState`]. It communicates that the actor should never die. As 146 | /// such, the [`Scheduler`] will not provide the actor state a method to shutdown. Also, the 147 | /// [`Tracker`](crate::sink::permanent::Tracker)s for request-response style messages will implictly unwrap responses from their 148 | /// oneshot channels. 149 | #[derive(Debug)] 150 | pub struct Permanent; 151 | 152 | /// A marker type used in the [`ActorState`]. It communicates that the actor should exist for a 153 | /// non-infinite amount of time. The [`Scheduler`] will provide the actor state a method to 154 | /// shutdown. Also, the [`Tracker`](crate::sink::permanent::Tracker)s for request-response style messages will not implictly unwrap 155 | /// responses from their oneshot channels. 156 | #[derive(Debug)] 157 | pub struct Transient; 158 | 159 | /// Holds a type that implements [`ActorState`], helps aggregate all data that the actor needs, and 160 | /// then launches the async actor process. When the actor process is launched, a client is returned 161 | /// to the caller. This client's type depends on the actor's type. 162 | #[allow(missing_debug_implementations)] 163 | pub struct ActorBuilder { 164 | /// The type of actor that is being built. This is the same as `A::ActorType` but 165 | /// specialization is not yet supported. 166 | ty: PhantomData, 167 | send: UnboundedSender, 168 | edges: SendableAnyMap, 169 | #[cfg(not(target_family = "wasm"))] 170 | #[allow(clippy::type_complexity)] 171 | broadcast: Option<(broadcast::Sender, broadcast::Receiver)>, 172 | #[cfg(target_family = "wasm")] 173 | #[allow(clippy::type_complexity)] 174 | broadcast: Option<( 175 | broadcast::Sender>, 176 | broadcast::Receiver>, 177 | )>, 178 | recv: Vec>, 179 | state: A, 180 | } 181 | 182 | /* --------- All actors --------- */ 183 | impl ActorBuilder 184 | where 185 | A: ActorState, 186 | { 187 | /// Constructs a new builder for an actor that uses the given state. 188 | pub fn new(state: A) -> Self { 189 | let (send, recv) = unbounded_channel(); 190 | let recv = vec![recv.into()]; 191 | Self { 192 | state, 193 | send, 194 | recv, 195 | broadcast: None, 196 | ty: PhantomData, 197 | edges: SendableAnyMap::new(), 198 | } 199 | } 200 | 201 | /// Attaches a stream that will be used by the actor once its spawned. No messages will be 202 | /// processed until after the actor is launched. 203 | pub fn attach_stream(&mut self, stream: S) 204 | where 205 | S: SendableFusedStream, 206 | I: Into, 207 | { 208 | self.recv 209 | .push(ActorStream::Secondary(Box::new(stream.map(|m| m.into())))); 210 | } 211 | 212 | /// Adds a client to the builder, which the state can access later. 213 | pub fn add_edge(&mut self, client: SinkClient) { 214 | _ = self.edges.insert(client); 215 | } 216 | 217 | /// Adds an arbitrary data to the builder, which the state can access later. This method is 218 | /// intended to be used with containers hold that multiple clients of the same type. 219 | /// 220 | /// For example, you can attach a series of actor clients that are indexed using a hashmap. 221 | pub fn add_multi_edge(&mut self, container: C) { 222 | _ = self.edges.insert(container); 223 | } 224 | } 225 | 226 | /* --------- Sink actors --------- */ 227 | impl ActorBuilder 228 | where 229 | A: ActorState, 230 | { 231 | /// Returns a client for the actor that will be spawned. While the returned client will be able 232 | /// to send messages, those messages will not be processed until after the actor is launched by 233 | /// the builder. 234 | pub fn client(&self) -> SinkClient { 235 | SinkClient::new(self.send.clone()) 236 | } 237 | 238 | /// Launches an actor that uses the given state and returns a client to the actor. 239 | pub fn launch(self) -> SinkClient { 240 | let Self { 241 | send, 242 | recv, 243 | state, 244 | edges, 245 | .. 246 | } = self; 247 | let mut runner = ActorRunner::new(state, edges); 248 | recv.into_iter().for_each(|r| runner.add_stream(r)); 249 | runner.launch(); 250 | SinkClient::new(send) 251 | } 252 | } 253 | 254 | /* --------- Stream actors --------- */ 255 | impl ActorBuilder 256 | where 257 | A: ActorState, 258 | { 259 | /// Returns a client for the actor that will be spawned. The client will not yield any messages 260 | /// until after the actor is launched and has sent a message. 261 | pub fn client(&mut self) -> StreamClient { 262 | let (_, broad) = self 263 | .broadcast 264 | .get_or_insert_with(|| broadcast::channel(100)); 265 | StreamClient::new(broad.resubscribe()) 266 | } 267 | 268 | /// Launches an actor that uses the given state. Returns a client to the actor. 269 | pub fn launch(self, stream: S) -> StreamClient 270 | where 271 | S: SendableFusedStream, 272 | { 273 | let Self { 274 | mut recv, 275 | state, 276 | broadcast, 277 | edges, 278 | .. 279 | } = self; 280 | let (broad, sub) = broadcast.unwrap_or_else(|| broadcast::channel(100)); 281 | recv.push(ActorStream::Secondary(Box::new(stream))); 282 | let mut runner = ActorRunner::new(state, edges); 283 | runner.add_broadcaster(broad); 284 | recv.into_iter().for_each(|r| runner.add_stream(r)); 285 | runner.launch(); 286 | StreamClient::new(sub) 287 | } 288 | } 289 | 290 | /* --------- Joint actors --------- */ 291 | impl ActorBuilder 292 | where 293 | A: ActorState, 294 | { 295 | /// Returns a stream client for the actor that will be spawned. The client will not yield any 296 | /// messages until after the actor is launched and has sent a message. 297 | pub fn stream_client(&self) -> StreamClient { 298 | StreamClient::new(self.broadcast.as_ref().unwrap().1.resubscribe()) 299 | } 300 | 301 | /// Returns a sink client for the actor that will be spawned. While the returned client will be 302 | /// able to send messages, those messages will not be processed until after the actor is 303 | /// launched by the builder. 304 | pub fn sink_client(&self) -> SinkClient { 305 | SinkClient::new(self.send.clone()) 306 | } 307 | 308 | /// Returns a joint client for the actor that will be spawned. While the returned client will be 309 | /// able to send messages, those messages will not be processed until after the actor is 310 | /// launched by the builder. The client will also not yield any messages until after the actor 311 | /// is launched and has sent a message. 312 | pub fn client(&self) -> SinkClient { 313 | SinkClient::new(self.send.clone()) 314 | } 315 | 316 | /// Launches an actor that uses the given state and stream. Returns a client to the actor. 317 | pub fn launch_with_stream( 318 | mut self, 319 | stream: S, 320 | ) -> JointClient 321 | where 322 | S: SendableFusedStream, 323 | { 324 | self.attach_stream(stream); 325 | self.launch() 326 | } 327 | 328 | /// Launches an actor that uses the given state. Returns a client to the actor. 329 | pub fn launch(self) -> JointClient { 330 | let Self { 331 | send, 332 | recv, 333 | state, 334 | broadcast, 335 | edges, 336 | .. 337 | } = self; 338 | let (broad, sub) = broadcast.unwrap_or_else(|| broadcast::channel(100)); 339 | let mut runner = ActorRunner::new(state, edges); 340 | recv.into_iter().for_each(|r| runner.add_stream(r)); 341 | runner.add_broadcaster(broad); 342 | runner.launch(); 343 | let sink = SinkClient::new(send); 344 | let stream = StreamClient::new(sub); 345 | JointClient::new(sink, stream) 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /src/prelude.rs: -------------------------------------------------------------------------------- 1 | //! Re-exports of commonly used items. 2 | 3 | pub use crate::{ 4 | compat::SendableFuture, 5 | joint::{JointActor, JointClient}, 6 | oneshot_channel, 7 | scheduler::Scheduler, 8 | sink::{SinkActor, SinkClient}, 9 | stream::{StreamActor, StreamClient}, 10 | ActorBuilder, ActorState, OneshotReceiver, OneshotSender, Permanent, Transient, 11 | }; 12 | -------------------------------------------------------------------------------- /src/scheduler.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_family = "wasm")] 2 | use send_wrapper::SendWrapper; 3 | 4 | use futures::{ 5 | stream::{select_all, Fuse, FusedStream, FuturesUnordered, SelectAll}, 6 | FutureExt, Stream, StreamExt, 7 | }; 8 | use instant::Instant; 9 | use pin_project::pin_project; 10 | use tokio::sync::{broadcast, mpsc::UnboundedReceiver}; 11 | use tokio_stream::wrappers::UnboundedReceiverStream; 12 | 13 | use std::{ 14 | future::Future, 15 | pin::Pin, 16 | task::{Context, Poll}, 17 | }; 18 | 19 | use crate::{ 20 | compat::{ 21 | sleep_until, spawn_task, Sendable, SendableAnyMap, SendableFusedStream, SendableFuture, 22 | SendableStream, Sleep, 23 | }, 24 | sink::SinkClient, 25 | ActorState, Transient, 26 | }; 27 | 28 | type FuturesCollection = FuturesUnordered>>>; 29 | 30 | /// Encapulates the different states a scheduler can be. Largely used to communicate how a state 31 | /// wishes to shutdown. 32 | enum SchedulerStatus { 33 | Alive, 34 | Marked, 35 | MarkedToFinish, 36 | } 37 | 38 | /// The primary bookkeeper for the actor. The state attach stream and queue manage futures that 39 | /// will be managed by the Scheduler. 40 | /// The scheduler also tracks if it is possible that no other message will be 41 | /// yielded for the actor to process. If it finds itself in a state where all streams are closed 42 | /// and there are no queued futures, it will close the actor; otherwise, the deadlocked actor will 43 | /// stay in memory doing nothing. 44 | /// 45 | /// Note: If the scheduler finds that the actor is dead but is also managing futures for it, the 46 | /// scheduler will spawn a new async task to poll those futures to completion. 47 | #[allow(missing_debug_implementations)] 48 | pub struct Scheduler { 49 | /// The inbound streams to the actor. 50 | recv: SelectAll>>, 51 | /// Futures that the actor has queued that will yield a message. 52 | queue: FuturesCollection, 53 | /// Futures that yield nothing that the scheduler will manage and poll for the actor. 54 | tasks: FuturesCollection<()>, 55 | /// The manager for outbound messages that will be broadcast from the actor. 56 | outbound: Option>, 57 | /// Stores edges in the form of `EdgeType`s. This is used to access connections to other actors 58 | /// at runtime without needing to embed them into the actor state directly. 59 | edges: SendableAnyMap, 60 | /// The number of stream that could yield a message for the actor to process. Once this and the 61 | /// `future_count` hit both reach zero, the actor is dead as it can no longer process any 62 | /// messages. 63 | stream_count: usize, 64 | /// The number of futures that could yield a message for the actor to process. Once this and 65 | /// the `stream_count` hit both reach zero, the actor is dead as it can no longer process any 66 | /// messages. 67 | future_count: usize, 68 | /// Tracks the status of the scheduler, mostly used to track how the state wants to shutdown. 69 | status: SchedulerStatus, 70 | } 71 | 72 | struct OutboundQueue { 73 | #[cfg(not(target_family = "wasm"))] 74 | send: broadcast::Sender, 75 | #[cfg(target_family = "wasm")] 76 | send: broadcast::Sender>, 77 | } 78 | 79 | impl OutboundQueue { 80 | #[cfg(not(target_family = "wasm"))] 81 | fn new(send: broadcast::Sender) -> Self { 82 | Self { send } 83 | } 84 | 85 | #[cfg(target_family = "wasm")] 86 | fn new(send: broadcast::Sender>) -> Self { 87 | Self { send } 88 | } 89 | 90 | fn send(&mut self, msg: M) { 91 | #[cfg(target_family = "wasm")] 92 | let msg = SendWrapper::new(msg); 93 | let _ = self.send.send(msg); 94 | } 95 | } 96 | 97 | /// The container for the actor state and its scheduler. The runner polls the scheduler, aids in 98 | /// bookkeeping if the actor is dead or not, and passes messages off to the state. 99 | pub(crate) struct ActorRunner { 100 | state: A, 101 | scheduler: Scheduler, 102 | } 103 | 104 | impl ActorRunner { 105 | pub(crate) fn new(state: A, edges: SendableAnyMap) -> Self { 106 | let scheduler = Scheduler::new(edges); 107 | Self { scheduler, state } 108 | } 109 | 110 | #[cfg(not(target_family = "wasm"))] 111 | pub(crate) fn add_broadcaster(&mut self, broad: broadcast::Sender) { 112 | self.scheduler.outbound = Some(OutboundQueue::new(broad)); 113 | } 114 | 115 | #[cfg(target_family = "wasm")] 116 | pub(crate) fn add_broadcaster(&mut self, broad: broadcast::Sender>) { 117 | self.scheduler.outbound = Some(OutboundQueue::new(broad)); 118 | } 119 | 120 | pub(crate) fn add_stream(&mut self, stream: ActorStream) { 121 | self.scheduler.attach_stream_inner(stream); 122 | } 123 | 124 | pub(crate) fn launch(self) { 125 | spawn_task(self.run()) 126 | } 127 | 128 | async fn run(mut self) { 129 | self.state.start_up(&mut self.scheduler).await; 130 | loop { 131 | match self.scheduler.next().await { 132 | Some(msg) => self.state.process(&mut self.scheduler, msg).await, 133 | None => return self.close().await, 134 | } 135 | } 136 | } 137 | 138 | /// Closes the actor state. 139 | async fn close(self) { 140 | let Self { 141 | state, 142 | mut scheduler, 143 | .. 144 | } = self; 145 | state.finalize(&mut scheduler).await; 146 | scheduler.finalize(); 147 | } 148 | } 149 | 150 | impl Scheduler { 151 | /// The constructor for the scheduler. 152 | fn new(edges: SendableAnyMap) -> Self { 153 | let recv = select_all([]); 154 | let queue = FuturesCollection::new(); 155 | let tasks = FuturesCollection::new(); 156 | Self { 157 | recv, 158 | queue, 159 | tasks, 160 | edges, 161 | outbound: None, 162 | stream_count: 0, 163 | future_count: 0, 164 | status: SchedulerStatus::Alive, 165 | } 166 | } 167 | 168 | /// Returns if the actor is dead and should be dropped. 169 | fn is_dead(&self) -> bool { 170 | self.stream_count + self.future_count == 0 171 | || matches!( 172 | self.status, 173 | SchedulerStatus::Marked | SchedulerStatus::MarkedToFinish 174 | ) 175 | } 176 | 177 | /// Performs that final actions before closing the actor process. 178 | fn finalize(self) { 179 | if matches!( 180 | self.status, 181 | SchedulerStatus::Alive | SchedulerStatus::MarkedToFinish 182 | ) { 183 | spawn_task(poll_to_completion(self.tasks)) 184 | } 185 | } 186 | 187 | /// Yields the next message to be processed by the actor state. 188 | async fn next(&mut self) -> Option { 189 | loop { 190 | if self.is_dead() { 191 | return None; 192 | } 193 | tokio::select! { 194 | msg = self.recv.next(), if self.stream_count != 0 => { 195 | match msg { 196 | Some(msg) => return Some(msg), 197 | None => { 198 | self.stream_count -= 1; 199 | } 200 | } 201 | }, 202 | msg = self.queue.next(), if self.future_count != 0 => { 203 | self.future_count -= 1; 204 | return msg 205 | }, 206 | _ = self.tasks.next(), if !self.tasks.is_empty() => {}, 207 | else => { 208 | return None 209 | } 210 | } 211 | } 212 | } 213 | 214 | /// Queues a future in the scheduler that it will manage and poll. The output from the future 215 | /// must be convertible into a message for the actor to process. If you'd like the scheduler to 216 | /// just manage and poll the future, see the [`manage_future`](Scheduler::manage_future) 217 | /// method. 218 | /// 219 | /// Note: There is no ordering to the collection of futures. Moreover, there is no ordering 220 | /// between the queued futures and attached streams. The first to yield an item is the first to 221 | /// be processed. For this reason, the futures queued this way must be `'static`, i.e. they 222 | /// can't reference the actor's state. 223 | pub fn queue_task(&mut self, fut: F) 224 | where 225 | F: Sendable + Future, 226 | I: 'static + Into, 227 | { 228 | self.future_count += 1; 229 | self.queue.push(Box::pin(fut.map(Into::into))); 230 | } 231 | 232 | /// Adds the given future to an internal queue of futures that the scheduler will manage; 233 | /// however, anything that the future yields will be dropped immediately. If the item yielded 234 | /// by the future can be turned into a message for the actor and you would the actor to process 235 | /// it, see the [`queue_task`](Scheduler::queue_task) method. 236 | /// 237 | /// Note: Managed futures are polled at the same time as the queued futures that yield messages 238 | /// and the attached streams. For this reason, the futures managed this way must be `'static`, 239 | /// i.e. they can't reference the actor's state. 240 | pub fn manage_future(&mut self, fut: F) 241 | where 242 | F: Sendable + Future, 243 | T: Sendable, 244 | { 245 | self.tasks.push(Box::pin(fut.map(drop))); 246 | } 247 | 248 | /// Attaches a stream that will be polled and managed by the scheduler. Messages yielded by the 249 | /// streams must be able to be converted into the actor's message type so that the actor can 250 | /// process it. The given stream must be a [`FusedStream`]; however, the scheduler requires a 251 | /// stronger invariant than that given by `FusedStream`. The scheduler will mark a stream as 252 | /// "done" once the stream yields its first `None`. After that, the scheduler will never poll 253 | /// that stream again. 254 | pub fn attach_stream(&mut self, stream: S) 255 | where 256 | S: SendableStream + FusedStream, 257 | I: Into, 258 | { 259 | let stream = ActorStream::Secondary(Box::new(stream.map(|m| m.into()))); 260 | self.attach_stream_inner(stream) 261 | } 262 | 263 | /// Adds an actor stream to the scheduler. 264 | fn attach_stream_inner(&mut self, stream: ActorStream) { 265 | self.stream_count += 1; 266 | self.recv.push(stream.fuse()); 267 | } 268 | 269 | /// Schedules a message to be given to the actor to process at a given time. 270 | pub fn schedule(&mut self, deadline: Instant, msg: M) 271 | where 272 | M: Sendable + Into, 273 | { 274 | self.future_count += 1; 275 | self.queue.push(Box::pin(Timer::new(deadline, msg.into()))); 276 | } 277 | 278 | /// Broadcasts a message to all listening clients. If the message fails to send, the message 279 | /// will be dropped. 280 | /// 281 | /// Note: This method does nothing if the actor is a [`SinkActor`](crate::sink::SinkActor). 282 | /// [`StreamActor`](crate::stream::StreamActor)s and [`JointActor`](crate::joint::JointActor) 283 | /// will be able to broadcast. 284 | pub fn broadcast(&mut self, msg: M) 285 | where 286 | M: Into, 287 | { 288 | if let Some(out) = self.outbound.as_mut() { 289 | out.send(msg.into()) 290 | } 291 | } 292 | 293 | /// Adds a client to the set of connections to other actors, which the state can access later. 294 | pub fn add_edge(&mut self, client: SinkClient) { 295 | _ = self.edges.insert(client); 296 | } 297 | 298 | /// Gets a reference to a sink client to another actor. 299 | pub fn get_edge(&self) -> Option<&SinkClient> { 300 | self.edges.get::>() 301 | } 302 | 303 | /// Gets mutable reference to a sink client to another actor. 304 | pub fn get_edge_mut( 305 | &mut self, 306 | ) -> Option<&mut SinkClient> { 307 | self.edges.get_mut::>() 308 | } 309 | 310 | /// Removes a client to the set of connections to other actors. 311 | pub fn remove_edge(&mut self) { 312 | _ = self.edges.remove::>(); 313 | } 314 | 315 | /// Adds an arbitrary data to the set of connections to other actors, which the state can 316 | /// access later. This method is intended to be used with containers hold that multiple clients 317 | /// of the same type. 318 | /// 319 | /// For example, you can attach a series of actor clients that are indexed using a hashmap. 320 | pub fn add_multi_edge(&mut self, container: C) { 321 | _ = self.edges.insert(container); 322 | } 323 | 324 | /// Gets a reference to an arbitary type held in the container that holds connections to other 325 | /// actors. This method is intended to be used with containers that hold multiple clients of 326 | /// the same type. 327 | /// 328 | /// For example, you can store and access a series of actor clients that are indexed using a 329 | /// hashmap. 330 | pub fn get_multi_edge(&self) -> Option<&C> { 331 | self.edges.get::() 332 | } 333 | 334 | /// Gets a mutable reference to an arbitary type held in the container that holds connections to other 335 | /// actors. This method is intended to be used with containers that hold multiple clients of 336 | /// the same type. 337 | pub fn get_multi_edge_mut(&mut self) -> Option<&mut C> { 338 | self.edges.get_mut::() 339 | } 340 | 341 | /// Adds a piece of arbitrary data from the set of connections to other actors. 342 | pub fn remove_multi_edge(&mut self) { 343 | _ = self.edges.remove::(); 344 | } 345 | } 346 | 347 | impl Scheduler 348 | where 349 | A: ActorState, 350 | { 351 | /// Marks the actor as ready to shutdown. After the state finishes processing the current 352 | /// message, actor process will shutdown. Any unprocessed messages will be dropped, all 353 | /// attached streams will be closed, all futures that will yield a message will be cancelled, 354 | /// and all managed futures will dropped. If you would like the managed (non-message) futures 355 | /// to be processed, use the [`shutdown_and_finish`](Scheduler::shutdown_and_finish) method 356 | /// instead. 357 | pub fn shutdown(&mut self) { 358 | self.status = SchedulerStatus::Marked; 359 | } 360 | 361 | /// Marks the actor as ready to shutdown. After the state finishes processing the current 362 | /// message, it will shutdown the actor processes. Any unprocessed messages will be dropped, 363 | /// all attached streams will be closed, all futures that will yield a message will be 364 | /// cancelled, but all managed futures will be polled to completion in a new async process. If 365 | /// you would like for the managed futures and streams to be dropped instead, use the 366 | /// [`shutdown`](Scheduler::shutdown) method. 367 | pub fn shutdown_and_finish(&mut self) { 368 | self.status = SchedulerStatus::MarkedToFinish; 369 | } 370 | } 371 | 372 | impl From> for ActorStream { 373 | fn from(value: UnboundedReceiver) -> Self { 374 | Self::Main(UnboundedReceiverStream::new(value)) 375 | } 376 | } 377 | 378 | pub(crate) enum ActorStream { 379 | Main(UnboundedReceiverStream), 380 | Secondary(Box>), 381 | } 382 | 383 | impl Stream for ActorStream { 384 | type Item = M; 385 | 386 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 387 | match *self { 388 | ActorStream::Main(ref mut stream) => Pin::new(stream).poll_next(cx), 389 | ActorStream::Secondary(ref mut stream) => Pin::new(stream).poll_next(cx), 390 | } 391 | } 392 | } 393 | 394 | /// A simple function to poll a stream until it is closed. Largely used when the scheduler closes. 395 | async fn poll_to_completion(mut stream: S) 396 | where 397 | S: SendableFusedStream, 398 | { 399 | loop { 400 | if stream.next().await.is_none() { 401 | return; 402 | } 403 | } 404 | } 405 | 406 | /// A message and sleep timer pair. Once the timer has elapsed and is polled, the message is taken 407 | /// from the inner option and returned. After that point, the timer should not be polled again. 408 | #[pin_project] 409 | #[allow(missing_debug_implementations)] 410 | pub(crate) struct Timer { 411 | #[pin] 412 | deadline: Sleep, 413 | msg: Option, 414 | } 415 | 416 | impl Timer { 417 | pub(crate) fn new(deadline: Instant, msg: T) -> Self { 418 | Self { 419 | deadline: sleep_until(deadline), 420 | msg: Some(msg), 421 | } 422 | } 423 | } 424 | 425 | impl Future for Timer { 426 | type Output = T; 427 | 428 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 429 | let this = self.project(); 430 | match this.deadline.poll(cx) { 431 | Poll::Ready(()) => Poll::Ready(this.msg.take().unwrap()), 432 | Poll::Pending => Poll::Pending, 433 | } 434 | } 435 | } 436 | -------------------------------------------------------------------------------- /src/sink.rs: -------------------------------------------------------------------------------- 1 | //! Actors that are only sent messages (either fire-and-forget messages or request-response 2 | //! messages). 3 | 4 | use std::marker::PhantomData; 5 | 6 | use tokio::sync::mpsc::UnboundedSender; 7 | 8 | use crate::{oneshot_channel, OneshotSender, Permanent, Transient}; 9 | 10 | /// A marker type used by the [`ActorBuilder`](crate::ActorBuilder) to know what kind of 11 | /// [`ActorState`](crate::ActorState) it is dealing with. A sink actor is one that receives 12 | /// messages from other parts of the application. By adding a oneshot channel to the message, 13 | /// the actor can respond with a particular piece of data. This allows for type-safe communication 14 | /// between different parts of your program. 15 | /// 16 | /// The client of a [`SinkActor`] is the [`SinkClient`]. This client implements methods that allow 17 | /// for the sending of messages to this client. Communication between a sink client and sink actor 18 | /// uses an MPSC-style channel (see [`mpsc::channel`](tokio::sync::mpsc)). 19 | #[derive(Debug)] 20 | pub struct SinkActor; 21 | 22 | /// A client to an actor. This client sends messages to the actor and supports two styles of 23 | /// messaging. The first is fire-and-forget messages. These messages are sent to the client 24 | /// immediately (no `.await` needed). The actor will process them eventually. The second kind is 25 | /// request-response or "trackable" messages. These messages are identical to the last kind except 26 | /// they contain a one-time use channel that the actor will use to send a message back. 27 | /// 28 | /// It is helpful to use the [`derive_more`](https://crates.io/crates/derive_more) crate's 29 | /// [`From`](https://jeltef.github.io/derive_more/derive_more/from.html) derive macro with a sink 30 | /// actor's message type. The [`send`](SinkClient::send) and [`track`](SinkClient::track) methods 31 | /// of the `SinkClient` perform automatic convertion between the provided data and the actor's 32 | /// message type. Say you have an actor like the one below. You can send messages to that actor 33 | /// like so: 34 | /// ```ignore 35 | /// # extern crate derive_more; 36 | /// # use std::collections::HashMap; 37 | /// # use troupe::prelude::*; 38 | /// # use derive_more::From; 39 | /// #[derive(Default)] 40 | /// struct CacheState(HashMap); 41 | /// 42 | /// #[derive(From)] 43 | /// enum CacheCommand { 44 | /// Insert(usize, String), 45 | /// Get(usize, OneshotSender>), 46 | /// Delete(usize), 47 | /// } 48 | /// # #[async_trait] 49 | /// # impl ActorState for CacheState { 50 | /// # type Message = CacheCommand; 51 | /// # type ActorType = SinkActor; 52 | /// # type Permanence = Permanent; 53 | /// # type Output = (); 54 | /// # 55 | /// # async fn process(&mut self, scheduler: &mut Scheduler, msg: Self::Message) { () } 56 | /// # } 57 | /// 58 | /// let client = ActorBuilder::new(CacheState::default()).launch(); 59 | /// 60 | /// // Sends CacheCommand::Inset(42, "Hello world") 61 | /// client.send((42, String::from("Hello World"))); 62 | /// // Sends CacheCommand::Get(42, OneshotSender) and returns a tracker which will listen for a 63 | /// // response from the actor. 64 | /// let tracker = client.track(42); 65 | /// // Sends CacheCommand::Delete(42) 66 | /// client.send(42); 67 | /// ``` 68 | #[derive(Debug)] 69 | pub struct SinkClient { 70 | ty: PhantomData, 71 | send: UnboundedSender, 72 | } 73 | 74 | impl SinkClient { 75 | pub(crate) fn new(send: UnboundedSender) -> Self { 76 | Self { 77 | send, 78 | ty: PhantomData, 79 | } 80 | } 81 | 82 | /// Returns if the actor that the client is connected to is dead or not. 83 | pub fn is_closed(&self) -> bool { 84 | self.send.is_closed() 85 | } 86 | 87 | /// Sends a fire-and-forget style message to the actor and returns if the message was sent 88 | /// successfully. 89 | pub fn send(&self, msg: impl Into) -> bool { 90 | self.send.send(msg.into()).is_ok() 91 | } 92 | } 93 | 94 | impl SinkClient { 95 | /// Sends a request-response style message to a [`Permanent`] actor. The given data is paired 96 | /// with a one-time use channel and sent to the actor. A [`Tracker`](permanent::Tracker) that 97 | /// will receive a response from the actor is returned. 98 | /// 99 | /// Note: Since this client is one for a permanent actor, there is an implicit unwrap once the 100 | /// tracker receives a message from the actor. If the actor drops the other half of the channel 101 | /// or has died somehow (likely from a panic), the returned tracker will panic too. So, it is 102 | /// important that the actor always sends back a message 103 | pub fn track(&self, msg: I) -> permanent::Tracker 104 | where 105 | M: From<(I, OneshotSender)>, 106 | { 107 | let (send, recv) = oneshot_channel(); 108 | let msg = M::from((msg, send)); 109 | let _ = self.send(msg); 110 | permanent::Tracker::new(recv) 111 | } 112 | } 113 | 114 | impl SinkClient { 115 | /// Sends a request-response style message to a [`Transient`] actor. The given data is paired 116 | /// with a one-time use channel and sent to the actor. A [`Tracker`](transient::Tracker) that 117 | /// will receive a response from the actor is returned. 118 | pub fn track(&self, msg: I) -> transient::Tracker 119 | where 120 | M: From<(I, OneshotSender)>, 121 | { 122 | let (send, recv) = oneshot_channel(); 123 | let msg = M::from((msg, send)); 124 | let _ = self.send(msg); 125 | transient::Tracker::new(recv) 126 | } 127 | } 128 | 129 | impl Clone for SinkClient { 130 | fn clone(&self) -> Self { 131 | Self::new(self.send.clone()) 132 | } 133 | } 134 | 135 | /// A module for things used to interact with the [`Permanent`] actors. 136 | pub mod permanent { 137 | use std::{ 138 | future::Future, 139 | pin::Pin, 140 | task::{Context, Poll}, 141 | }; 142 | 143 | use crate::OneshotReceiver; 144 | 145 | /// A tracker for a request-response style message sent to a [`Permanent`](crate::Permanent) actor. 146 | /// 147 | /// Note: This tracker implicitly unwraps the message produced by its channel receiver. If the 148 | /// actor drops the other half of the channel or has died somehow (likely from a panic), this 149 | /// tracker will panic when polled. 150 | #[derive(Debug)] 151 | pub struct Tracker { 152 | recv: OneshotReceiver, 153 | } 154 | 155 | impl Tracker { 156 | /// A constructor for the tracker. 157 | pub(crate) fn new(recv: OneshotReceiver) -> Self { 158 | Self { recv } 159 | } 160 | } 161 | 162 | impl Future for Tracker { 163 | type Output = T; 164 | 165 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 166 | Pin::new(&mut self.recv).poll(cx).map(Result::unwrap) 167 | } 168 | } 169 | } 170 | 171 | /// A module for things used to interact with the [`Transient`] actors. 172 | pub mod transient { 173 | use std::{ 174 | fmt::Debug, 175 | future::Future, 176 | pin::Pin, 177 | task::{Context, Poll}, 178 | }; 179 | 180 | use crate::OneshotReceiver; 181 | 182 | /// A tracker for a request-response style message sent to a [`Transient`](crate::Transient) actor. 183 | /// 184 | /// Note: This tracker might be created after a failed attempt to send a message to a dead 185 | /// actor. This means that the tracker will return `None` when polled; however, that does not 186 | /// mean that the message was successfully received by the actor. 187 | #[derive(Debug)] 188 | pub struct Tracker { 189 | recv: OneshotReceiver, 190 | } 191 | 192 | impl Tracker { 193 | /// A constuctor for the tracker. 194 | pub(crate) fn new(recv: OneshotReceiver) -> Self { 195 | Self { recv } 196 | } 197 | } 198 | 199 | impl Future for Tracker { 200 | type Output = Option; 201 | 202 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 203 | Pin::new(&mut self.recv).poll(cx).map(Result::ok) 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/stream.rs: -------------------------------------------------------------------------------- 1 | //! Actors that broadcast messages. 2 | 3 | use std::{ 4 | fmt::Debug, 5 | pin::Pin, 6 | task::{Context, Poll}, 7 | }; 8 | 9 | #[cfg(target_family = "wasm")] 10 | use send_wrapper::SendWrapper; 11 | 12 | use futures::{ready, Stream, StreamExt}; 13 | use tokio::sync::broadcast; 14 | use tokio_stream::wrappers::errors::BroadcastStreamRecvError; 15 | 16 | use crate::compat::Sendable; 17 | 18 | /// A marker type used by the [`ActorBuilder`](crate::ActorBuilder) to know what kind of 19 | /// [`ActorState`](crate::ActorState) it is dealing with. A stream actor is one that receives 20 | /// messages from one or more streams and then forwards messages to its clients. 21 | /// 22 | /// The client of a [`StreamActor`] is the [`StreamClient`]. This client implements methods for 23 | /// receiving methods that are "forwarded" by the actor. Unlike the 24 | /// [`SinkActor`](crate::sink::SinkActor), stream actors and clients don't directly support 25 | /// request/response style communication. Communication between a stream actor and client(s) can be 26 | /// modelled with a broadcast-style channel (see [`broadcast::channel`]). 27 | #[derive(Debug)] 28 | pub struct StreamActor; 29 | 30 | /// A client that receives messages from an actor that broadcasts them. 31 | #[derive(Debug)] 32 | pub struct StreamClient { 33 | recv: BroadcastStream, 34 | } 35 | 36 | /// Because of how broadcast streams are implemented in `tokio_streams`, we can not create a 37 | /// broadcast stream from another broadcast stream. Because of this, we must track a second, inner 38 | /// receiver. 39 | struct BroadcastStream { 40 | /// A copy of the original channel, used for cloning the client. 41 | #[cfg(not(target_family = "wasm"))] 42 | copy: broadcast::Receiver, 43 | #[cfg(target_family = "wasm")] 44 | copy: broadcast::Receiver>, 45 | #[cfg(not(target_family = "wasm"))] 46 | /// The stream that is polled. 47 | inner: tokio_stream::wrappers::BroadcastStream, 48 | #[cfg(target_family = "wasm")] 49 | inner: tokio_stream::wrappers::BroadcastStream>, 50 | } 51 | 52 | impl StreamClient 53 | where 54 | M: Sendable + Clone, 55 | { 56 | #[cfg(not(target_family = "wasm"))] 57 | pub(crate) fn new(recv: broadcast::Receiver) -> Self { 58 | Self { 59 | recv: BroadcastStream::new(recv), 60 | } 61 | } 62 | 63 | #[cfg(target_family = "wasm")] 64 | pub(crate) fn new(recv: broadcast::Receiver>) -> Self { 65 | Self { 66 | recv: BroadcastStream::new(recv), 67 | } 68 | } 69 | } 70 | 71 | impl BroadcastStream 72 | where 73 | M: Sendable + Clone, 74 | { 75 | #[cfg(not(target_family = "wasm"))] 76 | fn new(stream: broadcast::Receiver) -> Self { 77 | let copy = stream.resubscribe(); 78 | let inner = tokio_stream::wrappers::BroadcastStream::new(stream); 79 | Self { copy, inner } 80 | } 81 | 82 | #[cfg(target_family = "wasm")] 83 | fn new(stream: broadcast::Receiver>) -> Self { 84 | let copy = stream.resubscribe(); 85 | let inner = tokio_stream::wrappers::BroadcastStream::new(stream); 86 | Self { copy, inner } 87 | } 88 | } 89 | 90 | impl Clone for StreamClient 91 | where 92 | M: Sendable + Clone, 93 | { 94 | fn clone(&self) -> Self { 95 | let recv = BroadcastStream::new(self.recv.copy.resubscribe()); 96 | Self { recv } 97 | } 98 | } 99 | 100 | impl Stream for StreamClient 101 | where 102 | M: Sendable + Clone, 103 | { 104 | type Item = Result; 105 | 106 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 107 | let digest = Pin::new(&mut self.get_mut().recv).poll_next(cx); 108 | digest 109 | } 110 | } 111 | 112 | impl Stream for BroadcastStream 113 | where 114 | M: Sendable + Clone, 115 | { 116 | type Item = Result; 117 | 118 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 119 | let done = ready!(self.inner.poll_next_unpin(cx)); 120 | drop(self.copy.try_recv()); 121 | match done { 122 | Some(Ok(val)) => { 123 | #[cfg(target_family = "wasm")] 124 | let val = val.take(); 125 | Poll::Ready(Some(Ok(val))) 126 | } 127 | Some(Err(BroadcastStreamRecvError::Lagged(count))) => Poll::Ready(Some(Err(count))), 128 | None => Poll::Ready(None), 129 | } 130 | } 131 | } 132 | 133 | impl Debug for BroadcastStream { 134 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 135 | write!(f, "BroadcastStream({:?})", self.copy) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /tests/joint.rs: -------------------------------------------------------------------------------- 1 | use futures::StreamExt; 2 | use instant::Duration; 3 | use tokio::sync::oneshot::error::TryRecvError; 4 | use troupe::{compat::sleep_for, prelude::*}; 5 | 6 | #[derive(Debug, PartialEq, Eq)] 7 | struct Started; 8 | 9 | #[derive(Debug, Clone, PartialEq, Eq)] 10 | struct Processed; 11 | 12 | #[derive(Debug, PartialEq, Eq)] 13 | struct Completed; 14 | 15 | struct DummyJoint { 16 | started: Option>, 17 | completed: OneshotSender, 18 | } 19 | 20 | impl ActorState for DummyJoint { 21 | type ActorType = JointActor; 22 | type Permanence = Permanent; 23 | type Message = Processed; 24 | type Output = Processed; 25 | 26 | fn start_up(&mut self, _: &mut Scheduler) -> impl SendableFuture { 27 | self.started.take().unwrap().send(Started).unwrap(); 28 | std::future::ready(()) 29 | } 30 | 31 | fn process( 32 | &mut self, 33 | scheduler: &mut Scheduler, 34 | msg: Self::Message, 35 | ) -> impl SendableFuture { 36 | scheduler.broadcast(msg); 37 | std::future::ready(()) 38 | } 39 | 40 | fn finalize(self, _: &mut Scheduler) -> impl SendableFuture { 41 | self.completed.send(Completed).unwrap(); 42 | std::future::ready(()) 43 | } 44 | } 45 | 46 | async fn sleep() { 47 | sleep_for(Duration::from_millis(10)).await 48 | } 49 | 50 | #[test] 51 | fn are_send() { 52 | fn is_send() {} 53 | 54 | is_send::(); 55 | is_send::(); 56 | is_send::(); 57 | is_send::(); 58 | is_send::(); 59 | is_send::(); 60 | } 61 | 62 | #[tokio::test] 63 | async fn startup_and_teardown() { 64 | let (started, mut started_recv) = oneshot_channel(); 65 | let (completed, mut comped_recv) = oneshot_channel(); 66 | let state = DummyJoint { 67 | started: Some(started), 68 | completed, 69 | }; 70 | assert_eq!(Err(TryRecvError::Empty), started_recv.try_recv()); 71 | assert_eq!(Err(TryRecvError::Empty), comped_recv.try_recv()); 72 | let stream = futures::stream::iter(std::iter::once(Processed)).fuse(); 73 | let mut client = ActorBuilder::new(state).launch_with_stream(stream); 74 | /* ----- Successful startup test ----- */ 75 | tokio::select! { 76 | _ = sleep() => { 77 | panic!("Actor failed to start!!"); 78 | } 79 | _ = started_recv => { } 80 | } 81 | /* ----- Processes stream test ----- */ 82 | tokio::select! { 83 | _ = sleep() => { 84 | panic!("Actor failed to process message"); 85 | } 86 | msg = client.next() => { 87 | assert_eq!(Some(Ok(Processed)), msg); 88 | } 89 | } 90 | tokio::select! { 91 | _ = &mut comped_recv => { 92 | panic!("Actor closed unexpectedly early!!"); 93 | } 94 | _ = sleep() => { } 95 | } 96 | client.send(Processed); 97 | tokio::select! { 98 | _ = sleep() => { 99 | panic!("Actor failed to process message"); 100 | } 101 | msg = client.next() => { 102 | assert_eq!(Some(Ok(Processed)), msg); 103 | } 104 | } 105 | /* ----- Successful shutdown test ----- */ 106 | let (sink, mut stream) = client.split(); 107 | drop(sink); 108 | tokio::select! { 109 | _ = sleep() => { 110 | panic!("Actor failed to shutdown!!"); 111 | } 112 | _ = comped_recv => { } 113 | } 114 | tokio::select! { 115 | _ = sleep() => { 116 | panic!("Actor stream did not close"); 117 | } 118 | msg = stream.next() => { 119 | assert_eq!(None, msg); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /tests/sink.rs: -------------------------------------------------------------------------------- 1 | use instant::Duration; 2 | use tokio::sync::oneshot::error::TryRecvError; 3 | use troupe::{ 4 | compat::{sleep_for, SendableFuture}, 5 | prelude::*, 6 | }; 7 | 8 | #[derive(Debug, PartialEq, Eq)] 9 | struct Started; 10 | 11 | #[derive(Debug, PartialEq, Eq)] 12 | struct Completed; 13 | 14 | struct DummySink { 15 | started: Option>, 16 | completed: OneshotSender, 17 | } 18 | 19 | impl ActorState for DummySink { 20 | type ActorType = SinkActor; 21 | type Permanence = Permanent; 22 | type Message = (); 23 | type Output = (); 24 | 25 | fn start_up( 26 | &mut self, 27 | _: &mut Scheduler, 28 | ) -> impl troupe::compat::SendableFuture { 29 | self.started.take().unwrap().send(Started).unwrap(); 30 | std::future::ready(()) 31 | } 32 | 33 | fn process( 34 | &mut self, 35 | _: &mut Scheduler, 36 | _: Self::Message, 37 | ) -> impl SendableFuture { 38 | std::future::ready(()) 39 | } 40 | 41 | fn finalize(self, _: &mut Scheduler) -> impl SendableFuture { 42 | self.completed.send(Completed).unwrap(); 43 | std::future::ready(()) 44 | } 45 | } 46 | 47 | #[tokio::test] 48 | async fn startup_and_teardown() { 49 | let (started, mut started_recv) = oneshot_channel(); 50 | let (completed, mut comped_recv) = oneshot_channel(); 51 | let state = DummySink { 52 | started: Some(started), 53 | completed, 54 | }; 55 | assert_eq!(Err(TryRecvError::Empty), started_recv.try_recv()); 56 | assert_eq!(Err(TryRecvError::Empty), comped_recv.try_recv()); 57 | let client = ActorBuilder::new(state).launch(); 58 | /* ----- Successful startup test ----- */ 59 | tokio::select! { 60 | _ = sleep_for(Duration::from_millis(10)) => { 61 | panic!("Actor failed to start!!"); 62 | } 63 | _ = started_recv => { } 64 | } 65 | /* ----- Stablized actor test ----- */ 66 | tokio::select! { 67 | _ = &mut comped_recv => { 68 | panic!("Actor closed unexpectedly early!!"); 69 | } 70 | _ = sleep_for(Duration::from_millis(10)) => { } 71 | } 72 | client.send(()); 73 | /* ----- Successful shutdown test ----- */ 74 | drop(client); 75 | tokio::select! { 76 | _ = sleep_for(Duration::from_millis(10)) => { 77 | panic!("Actor failed to shutdown!!"); 78 | } 79 | _ = comped_recv => { } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/stream.rs: -------------------------------------------------------------------------------- 1 | use futures::StreamExt; 2 | use instant::{Duration, Instant}; 3 | use tokio::sync::oneshot::error::TryRecvError; 4 | use troupe::{compat::sleep_for, prelude::*}; 5 | 6 | #[derive(Debug, PartialEq, Eq)] 7 | struct Started; 8 | 9 | #[derive(Debug, Clone, PartialEq, Eq)] 10 | struct Processed; 11 | 12 | #[derive(Debug, PartialEq, Eq)] 13 | struct Completed; 14 | 15 | struct DummyStream { 16 | started: Option>, 17 | completed: OneshotSender, 18 | } 19 | 20 | impl ActorState for DummyStream { 21 | type ActorType = StreamActor; 22 | type Permanence = Permanent; 23 | type Message = Processed; 24 | type Output = Processed; 25 | 26 | async fn start_up(&mut self, _: &mut Scheduler) { 27 | self.started.take().unwrap().send(Started).unwrap(); 28 | } 29 | 30 | async fn process(&mut self, scheduler: &mut Scheduler, msg: Self::Message) { 31 | scheduler.broadcast(msg); 32 | } 33 | 34 | async fn finalize(self, _: &mut Scheduler) { 35 | self.completed.send(Completed).unwrap(); 36 | } 37 | } 38 | 39 | async fn sleep() { 40 | sleep_for(Duration::from_millis(10)).await 41 | } 42 | 43 | #[tokio::test] 44 | async fn startup_and_teardown() { 45 | let (started, mut started_recv) = oneshot_channel(); 46 | let (completed, mut comped_recv) = oneshot_channel(); 47 | let state = DummyStream { 48 | started: Some(started), 49 | completed, 50 | }; 51 | assert_eq!(Err(TryRecvError::Empty), started_recv.try_recv()); 52 | assert_eq!(Err(TryRecvError::Empty), comped_recv.try_recv()); 53 | let stream = futures::stream::iter(std::iter::once(Processed)).fuse(); 54 | let mut client = ActorBuilder::new(state).launch(stream); 55 | /* ----- Successful startup test ----- */ 56 | tokio::select! { 57 | _ = sleep() => { 58 | panic!("Actor failed to start!!"); 59 | } 60 | _ = started_recv => { } 61 | } 62 | /* ----- Processes stream test ----- */ 63 | tokio::select! { 64 | _ = sleep() => { 65 | panic!("Actor failed to process message!!"); 66 | } 67 | msg = client.next() => { 68 | assert_eq!(Some(Ok(Processed)), msg) 69 | } 70 | } 71 | /* ----- Successful shutdown test ----- */ 72 | tokio::select! { 73 | _ = sleep() => { 74 | panic!("Actor failed to shutdown!!"); 75 | } 76 | _ = comped_recv => { } 77 | } 78 | tokio::select! { 79 | _ = sleep() => { 80 | panic!("Actor failed to process message!!"); 81 | } 82 | msg = client.next() => { 83 | assert_eq!(None, msg) 84 | } 85 | } 86 | } 87 | 88 | struct Counter { 89 | started: Option>, 90 | completed: OneshotSender, 91 | count: usize, 92 | } 93 | 94 | impl ActorState for Counter { 95 | type ActorType = StreamActor; 96 | type Permanence = Transient; 97 | type Message = (); 98 | type Output = usize; 99 | 100 | fn start_up(&mut self, scheduler: &mut Scheduler) -> impl SendableFuture { 101 | self.started.take().unwrap().send(Started).unwrap(); 102 | scheduler.schedule(Instant::now() + Duration::from_millis(5), ()); 103 | std::future::ready(()) 104 | } 105 | 106 | fn process( 107 | &mut self, 108 | scheduler: &mut Scheduler, 109 | (): Self::Message, 110 | ) -> impl SendableFuture { 111 | println!("Processing message!!"); 112 | let next = self.count + 1; 113 | scheduler.broadcast(std::mem::replace(&mut self.count, next)); 114 | scheduler.schedule(Instant::now() + Duration::from_millis(5), ()); 115 | if self.count > 10 { 116 | scheduler.shutdown(); 117 | } 118 | std::future::ready(()) 119 | } 120 | 121 | fn finalize(self, _: &mut Scheduler) -> impl SendableFuture { 122 | self.completed.send(Completed).unwrap(); 123 | std::future::ready(()) 124 | } 125 | } 126 | 127 | #[tokio::test] 128 | async fn stop_after_ten_messages() { 129 | let (started, mut started_recv) = oneshot_channel(); 130 | let (completed, mut comped_recv) = oneshot_channel(); 131 | let state = Counter { 132 | started: Some(started), 133 | completed, 134 | count: 0, 135 | }; 136 | assert_eq!(Err(TryRecvError::Empty), started_recv.try_recv()); 137 | assert_eq!(Err(TryRecvError::Empty), comped_recv.try_recv()); 138 | let stream = futures::stream::iter(std::iter::empty()).fuse(); 139 | let mut builder = ActorBuilder::new(state); 140 | let _client = builder.client(); 141 | let mut client = builder.launch(stream); 142 | /* ----- Successful startup test ----- */ 143 | tokio::select! { 144 | _ = sleep() => { 145 | panic!("Actor failed to start!!"); 146 | } 147 | _ = started_recv => { } 148 | } 149 | /* ----- Processes stream test ----- */ 150 | let mut counter = 0; 151 | while counter < 11 { 152 | let Some(Ok(num)) = client.next().await else { 153 | panic!("Actor closed prematurely!!") 154 | }; 155 | println!("Broadcast received: {num}"); 156 | assert_eq!(num, counter); 157 | counter += 1; 158 | } 159 | /* ----- Successful shutdown test ----- */ 160 | tokio::select! { 161 | _ = sleep() => { 162 | panic!("Actor failed to shutdown!!"); 163 | } 164 | _ = comped_recv => { } 165 | } 166 | tokio::select! { 167 | _ = sleep() => { 168 | panic!("Actor failed to close stream!!"); 169 | } 170 | msg = client.next() => { 171 | assert_eq!(None, msg) 172 | } 173 | } 174 | } 175 | --------------------------------------------------------------------------------