├── .github
└── workflows
│ ├── preview.yml
│ ├── publish.yml
│ └── verify.yaml
├── .gitignore
├── .projectile
├── .vscode
└── settings.json
├── CODE_OF_CONDUCT.md
├── Changelog.md
├── LICENSE
├── README.md
├── deno.json
├── docs
├── actions.mdx
├── async-rosetta-stone.mdx
├── collections.mdx
├── context.mdx
├── docs.ts
├── errors.mdx
├── events.mdx
├── installation.mdx
├── operations.mdx
├── processes.mdx
├── resources.mdx
├── scope.mdx
├── spawn.mdx
├── structure.json
├── thinking-in-effection.mdx
├── tutorial.mdx
└── typescript.mdx
├── lib
├── abort-signal.ts
├── all.ts
├── async.ts
├── call.ts
├── channel.ts
├── context.ts
├── deps.ts
├── each.ts
├── ensure.ts
├── events.ts
├── instructions.ts
├── lazy.ts
├── lift.ts
├── main.ts
├── mod.ts
├── pause.ts
├── queue.ts
├── race.ts
├── result.ts
├── run.ts
├── run
│ ├── create.ts
│ ├── frame.ts
│ ├── scope.ts
│ ├── task.ts
│ ├── types.ts
│ └── value.ts
├── scoped.ts
├── shift-sync.ts
├── signal.ts
├── sleep.ts
├── types.ts
└── with-resolvers.ts
├── mod.ts
├── tasks
├── build-jsr.ts
└── build-npm.ts
└── test
├── abort-signal.test.ts
├── action.test.ts
├── all.test.ts
├── call.test.ts
├── channel.test.ts
├── context.test.ts
├── each.test.ts
├── ensure.test.ts
├── events.test.ts
├── lift.test.ts
├── main.test.ts
├── main
├── fail.exit.ts
├── fail.unexpected.ts
├── just.suspend.ts
├── node.mjs
├── ok.daemon.ts
├── ok.exit.ts
└── ok.implicit.ts
├── queue.test.ts
├── race.test.ts
├── resource.test.ts
├── run.test.ts
├── scope.test.ts
├── scoped.test.ts
├── signal.test.ts
├── spawn.test.ts
├── suite.ts
└── with-resolvers.test.ts
/.github/workflows/preview.yml:
--------------------------------------------------------------------------------
1 | name: preview
2 |
3 | on: [pull_request]
4 |
5 | permissions:
6 | contents: read
7 |
8 | jobs:
9 | preview:
10 | runs-on: ubuntu-latest
11 | timeout-minutes: 10
12 | steps:
13 | - name: checkout
14 | uses: actions/checkout@v4
15 | with:
16 | fetch-depth: 0
17 | - name: setup deno
18 | uses: denoland/setup-deno@v2
19 | with:
20 | deno-version: v2.x
21 |
22 | - name: Get Version
23 | id: vars
24 | # this will pull the last tag off the current branch
25 | run: echo ::set-output name=version::$(git describe --abbrev=0 --tags | sed 's/^effection-v//')-pr+$(git rev-parse HEAD)
26 |
27 | - name: Setup Node
28 | uses: actions/setup-node@v4
29 | with:
30 | node-version: 20.x
31 | registry-url: https://registry.npmjs.com
32 |
33 | - name: Build NPM
34 | run: deno task build:npm ${{steps.vars.outputs.version}}
35 |
36 | - name: Publish Preview Versions
37 | run: npx pkg-pr-new publish './build/npm'
38 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 |
3 | on:
4 | push:
5 | tags:
6 | - "effection-v*"
7 |
8 | permissions:
9 | contents: read
10 | id-token: write
11 |
12 | jobs:
13 | publish-npm:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: checkout
18 | uses: actions/checkout@v4
19 |
20 | - name: setup deno
21 | uses: denoland/setup-deno@v2
22 | with:
23 | deno-version: v2.x
24 |
25 | - name: Get Version
26 | id: vars
27 | run: echo ::set-output name=version::$(echo ${{github.ref_name}} | sed 's/^effection-v//')
28 |
29 | - name: Setup Node
30 | uses: actions/setup-node@v4.1.0
31 | with:
32 | node-version: 18.x
33 | registry-url: https://registry.npmjs.com
34 |
35 | - name: Build NPM
36 | run: deno task build:npm ${{steps.vars.outputs.version}}
37 |
38 | - name: Publish NPM
39 | run: npm publish --access=public
40 | working-directory: ./build/npm
41 | env:
42 | NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}}
43 |
44 | publish-jsr:
45 | runs-on: ubuntu-latest
46 |
47 | steps:
48 | - name: checkout
49 | uses: actions/checkout@v4
50 |
51 | - name: setup deno
52 | uses: denoland/setup-deno@v2
53 | with:
54 | deno-version: v2.x
55 |
56 | - name: Get Version
57 | id: vars
58 | run: echo ::set-output name=version::$(echo ${{github.ref_name}} | sed 's/^effection-v//')
59 |
60 | - name: Build JSR
61 | run: deno task build:jsr ${{steps.vars.outputs.version}}
62 |
63 | - name: Publish JSR
64 | run: npx jsr publish --allow-dirty --token=${{secrets.JSR_TOKEN}}
65 |
--------------------------------------------------------------------------------
/.github/workflows/verify.yaml:
--------------------------------------------------------------------------------
1 | name: Verify
2 |
3 | on:
4 | push:
5 | branches: v[3-9]
6 | pull_request:
7 | branches: v[3-9]
8 |
9 | permissions:
10 | contents: read
11 |
12 | jobs:
13 | test:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: checkout
18 | uses: actions/checkout@v4
19 |
20 | - name: setup deno
21 | uses: denoland/setup-deno@v2
22 | with:
23 | deno-version: v2.x
24 |
25 | - name: Setup Node
26 | uses: actions/setup-node@v2
27 | with:
28 | node-version: 20.x
29 | registry-url: https://registry.npmjs.com
30 |
31 | - name: format
32 | run: deno fmt --check
33 |
34 | - name: lint
35 | run: deno lint
36 |
37 | - name: test
38 | run: deno task test
39 | - name: test:node
40 | run: deno task test:node
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | !website/yarn.lock
2 | .DS_Store
3 |
4 | # Local Netlify folder
5 | .netlify
6 | /build/
--------------------------------------------------------------------------------
/.projectile:
--------------------------------------------------------------------------------
1 | -/dist
2 | - /node_modules
3 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "deno.enable": true
3 | }
4 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | ## Our Standards
8 |
9 | Examples of behavior that contributes to creating a positive environment include:
10 |
11 | * Using welcoming and inclusive language
12 | * Being respectful of differing viewpoints and experiences
13 | * Gracefully accepting constructive criticism
14 | * Focusing on what is best for the community
15 | * Showing empathy towards other community members
16 |
17 | Examples of unacceptable behavior by participants include:
18 |
19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
20 | * Trolling, insulting/derogatory comments, and personal or political attacks
21 | * Public or private harassment
22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
23 | * Other conduct which could reasonably be considered inappropriate in a professional setting
24 |
25 | ## Our Responsibilities
26 |
27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28 |
29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30 |
31 | ## Scope
32 |
33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34 |
35 | ## Enforcement
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at oss@frontside.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38 |
39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40 |
41 | ## Attribution
42 |
43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44 |
45 | [homepage]: http://contributor-covenant.org
46 | [version]: http://contributor-covenant.org/version/1/4/
47 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018-present Charles Lowell
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://www.npmjs.com/package/effection)
2 | [](https://bundlephobia.com/result?p=effection)
3 | [](https://opensource.org/licenses/MIT)
4 | [](https://frontside.com)
5 | [](https://discord.gg/Ug5nWH8)
6 |
7 | # Effection
8 |
9 | Structured concurrency and effects for JavaScript.
10 |
11 | ## Why use Effection?
12 |
13 | Effection leverages the idea of [structured concurrency][structured concurrency]
14 | to ensure that you don't leak any resources, effects, and that cancellation is
15 | always properly handled. It helps you build concurrent code that feels rock
16 | solid _at scale_, and it does all of this while feeling like normal JavaScript.
17 |
18 | [Learn how to use Effection in your own project](https://frontside.com/effection)
19 |
20 | ## Platforms
21 |
22 | Effection runs on all major JavaScript platforms including NodeJs, Browser, and
23 | Deno. It is published on both [npm][npm-effection] and [deno.land][deno-land-effection].
24 |
25 | ## Contributing to Website
26 |
27 | Go to [website's readme](www) to learn how to contribute to the website.
28 |
29 | ## Development
30 |
31 | [Deno][] is the primary tool used for development, testing, and packaging.
32 |
33 | ### Testing
34 |
35 | To run tests:
36 |
37 | ```text
38 | $ deno task test
39 | ```
40 |
41 | ### Building NPM Packages
42 |
43 | If you want to build a development version of the NPM package so that you can
44 | link it locally, you can use the `build:npm` script and passing it a version
45 | number. for example:
46 |
47 | ``` text
48 | $ deno task build:npm 3.0.0-mydev-snapshot.0
49 | Task build:npm deno run -A tasks/build-npm.ts "3.0.0-mydev-snapshot.0"
50 | [dnt] Transforming...
51 | [dnt] Running npm install...
52 |
53 | up to date, audited 1 package in 162ms
54 |
55 | found 0 vulnerabilities
56 | [dnt] Building project...
57 | [dnt] Emitting ESM package...
58 | [dnt] Emitting script package...
59 | [dnt] Complete!
60 | ```
61 |
62 | Now, the built npm package can be found in the `build/npm` directory.
63 |
64 | [structured concurrency]: https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/
65 | [discord]: https://discord.gg/Ug5nWH8
66 | [Deno]: https://deno.land
67 | [npm-effection]: https://www.npmjs.com/package/effection
68 | [deno-land-effection]: https://deno.land/x/effection
69 |
--------------------------------------------------------------------------------
/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@effection/effection",
3 | "exports": "./mod.ts",
4 | "license": "ISC",
5 | "publish": {
6 | "include": ["lib", "mod.ts", "README.md"]
7 | },
8 | "lock": false,
9 | "tasks": {
10 | "test": "deno test --allow-run=deno",
11 | "test:node": "deno task build:npm 0.0.0 && node test/main/node.mjs hello world",
12 | "build:jsr": "deno run -A tasks/build-jsr.ts",
13 | "build:npm": "deno run -A tasks/build-npm.ts"
14 | },
15 | "lint": {
16 | "rules": {
17 | "exclude": ["prefer-const", "require-yield"]
18 | },
19 | "exclude": [
20 | "build",
21 | "website",
22 | "www",
23 | "packages"
24 | ]
25 | },
26 | "fmt": {
27 | "exclude": [
28 | "build",
29 | "website",
30 | "www",
31 | "packages",
32 | "CODE_OF_CONDUCT.md",
33 | "README.md"
34 | ]
35 | },
36 | "test": {
37 | "exclude": [
38 | "build",
39 | "packages"
40 | ]
41 | },
42 | "compilerOptions": {
43 | "lib": [
44 | "deno.ns",
45 | "esnext",
46 | "dom",
47 | "dom.iterable"
48 | ]
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/docs/async-rosetta-stone.mdx:
--------------------------------------------------------------------------------
1 | When we say "Effection is Structured Concurrency and Effects for
2 | JavaScript", we mean "JavaScript" seriously. You shouldn't have to
3 | learn an entirely new way of programming just to achieve structured
4 | concurrency. That's why the Effection APIs mirror ordinary JavaScript
5 | APIs so closely. That way, if you know how to do it in JavaScript, you
6 | know how to do it in Effection.
7 |
8 | The congruence between vanilla JavaScript constructs and their Effection
9 | counterparts is reflected in the “Async Rosetta Stone.”
10 |
11 | | Async/Await | Effection |
12 | | ------------------------- | ----------------- |
13 | | `await` | `yield*` |
14 | | `async function` | `function*` |
15 | | `Promise` | `Operation` |
16 | | `new Promise()` | `action()` |
17 | | `Promise.withResolvers()` | `withResolvers()` |
18 | | `for await` | `for yield* each` |
19 | | `AsyncIterable` | `Stream` |
20 | | `AsyncIterator` | `Subscription` |
21 |
22 | ## `await` \<=> `yield*`
23 |
24 | Pause a computation and resume it when the value represented by the right hand
25 | side becomes available.
26 |
27 | Continue once a promise has settled:
28 |
29 | ```javascript
30 | await promise;
31 | ```
32 |
33 | Continue when operation is complete.
34 |
35 | ```js
36 | yield* operation;
37 | ```
38 |
39 | ## `async function` \<=> `function*`
40 |
41 | Compose a set of computations together with logic defined by JavaScript syntax:
42 |
43 | Count down from 5 to 1 with an async function:
44 |
45 | ```js
46 | async function countdown() {
47 | for (let i = 5; i > 1; i--) {
48 | console.log(`${i}`);
49 | await sleep(1000);
50 | }
51 | console.log('blastoff!');
52 | }
53 | ```
54 |
55 | Count down from 5 to 1 with a generator function:
56 |
57 | ```js
58 | import { sleep } from 'effection';
59 |
60 | function* countdown() {
61 | for (let i = 5; i > 1; i--) {
62 | console.log(`${i}`);
63 | yield* sleep(1000);
64 | }
65 | console.log('blastoff!');
66 | }
67 | ```
68 |
69 | Both will print:
70 |
71 | ```
72 | 5
73 | 4
74 | 3
75 | 2
76 | 1
77 | blastoff!
78 | ```
79 |
80 | To call an async function within an operation use [`call()`][call]:
81 |
82 | ```js
83 | import { call } from 'effection';
84 |
85 | yield* call(async function() {
86 | return "hello world";
87 | });
88 | ```
89 |
90 | To run an operation from an async function use [`run()`][run] or [`Scope.run`][scope-run]:
91 |
92 | ```js
93 | import { run } from 'effection';
94 |
95 | await run(function*() {
96 | return "hello world";
97 | });
98 | ```
99 |
100 | ## `Promise` \<=> `Operation`
101 |
102 | The `Promise` type serves roughly the same purpose as the `Operation`. It is a
103 | abstract value that you can use to pause a computation, and resume when the
104 | value has been computed.
105 |
106 | To use a promise:
107 |
108 | ```js
109 | let result = await promise;
110 | ```
111 |
112 | To use an operation:
113 |
114 | ```js
115 | let result = yield* operation;
116 | ```
117 |
118 | To convert from a promise to an operation, use [`until()`][until]
119 |
120 | ```js
121 | import { until } from 'effection';
122 |
123 | let operation = until(promise);
124 | ```
125 |
126 | to convert from an operation to a promise, use [`run()`][run] or [`Scope.run`][scope-run]
127 |
128 | ```js
129 | import { run } from 'effection';
130 |
131 | let promise = run(operation);
132 | ```
133 |
134 | ## `new Promise()` \<=> `action()`
135 |
136 | Construct a reference to a computation that can be resolved with a callback.
137 | In the case of `Promise()` the value will resolve in the next tick of the run
138 | loop.
139 |
140 | Create a promise that resolves in ten seconds:
141 |
142 | ```js
143 | new Promise((resolve) => { setTimeout(resolve, 10000) });
144 | ```
145 |
146 | Create an Operation that resolves in ten seconds:
147 |
148 | ```js
149 | import { action } from 'effection';
150 |
151 | action(function*(resolve) { setTimeout(resolve, 10000) });
152 | ```
153 |
154 | A key difference is that the promise body will be executing immediately, but the
155 | action body is only executed when the action is evaluated. Also, it is executed
156 | anew every time the action is evaluated.
157 |
158 | ## `Promise.withResolvers()` \<=> `withResolvers()`
159 |
160 | Both `Promise` and `Operation` can be constructed ahead of time without needing to begin the process that will resolve it. To do this with
161 | a `Promise`, use the `Promise.withResolvers()` function:
162 |
163 | ```ts
164 | async function main() {
165 | let { promise, resolve } = Promise.withResolvers();
166 |
167 | setTimeout(resolve, 1000);
168 |
169 | await promise;
170 |
171 | console.log("done!")
172 | }
173 | ```
174 |
175 | In effection:
176 |
177 | ```ts
178 | import { withResolvers } from "effection";
179 |
180 | function* main() {
181 | let { operation, resolve } = withResolvers();
182 |
183 | setTimeout(resolve, 1000);
184 |
185 | yield* operation;
186 |
187 | console.log("done!");
188 | };
189 | ```
190 |
191 | ## `Promise.withResolvers()` \<=> `withResolvers()`
192 |
193 | Both `Promise` and `Operation` can be constructed ahead of time without needing to begin the process that will resolve it. To do this with
194 | a `Promise`, use the `Promise.withResolvers()` function:
195 |
196 | ```ts
197 | async function main() {
198 | let { promise, resolve } = Promise.withResolvers();
199 |
200 | setTimeout(resolve, 1000);
201 |
202 | await promise;
203 |
204 | console.log("done!")
205 | }
206 | ```
207 |
208 | In effection:
209 |
210 | ```ts
211 | import { withResolvers } from "effection";
212 |
213 | function* main() {
214 | let { operation, resolve } = withResolvers();
215 |
216 | setTimeout(resolve, 1000);
217 |
218 | yield* operation;
219 |
220 | console.log("done!");
221 | };
222 | ```
223 |
224 | ## `for await` \<=> `for yield* each`
225 |
226 | Loop over an AsyncIterable with `for await`:
227 |
228 | ```js
229 | for await (let item of iterable) {
230 | //item logic
231 | }
232 | ```
233 |
234 | Loop over a `Stream` with `for yield* each`
235 |
236 | ```js
237 | import { each } from 'effection';
238 |
239 | for (let item of yield* each(stream)) {
240 | // item logic
241 | yield* each.next();
242 | }
243 | ```
244 |
245 | See the definition of [`each()`][each] for more detail.
246 |
247 | ## `AsyncIterable` \<=> `Stream`
248 |
249 | A recipe for instantiating a sequence of items that can arrive over time. It is not
250 | the sequence itself, just how to create it.
251 |
252 | Use an `AsyncIterable` to create an `AsyncIterator`:
253 |
254 | ```js
255 | let iterator = asyncIterable[Symbol.asyncIterator]();
256 | ```
257 |
258 | Use a `Stream` to create a `Subscription`:
259 |
260 | ```js
261 | let subscription = yield* stream;
262 | ```
263 |
264 | To convert an `AsyncIterable` to a `Stream` use the [`stream()`][stream]
265 | function.
266 |
267 | ```js
268 | import { stream } from 'effection';
269 |
270 | let itemStream = stream(asyncIterable);
271 | ```
272 |
273 | ## `AsyncIterator` \<=> `Subscription`
274 |
275 | A stateful sequence of items that can be evaluated one at a time.
276 |
277 | Access the next item in an async iterator:
278 |
279 | ```js
280 | let next = await iterator.next();
281 | if (next.done) {
282 | return next.value;
283 | } else {
284 | console.log(next.value)
285 | }
286 | ```
287 |
288 | Access the next item in a subscription:
289 |
290 | ```js
291 | let next = yield* subscription.next();
292 | if (next.done) {
293 | return next.value;
294 | } else {
295 | console.log(next.value);
296 | }
297 | ```
298 |
299 | To convert an `AsyncIterator` to a `Subscription`, use the
300 | [`subscribe()`][subscribe] function.
301 |
302 | ```js
303 | let subscription = subscribe(asyncIterator);
304 | ```
305 |
306 | [call]: /api/v3/call
307 | [until]: /api/v3/until
308 | [run]: /api/v3/run
309 | [scope-run]: /api/v3/Scope#interface_Scope-methods
310 | [each]: /api/v3/each
311 | [stream]: /api/v3/stream
312 | [subscribe]: /api/v3/subscribe
313 |
--------------------------------------------------------------------------------
/docs/context.mdx:
--------------------------------------------------------------------------------
1 | Effection contexts store ambient values that are needed by the operations in your application in order to run. Some of the most common
2 | use cases of `Context` are
3 |
4 |
5 | * **Configuration**: most apps require values that come from the environment such as environment variables or configuration files. Use context to easily retrieve such values from within any operation.
6 | * **Client APIs**: many APIs provide client libraries to connect with them. With an Effection context, you can create a client instance once and share it between all child operations.
7 | * **Services**: Database connections, Websockets, and many other types of APIs involve a handle to a stateful object that needs to be
8 | destroyed accordingly to a set lifecycle. Combined with [resource api][resources], Effection context allows you to share a service between child operations and also to guarantee that it is disposed of properly when it is no longer needed.
9 |
10 | ## The problem
11 |
12 | Usually, you can pass information from a parent operation to a child operation
13 | via function argument or lexical scope. But passing function arguments can become
14 | verbose and inconvenient when you have to pass them through many operations in
15 | the middle, or if many operations need the same information. Likewise, passing information
16 | via lexical scope is only possible if you define the child operation in the body of the
17 | parent operation. `Context` lets the parent operation make some information available to any
18 | operation in the tree below it-no matter how deep-without passing it explicitly through function
19 | arguments or lexical scope.
20 |
21 | > 💁 If you're familiar with React Context, you already know most of
22 | > what you need to know about Effection Context. The biggest difference
23 | > is the API but general concepts are the same.
24 |
25 | ## What is argument drilling?
26 |
27 | Passing argument to operations is a convenient way to make data from parent operation available to the child operation.
28 |
29 | But passing arguments can become inconvenient when the child operation is nested deeply in the tree of operations, or
30 | the same arguments need to be passed to many operations. This situation is called "argument drilling".
31 |
32 | Wouldn't it be great if you could access information in a deeply nested operation from a parent operation without
33 | modifying operations in the middle? That's exactly what Effection Context does.
34 |
35 | ## Context: an alternative to passing arguments
36 |
37 | Context makes a value available to any child process within a tree of processes.
38 |
39 | ``` typescript
40 | import { createContext, main } from 'effection';
41 |
42 | // 1. create the context
43 | const GithubTokenContext = createContext("token");
44 |
45 | await main(function* () {
46 | // 2. set the context value
47 | yield* GithubTokenContext.set("gha-1234567890");
48 |
49 | yield* fetchGithubData();
50 | })
51 |
52 | function* fetchGithubData() {
53 | yield* fetchRepositories();
54 | }
55 |
56 | function* fetchRepositories() {
57 | // 3. use the context value in a child operation
58 | const token = yield* GithubTokenContext.expect();
59 | // or
60 | // const token = yield* GithubTokenContext.get();
61 |
62 | console.log(token);
63 | // -> gha-1234567890
64 | }
65 | ```
66 |
67 | ## Context: overriding nested context
68 |
69 | Context is attached to the scope of the parent operation. That means that the operation and _all of its children_ will see that same context.
70 |
71 | However, if a child operation sets its own value for the context, it will _not_ affect the value of any of its ancestors.
72 |
73 |
74 | 
75 |
76 |
77 |
78 | ## Using Context
79 |
80 | To use context in your operations, you need to do the following 3 steps:
81 | 1. **Create** a context.
82 | 2. **Set** the context value.
83 | 3. **Yield** the context value.
84 |
85 | ### Step 1: Create a context.
86 |
87 | Effection provides a function for creating a context appropriately called `createContext`. This function will return
88 | a reference that you use to identify the context value in the scope.
89 |
90 | ``` javascript
91 | import { createContext } from 'effection'
92 |
93 | const MyValueContext = createContext("my-value");
94 | ```
95 |
96 | ### Step 2: Set the context value.
97 |
98 | ``` javascript
99 | await main(function* () {
100 | yield* MyValueContext.set("Hello World!");
101 | });
102 | ```
103 |
104 | ### Step 3: Yield the context value.
105 |
106 | ```javascript
107 |
108 | await main(function* () {
109 | yield* MyValueContext.set("Hello World!");
110 |
111 | yield* logMyValue();
112 | });
113 |
114 | function* logMyValue() {
115 | const value = yield* MyValueContext.expect();
116 | // or
117 | // const value = yield* MyValueContext.get();
118 |
119 | console.log(value);
120 | }
121 | ```
122 |
123 | [scope]: /docs/guides/scope
124 | [resources]: /docs/guides/resources
125 | [React Context]: https://react.dev/learn/passing-data-deeply-with-context
126 |
--------------------------------------------------------------------------------
/docs/docs.ts:
--------------------------------------------------------------------------------
1 | import {
2 | all,
3 | call,
4 | type Operation,
5 | resource,
6 | type Task,
7 | useScope,
8 | } from "effection";
9 | import structure from "./structure.json" with { type: "json" };
10 |
11 | import { basename } from "https://deno.land/std@0.205.0/path/posix/basename.ts";
12 |
13 | import remarkGfm from "npm:remark-gfm@3.0.1";
14 | import rehypePrismPlus from "npm:rehype-prism-plus@1.5.1";
15 |
16 | import { evaluate } from "npm:@mdx-js/mdx@2.3.0";
17 |
18 | import { Fragment, jsx, jsxs } from "revolution/jsx-runtime";
19 |
20 | export interface DocModule {
21 | default: () => JSX.Element;
22 | frontmatter: {
23 | id: string;
24 | title: string;
25 | };
26 | }
27 |
28 | export interface Docs {
29 | all(): Operation;
30 | getDoc(id?: string): Operation;
31 | }
32 |
33 | export interface Topic {
34 | name: string;
35 | items: DocMeta[];
36 | }
37 |
38 | export interface DocMeta {
39 | id: string;
40 | title: string;
41 | filename: string;
42 | topics: Topic[];
43 | next?: DocMeta;
44 | prev?: DocMeta;
45 | }
46 |
47 | export interface Doc extends DocMeta {
48 | MDXContent: () => JSX.Element;
49 | }
50 |
51 | export function loadDocs(): Operation {
52 | return resource(function* (provide) {
53 | let loaders: Map> | undefined = undefined;
54 |
55 | let scope = yield* useScope();
56 |
57 | function* load() {
58 | let tasks = new Map>();
59 | let entries = Object.entries(structure);
60 |
61 | let topics: Topic[] = [];
62 |
63 | for (let [name, contents] of entries) {
64 | let topic: Topic = { name, items: [] };
65 | topics.push(topic);
66 |
67 | let current: DocMeta | undefined = void (0);
68 | for (let i = 0; i < contents.length; i++) {
69 | let prev: DocMeta | undefined = current;
70 | let [filename, title] = contents[i];
71 | let meta: DocMeta = current = {
72 | id: basename(filename, ".mdx"),
73 | title,
74 | filename,
75 | topics,
76 | prev,
77 | };
78 | if (prev) {
79 | prev.next = current;
80 | }
81 | topic.items.push(current);
82 |
83 | tasks.set(
84 | meta.id,
85 | scope.run(function* () {
86 | let location = new URL(filename, import.meta.url);
87 | let source = yield* call(Deno.readTextFile(location));
88 | let mod = yield* call(evaluate(source, {
89 | jsx,
90 | jsxs,
91 | jsxDEV: jsx,
92 | Fragment,
93 | remarkPlugins: [
94 | remarkGfm,
95 | ],
96 | rehypePlugins: [
97 | [rehypePrismPlus, { showLineNumbers: true }],
98 | ],
99 | }));
100 |
101 | return {
102 | ...meta,
103 | MDXContent: () => mod.default({}),
104 | } as Doc;
105 | }),
106 | );
107 | }
108 | }
109 | return tasks;
110 | }
111 |
112 | yield* provide({
113 | *all() {
114 | if (!loaders) {
115 | loaders = yield* load();
116 | }
117 | return yield* all([...loaders.values()]);
118 | },
119 | *getDoc(id) {
120 | if (id) {
121 | if (!loaders) {
122 | loaders = yield* load();
123 | }
124 | let task = loaders.get(id);
125 | if (task) {
126 | return yield* task;
127 | }
128 | }
129 | },
130 | });
131 | });
132 | }
133 |
--------------------------------------------------------------------------------
/docs/errors.mdx:
--------------------------------------------------------------------------------
1 | We have previously discussed how correctness and proper handling of failure
2 | cases is why we wrote Effection in the first place. In this chapter we will
3 | take a more in-depth look at how Effection handles failures and how you can
4 | react to failure conditions.
5 |
6 | ## Tasks as Promises
7 |
8 | Whenever you call `run` or use the `spawn` operation, Effection
9 | creates a [`Task`][task] for you. This value is a handle that lets you
10 | both respond to the operation's outcome as well as stop its execution.
11 |
12 | As we have seen, a Task is not only an Operation that yields the
13 | result of the operation it is running, it is also a promise that can
14 | be used to integrate Effection code into promise based or async/await
15 | code. That promise will resolve when the operation completes successfully:
16 |
17 | ``` typescript
18 | import { run, sleep } from 'effection';
19 |
20 | async function runExample() {
21 | let value = await run(function*() {
22 | yield* sleep(1000);
23 | return "world!";
24 | });
25 |
26 | console.log("hello", value);
27 | }
28 | ```
29 |
30 | By the same token, if a task's operation results in an error, then the
31 | task's promise also becomes rejected:
32 |
33 | ``` typescript
34 | import { run } from 'effection';
35 |
36 | async function runExample() {
37 | try {
38 | await run(function*() {
39 | throw new Error('boom');
40 | });
41 | } catch(err) {
42 | console.log("got error", err.message) // => "got error boom"
43 | }
44 | }
45 | ```
46 |
47 | However, when an operation is halted, it will never produce a value,
48 | nor will it ever raise an error. And, because it will produce neither
49 | a positive nor negative outcome, it is an error to `await` the result
50 | of a halted operation.
51 |
52 | When this happens, the promise is rejected with a special halt error.
53 | In this example, we show a very long running task that is stopped in the
54 | middle of its work even though it would eventually return a value if we
55 | waited long enough.
56 |
57 | ``` typescript
58 | import { run, sleep } from 'effection';
59 |
60 | async function runExample() {
61 | // this task takes a long time to return
62 | let task = run(function*() {
63 | yield* sleep(10_000_000);
64 | return "hello world";
65 | });
66 |
67 | await task.halt();
68 |
69 | try {
70 | let value = await task // 💥 throws "halted" error;
71 |
72 | // will never reach here because halted tasks do not produce values
73 | console.log(value);
74 | } catch(err) {
75 | console.log(err.message) // => "halted"
76 | }
77 | }
78 | ```
79 |
80 | An important point is that `task.halt()` is an operation in its own
81 | right and will only fail if there is a failure in the teardown. Thus
82 | it is not an error to await the `task.halt()` operation; it is only an
83 | error to await the outcome of the operation which has been halted.
84 |
85 | ## Error propagation
86 |
87 | One of the key principles of structured concurrency is that when a child fails,
88 | the parent should fail as well. In Effection, when we spawn a task, that task
89 | becomes linked to its parent. When the child operation fails, it will
90 | also cause its parent to fail.
91 |
92 | This is similar to the intuition you've built up about how synchronous code
93 | works: if an error is thrown in a function, that error propagates up the stack
94 | and causes the entire stack to fail, until someone catches the error.
95 |
96 | One of the innovations of async/await code over plain promises and
97 | callbacks, is that you can use regular error handling with
98 | `try/catch`, instead of using special error handling constructs. This
99 | makes asynchronous code look and feel more like regular synchronous
100 | code. The same is true in Effection where we can use a regular `try/catch`
101 | to deal with errors.
102 |
103 | ``` typescript
104 | import { main, sleep } from 'effection';
105 |
106 | function* tickingBomb() {
107 | yield* sleep(1000);
108 | throw new Error('boom');
109 | }
110 |
111 | await main(function*() {
112 | try {
113 | yield* tickingBomb()
114 | } catch(err) {
115 | console.log("it blew up:", err.message);
116 | }
117 | });
118 | ```
119 |
120 | However, note that something interesting happens when we instead `spawn` the
121 | `tickingBomb` operation:
122 |
123 | ``` typescript
124 | import { main, suspend } from 'effection';
125 | import { tickingBomb } from './ticking-bomb';
126 |
127 | await main(function*() {
128 | yield* spawn(tickingBomb);
129 | try {
130 | yield* suspend(); // sleep forever
131 | } catch(err) {
132 | console.log("it blew up:", err.message);
133 | }
134 | });
135 | ```
136 |
137 | You might be surprised that we do *not* enter the catch handler here. Instead,
138 | our entire main task just fails. This is by design! We are only allowed to
139 | catch errors thrown by whatever we yield to directly, _not_ by any spawned
140 | children or resources running in the background. This makes error handling more
141 | predictable, since our catch block will not receive errors from any background
142 | task, we're better able to specify which errors we actually want to deal with.
143 |
144 | ## Error boundary
145 |
146 | If we do want to catch an error from a spawned task (or from a [Resource][]) then
147 | we need to introduce an intermediate task which allows us to bring the error into
148 | the foreground. We call this pattern an "error boundary":
149 |
150 | ``` typescript
151 | import { main, call, spawn, suspend } from 'effection';
152 | import { tickingBomb } from './ticking-bomb';
153 |
154 | main(function*() {
155 | try {
156 | yield* call(function*() { // error boundary
157 | yield* spawn(tickingBomb); // will blow up in the background
158 | yield* suspend(); // sleep forever
159 | });
160 | } catch(err) {
161 | console.log("it blew up:", err.message);
162 | }
163 | });
164 | ```
165 |
166 | [Resource]: ./resources
167 | [task]: /api/v3/Task
168 |
--------------------------------------------------------------------------------
/docs/events.mdx:
--------------------------------------------------------------------------------
1 | Asynchronous code often needs to interact with evented code. Using
2 | `async/await` this can be quite challenging. Evented code often needs
3 | to be synchronous, because the timing of when to subscribe and
4 | unsubscribe is very critical, otherwise race conditions can occur
5 | where events get missed. Effection has convenient apis to ensure that you never
6 | run into these problems.
7 |
8 | ## Single events
9 |
10 | The simplest operation for working with events that Effection provides
11 | is the [`once()`][once] operation. This operation works with any
12 | [`EventTarget`][event-target] and blocks until one of its events
13 | occurs. For example, consider that we wanted to wait for the `open`
14 | event on a `WebSocket`, we could do something like this:
15 |
16 | ``` javascript
17 | import { main, once } from 'effection';
18 |
19 | await main(function*() {
20 | let socket = new WebSocket('ws://localhost:1234');
21 |
22 | yield* once(socket, 'open');
23 |
24 | console.log('socket is open!');
25 | });
26 | ```
27 |
28 | The `once` operation returns the argument passed to the event handler. For
29 | example we could use this to grab the [`code`][wscode] that the socket closed
30 | with:
31 |
32 | ``` javascript
33 | import { main, once } from 'effection';
34 |
35 | await main(function*() {
36 | let socket = new WebSocket('ws://localhost:1234');
37 |
38 | yield* once(socket, 'open');
39 |
40 | console.log('socket is open!');
41 |
42 | let closeEvent = yield* once(socket, 'close');
43 | console.log('socket closed with code', closeEvent.code);
44 | });
45 | ```
46 |
47 | ## Recurring events
48 |
49 | If you've been following the chapter on [streams and
50 | subscriptions](./collections), you may already have a feeling that it
51 | is not a good idea to repeatedly call `once(socket, 'message')` to
52 | grab the messages sent to a WebSocket. The risk here is that we miss
53 | messages if we're not very careful.
54 |
55 | Instead we can use [`on()`][on]. [`on()`][on] is a very convenient
56 | function which takes an [event target][event-target] and the name of
57 | an event, and returns a Stream of values corresponding to the occurences of
58 | that event.
59 |
60 | ``` javascript
61 | import { main, on, each } from 'effection';
62 |
63 | await main(function*() {
64 | let socket = new WebSocket('ws://localhost:1234');
65 |
66 | for (let value of yield* each(on(socket, 'message'))) {
67 | console.log('message:', message.data);
68 | yield* each.next();
69 | }
70 | });
71 | ```
72 |
73 | [wscode]: https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent
74 | [childprocess]: https://nodejs.org/api/child_process.html#child_process_class_childprocess
75 | [once]: /api/v3/once
76 | [on]: /api/v3/on
77 | [event-target]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
78 |
--------------------------------------------------------------------------------
/docs/installation.mdx:
--------------------------------------------------------------------------------
1 | Effection can be used in any JavaScript environment including Deno, Node and browsers. Effection packages are side-effect free and do not pollute your environment in any way, making it compatible with most JavaScript runtimes.
2 | If you encounter obstacles integrating with your environment, please create a [GitHub Issue in the Effection repository](https://github.com/thefrontside/effection/issues/new).
3 |
4 | ## Node.js & Browser
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Effection is available on [NPM][npm], as well as derived registries such as [Yarn][yarn] and [UNPKG][unpkg]. It comes with TypeScript types and can be consumed as both ESM and CommonJS.
13 |
14 | ```bash
15 | # install with npm
16 | npm install effection
17 |
18 | # install with yarn
19 | yarn add effection
20 | ```
21 |
22 | ## Deno
23 |
24 | Effection has first class support for Deno because it is developed with [Deno](https://deno.land). Releases are published to [https://deno.land/x/effection](https://deno.land/x/effection). For example, to import the `main()` function:
25 |
26 | ```ts
27 | import { main } from "https://jsr.io/@effection/effection/doc";
28 | ```
29 |
30 | > 💡 If you're curious how we keep NPM/YARN and Deno packages in-sync, you can [checkout the blog post on how publish Deno packages to NPM.][deno-npm-publish].
31 |
32 | [deno-npm-publish]: https://frontside.com/blog/2023-04-27-deno-is-the-easiest-way-to-author-npm-packages/
33 | [unpkg]: https://unpkg.com/effection
34 | [yarn]: https://yarnpkg.com/package?q=effection&name=effection
35 | [npm]: https://www.npmjs.com/package/effection
36 | [bundlephobia]: https://bundlephobia.com/package/effection
37 |
--------------------------------------------------------------------------------
/docs/processes.mdx:
--------------------------------------------------------------------------------
1 | Effection 3.0 does not ship with a process library out of the box, but what it
2 | _does_ do is make writing resources so easy, that you can do it yourself.
3 |
4 | Operating system processes are a perfect candidate for modeling as a resource
5 | because they match [the resource criteria](./resources) which are:
6 |
7 | 1. They are long running
8 | 2. We want to be able to interact with them while they are running.
9 |
10 | This section provides a sketch of how you might create a "command" resource for both
11 | Deno and Node that will run an operating system command, and then make sure that
12 | no matter what happens, that command is shut down properly.
13 |
14 | ```js
15 | // use-command
16 | import { action, resource, spawn, suspend, createSignal } from "effection";
17 | import { spawn as spawnChild} from "node:child_process";
18 |
19 | export function useCommand(...args) {
20 | return resource(function*(provide) {
21 | // spawn the OS process
22 | let proc = spawnChild(...args);
23 |
24 | let stdout = createSignal();
25 | let stderr = createSignal();
26 |
27 |
28 | // create a task that runs until the process finishes
29 | let exit = yield* spawn(function*() {
30 | // get the exit code
31 | let code = yield* action(function*(resolve) {
32 | proc.on("exit", resolve);
33 | try {
34 | yield* suspend();
35 | } finally {
36 | proc.off("exit", resolve);
37 | }
38 | });
39 |
40 | // close stdin, stdout
41 | stdout.close();
42 | stderr.close();
43 |
44 | // return the exitcode
45 | return code;
46 | })
47 |
48 | // connect stdin, stdout to signals
49 | proc.stdout.on("data", stdout.send);
50 | proc.stderr.on("data", stderr.send);
51 |
52 | try {
53 |
54 | // provide the resource handle
55 | yield* provide({ exit, stdout, stderr });
56 |
57 | } finally {
58 | proc.stdout.off("data", stdout.send);
59 | proc.stderr.off("data", stderr.send);
60 |
61 | proc.kill("SIGINT");
62 |
63 | // wait until the process is fully exited
64 | yield* exit;
65 | }
66 | });
67 | }
68 | ```
69 |
70 | ## using a command
71 |
72 | We can now use this command resource to create a process and stream its stdout
73 | to the console:
74 |
75 | ```js
76 |
77 | import { main, each } from "effection";
78 | import { useCommand } from "./use-command.mjs";
79 |
80 | await main(function*() {
81 | let ls = yield* useCommand("ls", ["-lh"]);
82 |
83 | for (let data of yield* each(ls.stdout)) {
84 | console.log(`stdout: ${data}`);
85 | yield* each.next();
86 | }
87 |
88 | console.log(`exit: ${yield* ls.exit}`)
89 | });
90 | ```
91 |
--------------------------------------------------------------------------------
/docs/resources.mdx:
--------------------------------------------------------------------------------
1 | The third fundamental Effection operation is [`resource()`][resource]. It can
2 | seem a little complicated at first, but the reason for its existence is
3 | rather simple. Sometimes there are operations which meet the following criteria:
4 |
5 | 1. They are long running
6 | 1. We want to be able to interact with them while they are running
7 |
8 | As an example, let's consider a program that creates a Socket and sends
9 | messages to it while it is open. This is fairly simple to write using regular
10 | operations like this:
11 |
12 | ``` javascript
13 | import { main, once } from 'effection';
14 | import { Socket } from 'net';
15 |
16 | await main(function*() {
17 | let socket = new Socket();
18 | socket.connect(1337, '127.0.0.1');
19 |
20 | yield* once(socket, 'connect');
21 |
22 | socket.write('hello');
23 |
24 | socket.close();
25 | });
26 | ```
27 |
28 | This works, but there are a lot of details we need to remember in order to use
29 | the socket safely. For example, whenever we use a socket, we don't want to have
30 | to remember to close it once we're done. Instead, we'd like that to happen
31 | automatically. Our first attempt to do so might look something like this:
32 |
33 | ``` javascript
34 | import { once, suspend } from 'effection';
35 | import { Socket } from 'net';
36 |
37 | export function* useSocket(port, host) {
38 | let socket = new Socket();
39 | socket.connect(port, host);
40 |
41 | yield* once(socket, 'connect');
42 |
43 | try {
44 | yield* suspend();
45 | return socket;
46 | } finally {
47 | socket.close();
48 | }
49 | }
50 | ```
51 |
52 | But when we actually try to call our `useSocket` operation, we run into a
53 | problem: because our `useSocket()` operation suspends, it will wait around
54 | forever and never return control back to our main.
55 |
56 | ``` javascript
57 | import { main } from 'effection';
58 | import { useSocket } from './use-socket';
59 |
60 | await main(function*() {
61 | let socket = yield* useSocket(1337, '127.0.0.1'); // blocks forever
62 | socket.write('hello'); // we never get here
63 | });
64 | ```
65 |
66 | Remember our criteria from before:
67 |
68 | 1. Socket is a long running process
69 | 1. We want to interact with the socket while it is running by sending messages
70 | to it
71 |
72 | This is a good use-case for using a resource operation. Let's look at how we can
73 | rewrite `useSocket()` as a resource.
74 |
75 | ``` javascript
76 | import { once, resource } from 'effection';
77 |
78 | export function useSocket(port, host) {
79 | return resource(function* (provide) {
80 | let socket = new Socket();
81 | socket.connect(port, host);
82 |
83 | yield* once(socket, 'connect');
84 |
85 | try {
86 | yield* provide(socket);
87 | } finally {
88 | socket.close();
89 | }
90 | }
91 | }
92 | ```
93 |
94 | Before we unpack what's going on, let's just note that how we call `useSocket()`
95 | has not changed at all, only it now works as expected!
96 |
97 | ``` javascript
98 | import { main } from 'effection';
99 | import { useSocket } from './use-socket';
100 |
101 | await main(function*() {
102 | let socket = yield* useSocket(1337, '127.0.0.1'); // waits for the socket to connect
103 | socket.write('hello'); // this works
104 | // once `main` finishes, the socket is closed
105 | });
106 | ```
107 |
108 | The body of a resource is used to *initialize* a value and make it
109 | available to the operation from which it was called. It can do any
110 | preparation it needs to, and take as long as it wants, but at some
111 | point, it has to "provide" the value back to the caller. This is done
112 | with the `provide()` function that is passed as an argument into each
113 | resource constructor. This special operation, when yielded to, passes
114 | control _back_ to the caller with our newly minted value as its result.
115 |
116 |
117 | However, its work is not done yet. The `provide()` operation will remain
118 | suspended until the resource passes out of scope, thus making sure that
119 | cleanup is guaranteed.
120 |
121 | >💡A simple mantra to repeat to yourself so that you remember how resources work
122 | > is this: resources ___provide___ values.
123 |
124 | Resources can depend on other resources, so we could use this to make a socket
125 | which sends a heart-beat every 10 seconds.
126 |
127 | ``` javascript
128 | import { main, resource, spawn } from 'effection';
129 | import { useSocket } from './use-socket';
130 |
131 | function useHeartSocket(port, host) {
132 | return resource(function* (provide) {
133 | let socket = yield* useSocket(port, host);
134 |
135 | yield* spawn(function*() {
136 | while (true) {
137 | yield* sleep(10_000);
138 | socket.send(JSON.stringify({ type: "heartbeat" }));
139 | }
140 | });
141 |
142 | yield* provide(socket);
143 | });
144 | }
145 |
146 | await main(function*() {
147 | let socket = yield* useHeartSocket(1337, '127.0.0.1'); // waits for the socket to connect
148 | socket.write({ hello: 'world' }); // this works
149 | // once `main` finishes:
150 | // 1. the heartbeat is stopped
151 | // 2. the socket is closed
152 | });
153 | ```
154 |
155 | The original socket is created, connected, and set up to pulse every ten
156 | seconds, and it's cleaned up just as easily as it was created.
157 |
158 | Note that the [`ensure()`][ensure] operation is an appropriate mechanism to
159 | enact cleanup within resources as well. While a matter of preference, the
160 | `useSocket()` resource above could also have been written as:
161 |
162 | ``` javascript
163 | import { ensure, once, resource } from 'effection';
164 |
165 | export function useSocket(port, host) {
166 | return resource(function* (provide) {
167 | let socket = new Socket();
168 | yield* ensure(() => { socket.close(); });
169 |
170 | socket.connect(port, host);
171 |
172 | yield* once(socket, 'connect');
173 | yield* provide(socket);
174 | }
175 | }
176 | ```
177 |
178 | Resources allow us to create powerful, reusable abstractions which are also able
179 | to clean up after themselves.
180 |
181 | [tasks]: /docs/guides/tasks
182 | [operation]: /api/v3/Operation
183 | [resource]: /api/v3/resource
184 | [ensure]: /api/v3/ensure
185 |
--------------------------------------------------------------------------------
/docs/spawn.mdx:
--------------------------------------------------------------------------------
1 | Suppose we are using the `fetchWeekDay` function from the introduction to fetch the current weekday in multiple timezones:
2 |
3 | ``` javascript
4 | import { main } from 'effection';
5 | import { fetchWeekDay } from './fetch-week-day';
6 |
7 | main(function*() {
8 | let dayUS = yield* fetchWeekDay('est');
9 | let daySweden = yield* fetchWeekDay('cet');
10 | console.log(`It is ${dayUS}, in the US and ${daySweden} in Sweden!`);
11 | });
12 | ```
13 |
14 | This works, but it slightly inefficient because we are running the fetches one
15 | after the other. How can we run both `fetch` operations at the same time?
16 |
17 | ## Using `async/await`
18 |
19 | If we were just using `async/await` and not using Effection, we might do
20 | something like this to fetch the dates at the same time:
21 |
22 | ``` javascript
23 | async function() {
24 | let dayUS = fetchWeekDay('est');
25 | let daySweden = fetchWeekDay('cet');
26 | console.log(`It is ${await dayUS}, in the US and ${await daySweden} in Sweden!`);
27 | }
28 | ```
29 |
30 | Or we could use a combinator such as `Promise.all`:
31 |
32 | ``` javascript
33 | async function() {
34 | let [dayUS, daySweden] = await Promise.all([fetchWeekDay('est'), fetchWeekDay('cet')]);
35 | console.log(`It is ${dayUS}, in the US and ${daySweden} in Sweden!`);
36 | }
37 | ```
38 |
39 | ## Dangling Promises
40 |
41 | This works fine as long as both fetches complete successfully, but what happens
42 | when one of them fails? Since there is no connection between the two tasks, a
43 | failure in one of them has no effect on the other. We will happily keep trying
44 | to fetch the US date, even when fetching the Swedish date has already failed!
45 |
46 | For `fetch`, the consequences of this are not so severe, the worst that happens is
47 | that we have a request which is running longer than necessary, but you can imagine
48 | that the more complex the operations we're trying to combine, the more opportunity
49 | for problems there are.
50 |
51 | We call these situations "dangling promises", and most significantly complex
52 | JavaScript applications suffer from this problem. `async/await` fundamentally does
53 | not handle cancellation very well when running multiple operations concurrently.
54 |
55 | ## With Effection
56 |
57 | How does Effection deal with this situation? If we wrote the example using
58 | Effection in the exact same way as the `async/await` example, then we will find
59 | that it doesn't behave the same:
60 |
61 | ``` javascript
62 | import { main } from 'effection';
63 | import { fetchWeekDay } from './fetch-week-day';
64 |
65 | main(function*() {
66 | let dayUS = fetchWeekDay('est');
67 | let daySweden = fetchWeekDay('cet');
68 | console.log(`It is ${yield* dayUS}, in the US and ${yield* daySweden} in Sweden!`);
69 | });
70 | ```
71 |
72 | This is still running one fetch after the other, and is not fetching both at
73 | the same time!
74 |
75 | To understand why, remember that unlike calling an async function to
76 | create a `Promise`, calling a generator function to create an
77 | [`Operation`][Operation] does not do anything by itself. An `Operation` is
78 | is only evaluated when passed to `yield*` or to `run()`. Therefore it isn't
79 | until we `yield*` do we actually start to fetch the dates.
80 |
81 | We could use `run` here to run our operations, and then wait for them, but this
82 | is not the correct way:
83 |
84 | ``` javascript
85 | // THIS IS NOT THE CORRECT WAY!
86 | import { main, run } from 'effection';
87 | import { fetchWeekDay } from './fetch-week-day';
88 |
89 | main(function*() {
90 | let dayUS = run(fetchWeekDay('est'));
91 | let daySweden = run(fetchWeekDay('cet'));
92 | console.log(`It is ${yield* dayUS}, in the US and ${yield* daySweden} in Sweden!`);
93 | });
94 | ```
95 |
96 | This has the same problem as our `async/await` example: a failure in one fetch
97 | has no effect on the other!
98 |
99 | ## Introducing `spawn`
100 |
101 | The `spawn` operation is Effection's solution to this problem!
102 |
103 | ``` javascript
104 | import { main, spawn } from 'effection';
105 | import { fetchWeekDay } from './fetch-week-day';
106 |
107 | main(function*() {
108 | let dayUS = yield* spawn(() => fetchWeekDay('est'));
109 | let daySweden = yield* spawn(() => fetchWeekDay('cet'));
110 | console.log(`It is ${yield* dayUS}, in the US and ${yield* daySweden} in Sweden!`);
111 | });
112 | ```
113 |
114 | Like `run` and `main`, `spawn` takes an `Operation` and returns a `Task`. The
115 | difference is that this `Task` becomes a child of the current `Task`. This
116 | means it is impossible for this task to outlive its parent. And it also means
117 | that an error in the task will cause the parent to fail.
118 |
119 | You can think of this as creating a hierarchy like this:
120 |
121 | ```
122 | +-- main
123 | |
124 | +-- fetchWeekDay('est')
125 | |
126 | +-- fetchWeekDay('cet')
127 | ```
128 |
129 | When `fetchWeekDay('cet')` fails, since it was spawned by `main`, it will also
130 | cause `main` to fail. When `main` fails it will make sure that none of its
131 | children outlive it, and it will `halt` all of its remaining children. We end
132 | up with a situation like this:
133 |
134 | ```
135 | +-- main [FAILED]
136 | |
137 | +-- fetchWeekDay('est') [HALTED]
138 | |
139 | +-- fetchWeekDay('cet') [FAILED]
140 | ```
141 |
142 | Effection tasks are tied to the lifetime of their parent, and it becomes
143 | impossible to create a task whose lifetime is undefined. Because of this, the
144 | behaviour of errors is very clearly defined. An error in a child will also
145 | cause the parent to error, which in turn halts any siblings.
146 |
147 | This idea is called [structured concurrency], and it has profound effects on
148 | the composability of concurrent code.
149 |
150 | ## Using combinators
151 |
152 | We previously showed how we can use the `Promise.all` combinator to implement
153 | the concurrent fetch. Effection also ships with some combinators, for example
154 | we can use the `all` combinator:
155 |
156 | ``` javascript
157 | import { all, main } from 'effection';
158 |
159 | main(function *() {
160 | let [dayUS, daySweden] = yield* all([fetchWeekDay('est'), fetchWeekDay('cet')]);
161 | console.log(`It is ${dayUS}, in the US and ${daySweden} in Sweden!`);
162 | });
163 | ```
164 |
165 | ## Spawning in a Scope
166 |
167 | The `spawn()` operation always runs its operation as a child of the current
168 | operation. Sometimes however, you might want to run an operation as a child of a
169 | _different_ operation. To do this we can use the [`Scope#run()`][scope.run]
170 | method.
171 |
172 | This is often useful when integrating Effection into existing promise or
173 | callback based frameworks. The following example creates a trivial
174 | [`express`][express] server that runs each request as an operation that is
175 | a child of the main operation.
176 |
177 |
178 | ```javascript {5,8}
179 | import express from 'express';
180 | import { main, suspend, useScope } from "effection";
181 |
182 | await main(function*() {
183 | let scope = yield* useScope();
184 | let app = express();
185 | app.get("/", async (_, res) => {
186 | await scope.run(function*() {
187 | res.send("Hello World!");
188 | })
189 | });
190 | let server = app.listen();
191 | try {
192 | yield* suspend()
193 | } finally {
194 | server.close();
195 | }
196 | });
197 | ```
198 |
199 | We capture a reference to the main operation's scope in line **(5)**
200 | and then use that scope to run the request as an async function on
201 | line **(8)**.
202 |
203 | You can learn more about this in the [scope guide](./scope).
204 |
205 | [structured concurrency]: https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/
206 | [express]: https://expressjs.org
207 | [scope.run]: /api/v3/Scope#method_run_0
208 | [Operation]: /api/v3/Operation
209 |
--------------------------------------------------------------------------------
/docs/structure.json:
--------------------------------------------------------------------------------
1 | {
2 | "Getting Started": [
3 | ["installation.mdx", "Installation"],
4 | ["typescript.mdx", "TypeScript"],
5 | ["thinking-in-effection.mdx", "Thinking in Effection"],
6 | ["async-rosetta-stone.mdx", "Async Rosetta Stone"],
7 | ["tutorial.mdx", "Tutorial"]
8 | ],
9 | "Learn Effection": [
10 | ["operations.mdx", "Operations"],
11 | ["actions.mdx", "Actions and Suspensions"],
12 | ["resources.mdx", "Resources"],
13 | ["spawn.mdx", "Spawn"],
14 | ["collections.mdx", "Streams and Subscriptions"],
15 | ["events.mdx", "Events"],
16 | ["errors.mdx", "Error Handling"],
17 | ["context.mdx", "Context"]
18 | ],
19 | "Advanced": [
20 | ["scope.mdx", "Scope"],
21 | ["processes.mdx", "Processes"]
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/docs/thinking-in-effection.mdx:
--------------------------------------------------------------------------------
1 | When we say that Effection is "Structured Concurrency and Effects for Javascript" we mean three things:
2 |
3 | 1. No operation runs longer than its parent.
4 | 2. Every operation exits fully.
5 | 3. It's just JavaScript, and except for the guarantees derived from (1) and (2), it should feel familiar in every other
6 | way.
7 |
8 | Developing a new intuition about how to leverage Structured Concurrency, while leaning on your existing intuition as a
9 | JavaScript developer will help you get the most out of Effection and have you attempting things that you would never
10 | have even dreamed before.
11 |
12 | ## No operation runs longer than its parent.
13 |
14 | In JavaScript, developers rarely have to think about memory allocation because memory lifetime is bound to the scope of
15 | the function that it was allocated for. When a function is finished, its scope is torn down and all of the memory
16 | allocated to variables in function's scope are released safely. Binding memory management to scope gives developers the
17 | freedom to focus on their application instead of worrying about leaking memory.
18 |
19 | Structured concurrency establishes the same relationship between scope and asynchrony. Every Effection operation is
20 | bound to the lifetime of its parent. Because of this, Effection automatically tears down
21 | child operations when the parent operation completes or is halted. Like with memory management, binding asynchrony to
22 | about the scope frees developers to focus on writing their applications instead of worrying about where and when to run
23 | asynchronous cleanup.
24 |
25 | The key to achieving this freedom is to make the following mental shift about the natural lifetime of an asynchronous
26 | operation.
27 |
28 | **before**
29 |
30 | > An asynchronous operation will run as long as it needs to
31 |
32 | **after**
33 |
34 | > An asynchronous operation runs only as long as it's needed.
35 |
36 | Effection provides this shift. Whenever an operation completes, none of its child operations are left
37 | around to pollute your runtime.
38 |
39 | ## Every operation exits fully.
40 |
41 | We expect synchronous functions to run completely from start to finish.
42 |
43 | ```js {5} showLineNumbers
44 | function main() {
45 | try {
46 | fn()
47 | } finally {
48 | // code here is GUARANTEED to run
49 | }
50 | }
51 | ```
52 |
53 | Knowing this makes code predictable. Developers can be confident that their functions will either return a result or
54 | throw an error. You can wrap a synchronous function in a `try/catch/finally` block to handle thrown errors. The
55 | `finally` block can be used to perform clean up after completion.
56 |
57 | However, the same guarantee is not provided for async functions.
58 |
59 | ```js {5} showLineNumbers
60 | async function main() {
61 | try {
62 | await new Promise((resolve) => setTimeout(resolve, 100,000));
63 | } finally {
64 | // code here is NOT GUARANTEED to run
65 | }
66 | }
67 |
68 | await main();
69 | ```
70 |
71 | Once an async function begins execution, the code in its `finally{}` blocks may never get
72 | a chance to run, and as a result, it is difficult to write code that always cleans up after itself.
73 |
74 | This hard limitation of the JavaScript runtime is called the [Await Event Horizon][await-event-horizon],
75 | and developers experience its impact on daily basis. For example, the very common [EADDRINUSE][eaddrinuse-error]
76 | error is caused by a caller not being able to execute clean up when a Node.js process is stopped.
77 |
78 | By contrast, Effection _does_ provide this guarantee.
79 |
80 | ```js {7} showLineNumbers
81 | import { main, action } from "effection";
82 |
83 | await main(function*() {
84 | try {
85 | yield* action(function*(resolve) { setTimeout(resolve, 100,000) });
86 | } finally {
87 | // code here is GUARANTEED to run
88 | }
89 | });
90 | ```
91 |
92 | When executing Effection operations you can expect that
93 | they will run to completion; giving every operation an opportunity to clean up. At first glance, this
94 | might seem like a small detail but it's fundamental to writing composable code.
95 |
96 | ## It's just JavaScript
97 |
98 | Effection is designed to provide Structured Concurrency guarantees using common JavaScript language constructs such as
99 | `let`, `const`, `if`, `for`, `while`, `switch` and `try/catch/finally`. Our goal is to allow JavaScript developers to
100 | leverage what they already know while gaining the guarantees of Structured Concurrency. You can use all of these
101 | constructs in an Effection function and they'll behave as you'd expect.
102 |
103 | However, one of the constructs we avoid in Effection is the syntax and semantics of `async/await`.
104 | This is because `async/await` is [not capable of modeling structured concurrency][await-event-horizon]. Instead, we make
105 | extensive use of [generator functions][generator-functions] which are a core feature of JavaScript and are supported by all browsers
106 | and JavaScript runtimes.
107 |
108 | Finally, we provide a handy Effection Rosetta Stone to show how _Async/Await_ concepts map into Effection APIs.
109 |
110 | [generator-functions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*
111 | [await-event-horizon]: https://frontside.com/blog/2023-12-11-await-event-horizon/
112 | [delimited-continuation-repo]: https://github.com/thefrontside/continuation
113 | [eaddrinuse-error]: https://stackoverflow.com/questions/14790910/stop-all-instances-of-node-js-server
114 |
--------------------------------------------------------------------------------
/docs/tutorial.mdx:
--------------------------------------------------------------------------------
1 | Let's write our first program using Effection.
2 |
3 | ``` javascript
4 | import { until, useAbortSignal } from "effection";
5 |
6 | export function* fetchWeekDay(timezone) {
7 | let signal = yield* useAbortSignal();
8 |
9 | let response = yield* until(fetch(`http://worldclockapi.com/api/json/${timezone}/now`, { signal }));
10 |
11 | let time = yield* until(response.json());
12 |
13 | return time.dayOfTheWeek;
14 | }
15 | ```
16 |
17 | To start using Effection, use the `main` function as an entry
18 | point. In this example, we'll use the previously defined
19 | `fetchWeekDay`.
20 |
21 | ```javascript
22 | import { main } from 'effection';
23 | import { fetchWeekDay } from './fetch-week-day';
24 |
25 | await main(function*() {
26 |
27 | let dayOfTheWeek = yield* fetchWeekDay('est');
28 |
29 | console.log(`It is ${dayOfTheWeek}, my friends!`);
30 | });
31 | ```
32 |
33 | Even with such a simple program, Effection is still providing critical
34 | power-ups that you don't get with callbacks,
35 | promises, or `async/await`. For example, notice how the Effection
36 | `fetchWeekDay()` operation does not accept an abort signal as a second argument.
37 | That's because there is no need to manually wire up any signal handlers and
38 | trigger an `AbortController` at the right time.
39 |
40 | If you run the above code in NodeJS and hit `CTRL-C` while the request
41 | to `http://worldclockapi.com` is still in progress, it will properly
42 | cancel the in-flight request as a well-behaved HTTP client should. All
43 | without you ever having to think about it. This is because every
44 | Effection operation contains the information on how to dispose of
45 | itself, and so the actual act of cancellation can be automated.
46 |
47 | This has powerful consequences when it comes to composing new
48 | operations out of existing ones. For example, we can add a time out of
49 | 1000 milliseconds to our `fetchWeekDay` operation (or any operation
50 | for that matter) by wrapping it in a `withTimeout` operation.
51 |
52 | ```javascript
53 | import { main, until, race } from 'effection';
54 | import { fetchWeekDay } from './fetch-week-day';
55 |
56 | await main(function*() {
57 | let dayOfTheWeek = yield* withTimeout(fetchWeekDay('est'), 1000);
58 | console.log(`It is ${dayOfTheWeek}, my friends!`);
59 | });
60 |
61 | function withTimeout(operation, delay) {
62 | return race([operation, until(function*() {
63 | yield* sleep(delay);
64 | throw new Error(`timeout!`);
65 | })]);
66 | }
67 | ```
68 |
69 | If more than 1000 milliseconds passes before the `fetchWeekDay()`
70 | operation completes, then an error will be raised.
71 |
72 | What's important to note however, is that when we actually defined our
73 | `fetchWeekDay()` operation, we never once had to worry about timeouts,
74 | or request cancellation. And in order to achieve both we didn't have
75 | to gum up our API by passing around cancellation tokens or [abort
76 | controllers][abort controller]. We just got it all for free.
77 |
78 | ## Discover more
79 |
80 | This is just the tip of the iceberg when it comes to the seemingly complex
81 | things that Effection can make simple. To find out more, jump
82 | into the conversation in our [Discord server][discord]. We're really
83 | excited about the things that Effection has enabled us to accomplish,
84 | and we'd love to hear your thoughts on it, and how you might see
85 | it working for you.
86 |
87 | [abort controller]: https://developer.mozilla.org/en-US/docs/Web/API/AbortController
88 | [discord]: https://discord.gg/Ug5nWH8
89 |
--------------------------------------------------------------------------------
/docs/typescript.mdx:
--------------------------------------------------------------------------------
1 | Effection is written in TypeScript and comes bundled with its own type
2 | definitions. As such, it doesn't require any special setup to use in a
3 | TypeScript project. This section only contains some helpful hints to make
4 | the most out of the Effection related typings in your project.
5 |
6 | ## TL;DR
7 |
8 | Use the `Operation` type for your operations
9 |
10 | ```ts
11 | import type { Operation } from "effection";
12 |
13 | export function* op(): Operation {
14 | yield* sleep(10);
15 | return 5;
16 | }
17 | ```
18 |
19 | If you see a weird type like `Generator` or
20 | `Iterable` or `IterableIterator`,
21 | replace it with `Operation`.
22 |
23 | ## Use Operations Everywhere
24 |
25 | The foundation of Effection is the [`Operation`][operation] type. This is the type that
26 | it uses internally to represent all actions and resources, and it is the type
27 | that you should both consume and return from your own functions.
28 | For the most part, TypeScript will infer this for you, and you don't need
29 | to worry about it. However, there are some cases you may want to give it a hint
30 | that what you have is an operation, even though it will work without it.
31 | This is because `Operation` is effectively an
32 | `Iterable`. This means that when you create this
33 | shape naturally in your code, then it will correctly slot into other
34 | operations, but it might be confusing when people look at types in
35 | their IDE. Take the following generator function:
36 |
37 | ```typescript
38 | function* op() {
39 | yield* sleep(10);
40 | return 5;
41 | }
42 | ```
43 |
44 | The natural inferred return type of this function is `Generator` which
45 | satisfies `Iterable` and so can be used as an operation,
46 | but it loses the higher-level intent of the operation.
47 | Instead, write `op()` like this:
48 | ```typescript
49 | function* op(): Operation {
50 | yield* sleep(10);
51 | return 5;
52 | }
53 | ```
54 | By pinning the return type to `Operation`, it communicates the intent
55 | of how the generator is to be used beyond just the literal shape of the
56 | generator itself.
57 |
58 | The same applies for Operation values:
59 |
60 | ```ts
61 | const op = {
62 | *[Symbol.iterator]() {
63 | yield* sleep(10);
64 | return 5;
65 | }
66 | }
67 | ```
68 | Strictly speaking the type of `op` is:
69 | ```ts
70 | {
71 | [Symbol.iterator](): Generator
72 | }
73 | ```
74 |
75 | While this is technically correct, and it will work if you use it as
76 | an operation, it isn't particularly helpful to your end users. Instead, give
77 | them a leg up, and make their editor's tooltip show that they can evaluate
78 | it and expect to receive a number.
79 |
80 | ```ts
81 | const op: Operation = {
82 | *[Symbol.iterator]() {
83 | yield* sleep(10);
84 | return 5;
85 | }
86 | }
87 | ```
88 |
89 | Now anybody using your operation won't have any doubt about what it is and how
90 | they can use it.
91 |
92 | [operation]: /api/v3/Operation
93 |
--------------------------------------------------------------------------------
/lib/abort-signal.ts:
--------------------------------------------------------------------------------
1 | import type { Operation } from "./types.ts";
2 | import { resource } from "./instructions.ts";
3 |
4 | /**
5 | * Create an
6 | * [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal)
7 | * bound to the current scope. Whenever that scope is completed,
8 | * errored, or halted, the abort signal will be triggered.
9 | *
10 | * Cancellation and teardown is handled automatically in Effection,
11 | * but in systems where it is not, the lifespan of a transaction needs
12 | * to be explicitly modeled. A very common way to do this is with the
13 | * `AbortSignal`. By creating an abort signal bound to an Effection
14 | * scope, we can pass that signal down to 3rd party consumers that can
15 | * use it to register shutdown callbacks that will be invoked whenever
16 | * the task is done (either by completion, failure, or
17 | * cancellation). An example is the native
18 | * [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch#syntax)
19 | * API which takes a `signal` parameter in order to trigger when the
20 | * HTTP request should be cancelled. This is how you would bind the
21 | * lifetime of the HTTP request to the lifetime of the current task.
22 | *
23 | * @example
24 | * ```javascript
25 | * function* request() {
26 | * let signal = yield* useAbortSignal();
27 | * return yield* fetch('/some/url', { signal });
28 | * }
29 | * ```
30 | */
31 | export function useAbortSignal(): Operation {
32 | return resource(function* AbortSignal(provide) {
33 | let controller = new AbortController();
34 | try {
35 | yield* provide(controller.signal);
36 | } finally {
37 | controller.abort();
38 | }
39 | });
40 | }
41 |
--------------------------------------------------------------------------------
/lib/all.ts:
--------------------------------------------------------------------------------
1 | import type { Operation, Task, Yielded } from "./types.ts";
2 | import { spawn } from "./instructions.ts";
3 | import { call } from "./call.ts";
4 |
5 | /**
6 | * Block and wait for all of the given operations to complete. Returns
7 | * an array of values that the given operations evaluated to. This has
8 | * the same purpose as
9 | * [Promise.all](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all).
10 | *
11 | * If any of the operations become errored, then `all` will also become errored.
12 | *
13 | * ### Example
14 | *
15 | * ``` javascript
16 | * import { all, expect, main } from 'effection';
17 | *
18 | * await main(function*() {
19 | * let [google, bing] = yield* all([
20 | * expect(fetch('http://google.com')),
21 | * expect(fetch('http://bing.com')),
22 | * ]);
23 | * // ...
24 | * });
25 | * ```
26 | *
27 | * @param ops a list of operations to wait for
28 | * @returns the list of values that the operations evaluate to, in the order they were given
29 | */
30 | export function all[] | []>(
31 | ops: T,
32 | ): Operation> {
33 | return call(function* () {
34 | let tasks: Task[] = [];
35 | for (let operation of ops) {
36 | tasks.push(yield* spawn(() => operation));
37 | }
38 | let results = [];
39 | for (let task of tasks) {
40 | results.push(yield* task);
41 | }
42 | return results as All;
43 | });
44 | }
45 |
46 | /**
47 | * This type allows you to infer heterogenous operation types.
48 | * e.g. `all([sleep(0), expect(fetch("https://google.com")])`
49 | * will have a type of `Operation<[void, Request]>`
50 | */
51 |
52 | type All[] | []> = {
53 | -readonly [P in keyof T]: Yielded;
54 | };
55 |
--------------------------------------------------------------------------------
/lib/async.ts:
--------------------------------------------------------------------------------
1 | import type { Stream, Subscription } from "./types.ts";
2 |
3 | import { call } from "./call.ts";
4 |
5 | /**
6 | * Convert any [`AsyncIterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncIterator) into an effection {@link Subscription}
7 | *
8 | * This allows you to consume any `AsyncIterator` as a {@link Subscription}.
9 | *
10 | * @param iter - the iterator to convert
11 | * @returns a subscription that will produce each item of `iter`
12 | */
13 | export function subscribe(iter: AsyncIterator): Subscription {
14 | return {
15 | next: () => call(() => iter.next()),
16 | };
17 | }
18 |
19 | /**
20 | * Convert any [`AsyncIterable`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols) into an Effection {@link Stream}.
21 | *
22 | * This allows you to consume any `AsyncIterable` as a {@link Stream}.
23 | *
24 | * @param iterable - the async iterable to convert
25 | * @returns a stream that will produce each item of `iterable`
26 | */
27 | export function stream(iterable: AsyncIterable): Stream {
28 | return {
29 | *[Symbol.iterator]() {
30 | return subscribe(iterable[Symbol.asyncIterator]());
31 | },
32 | };
33 | }
34 |
35 | interface AsyncIterable {
36 | [Symbol.asyncIterator](): AsyncIterator;
37 | }
38 |
--------------------------------------------------------------------------------
/lib/call.ts:
--------------------------------------------------------------------------------
1 | import type { Instruction, Operation } from "./types.ts";
2 | import { action } from "./instructions.ts";
3 | import { pause } from "./pause.ts";
4 |
5 | /**
6 | * A uniform integration type representing anything that can be evaluated
7 | * as a the parameter to {@link call}.
8 | *
9 | * {@link call} converts a `Callable` into an `Operation` which can then be used
10 | * anywhere within Effection.
11 | *
12 | * APIs that accept `Callable` values allow end developers to pass simple
13 | * functions without necessarily needing to know anything about Operations.
14 | *
15 | * ```javascript
16 | * function hello(to: Callable): Operation {
17 | * return function*() {
18 | * return `hello ${yield* call(to)}`;
19 | * }
20 | * }
21 | *
22 | * await run(() => hello(() => "world!")); // => "hello world!"
23 | * await run(() => hello(async () => "world!")); // => "hello world!"
24 | * await run(() => hello(function*() { return "world!" })); "hello world!";
25 | * ```
26 | */
27 | export type Callable =
28 | | Operation
29 | | Promise
30 | | (() => Operation)
31 | | (() => Promise)
32 | | (() => T);
33 |
34 | /**
35 | * Pause the current operation, then run an async function, or operation function in a new scope. The calling operation will be resumed (or errored)
36 | * once call is completed.
37 | *
38 | * `call()` is a uniform integration point for calling async functions,
39 | * and generator functions.
40 | *
41 | * It can be used to invoke an async function:
42 | *
43 | * @example
44 | * ```typescript
45 | * async function* googleSlowly() {
46 | * return yield* call(async function() {
47 | * await new Promise(resolve => setTimeout(resolve, 2000));
48 | * return await fetch("https://google.com");
49 | * });
50 | * }
51 | * ```
52 | *
53 | * It can be used to run an operation in a separate scope to ensure that any
54 | * resources allocated will be cleaned up:
55 | *
56 | * @example
57 | * ```javascript
58 | * yield* call(function*() {
59 | * let socket = yield* useSocket();
60 | * return yield* socket.read();
61 | * }); // => socket is destroyed before returning
62 | * ```
63 | *
64 | * Because `call()` runs within its own {@link Scope}, it can also be used to
65 | * establish [error boundaries](https://frontside.com/effection/docs/errors).
66 | *
67 | * @example
68 | * ```javascript
69 | * function* myop() {
70 | * let task = yield* spawn(function*() {
71 | * throw new Error("boom!");
72 | * });
73 | * yield* task;
74 | * }
75 | *
76 | * function* runner() {
77 | * try {
78 | * yield* myop();
79 | * } catch (err) {
80 | * // this will never get hit!
81 | * }
82 | * }
83 | *
84 | * function* runner() {
85 | * try {
86 | * yield* call(myop);
87 | * } catch(err) {
88 | * // properly catches `spawn` errors!
89 | * }
90 | * }
91 | * ```
92 | *
93 | * @param callable the operation, promise, async function, generator funnction, or plain function to call as part of this operation
94 | */
95 | export function call(callable: () => Operation): Operation;
96 | export function call(callable: () => Promise): Operation;
97 |
98 | /**
99 | * @deprecated Using call with simple functions will be removed in v4.
100 | * To convert simple functions into operations, use @{link lift}.
101 | */
102 | export function call(callable: () => T): Operation;
103 |
104 | /**
105 | * @deprecated calling bare promises, operations, and constants will
106 | * be removed in v4 use {@link until} instead
107 | *
108 | * before: call(operation);
109 | * after: until(operation);
110 | */
111 | export function call(callable: Operation): Operation;
112 |
113 | /**
114 | * @deprecated calling bare promises, operations, and constants will
115 | * be removed in v4, always pass a function to call()
116 | *
117 | * before: call(promise);
118 | * after: call(() => promise);
119 | */
120 | export function call(callable: Promise): Operation;
121 |
122 | export function call(callable: Callable): Operation {
123 | return action(function* (resolve, reject) {
124 | try {
125 | if (typeof callable === "function") {
126 | let fn = callable as () => Operation | Promise | T;
127 | resolve(yield* toop(fn()));
128 | } else {
129 | resolve(yield* toop(callable));
130 | }
131 | } catch (error) {
132 | reject(error as Error);
133 | }
134 | });
135 | }
136 |
137 | function toop(
138 | op: Operation | Promise | T,
139 | ): Operation {
140 | if (isPromise(op)) {
141 | return expect(op);
142 | } else if (isIterable(op)) {
143 | let iter = op[Symbol.iterator]();
144 | if (isInstructionIterator(iter)) {
145 | // operation
146 | return op;
147 | } else {
148 | // We are assuming that if an iterator does *not* have `.throw` then
149 | // it must be a built-in iterator and we should return the value as-is.
150 | return bare(op as T);
151 | }
152 | } else {
153 | return bare(op as T);
154 | }
155 | }
156 |
157 | function bare(val: T): Operation {
158 | return {
159 | [Symbol.iterator]() {
160 | return { next: () => ({ done: true, value: val }) };
161 | },
162 | };
163 | }
164 |
165 | function expect(promise: Promise): Operation {
166 | return pause((resolve, reject) => {
167 | promise.then(resolve, reject);
168 | return () => {};
169 | });
170 | }
171 |
172 | function isFunc(f: unknown): f is (...args: unknown[]) => unknown {
173 | return typeof f === "function";
174 | }
175 |
176 | function isPromise(p: unknown): p is Promise {
177 | if (!p) return false;
178 | return isFunc((p as Promise).then);
179 | }
180 |
181 | // iterator must implement both `.next` and `.throw`
182 | // built-in iterators are not considered iterators to `call()`
183 | function isInstructionIterator(it: unknown): it is Iterator {
184 | if (!it) return false;
185 | return isFunc((it as Iterator).next) &&
186 | isFunc((it as Iterator).throw);
187 | }
188 |
189 | function isIterable(it: unknown): it is Iterable {
190 | if (!it) return false;
191 | return typeof (it as Iterable)[Symbol.iterator] === "function";
192 | }
193 |
194 | /**
195 | * It can be used to treat a promise as an operation. This function
196 | * is a replacement to the deprecated `call(promise)` function form.
197 | *
198 | * @example
199 | * ```js
200 | * let response = yield* until(fetch('https://google.com'));
201 | * ```
202 | * @template {T}
203 | * @param promise
204 | * @returns {Operation}
205 | */
206 | export function until(promise: PromiseLike): Operation {
207 | return call(async () => await promise);
208 | }
209 |
--------------------------------------------------------------------------------
/lib/channel.ts:
--------------------------------------------------------------------------------
1 | import type { Operation, Stream } from "./types.ts";
2 | import { createSignal } from "./signal.ts";
3 | import { lift } from "./lift.ts";
4 |
5 | /**
6 | * A broadcast channel that multiple consumers can subscribe to the
7 | * via the same {@link Stream}, and messages sent to the channel are
8 | * received by all consumers. The channel is not buffered, so if there
9 | * are no consumers, the message is dropped.
10 | */
11 | export interface Channel extends Stream {
12 | /**
13 | * Send a message to all subscribers of this {@link Channel}
14 | */
15 | send(message: T): Operation;
16 |
17 | /**
18 | * End every subscription to this {@link Channel}
19 | */
20 | close(value: TClose): Operation;
21 | }
22 |
23 | /**
24 | * Create a new {@link Channel}. Use channels to communicate between operations.
25 | * In order to dispatch messages from outside an operation such as from a
26 | * callback, use {@link Signal}.
27 | *
28 | * See [the guide on Streams and
29 | * Subscriptions](https://frontside.com/effection/docs/guides/collections)
30 | * for more details.
31 | *
32 | * @example
33 | *
34 | * ``` javascript
35 | * import { main, createChannel } from 'effection';
36 | *
37 | * await main(function*() {
38 | * let channel = createChannel();
39 | *
40 | * yield* channel.send('too early'); // the channel has no subscribers yet!
41 | *
42 | * let subscription1 = yield* channel;
43 | * let subscription2 = yield* channel;
44 | *
45 | * yield* channel.send('hello');
46 | * yield* channel.send('world');
47 | *
48 | * console.log(yield* subscription1.next()); //=> { done: false, value: 'hello' }
49 | * console.log(yield* subscription1.next()); //=> { done: false, value: 'world' }
50 | * console.log(yield* subscription2.next()); //=> { done: false, value: 'hello' }
51 | * console.log(yield* subscription2.next()); //=> { done: false, value: 'world' }
52 | * });
53 | * ```
54 | */
55 | export function createChannel(): Channel {
56 | let signal = createSignal();
57 |
58 | return {
59 | send: lift(signal.send),
60 | close: lift(signal.close),
61 | [Symbol.iterator]: signal[Symbol.iterator],
62 | };
63 | }
64 |
--------------------------------------------------------------------------------
/lib/context.ts:
--------------------------------------------------------------------------------
1 | import type { Context, Operation } from "./types.ts";
2 | import { create } from "./run/create.ts";
3 | import { useScope } from "./run/scope.ts";
4 |
5 | export function createContext(key: string, defaultValue?: T): Context {
6 | let context: Context = create>(`Context`, { key }, {
7 | defaultValue,
8 | *get() {
9 | let scope = yield* useScope();
10 | return scope.get(context);
11 | },
12 | *set(value: T) {
13 | let scope = yield* useScope();
14 | return scope.set(context, value);
15 | },
16 | expect,
17 | *with(value: T, operation: (value: T) => Operation): Operation {
18 | let scope = yield* useScope();
19 | let original = scope.hasOwn(context) ? scope.get(context) : undefined;
20 | try {
21 | return yield* operation(scope.set(context, value));
22 | } finally {
23 | if (typeof original === "undefined") {
24 | scope.delete(context);
25 | } else {
26 | scope.set(context, original);
27 | }
28 | }
29 | },
30 | [Symbol.iterator]() {
31 | console.warn(
32 | `⚠️ using a context (${key}) directly as an operation is deprecated. Use context.expect() instead`,
33 | );
34 | context[Symbol.iterator] = expect;
35 | return expect();
36 | },
37 | });
38 |
39 | function* expect() {
40 | let value = yield* context.get();
41 | if (typeof value === "undefined") {
42 | throw new MissingContextError(`missing required context: '${key}'`);
43 | } else {
44 | return value;
45 | }
46 | }
47 |
48 | return context;
49 | }
50 |
51 | class MissingContextError extends Error {
52 | override name = "MissingContextError";
53 | }
54 |
--------------------------------------------------------------------------------
/lib/deps.ts:
--------------------------------------------------------------------------------
1 | export { assert } from "jsr:@std/assert@1.0.10";
2 | export * from "jsr:@frontside/continuation@0.1.6";
3 |
--------------------------------------------------------------------------------
/lib/each.ts:
--------------------------------------------------------------------------------
1 | import type { Operation, Stream, Subscription } from "./types.ts";
2 | import { createContext } from "./context.ts";
3 | import { useScope } from "./run/scope.ts";
4 | import { resource, spawn } from "./instructions.ts";
5 | import { withResolvers } from "./with-resolvers.ts";
6 |
7 | /**
8 | * Consume an effection stream using a simple for-of loop.
9 | *
10 | * Given any stream, you can access its values sequentially using the `each()`
11 | * operation just as you would use `for await of` loop with an async iterable:
12 | *
13 | * ```javascript
14 | * function* logvalues(stream) {
15 | * for (let value of yield* each(stream)) {
16 | * console.log(value);
17 | * yield* each.next()
18 | * }
19 | * }
20 | * ```
21 | * You must always invoke `each.next` at the end of each iteration of the loop,
22 | * including if the interation ends with a `continue` statement.
23 | *
24 | * Note that just as with async iterators, there is no way to consume the
25 | * `TClose` value of a stream using the `for-each` loop.
26 | *
27 | * @typeParam T - the type of each value in the stream.
28 | * @param stream - the stream to iterate
29 | * @returns an operation to iterate `stream`
30 | */
31 | export function each(stream: Stream): Operation> {
32 | return {
33 | *[Symbol.iterator]() {
34 | let scope = yield* useScope();
35 | let stack = scope.hasOwn(EachStack)
36 | ? scope.expect(EachStack)
37 | : yield* EachStack.set([]);
38 |
39 | let loop = yield* resource>(function* (provide) {
40 | let subscription = yield* stream;
41 | let current = yield* subscription.next();
42 | let { operation: finished, resolve: finish } = withResolvers();
43 |
44 | yield* spawn(() => provide({ current, subscription, finish }));
45 |
46 | yield* finished;
47 | });
48 |
49 | stack.push(loop);
50 |
51 | let iterator: Iterator = {
52 | next() {
53 | if (loop.stale) {
54 | let error = new Error(
55 | `for each loop did not use each.next() operation before continuing`,
56 | );
57 | error.name = "IterationError";
58 | throw error;
59 | } else {
60 | loop.stale = true;
61 | return loop.current;
62 | }
63 | },
64 | return() {
65 | stack.pop();
66 | loop.finish();
67 | return { done: true, value: void 0 };
68 | },
69 | };
70 |
71 | return {
72 | [Symbol.iterator]: () => iterator,
73 | };
74 | },
75 | };
76 | }
77 |
78 | each.next = function next(): Operation {
79 | return {
80 | name: "each.next()",
81 | *[Symbol.iterator]() {
82 | let stack = yield* EachStack.expect();
83 | let context = stack[stack.length - 1];
84 | if (!context) {
85 | let error = new Error(`cannot call next() outside of an iteration`);
86 | error.name = "IterationError";
87 | throw error;
88 | }
89 | let current = yield* context.subscription.next();
90 | delete context.stale;
91 | context.current = current;
92 | if (current.done) {
93 | stack.pop();
94 | }
95 | },
96 | } as Operation;
97 | };
98 |
99 | interface EachLoop {
100 | subscription: Subscription;
101 | current: IteratorResult;
102 | finish(): void;
103 | stale?: true;
104 | }
105 |
106 | const EachStack = createContext[]>("each");
107 |
--------------------------------------------------------------------------------
/lib/ensure.ts:
--------------------------------------------------------------------------------
1 | import type { Operation } from "./types.ts";
2 | import { resource } from "./instructions.ts";
3 |
4 | /**
5 | * Run the given function or operation when the current operation
6 | * shuts down. This is equivalent to running the function or operation
7 | * in a `finally {}` block, but it can help you avoid rightward drift.
8 | *
9 | * @example
10 | *
11 | * ```javascript
12 | * import { main, ensure } from 'effection';
13 | * import { createServer } from 'http';
14 | *
15 | * await main(function*() {
16 | * let server = createServer(...);
17 | * yield* ensure(() => { server.close() });
18 | * });
19 | * ```
20 | *
21 | * Note that you should wrap the function body in braces, so the function
22 | * returns `undefined`.
23 | *
24 | * @example
25 | *
26 | * ```javascript
27 | * import { main, ensure, once } from 'effection';
28 | * import { createServer } from 'http';
29 | *
30 | * await main(function*() {
31 | * let server = createServer(...);
32 | * yield* ensure(function* () {
33 | * server.close();
34 | * yield* once(server, 'closed');
35 | * });
36 | * });
37 | * ```
38 | *
39 | * Your ensure function should return an operation whenever you need to do
40 | * asynchronous cleanup. Otherwise, you can return `void`
41 | *
42 | * @param fn - a function which returns an {@link Operation} or void
43 | */
44 | export function ensure(fn: () => Operation | void): Operation {
45 | return resource(function* (provide) {
46 | try {
47 | yield* provide();
48 | } finally {
49 | let result = fn();
50 | if (result && typeof result[Symbol.iterator] === "function") {
51 | yield* result;
52 | }
53 | }
54 | });
55 | }
56 |
--------------------------------------------------------------------------------
/lib/events.ts:
--------------------------------------------------------------------------------
1 | // deno-lint-ignore-file no-explicit-any ban-types
2 | import { createSignal } from "./signal.ts";
3 | import { resource } from "./instructions.ts";
4 | import type { Operation, Stream, Subscription } from "./types.ts";
5 |
6 | type FN = (...any: any[]) => any;
7 |
8 | type EventTypeFromEventTarget = `on${K}` extends keyof T
9 | ? Parameters>[0]
10 | : Event;
11 |
12 | /**
13 | * @ignore
14 | */
15 | export type EventList = T extends {
16 | addEventListener(type: infer P, ...args: any): void;
17 | // we basically ignore this but we need it so we always get the first override of addEventListener
18 | addEventListener(type: infer P2, ...args: any): void;
19 | } ? P & string
20 | : never;
21 |
22 | /**
23 | * Create an {@link Operation} that yields the next event to be emitted by an
24 | * [EventTarget](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget).
25 | *
26 | * @param target - the event target to be watched
27 | * @param name - the name of the event to watch. E.g. "click"
28 | * @returns an Operation that yields the next emitted event
29 | */
30 | export function once<
31 | T extends EventTarget,
32 | K extends EventList | (string & {}),
33 | >(target: T, name: K): Operation> {
34 | return {
35 | *[Symbol.iterator]() {
36 | let subscription = yield* on(target, name);
37 | let next = yield* subscription.next();
38 | return next.value;
39 | },
40 | };
41 | }
42 |
43 | /**
44 | * Create a {@link Stream} of events from any
45 | * [EventTarget](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget).
46 | *
47 | * See the guide on [Streams and Subscriptions](https://frontside.com/effection/docs/collections)
48 | * for details on how to use streams.
49 | *
50 | * @param target - the event target whose events will be streamed
51 | * @param name - the name of the event to stream. E.g. "click"
52 | * @returns a stream that will see one item for each event
53 | */
54 | export function on<
55 | T extends EventTarget,
56 | K extends EventList | (string & {}),
57 | >(target: T, name: K): Stream, never> {
58 | return resource(function* (provide) {
59 | let signal = createSignal();
60 |
61 | target.addEventListener(name, signal.send);
62 |
63 | try {
64 | yield* provide(
65 | yield* signal as Operation<
66 | Subscription, never>
67 | >,
68 | );
69 | } finally {
70 | target.removeEventListener(name, signal.send);
71 | }
72 | });
73 | }
74 |
--------------------------------------------------------------------------------
/lib/instructions.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | Frame,
3 | Instruction,
4 | Operation,
5 | Provide,
6 | Reject,
7 | Resolve,
8 | Result,
9 | Task,
10 | } from "./types.ts";
11 |
12 | import { reset, shift } from "./deps.ts";
13 | import { shiftSync } from "./shift-sync.ts";
14 | import { Err, Ok } from "./result.ts";
15 |
16 | /**
17 | * Indefinitely pause execution of the current operation. It is typically
18 | * used in conjunction with an {@link action} to mark the boundary
19 | * between setup and teardown.
20 | *
21 | * ```js
22 | * function onEvent(listener, name) {
23 | * return action(function* (resolve) {
24 | * try {
25 | * listener.addEventListener(name, resolve);
26 | * yield* suspend();
27 | * } finally {
28 | * listener.removeEventListener(name, resolve);
29 | * }
30 | * });
31 | * }
32 | * ```
33 | *
34 | * An operation will remain suspended until its enclosing scope is destroyed,
35 | * at which point it proceeds as though return had been called from the point
36 | * of suspension. Once an operation suspends once, further suspend operations
37 | * are ignored.
38 | *
39 | * @returns an operation that suspends the current operation
40 | */
41 | export function suspend(): Operation {
42 | return instruction(Suspend);
43 | }
44 |
45 | function Suspend(frame: Frame) {
46 | return shiftSync>((k) => {
47 | if (frame.aborted) {
48 | k.tail(Ok(void 0));
49 | }
50 | });
51 | }
52 |
53 | /**
54 | * Create an {@link Operation} that can be either resolved (or rejected) with
55 | * a synchronous callback. This is the Effection equivalent of `new Promise()`.
56 | *
57 | * The action body is a function that enters the effect, and returns a function that
58 | * will be called to exit the action..
59 | *
60 | * For example:
61 | *
62 | * ```js
63 | * let five = yield* action((resolve, reject) => {
64 | * let timeout = setTimeout(() => {
65 | * if (Math.random() > 5) {
66 | * resolve(5)
67 | * } else {
68 | * reject(new Error("bad luck!"));
69 | * }
70 | * }, 1000);
71 | * return () => clearTimeout(timeout);
72 | * });
73 | * ```
74 | *
75 | * @typeParam T - type of the action's result.
76 | * @param body - enter and exit the action
77 | * @returns an operation producing the resolved value, or throwing the rejected error
78 | */
79 | export function action(
80 | enter: (resolve: Resolve, reject: Reject) => () => void,
81 | ): Operation;
82 |
83 | /**
84 | * @deprecated `action()` used with an operation will be removed in v4.
85 | */
86 | export function action(
87 | operation: (resolve: Resolve, reject: Reject) => Operation,
88 | ): Operation;
89 | export function action(
90 | operation:
91 | | ((resolve: Resolve, reject: Reject) => Operation)
92 | | ((resolve: Resolve, reject: Reject) => () => void),
93 | ): Operation {
94 | return instruction(function Action(frame) {
95 | return shift>(function* (k) {
96 | let settle = yield* reset>>(function* () {
97 | let result = yield* shiftSync>((k) => k.tail);
98 |
99 | let destruction = yield* child.destroy();
100 |
101 | if (!destruction.ok) {
102 | k.tail(destruction);
103 | } else {
104 | k.tail(result);
105 | }
106 | });
107 |
108 | let resolve: Resolve = (value) => settle(Ok(value));
109 | let reject: Reject = (error) => settle(Err(error));
110 |
111 | let child = frame.createChild(function* () {
112 | let iterable = operation(resolve, reject);
113 | if (typeof iterable === "function") {
114 | try {
115 | yield* suspend();
116 | } finally {
117 | iterable();
118 | }
119 | } else {
120 | yield* iterable;
121 | yield* suspend();
122 | }
123 | });
124 |
125 | yield* reset(function* () {
126 | let result = yield* child;
127 | if (!result.ok) {
128 | k.tail(result);
129 | }
130 | });
131 |
132 | child.enter();
133 | });
134 | });
135 | }
136 |
137 | /**
138 | * Run another operation concurrently as a child of the current one.
139 | *
140 | * The spawned operation will begin executing immediately and control will
141 | * return to the caller when it reaches its first suspend point.
142 | *
143 | * ### Example
144 | *
145 | * ```typescript
146 | * import { main, sleep, suspend, spawn } from 'effection';
147 | *
148 | * await main(function*() {
149 | * yield* spawn(function*() {
150 | * yield* sleep(1000);
151 | * console.log("hello");
152 | * });
153 | * yield* spawn(function*() {
154 | * yield* sleep(2000);
155 | * console.log("world");
156 | * });
157 | * yield* suspend();
158 | * });
159 | * ```
160 | *
161 | * You should prefer using the spawn operation over calling
162 | * {@link Scope.run} from within Effection code. The reason being that a
163 | * synchronous failure in the spawned operation will not be caught
164 | * until the next yield point when using `run`, which results in lines
165 | * being executed that should not.
166 | *
167 | * ### Example
168 | *
169 | * ```typescript
170 | * import { main, suspend, spawn, useScope } from 'effection';
171 | *
172 | * await main(function*() {
173 | * yield* useScope();
174 | *
175 | * scope.run(function*() {
176 | * throw new Error('boom!');
177 | * });
178 | *
179 | * console.log('this code will run and probably should not');
180 | *
181 | * yield* suspend(); // <- error is thrown after this.
182 | * });
183 | * ```
184 | * @param operation the operation to run as a child of the current task
185 | * @typeParam T the type that the spawned task evaluates to
186 | * @returns a {@link Task} representing a handle to the running operation
187 | */
188 | export function spawn(operation: () => Operation): Operation> {
189 | return instruction(function Spawn(frame) {
190 | return shift>>(function (k) {
191 | let child = frame.createChild(operation);
192 |
193 | child.enter();
194 |
195 | k.tail(Ok(child.getTask()));
196 |
197 | return reset(function* () {
198 | let result = yield* child;
199 | if (!result.ok) {
200 | yield* frame.crash(result.error);
201 | }
202 | });
203 | });
204 | });
205 | }
206 |
207 | /**
208 | * Define an Effection [resource](https://frontside.com/effection/docs/resources)
209 | *
210 | * Resources are a type of operation that passes a value back to its caller
211 | * while still allowing that operation to run in the background. It does this
212 | * by invoking the special `provide()` operation. The caller pauses until the
213 | * resource operation invokes `provide()` at which point the caller resumes with
214 | * passed value.
215 | *
216 | * `provide()` suspends the resource operation until the caller passes out of
217 | * scope.
218 | *
219 | * @example
220 | * ```javascript
221 | * function useWebSocket(url) {
222 | * return resource(function*(provide) {
223 | * let socket = new WebSocket(url);
224 | * yield* once(socket, 'open');
225 | *
226 | * try {
227 | * yield* provide(socket);
228 | * } finally {
229 | * socket.close();
230 | * yield* once(socket, 'close');
231 | * }
232 | * })
233 | * }
234 | *
235 | * await main(function*() {
236 | * let socket = yield* useWebSocket("wss://example.com");
237 | * socket.send("hello world");
238 | * });
239 | * ```
240 | *
241 | * @param operation the operation defining the lifecycle of the resource
242 | * @returns an operation yielding the resource
243 | */
244 | export function resource(
245 | operation: (provide: Provide) => Operation,
246 | ): Operation {
247 | return instruction((frame) =>
248 | shift>(function (k) {
249 | function provide(value: T) {
250 | k.tail(Ok(value));
251 | return suspend();
252 | }
253 |
254 | let child = frame.createChild(() => operation(provide));
255 |
256 | child.enter();
257 |
258 | return reset(function* () {
259 | let result = yield* child;
260 | if (!result.ok) {
261 | k.tail(result);
262 | yield* frame.crash(result.error);
263 | }
264 | });
265 | })
266 | );
267 | }
268 |
269 | /**
270 | * @ignore
271 | */
272 | export function getframe(): Operation {
273 | return instruction((frame) =>
274 | shiftSync>((k) => k.tail(Ok(frame)))
275 | );
276 | }
277 |
278 | // An optimized iterator that yields the instruction on the first call
279 | // to next, then returns its value on the second. Equivalent to:
280 | // {
281 | // *[Symbol.iterator]() { return yield instruction; }
282 | // }
283 | function instruction(i: Instruction): Operation {
284 | return {
285 | [Symbol.iterator]() {
286 | let entered = false;
287 | return {
288 | next(value) {
289 | if (!entered) {
290 | entered = true;
291 | return { done: false, value: i };
292 | } else {
293 | return { done: true, value };
294 | }
295 | },
296 | throw(error) {
297 | throw error;
298 | },
299 | };
300 | },
301 | };
302 | }
303 |
--------------------------------------------------------------------------------
/lib/lazy.ts:
--------------------------------------------------------------------------------
1 | export function lazy(create: () => T): () => T {
2 | let thunk = () => {
3 | let value = create();
4 | thunk = () => value;
5 | return value;
6 | };
7 | return () => thunk();
8 | }
9 |
--------------------------------------------------------------------------------
/lib/lift.ts:
--------------------------------------------------------------------------------
1 | import { shift } from "./deps.ts";
2 | import type { Operation } from "./types.ts";
3 |
4 | /**
5 | * Convert a simple function into an {@link Operation}
6 | *
7 | * @example
8 | * ```javascript
9 | * let log = lift((message) => console.log(message));
10 | *
11 | * export function* run() {
12 | * yield* log("hello world");
13 | * yield* log("done");
14 | * }
15 | * ```
16 | *
17 | * @returns a function returning an operation that invokes `fn` when evaluated
18 | */
19 | export function lift(
20 | fn: (...args: TArgs) => TReturn,
21 | ): (...args: TArgs) => Operation {
22 | return (...args: TArgs) => {
23 | return ({
24 | *[Symbol.iterator]() {
25 | return yield () => {
26 | return shift(function* (k) {
27 | k.tail({ ok: true, value: fn(...args) });
28 | });
29 | };
30 | },
31 | });
32 | };
33 | }
34 |
--------------------------------------------------------------------------------
/lib/main.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from "./context.ts";
2 | import type { Operation } from "./types.ts";
3 | import { action } from "./instructions.ts";
4 | import { run } from "./run.ts";
5 | import { call } from "./call.ts";
6 |
7 | /**
8 | * Halt process execution immediately and initiate shutdown. If a message is
9 | * provided, it will be logged to the console after shutdown:
10 | *
11 | * ```js
12 | * if (invalidArgs()) {
13 | * yield* exit(5, "invalid arguments")
14 | * }
15 | * ```
16 | * @param status - the exit code to use for the process exit
17 | * @param message - message to print to the console before exiting.
18 | * @param returns an operation that exits the program
19 | */
20 | export function* exit(status: number, message?: string): Operation {
21 | let escape = yield* ExitContext.expect();
22 | escape({ status, message });
23 | }
24 |
25 | /**
26 | * Top-level entry point to programs written in Effection. That means that your
27 | * program should only call `main` once, and everything the program does is
28 | * handled from within `main` including an orderly shutdown. Unlike `run`, `main`
29 | * automatically prints errors that occurred to the console.
30 | *
31 | * Use the {@link exit} operation form within to halt program execution
32 | * immediately and initiate shutdown.
33 | *
34 | * The behavior of `main` is slightly different depending on the environment it
35 | * is running in.
36 | *
37 | * ### Deno, Node
38 | *
39 | * When running within Deno or Node, any error which reaches `main` causes the
40 | * entire process to exit with an exit code of `1`.
41 | *
42 | * Additionally, handlers for `SIGINT` are attached to the
43 | * process, so that sending an exit signal to it causes the main task
44 | * to become halted. This means that hitting CTRL-C on an Effection program
45 | * using `main` will cause an orderly shutdown and run all cleanup code.
46 | *
47 | * > Warning! do not call `Deno.exit()` on Deno or `process.exit()` on Node
48 | * > directly, as this will not gracefully shutdown. Instead, use the
49 | * > {@link exit} operation.
50 | *
51 | * ### Browser
52 | *
53 | * When running in a browser, The `main` operation gets shut down on the
54 | * `unload` event.
55 | *
56 | * @param body - an operation to run as the body of the program
57 | * @returns a promise that resolves right after the program exits
58 | */
59 |
60 | export async function main(
61 | body: (args: string[]) => Operation,
62 | ): Promise {
63 | let hardexit = (_status: number) => {};
64 |
65 | let result = await run(() =>
66 | action(function* (resolve) {
67 | // action will return shutdown immediately upon resolve, so stash
68 | // this function in the exit context so it can be called anywhere.
69 | yield* ExitContext.set(resolve);
70 |
71 | // this will hold the event loop and prevent runtimes such as
72 | // Node and Deno from exiting prematurely.
73 | let interval = setInterval(() => {}, Math.pow(2, 30));
74 |
75 | try {
76 | let interrupt = {
77 | SIGINT: () => resolve({ status: 130, signal: "SIGINT" }),
78 | SIGTERM: () => resolve({ status: 143, signal: "SIGTERM" }),
79 | };
80 |
81 | yield* withHost({
82 | *deno() {
83 | hardexit = (status) => Deno.exit(status);
84 | try {
85 | Deno.addSignalListener("SIGINT", interrupt.SIGINT);
86 | Deno.addSignalListener("SIGTERM", interrupt.SIGTERM);
87 | yield* body(Deno.args.slice());
88 | } finally {
89 | Deno.removeSignalListener("SIGINT", interrupt.SIGINT);
90 | Deno.removeSignalListener("SIGTERM", interrupt.SIGTERM);
91 | }
92 | },
93 | *node() {
94 | let { default: process } = yield* call(() =>
95 | import("node:process")
96 | );
97 | hardexit = (status) => process.exit(status);
98 | try {
99 | process.on("SIGINT", interrupt.SIGINT);
100 | process.on("SIGTERM", interrupt.SIGTERM);
101 | yield* body(process.argv.slice(2));
102 | } finally {
103 | process.off("SIGINT", interrupt.SIGINT);
104 | process.off("SIGTERM", interrupt.SIGINT);
105 | }
106 | },
107 | *browser() {
108 | try {
109 | self.addEventListener("unload", interrupt.SIGINT);
110 | yield* body([]);
111 | } finally {
112 | self.removeEventListener("unload", interrupt.SIGINT);
113 | }
114 | },
115 | });
116 |
117 | yield* exit(0);
118 | } catch (error) {
119 | resolve({ status: 1, error: error as Error });
120 | } finally {
121 | clearInterval(interval);
122 | }
123 | })
124 | );
125 |
126 | if (result.message) {
127 | if (result.status === 0) {
128 | console.log(result.message);
129 | } else {
130 | console.error(result.message);
131 | }
132 | }
133 |
134 | if (result.error) {
135 | console.error(result.error);
136 | }
137 |
138 | hardexit(result.status);
139 | }
140 |
141 | const ExitContext = createContext<(exit: Exit) => void>("exit");
142 |
143 | interface Exit {
144 | status: number;
145 | message?: string;
146 | signal?: string;
147 | error?: Error;
148 | }
149 |
150 | interface HostOperation {
151 | deno(): Operation;
152 | node(): Operation;
153 | browser(): Operation;
154 | }
155 |
156 | function* withHost(op: HostOperation): Operation {
157 | let global = globalThis as Record;
158 |
159 | if (typeof global.Deno !== "undefined") {
160 | return yield* op.deno();
161 | // this snippet is from the detect-node npm package
162 | // @see https://github.com/iliakan/detect-node/blob/master/index.js
163 | } else if (
164 | Object.prototype.toString.call(
165 | typeof globalThis.process !== "undefined" ? globalThis.process : 0,
166 | ) === "[object process]"
167 | ) {
168 | return yield* op.node();
169 | } else {
170 | return yield* op.browser();
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/lib/mod.ts:
--------------------------------------------------------------------------------
1 | export * from "./types.ts";
2 | export * from "./channel.ts";
3 | export * from "./context.ts";
4 | export * from "./instructions.ts";
5 | export * from "./call.ts";
6 | export * from "./run.ts";
7 | export * from "./sleep.ts";
8 | export * from "./async.ts";
9 | export * from "./abort-signal.ts";
10 | export * from "./result.ts";
11 | export * from "./lift.ts";
12 | export * from "./events.ts";
13 | export * from "./main.ts";
14 | export * from "./all.ts";
15 | export * from "./each.ts";
16 | export * from "./queue.ts";
17 | export * from "./signal.ts";
18 | export * from "./ensure.ts";
19 | export * from "./race.ts";
20 | export * from "./with-resolvers.ts";
21 | export * from "./scoped.ts";
22 |
--------------------------------------------------------------------------------
/lib/pause.ts:
--------------------------------------------------------------------------------
1 | import type { Operation, Reject, Resolve, Result } from "./types.ts";
2 | import { Err, Ok } from "./result.ts";
3 | import { shift } from "./deps.ts";
4 |
5 | export function* pause(
6 | install: (resolve: Resolve, reject: Reject) => Resolve,
7 | ): Operation {
8 | let uninstall = () => {};
9 | try {
10 | return yield function pause_i() {
11 | return shift>(function* (k) {
12 | let resolve = (value: T) => k.tail(Ok(value));
13 | let reject = (error: Error) => k.tail(Err(error));
14 | uninstall = install(resolve, reject);
15 | });
16 | };
17 | } finally {
18 | if (uninstall) {
19 | uninstall();
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/lib/queue.ts:
--------------------------------------------------------------------------------
1 | import type { Resolve, Subscription } from "./types.ts";
2 | import { pause } from "./pause.ts";
3 |
4 | /**
5 | * A FIFO queue which can be used to implement the {@link Subscription}
6 | * interface directly. Most of the time, you will use either a {@link Signal}
7 | * or a {@link Channel} as the mechanism, but `Queue` allows you to manage
8 | * a single subscription directly.
9 | *
10 | * @typeParam T the type of the items in the queue
11 | * @typeParam TClose the type of the value that the queue is closed with
12 | */
13 | export interface Queue extends Subscription {
14 | /**
15 | * Add a value to the queue. The oldest value currently in the queue will
16 | * be the first to be read.
17 | * @param item - the value to add
18 | */
19 | add(item: T): void;
20 |
21 | /**
22 | * Close the queue.
23 | * @param value - the queue's final value.
24 | */
25 | close(value: TClose): void;
26 | }
27 |
28 | /**
29 | * Creates a new queue. Queues are unlimited in size and sending a message to a
30 | * queue is always synchronous.
31 | *
32 | * @example
33 | *
34 | * ```javascript
35 | * import { each, main, createQueue } from 'effection';
36 | *
37 | * await main(function*() {
38 | * let queue = createQueue();
39 | * queue.send(1);
40 | * queue.send(2);
41 | * queue.send(3);
42 | *
43 | * let next = yield* queue.subscription.next();
44 | * while (!next.done) {
45 | * console.log("got number", next.value);
46 | * next = yield* queue.subscription.next();
47 | * }
48 | * });
49 | * ```
50 | *
51 | * @typeParam T the type of the items in the queue
52 | * @typeParam TClose the type of the value that the queue is closed with
53 | */
54 | export function createQueue(): Queue {
55 | type Item = IteratorResult;
56 |
57 | let items: Item[] = [];
58 | let consumers = new Set>();
59 |
60 | function enqueue(item: Item) {
61 | items.unshift(item);
62 | while (items.length > 0 && consumers.size > 0) {
63 | let [consume] = consumers;
64 | let top = items.pop() as Item;
65 | consume(top);
66 | }
67 | }
68 |
69 | return {
70 | add: (value) => enqueue({ done: false, value }),
71 | close: (value) => enqueue({ done: true, value }),
72 | *next() {
73 | let item = items.pop();
74 | if (item) {
75 | return item;
76 | } else {
77 | return yield* pause- ((resolve) => {
78 | consumers.add(resolve);
79 | return () => consumers.delete(resolve);
80 | });
81 | }
82 | },
83 | };
84 | }
85 |
--------------------------------------------------------------------------------
/lib/race.ts:
--------------------------------------------------------------------------------
1 | import type { Operation, Yielded } from "./types.ts";
2 | import { action, spawn } from "./instructions.ts";
3 |
4 | /**
5 | * Race the given operations against each other and return the value of
6 | * whichever operation returns first. This has the same purpose as
7 | * [Promise.race](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race).
8 | *
9 | * If an operation become errored first, then `race` will fail with this error.
10 | * After the first operation wins the race, all other operations will become
11 | * halted and therefore cannot throw any further errors.
12 | *
13 | * @example
14 | *
15 | * ```typescript
16 | * import { main, race, fetch } from 'effection';
17 | *
18 | * await main(function*() {
19 | * let fastest = yield* race([fetch('http://google.com'), fetch('http://bing.com')]);
20 | * // ...
21 | * });
22 | * ```
23 | *
24 | * @param operations a list of operations to race against each other
25 | * @returns the value of the fastest operation
26 | */
27 | export function race>(
28 | operations: readonly T[],
29 | ): Operation> {
30 | return action>(function* (resolve, reject) {
31 | for (let operation of operations) {
32 | yield* spawn(function* () {
33 | try {
34 | resolve((yield* operation) as Yielded);
35 | } catch (error) {
36 | reject(error as Error);
37 | }
38 | });
39 | }
40 | });
41 | }
42 |
--------------------------------------------------------------------------------
/lib/result.ts:
--------------------------------------------------------------------------------
1 | import type { Result } from "./types.ts";
2 |
3 | /**
4 | * @ignore
5 | */
6 | export const Ok = (value: T): Result => ({ ok: true, value });
7 |
8 | /**
9 | * @ignore
10 | */
11 | export const Err = (error: Error): Result => ({ ok: false, error });
12 |
--------------------------------------------------------------------------------
/lib/run.ts:
--------------------------------------------------------------------------------
1 | import type { Operation, Task } from "./types.ts";
2 | import { createFrame } from "./run/frame.ts";
3 | export * from "./run/scope.ts";
4 |
5 | /**
6 | * Execute an operation.
7 | *
8 | * Run is an entry point into Effection, and is especially useful when
9 | * embedding Effection code into existing code. However, If you are writing your
10 | * whole program using Effection, you should prefer {@link main}.
11 | *
12 | * @example
13 | * ```javascript
14 | * import { run, useAbortSignal } from 'effection';
15 | *
16 | * async function fetchExample() {
17 | * await run(function*() {
18 | * let signal = yield* useAbortSignal();
19 | * let response = yield* call(() => fetch('http://www.example.com', { signal }));
20 | * yield* call(() => response.text());
21 | * });
22 | * });
23 | * ```
24 | *
25 | * Run will create a new top-level scope for the operation. However, to run an
26 | * operation in an existing scope, you can use {@link Scope.run}.
27 | *
28 | * @param operation the operation to run
29 | * @returns a task representing the running operation.
30 | */
31 | export function run(operation: () => Operation): Task {
32 | let frame = createFrame({ operation });
33 | frame.enter();
34 | return frame.getTask();
35 | }
36 |
--------------------------------------------------------------------------------
/lib/run/create.ts:
--------------------------------------------------------------------------------
1 | export function create(
2 | tag: string,
3 | attrs: Partial,
4 | prototype: Partial,
5 | ): T {
6 | let properties: Record = {};
7 | for (let [key, value] of Object.entries(attrs)) {
8 | properties[key] = { enumerable: true, value };
9 | }
10 | return Object.create({
11 | ...prototype,
12 | [Symbol.toStringTag]: tag,
13 | }, properties);
14 | }
15 |
--------------------------------------------------------------------------------
/lib/run/frame.ts:
--------------------------------------------------------------------------------
1 | import type { Frame, Instruction, Operation, Result } from "../types.ts";
2 |
3 | import { evaluate, shift } from "../deps.ts";
4 | import { shiftSync } from "../shift-sync.ts";
5 | import { lazy } from "../lazy.ts";
6 | import { Err, Ok } from "../result.ts";
7 |
8 | import type { Exit, FrameResult } from "./types.ts";
9 | import { createValue } from "./value.ts";
10 | import { createTask } from "./task.ts";
11 | import { create } from "./create.ts";
12 |
13 | let ids = 0;
14 |
15 | export interface FrameOptions {
16 | operation(): Operation;
17 | parent?: Frame["context"];
18 | }
19 |
20 | export function createFrame(options: FrameOptions): Frame {
21 | return evaluate>(function* () {
22 | let { operation, parent } = options;
23 | let children = new Set();
24 | let context = Object.create(parent ?? {});
25 | let thunks: IteratorResult>[] = [{
26 | done: false,
27 | value: $next(void 0),
28 | }];
29 |
30 | let crash: Error | undefined = void 0;
31 |
32 | let interrupt = () => {};
33 |
34 | let [setResults, results] = yield* createValue>();
35 |
36 | let frame = yield* shiftSync>((k) => {
37 | let self: Frame = create>("Frame", { id: ids++, context }, {
38 | createChild(operation: () => Operation) {
39 | let child = createFrame({ operation, parent: self.context });
40 | children.add(child);
41 | evaluate(function* () {
42 | yield* child;
43 | children.delete(child);
44 | });
45 | return child;
46 | },
47 | getTask() {
48 | let task = createTask(self);
49 | self.getTask = () => task;
50 | return task;
51 | },
52 | enter() {
53 | k.tail(self);
54 | },
55 | crash(error: Error) {
56 | abort(error);
57 | return results;
58 | },
59 | destroy() {
60 | abort();
61 | return results;
62 | },
63 | [Symbol.iterator]: results[Symbol.iterator],
64 | });
65 |
66 | let abort = (reason?: Error) => {
67 | if (!self.aborted) {
68 | self.aborted = true;
69 | crash = reason;
70 | thunks.unshift({ done: false, value: $abort() });
71 | interrupt();
72 | }
73 | };
74 |
75 | return self;
76 | });
77 |
78 | let iterator = lazy(() => operation()[Symbol.iterator]());
79 |
80 | let thunk = thunks.pop()!;
81 |
82 | while (!thunk.done) {
83 | let getNext = thunk.value;
84 | try {
85 | let next: IteratorResult = getNext(iterator());
86 |
87 | if (next.done) {
88 | thunks.unshift({ done: true, value: Ok(next.value) });
89 | } else {
90 | let instruction = next.value;
91 |
92 | let outcome = yield* shift(function* (k) {
93 | interrupt = () => k.tail({ type: "interrupted" });
94 |
95 | try {
96 | k.tail({
97 | type: "settled",
98 | result: yield* instruction(frame),
99 | });
100 | } catch (error) {
101 | k.tail({ type: "settled", result: Err(error as Error) });
102 | }
103 | });
104 |
105 | if (outcome.type === "settled") {
106 | if (outcome.result.ok) {
107 | thunks.unshift({
108 | done: false,
109 | value: $next(outcome.result.value),
110 | });
111 | } else {
112 | thunks.unshift({
113 | done: false,
114 | value: $throw(outcome.result.error),
115 | });
116 | }
117 | }
118 | }
119 | } catch (error) {
120 | thunks.unshift({ done: true, value: Err(error as Error) });
121 | }
122 | thunk = thunks.pop()!;
123 | }
124 |
125 | frame.exited = true;
126 |
127 | let result = thunk.value;
128 |
129 | let exit: Exit;
130 |
131 | if (!result.ok) {
132 | exit = { type: "result", result };
133 | } else if (crash) {
134 | exit = { type: "crashed", error: crash };
135 | } else if (frame.aborted) {
136 | exit = { type: "aborted" };
137 | } else {
138 | exit = { type: "result", result };
139 | }
140 |
141 | let destruction = Ok(void 0);
142 |
143 | while (children.size !== 0) {
144 | for (let child of [...children].reverse()) {
145 | let teardown = yield* child.destroy();
146 | if (!teardown.ok) {
147 | destruction = teardown;
148 | }
149 | }
150 | }
151 |
152 | if (!destruction.ok) {
153 | setResults({ ok: false, error: destruction.error, exit, destruction });
154 | } else {
155 | if (exit.type === "aborted") {
156 | setResults({ ok: true, value: void 0, exit, destruction });
157 | } else if (exit.type === "result") {
158 | let { result } = exit;
159 | if (result.ok) {
160 | setResults({ ok: true, value: void 0, exit, destruction });
161 | } else {
162 | setResults({ ok: false, error: result.error, exit, destruction });
163 | }
164 | } else {
165 | setResults({ ok: false, error: exit.error, exit, destruction });
166 | }
167 | }
168 | });
169 | }
170 |
171 | type Thunk = ReturnType;
172 |
173 | // deno-lint-ignore no-explicit-any
174 | const $next = (value: any) =>
175 | function $next(i: Iterator) {
176 | return i.next(value);
177 | };
178 |
179 | const $throw = (error: Error) =>
180 | function $throw(i: Iterator) {
181 | if (i.throw) {
182 | return i.throw(error);
183 | } else {
184 | throw error;
185 | }
186 | };
187 |
188 | const $abort = (value?: unknown) =>
189 | function $abort(i: Iterator) {
190 | if (i.return) {
191 | return i.return(value as unknown as T);
192 | } else {
193 | return { done: true, value } as IteratorResult;
194 | }
195 | };
196 |
197 | type InstructionResult =
198 | | {
199 | type: "settled";
200 | result: Result;
201 | }
202 | | {
203 | type: "interrupted";
204 | };
205 |
--------------------------------------------------------------------------------
/lib/run/scope.ts:
--------------------------------------------------------------------------------
1 | import type { Context, Frame, Future, Operation, Scope } from "../types.ts";
2 | import { evaluate } from "../deps.ts";
3 | import { create } from "./create.ts";
4 | import { createFrame } from "./frame.ts";
5 | import { getframe, suspend } from "../instructions.ts";
6 |
7 | /**
8 | * Get the scope of the currently running {@link Operation}.
9 | *
10 | * @returns an operation yielding the current scope
11 | */
12 | export function* useScope(): Operation {
13 | let frame = yield* getframe();
14 | let [scope] = createScope(frame);
15 | return scope;
16 | }
17 |
18 | /**
19 | * Create a new {@link Scope} as a child of `parent`, inheriting all its contexts.
20 | * along with a method to destroy the scope. Whenever the scope is destroyd, all
21 | * tasks and resources it contains will be halted.
22 | *
23 | * This function is used mostly by frameworks as an intergration point to enter
24 | * Effection.
25 | *
26 | * @example
27 | * ```js
28 | * import { createScope, sleep, suspend } from "effection";
29 | *
30 | * let [scope, destroy] = createScope();
31 | *
32 | * let delay = scope.run(function*() {
33 | * yield* sleep(1000);
34 | * });
35 | * scope.run(function*() {
36 | * try {
37 | * yield* suspend();
38 | * } finally {
39 | * console.log('done!');
40 | * }
41 | * });
42 | * await delay;
43 | * await destroy(); // prints "done!";
44 | * ```
45 | *
46 | * @param parent scope. If no parent is specified it will be free standing.
47 | * @returns a tuple containing the freshly created scope, along with a function to
48 | * destroy it.
49 | */
50 | export function createScope(parent?: Scope): [Scope, () => Future];
51 |
52 | /* @ignore */
53 | export function createScope(parent: Frame): [Scope, () => Future];
54 | /* @ignore */
55 | export function createScope(
56 | parent?: Frame | Scope,
57 | ): [Scope, () => Future] {
58 | let frame = isScopeInternal(parent)
59 | ? parent.frame.createChild(suspend)
60 | : (parent as Frame) ?? createFrame({ operation: suspend });
61 |
62 | let scope = create("Scope", {}, {
63 | frame,
64 | run(operation: () => Operation) {
65 | if (frame.exited) {
66 | let error = new Error(
67 | `cannot call run() on a scope that has already been exited`,
68 | );
69 | error.name = "InactiveScopeError";
70 | throw error;
71 | }
72 |
73 | let child = frame.createChild(operation);
74 | child.enter();
75 |
76 | evaluate(function* () {
77 | let result = yield* child;
78 | if (!result.ok) {
79 | yield* frame.crash(result.error);
80 | }
81 | });
82 |
83 | return child.getTask();
84 | },
85 |
86 | get spawn() {
87 | let scope = this!;
88 | return function spawn(operation: () => Operation) {
89 | return {
90 | *[Symbol.iterator]() {
91 | return scope.run(operation);
92 | },
93 | };
94 | };
95 | },
96 |
97 | get(context: Context) {
98 | let { key, defaultValue } = context;
99 | return (frame.context[key] ?? defaultValue) as T | undefined;
100 | },
101 | set(context: Context, value: T) {
102 | let { key } = context;
103 | frame.context[key] = value;
104 | return value;
105 | },
106 | expect(context: Context): T {
107 | let value = scope.get(context);
108 | if (typeof value === "undefined") {
109 | let error = new Error(context.key);
110 | error.name = `MissingContextError`;
111 | throw error;
112 | }
113 | return value;
114 | },
115 | delete(context: Context): boolean {
116 | let { key } = context;
117 | return delete frame.context[key];
118 | },
119 | hasOwn(context: Context): boolean {
120 | return !!Reflect.getOwnPropertyDescriptor(frame.context, context.key);
121 | },
122 | });
123 |
124 | frame.enter();
125 |
126 | return [scope, frame.getTask().halt];
127 | }
128 |
129 | interface ScopeInternal extends Scope {
130 | frame: Frame;
131 | }
132 |
133 | function isScopeInternal(value?: Frame | Scope): value is ScopeInternal {
134 | return !!value && typeof (value as ScopeInternal).run === "function";
135 | }
136 |
--------------------------------------------------------------------------------
/lib/run/task.ts:
--------------------------------------------------------------------------------
1 | import type { Frame, Future, Reject, Resolve, Result, Task } from "../types.ts";
2 |
3 | import { evaluate } from "../deps.ts";
4 | import { Err } from "../result.ts";
5 | import { action } from "../instructions.ts";
6 |
7 | import type { FrameResult } from "./types.ts";
8 | import { create } from "./create.ts";
9 |
10 | export function createTask(
11 | frame: Frame,
12 | ): Task {
13 | let promise: Promise;
14 |
15 | let awaitResult = (resolve: Resolve, reject: Reject) => {
16 | evaluate(function* () {
17 | let result = getResult(yield* frame);
18 |
19 | if (result.ok) {
20 | resolve(result.value);
21 | } else {
22 | reject(result.error);
23 | }
24 | });
25 | };
26 |
27 | let getPromise = () => {
28 | promise = new Promise((resolve, reject) => {
29 | awaitResult(resolve, reject);
30 | });
31 | getPromise = () => promise;
32 | return promise;
33 | };
34 |
35 | let task = create>("Task", {}, {
36 | *[Symbol.iterator]() {
37 | let frameResult = evaluate | void>(() => frame);
38 | if (frameResult) {
39 | let result = getResult(frameResult);
40 | if (result.ok) {
41 | return result.value;
42 | } else {
43 | throw result.error;
44 | }
45 | } else {
46 | return yield* action(function* (resolve, reject) {
47 | awaitResult(resolve, reject);
48 | });
49 | }
50 | },
51 | then: (...args) => getPromise().then(...args),
52 | catch: (...args) => getPromise().catch(...args),
53 | finally: (...args) => getPromise().finally(...args),
54 | halt() {
55 | let haltPromise: Promise;
56 | let getHaltPromise = () => {
57 | haltPromise = new Promise((resolve, reject) => {
58 | awaitHaltResult(resolve, reject);
59 | });
60 | getHaltPromise = () => haltPromise;
61 | frame.destroy();
62 | return haltPromise;
63 | };
64 | let awaitHaltResult = (resolve: Resolve, reject: Reject) => {
65 | evaluate(function* () {
66 | let { destruction } = yield* frame;
67 | if (destruction.ok) {
68 | resolve();
69 | } else {
70 | reject(destruction.error);
71 | }
72 | });
73 | };
74 | return create>("Future", {}, {
75 | *[Symbol.iterator]() {
76 | let result = evaluate | void>(() => frame);
77 |
78 | if (result) {
79 | if (!result.ok) {
80 | throw result.error;
81 | }
82 | } else {
83 | yield* action(function* (resolve, reject) {
84 | awaitHaltResult(resolve, reject);
85 | frame.destroy();
86 | });
87 | }
88 | },
89 | then: (...args) => getHaltPromise().then(...args),
90 | catch: (...args) => getHaltPromise().catch(...args),
91 | finally: (...args) => getHaltPromise().finally(...args),
92 | });
93 | },
94 | });
95 | return task;
96 | }
97 |
98 | function getResult(result: FrameResult