├── .editorconfig ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── bun.lock ├── cspell.json ├── dprint.json ├── examples ├── main.ts ├── package.json └── tsconfig.json ├── package.json ├── toyffect ├── Context.ts ├── Fiber.ts ├── Instruction.ts ├── Result.ts ├── Toyffect.ts ├── index.ts ├── package.json └── tsconfig.json ├── tsconfig.base.json ├── tsconfig.json └── words.txt /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | max_line_length = 120 10 | tab_width = 2 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.tsbuildinfo 2 | .DS_Store 3 | dist 4 | node_modules 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[github-actions-workflow]": { 3 | "editor.defaultFormatter": "dprint.dprint" 4 | }, 5 | "[html]": { 6 | "editor.defaultFormatter": "dprint.dprint" 7 | }, 8 | "[json]": { 9 | "editor.defaultFormatter": "dprint.dprint" 10 | }, 11 | "[jsonc]": { 12 | "editor.defaultFormatter": "dprint.dprint" 13 | }, 14 | "[markdown]": { 15 | "editor.defaultFormatter": "dprint.dprint" 16 | }, 17 | "[typescript]": { 18 | "editor.defaultFormatter": "dprint.dprint" 19 | }, 20 | "[typescriptreact]": { 21 | "editor.defaultFormatter": "dprint.dprint" 22 | }, 23 | "[yaml]": { 24 | "editor.defaultFormatter": "dprint.dprint" 25 | }, 26 | "editor.formatOnSave": true, 27 | "editor.tabSize": 2, 28 | "dprint.path": "node_modules/.bin/dprint" 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2024 Harry Solovay 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "devDependencies": { 6 | "@types/bun": "^1.2.8", 7 | "@types/node": "^22.13.16", 8 | "dprint": "^0.49.1", 9 | "effect": "^3.17.2", 10 | "typescript": "^5.8.3", 11 | }, 12 | }, 13 | "examples": { 14 | "name": "toyffect-examples", 15 | "dependencies": { 16 | "toyffect": "workspace:*", 17 | }, 18 | }, 19 | "toyffect": { 20 | "name": "toyffect", 21 | "version": "0.0.0", 22 | }, 23 | }, 24 | "packages": { 25 | "@dprint/darwin-arm64": ["@dprint/darwin-arm64@0.49.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ib6KcJWo/M5RJWXOQKhP664FG1hAvG7nrbkh+j8n+oXdzmbyDdXTP+zW+aM3/sIQUkGaZky1xy1j2VeScMEEHQ=="], 26 | 27 | "@dprint/darwin-x64": ["@dprint/darwin-x64@0.49.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-vIVgnYxV7YYa1d6Uyz707RbgB9rwefGPam+rzaueFNPQjdOxPOTQDuMEJDS+Z3BlI00MfeoupIfIUGsXoM4dpQ=="], 28 | 29 | "@dprint/linux-arm64-glibc": ["@dprint/linux-arm64-glibc@0.49.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZeIh6qMPWLBBifDtU0XadpK36b4WoaTqCOt0rWKfoTjq1RAt78EgqETWp43Dbr6et/HvTgYdoWF0ZNEu2FJFFA=="], 30 | 31 | "@dprint/linux-arm64-musl": ["@dprint/linux-arm64-musl@0.49.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-/nuRyx+TykN6MqhlSCRs/t3o1XXlikiwTc9emWdzMeLGllYvJrcht9gRJ1/q1SqwCFhzgnD9H7roxxfji1tc+Q=="], 32 | 33 | "@dprint/linux-riscv64-glibc": ["@dprint/linux-riscv64-glibc@0.49.1", "", { "os": "linux", "cpu": "none" }, "sha512-RHBqrnvGO+xW4Oh0QuToBqWtkXMcfjqa1TqbBFF03yopFzZA2oRKX83PhjTWgd/IglaOns0BgmaLJy/JBSxOfQ=="], 34 | 35 | "@dprint/linux-x64-glibc": ["@dprint/linux-x64-glibc@0.49.1", "", { "os": "linux", "cpu": "x64" }, "sha512-MjFE894mIQXOKBencuakKyzAI4KcDe/p0Y9lRp9YSw/FneR4QWH9VBH90h8fRxcIlWMArjFFJJAtsBnn5qgxeg=="], 36 | 37 | "@dprint/linux-x64-musl": ["@dprint/linux-x64-musl@0.49.1", "", { "os": "linux", "cpu": "x64" }, "sha512-CvGBWOksHgrL1uzYqtPFvZz0+E82BzgoCIEHJeuYaveEn37qWZS5jqoCm/vz6BfoivE1dVuyyOT78Begj9KxkQ=="], 38 | 39 | "@dprint/win32-arm64": ["@dprint/win32-arm64@0.49.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-gQa4s82lMcXjfdxjWBQun6IJlXdPZZaIj2/2cqXWVEOYPKxAZ/JvGzt2pPG+i73h9KHjNLIV8M9ckqEH3oHufg=="], 40 | 41 | "@dprint/win32-x64": ["@dprint/win32-x64@0.49.1", "", { "os": "win32", "cpu": "x64" }, "sha512-nPU6+hoVze5JJlgET7woYWElBw0IUaB/9XKTaglknQuUUfsmD75D9pkgJTxdIxl9Bg/i5O7c9wb3Nj4XNiTIfw=="], 42 | 43 | "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], 44 | 45 | "@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="], 46 | 47 | "@types/node": ["@types/node@22.15.34", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-8Y6E5WUupYy1Dd0II32BsWAx5MWdcnRd8L84Oys3veg1YrYtNtzgO4CFhiBg6MDSjk7Ay36HYOnU7/tuOzIzcw=="], 48 | 49 | "bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], 50 | 51 | "dprint": ["dprint@0.49.1", "", { "optionalDependencies": { "@dprint/darwin-arm64": "0.49.1", "@dprint/darwin-x64": "0.49.1", "@dprint/linux-arm64-glibc": "0.49.1", "@dprint/linux-arm64-musl": "0.49.1", "@dprint/linux-riscv64-glibc": "0.49.1", "@dprint/linux-x64-glibc": "0.49.1", "@dprint/linux-x64-musl": "0.49.1", "@dprint/win32-arm64": "0.49.1", "@dprint/win32-x64": "0.49.1" }, "bin": { "dprint": "bin.js" } }, "sha512-pO9XH79SyXybj2Vhc9ITZMEI8cJkdlQQRoD8oEfPH6Jjpp/7WX5kIgECVd3DBOjjAdCSiW6R47v3gJBx/qZVkw=="], 52 | 53 | "effect": ["effect@3.17.2", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-xYHNOw90nfD9blT+VHgbK2uogu1gWQjfd1f1HIiRjnxIH9/cAmm3qj/muZZXL4VDSwTUZXZNhahUCxCBc4ZaHg=="], 54 | 55 | "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], 56 | 57 | "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], 58 | 59 | "toyffect": ["toyffect@workspace:toyffect"], 60 | 61 | "toyffect-examples": ["toyffect-examples@workspace:examples"], 62 | 63 | "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], 64 | 65 | "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", 3 | "version": "0.2", 4 | "dictionaryDefinitions": [ 5 | { 6 | "name": "project-words", 7 | "path": "words.txt" 8 | } 9 | ], 10 | "enableGlobDot": true, 11 | "dictionaries": ["project-words"], 12 | "ignorePaths": ["LICENSE"], 13 | "useGitignore": true 14 | } 15 | -------------------------------------------------------------------------------- /dprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "markdown": { 3 | "textWrap": "always" 4 | }, 5 | "typescript": { 6 | "arrowFunction.useParentheses": "force", 7 | "lineWidth": 120, 8 | "quoteProps": "asNeeded", 9 | "semiColons": "asi" 10 | }, 11 | "plugins": [ 12 | "https://plugins.dprint.dev/g-plane/markup_fmt-v0.20.0.wasm", 13 | "https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.1.wasm", 14 | "https://plugins.dprint.dev/markdown-0.18.0.wasm", 15 | "https://plugins.dprint.dev/typescript-0.95.5.wasm", 16 | "https://plugins.dprint.dev/json-0.20.0.wasm", 17 | "https://plugins.dprint.dev/g-plane/malva-v0.12.1.wasm" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /examples/main.ts: -------------------------------------------------------------------------------- 1 | import { Context, Toyffect } from "toyffect" 2 | 3 | class Rand extends Context.Tag("Rand") 5 | }>() {} 6 | 7 | class Logger extends Context.Tag("Logger") void 9 | }>() {} 10 | 11 | const result = await Toyffect 12 | .gen(function*() { 13 | const logger = yield* Logger 14 | logger.log("Greetings from toyffect!") 15 | 16 | const a = yield* Toyffect.promise(() => Promise.resolve(42)) 17 | console.log({ a }) 18 | 19 | const dep = yield* Rand 20 | console.log({ dep }) 21 | 22 | const b = yield* dep.next 23 | console.log({ b }) 24 | 25 | const c = yield* Toyffect.all([ 26 | Toyffect.promise(() => Promise.resolve("A")), 27 | Toyffect.promise(() => Promise.resolve("B")), 28 | ]) 29 | console.log({ c }) 30 | 31 | return "HI" 32 | }) 33 | .provide(Rand, { 34 | next: Toyffect.promise(() => Promise.resolve(Math.random())), 35 | }) 36 | .provide(Logger, { 37 | log: (message) => console.log(message), 38 | }) 39 | .run() 40 | 41 | console.log(result) 42 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "toyffect-examples", 3 | "private": true, 4 | "type": "module", 5 | "dependencies": { 6 | "toyffect": "workspace:*" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base", 3 | "include": ["."], 4 | "exclude": [], 5 | "references": [{ "path": "../toyffect" }] 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "workspaces": ["examples", "toyffect"], 5 | "scripts": { 6 | "build": "tsc -b", 7 | "watch": "tsc -b -w" 8 | }, 9 | "devDependencies": { 10 | "@types/bun": "^1.2.8", 11 | "@types/node": "^22.13.16", 12 | "dprint": "^0.49.1", 13 | "effect": "^3.17.2", 14 | "typescript": "^5.8.3" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /toyffect/Context.ts: -------------------------------------------------------------------------------- 1 | import type { Instruction } from "./Instruction.ts" 2 | 3 | export interface Tag { 4 | readonly V: V 5 | } 6 | 7 | export const Tag = (key: K) => () => 8 | class { 9 | declare static V: V 10 | readonly key = key 11 | 12 | static *[Symbol.iterator](): Generator, V> { 13 | return yield { 14 | _tag: "require", 15 | tag: this as never, 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /toyffect/Fiber.ts: -------------------------------------------------------------------------------- 1 | import * as Context from "./Context.ts" 2 | import type { Instruction } from "./Instruction.ts" 3 | import type { Result } from "./Result.ts" 4 | import { make, type Toyffect } from "./Toyffect.ts" 5 | 6 | export const fork = (toyffect: Toyffect): Toyffect, R> => 7 | make({ 8 | _tag: "fork", 9 | toyffect, 10 | }) 11 | 12 | export type FiberContext = Map 13 | 14 | export type FiberStatus = { 15 | _tag: "untouched" 16 | } | { 17 | _tag: "pending" 18 | promise: Promise> 19 | resolve: (result: Result) => void 20 | } | { 21 | _tag: "resolved" 22 | result: Result 23 | } | { 24 | _tag: "parent_aborted" 25 | parent: Fiber 26 | } 27 | 28 | export class Fiber { 29 | readonly #controller: AbortController = new AbortController() 30 | readonly signal: AbortSignal = this.#controller.signal 31 | 32 | status: FiberStatus = { _tag: "untouched" } 33 | 34 | readonly ctx: FiberContext 35 | readonly source: Iterable 36 | 37 | constructor({ ctx, source, parent }: { 38 | ctx: FiberContext 39 | source: Iterable 40 | parent?: Fiber 41 | }) { 42 | this.ctx = new Map([ 43 | ...parent?.ctx.entries() ?? [], 44 | ...ctx.entries(), 45 | ]) 46 | this.source = source 47 | parent?.signal.addEventListener("abort", () => { 48 | if (this.status._tag === "pending") { 49 | this.status = { 50 | _tag: "parent_aborted", 51 | parent, 52 | } 53 | } 54 | this.#controller.abort() 55 | }) 56 | } 57 | 58 | resolve = (): Promise> => { 59 | const { status, source } = this 60 | switch (status._tag) { 61 | case "pending": { 62 | return status.promise 63 | } 64 | case "resolved": { 65 | return Promise.resolve(status.result) 66 | } 67 | case "parent_aborted": { 68 | return Promise.resolve(null!) 69 | } 70 | } 71 | const { promise, resolve } = Promise.withResolvers>() 72 | this.status = { 73 | _tag: "pending", 74 | promise, 75 | resolve, 76 | } 77 | 78 | const setTerminalStatus = (result: Result) => { 79 | this.status = { 80 | _tag: "resolved", 81 | result, 82 | } 83 | this.#controller.abort() 84 | resolve(result) 85 | } 86 | 87 | const iterator = source[Symbol.iterator]() 88 | let nextArg: unknown 89 | let current = iterator.next() 90 | ;(async () => { 91 | while (!current.done) { 92 | const instruction = current.value 93 | switch (instruction._tag) { 94 | case "promise": { 95 | try { 96 | nextArg = await instruction.f(this.signal) 97 | } catch (exception: unknown) { 98 | setTerminalStatus({ 99 | _tag: "failed", 100 | exception, 101 | }) 102 | } 103 | break 104 | } 105 | case "require": { 106 | nextArg = this.ctx.get(instruction.tag) 107 | break 108 | } 109 | case "fork": { 110 | const fiber = new Fiber({ 111 | parent: this, 112 | source: instruction.toyffect, 113 | ctx: new Map(instruction.toyffect.provided), 114 | }) 115 | nextArg = fiber 116 | fiber.resolve() 117 | break 118 | } 119 | case "gen": { 120 | const fiber = new Fiber({ 121 | parent: this, 122 | source: instruction.f(), 123 | ctx: this.ctx, 124 | }) 125 | const resolved = await fiber.resolve() 126 | if (resolved._tag === "succeeded") { 127 | nextArg = resolved.value 128 | } else { 129 | setTerminalStatus(resolved) 130 | } 131 | break 132 | } 133 | } 134 | try { 135 | current = iterator.next(nextArg) 136 | } catch (exception: unknown) { 137 | try { 138 | iterator.return?.() 139 | } catch (_exception: unknown) {} 140 | setTerminalStatus({ 141 | _tag: "failed", 142 | exception, 143 | }) 144 | } 145 | } 146 | const { value } = current 147 | setTerminalStatus({ 148 | _tag: "succeeded", 149 | value, 150 | }) 151 | })() 152 | return promise 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /toyffect/Instruction.ts: -------------------------------------------------------------------------------- 1 | import * as Context from "./Context.ts" 2 | import * as Toyffect from "./Toyffect.ts" 3 | 4 | export type Instruction = 5 | | Instruction.Promise_ 6 | | Instruction.Require 7 | | Instruction.Gen 8 | | Instruction.Fork 9 | 10 | export declare namespace Instruction { 11 | export interface Promise_ { 12 | readonly _tag: "promise" 13 | readonly f: (signal: AbortSignal) => Promise 14 | } 15 | 16 | export interface Require { 17 | readonly _tag: "require" 18 | readonly tag: R 19 | } 20 | 21 | export interface Gen { 22 | readonly _tag: "gen" 23 | readonly f: () => Generator 24 | } 25 | 26 | export interface Fork { 27 | readonly _tag: "fork" 28 | readonly toyffect: Toyffect.Toyffect 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /toyffect/Result.ts: -------------------------------------------------------------------------------- 1 | export type Result = { 2 | _tag: "succeeded" 3 | value: A 4 | } | { 5 | _tag: "failed" 6 | exception: unknown 7 | } 8 | -------------------------------------------------------------------------------- /toyffect/Toyffect.ts: -------------------------------------------------------------------------------- 1 | import * as Context from "./Context.ts" 2 | import * as Fiber from "./Fiber.ts" 3 | import type { Instruction } from "./Instruction.ts" 4 | import type { Result } from "./Result.ts" 5 | 6 | export interface Toyffect extends Iterable, A> { 7 | run(this: Toyffect): Promise> 8 | 9 | readonly provided: Array<[Context.Tag, unknown]> 10 | readonly provide: ( 11 | tag: F, 12 | instance: F["V"], 13 | ) => Toyffect any>>>> 14 | } 15 | 16 | export declare namespace Toyffect { 17 | export type Any = Toyffect 18 | export type A = X extends Any ? A : never 19 | export type R = X extends Toyffect ? R : never 20 | } 21 | 22 | export const make = ( 23 | instruction: Instruction, 24 | provided: Array<[Context.Tag, unknown]> = [], 25 | ): Toyffect => ({ 26 | *[Symbol.iterator]() { 27 | return yield instruction as never 28 | }, 29 | provided, 30 | provide: (tag, instance) => make(instruction, [...provided, [tag, instance]]), 31 | run() { 32 | const fiber = new Fiber.Fiber({ 33 | ctx: new Map(provided), 34 | source: this, 35 | }) 36 | return fiber.resolve() 37 | }, 38 | }) 39 | 40 | export const promise = >(f: (signal: AbortSignal) => T): Toyffect> => 41 | make, never>({ 42 | _tag: "promise", 43 | f, 44 | }) 45 | 46 | export const gen = (f: () => Generator): Toyffect< 47 | A, 48 | Extract["tag"] 49 | > => 50 | make({ 51 | _tag: "gen", 52 | f, 53 | }) 54 | 55 | export const all = >(toyffects: L): Toyffect< 56 | { [K in keyof L]: Toyffect.A }, 57 | Toyffect.R 58 | > => 59 | gen(function*() { 60 | const fibers: Array = [] 61 | for (const toyffect of toyffects) { 62 | fibers.push(yield* Fiber.fork(toyffect)) 63 | } 64 | return (yield* promise(() => 65 | Promise.all(fibers.map(async (fiber) => { 66 | const result = await fiber.resolve() 67 | if (result._tag === "failed") { 68 | throw result.exception 69 | } 70 | return result.value 71 | })) 72 | )) as never 73 | }) 74 | -------------------------------------------------------------------------------- /toyffect/index.ts: -------------------------------------------------------------------------------- 1 | export * as Context from "./Context.ts" 2 | export * as Fiber from "./Fiber.ts" 3 | export * as Instruction from "./Instruction.ts" 4 | export * as Result from "./Result.ts" 5 | export * as Toyffect from "./Toyffect.ts" 6 | -------------------------------------------------------------------------------- /toyffect/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "toyffect", 3 | "private": true, 4 | "version": "0.0.0", 5 | "license": "Apache-2.0", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/harrysolovay/toyffect.git", 9 | "directory": "toyffect" 10 | }, 11 | "type": "module", 12 | "exports": { 13 | ".": "./dist/index.js" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /toyffect/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base", 3 | "compilerOptions": { 4 | "noEmit": false, 5 | "outDir": "dist" 6 | }, 7 | "include": ["."] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "exactOptionalPropertyTypes": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "isolatedModules": true, 9 | "lib": ["ESNext", "DOM", "DOM.AsyncIterable"], 10 | "module": "ESNext", 11 | "moduleDetection": "force", 12 | "moduleResolution": "bundler", 13 | "noEmit": true, 14 | "noErrorTruncation": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitAny": true, 17 | "noImplicitThis": true, 18 | "noImplicitReturns": true, 19 | "noUncheckedIndexedAccess": true, 20 | "noUncheckedSideEffectImports": true, 21 | "noUnusedParameters": true, 22 | "resolveJsonModule": true, 23 | "rewriteRelativeImportExtensions": true, 24 | "allowImportingTsExtensions": true, 25 | "skipLibCheck": true, 26 | "sourceMap": true, 27 | "strict": true, 28 | "strictBindCallApply": true, 29 | "strictFunctionTypes": true, 30 | "strictNullChecks": true, 31 | "strictPropertyInitialization": true, 32 | "target": "ESNext", 33 | "typeRoots": ["node_modules/@types"], 34 | "useDefineForClassFields": true, 35 | "verbatimModuleSyntax": true 36 | }, 37 | "exclude": ["**/dist", "node_modules"] 38 | } 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./examples" }, 5 | { "path": "./toyffect" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /words.txt: -------------------------------------------------------------------------------- 1 | toyffect 2 | toyffects 3 | --------------------------------------------------------------------------------