nanoeffects 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{action-creator-CcAEvUnw.d.ts → action-creator-jeEqIKgK.d.ts} +34 -32
- package/dist/actions.d.ts +18 -18
- package/dist/actions.js +1 -1
- package/dist/bus-0uYn_kRj.js +1 -0
- package/dist/context-DtUnzC-1.js +1 -0
- package/dist/context-types-Bme1mzkn.d.ts +31 -0
- package/dist/context.d.ts +8 -3
- package/dist/context.js +1 -1
- package/dist/{delay-18LJ8jSk.js → delay-5OhJZZQ0.js} +1 -1
- package/dist/dev.d.ts +2 -2
- package/dist/effects.d.ts +2 -2
- package/dist/effects.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/match.d.ts +3 -3
- package/dist/match.js +1 -1
- package/dist/{module-C_oEXw1n.d.ts → module-BWMW83qk.d.ts} +38 -38
- package/dist/notrace/{action-creator-DxOvVo8I.d.ts → action-creator-xpQ_I2h8.d.ts} +34 -32
- package/dist/notrace/actions.d.ts +52 -52
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/{bus-DmKn4SDD.js → bus-37nlVljc.js} +1 -1
- package/dist/notrace/effects.d.ts +2 -2
- package/dist/notrace/index.d.ts +2 -2
- package/dist/notrace/index.js +1 -1
- package/dist/notrace/{plugin-lJjjd-SN.d.ts → plugin-6LhNx0th.d.ts} +1 -1
- package/dist/notrace/policies.d.ts +1 -1
- package/dist/notrace/redux.d.ts +1 -1
- package/dist/observable.d.ts +43 -1
- package/dist/observable.js +1 -1
- package/dist/onAbort-BgMfspK4.js +1 -0
- package/dist/oop.d.ts +3 -3
- package/dist/oop.js +1 -1
- package/dist/{plugin-CuvIgjnX.d.ts → plugin-BaErevLy.d.ts} +2 -2
- package/dist/policies.d.ts +2 -2
- package/dist/policies.js +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/redux.d.ts +1 -1
- package/dist/scope.d.ts +9 -9
- package/dist/signal-field-key-CWT-3OXC.js +1 -0
- package/dist/signal.d.ts +34 -6
- package/dist/signal.js +1 -1
- package/dist/svelte.d.ts +1 -1
- package/dist/trace.d.ts +20 -5
- package/dist/trace.js +1 -1
- package/dist/{types-FV8fuvI9.d.ts → types-D5xsR37j.d.ts} +2 -2
- package/dist/vue.d.ts +1 -1
- package/dist/{with-policy-3bhFLEXs.d.ts → with-policy-BZVRBUOm.d.ts} +1 -1
- package/dist/{with-policy-D-SQRkPh.js → with-policy-C6BIiPIi.js} +1 -1
- package/docs/README.md +13 -1
- package/docs/actions.md +27 -1
- package/docs/chat-example.md +12 -0
- package/docs/chat-widget.md +7 -7
- package/docs/context.md +33 -1
- package/docs/effects.md +20 -9
- package/docs/guide.md +20 -2
- package/docs/migrating-from-effector.md +11 -0
- package/docs/observable.md +43 -0
- package/docs/oop.md +17 -0
- package/docs/package.md +22 -13
- package/docs/plugins.md +69 -11
- package/docs/query-libraries.md +273 -5
- package/docs/react.md +12 -2
- package/docs/scope.md +14 -2
- package/docs/semantics.md +10 -0
- package/docs/server.md +3 -3
- package/docs/stability.md +2 -2
- package/docs/tracing.md +74 -6
- package/docs/typescript.md +1 -1
- package/llms-full.txt +671 -66
- package/llms.txt +7 -7
- package/package.json +4 -4
- package/skills/nanoeffects/SKILL.md +2 -2
- package/dist/bus-DlpEluzd.js +0 -1
- package/dist/context-UcmrAIhP.js +0 -1
- package/dist/context-types-hCKgU8sC.d.ts +0 -14
package/docs/actions.md
CHANGED
|
@@ -10,6 +10,32 @@ A chat with two entities and three features, jobs under policies and effects, an
|
|
|
10
10
|
|
|
11
11
|
A whole project with two domains, a job and React as a thin layer: `npm create nanoeffects@latest my-app -- --template domains-ts` (see [Start a project from a template](starter.md#start-with-domains)).
|
|
12
12
|
|
|
13
|
+
## Pick the word: event, action, job, module
|
|
14
|
+
|
|
15
|
+
Four names, one idea each. Take the first row that fits.
|
|
16
|
+
|
|
17
|
+
| Word | What it is | Use it when |
|
|
18
|
+
| ---- | ---------- | ----------- |
|
|
19
|
+
| Event | A name and a payload; `createAction("x")` with no handler. | The UI or a handler tells that something happened. |
|
|
20
|
+
| Action with a handler | The same, plus work: `createAction("x", handler)`. `emit` returns the result. | The caller needs a value back. |
|
|
21
|
+
| Job | An action with a handler that also emits `pending`, `success`, `fail` and `cancel`. | The UI shows a loading state. |
|
|
22
|
+
| Module | A group of listeners, handlers and tasks, mounted and unmounted with one call. | One domain must start and stop as a unit. |
|
|
23
|
+
|
|
24
|
+
Start with events. Add a handler when you need a result, a job when you need a status, a module when you need a unit.
|
|
25
|
+
|
|
26
|
+
## On this page
|
|
27
|
+
|
|
28
|
+
- [Pick the word: event, action, job, module](#pick-the-word-event-action-job-module)
|
|
29
|
+
- [Return a value from `emit` with a handler](#return-a-value-from-emit-with-a-handler)
|
|
30
|
+
- [Describe events, handlers and jobs](#describe-events-handlers-and-jobs)
|
|
31
|
+
- [Actions](#actions)
|
|
32
|
+
- [Bind a handler later](#bind-a-handler-later)
|
|
33
|
+
- [Say what a bound handler returns](#say-what-a-bound-handler-returns)
|
|
34
|
+
- [Jobs](#jobs)
|
|
35
|
+
- [Show a loading state with a job](#show-a-loading-state-with-a-job)
|
|
36
|
+
- [Mount domains with modules](#mount-domains-with-modules)
|
|
37
|
+
- [Give an action or a module its own fields](#give-an-action-or-a-module-its-own-fields)
|
|
38
|
+
|
|
13
39
|
## Return a value from `emit` with a handler
|
|
14
40
|
|
|
15
41
|
Give `createAction` a handler, and `emit` runs it and returns its result. There is one creator for an event and for an action with work behind it; the handler is the only difference.
|
|
@@ -164,7 +190,7 @@ On a bus without an event map you can say it where you call: `const id = bus.emi
|
|
|
164
190
|
- `pending` / `success` / `fail` / `cancel` are a string (job makes an action creator) or an existing action creator. Missing ones become `<type>/pending`, `<type>/success`, `<type>/fail` or `<type>/cancel`.
|
|
165
191
|
- `bus.emit(loadUser(id))` returns a promise of the result. Handler errors are always handled: they go to `fail` and the promise resolves with `undefined`. For a result that must reject, give `createAction` the handler.
|
|
166
192
|
- Outcome actions are emitted as the task that ran the job (also for a handler bound with `actions.handle`). `success` and `fail` are not sent after abort; `cancel` is, at the moment of the abort.
|
|
167
|
-
- A job without a handler is not an outcome: `emit` throws `No handler for
|
|
193
|
+
- A job without a handler is not an outcome: `emit` throws `No handler for action "..."` and nothing is sent. An action without a handler is an event and does not throw.
|
|
168
194
|
- Calling the job only builds `{ type, payload }`; the handler runs on `emit`.
|
|
169
195
|
- The outcomes are events like any other: with `onUnhandled` set on the bus, it is called for a `pending` or a `cancel` that nobody listens to.
|
|
170
196
|
|
package/docs/chat-example.md
CHANGED
|
@@ -14,6 +14,18 @@ npm run dev
|
|
|
14
14
|
|
|
15
15
|
`npm run dev` starts the server and Vite together. Start it with `CHAOS=off npm run dev` to turn the failures off.
|
|
16
16
|
|
|
17
|
+
## On this page
|
|
18
|
+
|
|
19
|
+
- [Read how the server fails](#read-how-the-server-fails)
|
|
20
|
+
- [See how the files are split](#see-how-the-files-are-split)
|
|
21
|
+
- [Load a history that must not race](#load-a-history-that-must-not-race)
|
|
22
|
+
- [Send a message that can fail](#send-a-message-that-can-fail)
|
|
23
|
+
- [Search where one room is slow](#search-where-one-room-is-slow)
|
|
24
|
+
- [Wait for a room before marking a result](#wait-for-a-room-before-marking-a-result)
|
|
25
|
+
- [Poll for new messages](#poll-for-new-messages)
|
|
26
|
+
- [Know where each value lives](#know-where-each-value-lives)
|
|
27
|
+
- [Where to go next](#where-to-go-next)
|
|
28
|
+
|
|
17
29
|
## Read how the server fails
|
|
18
30
|
|
|
19
31
|
The server keeps its data in memory. These rules are on purpose, and they are the same every run, so you can follow each section below by doing what the table says:
|
package/docs/chat-widget.md
CHANGED
|
@@ -30,7 +30,7 @@ It is a level D example: the logic lives in one module, and the page only mounts
|
|
|
30
30
|
| `src/chat-handlers.ts` | `askBot` (`timeout` and `retry` inside) and `watchOperator` (`poll`): plain `async` functions that know nothing about the page. |
|
|
31
31
|
| `src/chat-module.ts` | The list of what is alive while the chat is open. |
|
|
32
32
|
| `src/panel.ts` | The view: shows the lines, sends the texts, returns one function that removes it. |
|
|
33
|
-
| `src/main.ts` | The launcher: the only file that knows both the module and the panel. |
|
|
33
|
+
| `src/main.ts` | The launcher: the only file that knows both the module and the panel; it listens for `closeRequested` only while the chat is open. |
|
|
34
34
|
|
|
35
35
|
## Bind everything in one module
|
|
36
36
|
|
|
@@ -80,7 +80,7 @@ Three details that make the cleanup complete:
|
|
|
80
80
|
|
|
81
81
|
## Open and close from the launcher
|
|
82
82
|
|
|
83
|
-
`app.mount` returns the function that unmounts the module. The launcher puts
|
|
83
|
+
`app.mount` returns the function that unmounts the module. `bus.on` returns the one that unsubscribes. The launcher puts them together with the one that removes the panel into a single `close`.
|
|
84
84
|
|
|
85
85
|
<!-- docs-check: skip - the other files of the example are in examples/chat-widget -->
|
|
86
86
|
```js
|
|
@@ -96,7 +96,10 @@ let close = null; // set while the chat is open
|
|
|
96
96
|
const open = () => {
|
|
97
97
|
const stopModule = app.mount(chatModule); // subscriptions, polling and Escape start
|
|
98
98
|
const stopPanel = renderPanel(document.body);
|
|
99
|
+
// the module emits closeRequested on Escape; nothing does while the chat is closed
|
|
100
|
+
const stopEscape = bus.on(closeRequested, () => close?.());
|
|
99
101
|
close = () => {
|
|
102
|
+
stopEscape();
|
|
100
103
|
stopPanel();
|
|
101
104
|
stopModule(); // everything the module started ends, a running request included
|
|
102
105
|
close = null;
|
|
@@ -110,18 +113,15 @@ button.addEventListener("click", () => {
|
|
|
110
113
|
open();
|
|
111
114
|
}
|
|
112
115
|
});
|
|
113
|
-
|
|
114
|
-
// the launcher stays subscribed: it must hear Escape to close the chat
|
|
115
|
-
bus.on(closeRequested, () => close?.());
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
While the chat is closed the launcher holds no subscription at all: it only has the click listener of the button, which is a DOM listener, not a listener of the bus.
|
|
119
119
|
|
|
120
120
|
## Check that closing cleans up
|
|
121
121
|
|
|
122
122
|
1. Open the chat and send a text. The bot answers after about a second. Every third question fails, and the retry asks again.
|
|
123
123
|
2. Send a text and close the chat before the answer arrives. No line appears, also when you open the chat again.
|
|
124
|
-
3. Look at the box "Listeners on the bus": it shows `bus.get(event).length` for four events and updates when the chat opens or closes. Closed:
|
|
124
|
+
3. Look at the box "Listeners on the bus": it shows `bus.get(event).length` for four events and updates when the chat opens or closes. Closed: every event shows `0`, and so does "all events". Open: each of the four has one listener, and "all events" is `4`. That is the proof that nothing is left subscribed.
|
|
125
125
|
4. Wait 10 s with the chat closed. The operator is not polled: no line from the operator appears.
|
|
126
126
|
5. Press `Escape` with the chat closed. Nothing happens. Open the chat and press it again: it closes.
|
|
127
127
|
6. Open the chat again. The old lines are back, and the polling runs.
|
package/docs/context.md
CHANGED
|
@@ -21,7 +21,7 @@ Library fields (`signal`, `emit`, `run`, …) win on name clashes. `context` may
|
|
|
21
21
|
|
|
22
22
|
To get `race`, `all`, `take` and the like on every task without passing `ctx`, see [`effectsPlugin`](effects.md#put-selected-effects-on-ctx).
|
|
23
23
|
|
|
24
|
-
To give fields to **one**
|
|
24
|
+
To give fields to **one** action, job or module, or to replace them for one `emit`, see [scopes](scope.md).
|
|
25
25
|
|
|
26
26
|
## Type the fields (optional)
|
|
27
27
|
|
|
@@ -45,6 +45,38 @@ bus.on("save", (draft, { store }) => store.put(draft));
|
|
|
45
45
|
- A name that the bus already has is a type error. The fields of the library (`signal`, `emit`, `run`, …) win at runtime, and a field that the context already has is not replaced.
|
|
46
46
|
- The plugin returns the same bus, so keep the result of the chain: `const bus = createBus().use(contextPlugin({ store }))`.
|
|
47
47
|
|
|
48
|
+
## Build a field from the signal of each task
|
|
49
|
+
|
|
50
|
+
Problem: an HTTP client or a database handle should stop when the task is aborted. Without help, every handler passes `ctx.signal` to it by hand, and one forgotten call leaves a request running.
|
|
51
|
+
|
|
52
|
+
`fromSignal(make)` from `nanoeffects/signal` marks a field of `contextPlugin` as a value that is built from the signal of the task. It does not run `make` for the bus: it runs it for each task that reads the field, once, with the `ctx.signal` of that task.
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
import { createBus } from "nanoeffects";
|
|
56
|
+
import { contextPlugin } from "nanoeffects/context";
|
|
57
|
+
import { fromSignal } from "nanoeffects/signal";
|
|
58
|
+
import axios from "axios";
|
|
59
|
+
|
|
60
|
+
// contextPlugin — every task gets these fields
|
|
61
|
+
// fromSignal — http is made from the signal of the task that reads ctx.http
|
|
62
|
+
const bus = createBus().use(
|
|
63
|
+
contextPlugin({
|
|
64
|
+
logger: console, // a plain field: the same object for every task
|
|
65
|
+
http: fromSignal((signal) => axios.create({ signal })),
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
bus.on("user/open", async (id, ctx) => {
|
|
70
|
+
const response = await ctx.http.get(`/users/${id}`); // aborts with the task
|
|
71
|
+
ctx.logger.log(response.data);
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- `make` runs at the first read, so a handler that never uses `ctx.http` pays nothing for it.
|
|
76
|
+
- A task that `ctx.run` starts builds its own value from its own signal, so aborting the child stops only the child's requests.
|
|
77
|
+
- The field is not copied by `{ ...ctx }` and `Object.keys(ctx)` does not list it. Read `ctx.http` first if you copy a context.
|
|
78
|
+
- Without `contextPlugin` a `fromSignal` marker is only an object.
|
|
79
|
+
|
|
48
80
|
### Name the fields in the event map instead
|
|
49
81
|
|
|
50
82
|
`createBus<Events>({ context: { store } })` also types the fields, but through the event map: `WithContext` is checker-only and says which fields the handlers of this bus have. Use it when you cannot call `use`, for example for the bus that `createEffectsMiddleware` makes.
|
package/docs/effects.md
CHANGED
|
@@ -4,6 +4,18 @@ Use this page for retry, timeouts, waiting, or parallel work — not as the firs
|
|
|
4
4
|
|
|
5
5
|
An abort only raises a flag. Your code decides to stop. Start with `retry` / `timeout` on a handler (see [Guide §5](guide.md#5-add-retry-and-timeout)).
|
|
6
6
|
|
|
7
|
+
## On this page
|
|
8
|
+
|
|
9
|
+
- [Wrap a handler with retry and timeout](#wrap-a-handler-with-retry-and-timeout)
|
|
10
|
+
- [Options for retry, timeout, and poll](#options-for-retry-timeout-and-poll)
|
|
11
|
+
- [Back off between retries](#back-off-between-retries)
|
|
12
|
+
- [Put selected effects on `ctx`](#put-selected-effects-on-ctx)
|
|
13
|
+
- [Tie a promise to the task signal](#tie-a-promise-to-the-task-signal)
|
|
14
|
+
- [Run cleanup on abort](#run-cleanup-on-abort)
|
|
15
|
+
- [Wait for the next event with `take`](#wait-for-the-next-event-with-take)
|
|
16
|
+
- [Run several starters together](#run-several-starters-together)
|
|
17
|
+
- [Pass `ctx` outside a task](#pass-ctx-outside-a-task)
|
|
18
|
+
|
|
7
19
|
## Wrap a handler with retry and timeout
|
|
8
20
|
|
|
9
21
|
`retry`, `timeout`, and `poll` have two forms:
|
|
@@ -49,9 +61,7 @@ bus.on("load", async (id, ctx) => {
|
|
|
49
61
|
() => fetchUser(id, { signal: ctx.signal }),
|
|
50
62
|
{ times: 4 },
|
|
51
63
|
);
|
|
52
|
-
const data = await timeout(ctx, 5000, (
|
|
53
|
-
return fetchData(id, { signal });
|
|
54
|
-
});
|
|
64
|
+
const data = await timeout(ctx, 5000, (child) => fetchData(id, child));
|
|
55
65
|
await poll(ctx, () => refresh(), 10_000);
|
|
56
66
|
ctx.emit("ready", { user, data });
|
|
57
67
|
});
|
|
@@ -160,7 +170,8 @@ const bus = createBus().use(effects);
|
|
|
160
170
|
bus.on("load", async (id, { race, all, delay }) => {
|
|
161
171
|
// race — same as race(ctx, …)
|
|
162
172
|
const result = await race({
|
|
163
|
-
|
|
173
|
+
// child — the context of this starter; it has the `signal`, so the request stops with the race
|
|
174
|
+
data: (child) => fetchData(id, child),
|
|
164
175
|
// a starter gets its own child with the same bound effects, so delay(5000) stops with the race
|
|
165
176
|
timeout: ({ delay }) => delay(5000),
|
|
166
177
|
});
|
|
@@ -168,8 +179,8 @@ bus.on("load", async (id, { race, all, delay }) => {
|
|
|
168
179
|
|
|
169
180
|
// all — same as all(ctx, …)
|
|
170
181
|
const { user, posts } = await all({
|
|
171
|
-
user: (
|
|
172
|
-
posts: (
|
|
182
|
+
user: (child) => fetchUser(id, child),
|
|
183
|
+
posts: (child) => fetchPosts(id, child),
|
|
173
184
|
});
|
|
174
185
|
});
|
|
175
186
|
```
|
|
@@ -292,8 +303,8 @@ const bus = createBus();
|
|
|
292
303
|
|
|
293
304
|
bus.on("open", async (id, ctx) => {
|
|
294
305
|
const { user, posts } = await allSettled(ctx, {
|
|
295
|
-
user: (
|
|
296
|
-
posts: (
|
|
306
|
+
user: (child) => fetchUser(id, child),
|
|
307
|
+
posts: (child) => fetchPosts(id, child),
|
|
297
308
|
});
|
|
298
309
|
|
|
299
310
|
if (user.status === "fulfilled") {
|
|
@@ -399,7 +410,7 @@ bus.on("sync", async (ids, ctx) => {
|
|
|
399
410
|
The result has the same order and shape as without the option. If one starter of `all` fails, the running ones are cancelled and the waiting ones are never called. `allSettled` goes on after a failure. When `ctx` aborts, both stop and start nothing more. A value that is not a positive integer (`0`, `-1`, `NaN`, `1.5`) throws a `RangeError` before anything starts; `Infinity` means no limit. `race` has no `concurrency`: it ends at the first result, so a starter that waited for a free slot would never run. The form without `ctx` takes no options, because a second argument would look like the form with `ctx`, so a group starter inside another group takes none; write `(child) => all(child, effects, { concurrency: 5 })` there.
|
|
400
411
|
|
|
401
412
|
|
|
402
|
-
**A starter gets the context of its own child task.** Pass `child` as the first argument to any effect, as a handler passes `ctx`: when the race ends, `child` is aborted and stops exactly this effect. The outer `ctx` is only for `race` itself. Your own API takes the signal in its options
|
|
413
|
+
**A starter gets the context of its own child task.** Pass `child` as the first argument to any effect, as a handler passes `ctx`: when the race ends, `child` is aborted and stops exactly this effect. The outer `ctx` is only for `race` itself. Your own API takes the signal in its options, and `child` has a `signal`, so pass it as the options: `(child) => fetchUser(id, child)` (`fetch` reads only `signal` from it). The same holds for `all`, `allSettled` and `timeout`.
|
|
403
414
|
|
|
404
415
|
If `ctx` is `{}` or a bare `{ signal }` (outside a task), `child` has only `signal` and `abort`, so it fits `delay` and `poll` but not `take`, which needs a bus.
|
|
405
416
|
|
package/docs/guide.md
CHANGED
|
@@ -11,6 +11,23 @@ Climb one step at a time: small bus → policies → effects → handlers and mo
|
|
|
11
11
|
|
|
12
12
|
Runnable code for every step: [`examples/guide.ts`](../examples/guide.ts).
|
|
13
13
|
|
|
14
|
+
## On this page
|
|
15
|
+
|
|
16
|
+
- [1. Create a bus](#1-create-a-bus)
|
|
17
|
+
- [Listen to every event](#listen-to-every-event)
|
|
18
|
+
- [Pick events with a function](#pick-events-with-a-function)
|
|
19
|
+
- [2. Use the handler context](#2-use-the-handler-context)
|
|
20
|
+
- [3. Abort the previous run with `latest`](#3-abort-the-previous-run-with-latest)
|
|
21
|
+
- [4. Pick another policy when overlap differs](#4-pick-another-policy-when-overlap-differs)
|
|
22
|
+
- [5. Add retry and timeout](#5-add-retry-and-timeout)
|
|
23
|
+
- [6. Run work in parallel or wait for an event](#6-run-work-in-parallel-or-wait-for-an-event)
|
|
24
|
+
- [7. Name events three ways](#7-name-events-three-ways)
|
|
25
|
+
- [8. Bind a handler to an action later](#8-bind-a-handler-to-an-action-later)
|
|
26
|
+
- [9. Mount a domain as a module](#9-mount-a-domain-as-a-module)
|
|
27
|
+
- [10. Add types when you want checking (optional)](#10-add-types-when-you-want-checking-optional)
|
|
28
|
+
- [11. Isolate errors and warn on typos](#11-isolate-errors-and-warn-on-typos)
|
|
29
|
+
- [Where next](#where-next)
|
|
30
|
+
|
|
14
31
|
## 1. Create a bus
|
|
15
32
|
|
|
16
33
|
`on` subscribes. `emit` calls the subscribers. `on` returns the unsubscribe function.
|
|
@@ -272,10 +289,11 @@ import { all, delay, race, take } from "nanoeffects/effects";
|
|
|
272
289
|
const bus = createBus();
|
|
273
290
|
|
|
274
291
|
// all — run together; one failure cancels the rest
|
|
292
|
+
// child — the context of that starter: it has the `signal`, so the request stops with the group
|
|
275
293
|
bus.on("search", async (id, ctx) => {
|
|
276
294
|
const { user, posts } = await all(ctx, {
|
|
277
|
-
user: (
|
|
278
|
-
posts: (
|
|
295
|
+
user: (child) => api.user(id, child),
|
|
296
|
+
posts: (child) => api.posts(id, child),
|
|
279
297
|
});
|
|
280
298
|
ctx.emit("loaded", { user, posts });
|
|
281
299
|
});
|
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
Read this page if your project uses Effector and you want to know where nanoeffects fits. They overlap in one place: events that start effects. Effector also holds state; nanoeffects does not. It maps the names, shows one effect both ways, and lists what has no equivalent.
|
|
4
4
|
|
|
5
|
+
## On this page
|
|
6
|
+
|
|
7
|
+
- [Split the work](#split-the-work)
|
|
8
|
+
- [Find the equivalent](#find-the-equivalent)
|
|
9
|
+
- [Rewrite an effect](#rewrite-an-effect)
|
|
10
|
+
- [Keep the state in Zustand](#keep-the-state-in-zustand)
|
|
11
|
+
- [Rewrite a form](#rewrite-a-form)
|
|
12
|
+
- [Replace the log with a request](#replace-the-log-with-a-request)
|
|
13
|
+
- [Use both in one app](#use-both-in-one-app)
|
|
14
|
+
- [What is different](#what-is-different)
|
|
15
|
+
|
|
5
16
|
## Split the work
|
|
6
17
|
|
|
7
18
|
| Job | Use |
|
package/docs/observable.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Use this page when your app already works with Observables or RxJS and you want a bus event as a stream. If you start from scratch, prefer `bus.on` with policies: they cancel work and show up in traces.
|
|
4
4
|
|
|
5
|
+
The same entry has `events`, which needs no stream library: it gives an event to a `for await` loop. See [Read an event with a `for await` loop](#read-an-event-with-a-for-await-loop).
|
|
6
|
+
|
|
5
7
|
`createObserve(bus)` from `nanoeffects/observable` turns a bus event into an Observable that follows the TC39 proposal (`subscribe`, `unsubscribe`, `Symbol.observable`). RxJS and other stream libraries take it as it is. Nothing here depends on them.
|
|
6
8
|
|
|
7
9
|
## Turn an event into a stream
|
|
@@ -145,6 +147,47 @@ bus.on(
|
|
|
145
147
|
|
|
146
148
|
`switchMap` drops the old inner stream. `latest` aborts the old task through `signal`, so a `fetch` that got the signal really stops.
|
|
147
149
|
|
|
150
|
+
## Read an event with a `for await` loop
|
|
151
|
+
|
|
152
|
+
Problem: a task wants to handle the payloads of one event one after another, in order, and stop when the screen closes. `take` waits for one payload, and a listener has no place for a loop.
|
|
153
|
+
|
|
154
|
+
`events(source, event, options)` from `nanoeffects/observable` returns an async iterator of the payloads. It listens from the call, so a payload that comes before the first read is kept. `source` is a bus or a task context; with a context the loop also ends when the task aborts.
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
import { createBus } from "nanoeffects";
|
|
158
|
+
import { events } from "nanoeffects/observable";
|
|
159
|
+
|
|
160
|
+
const bus = createBus();
|
|
161
|
+
const controller = new AbortController();
|
|
162
|
+
|
|
163
|
+
// events — the "query/changed" payloads one by one; keeps the last 3 while the loop is busy
|
|
164
|
+
const queries = events(bus, "query/changed", {
|
|
165
|
+
signal: controller.signal,
|
|
166
|
+
buffer: 3,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
async function watchQueries() {
|
|
170
|
+
for await (const query of queries) {
|
|
171
|
+
console.log("search for", query); // runs once per payload, in order
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
watchQueries().catch((reason) => console.log("stopped:", reason)); // the abort rejects the loop
|
|
176
|
+
bus.emit("query/changed", "cat");
|
|
177
|
+
controller.abort("screen closed"); // the loop ends with this reason
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
| Option | What it does | Default |
|
|
181
|
+
| ------ | ------------ | ------- |
|
|
182
|
+
| `signal` | Ends the loop. The waiting read rejects with the abort reason. | none |
|
|
183
|
+
| `buffer` | The most payloads kept while the loop is busy. | `Infinity` |
|
|
184
|
+
| `overflow` | Which payload goes when the buffer is full: `"drop-oldest"` or `"drop-newest"`. | `"drop-oldest"` |
|
|
185
|
+
|
|
186
|
+
- `break`, `return` and a throw inside the loop remove the listener.
|
|
187
|
+
- With no `buffer`, a loop that never reads keeps every payload. Set `buffer` for a slow loop.
|
|
188
|
+
- An event of a bus does not end, so the loop ends only by `break`, the `signal` or the task.
|
|
189
|
+
- Inside a task, `events(ctx, "query/changed")` needs no `signal`: the task ending is the end.
|
|
190
|
+
|
|
148
191
|
## Convert back to a stream or a promise
|
|
149
192
|
|
|
150
193
|
| You need | Write |
|
package/docs/oop.md
CHANGED
|
@@ -9,6 +9,23 @@ Use this page when **your domain logic lives in classes**: a service, a store or
|
|
|
9
9
|
|
|
10
10
|
If your logic is plain functions, stay with [modules](actions.md#mount-domains-with-modules): a module is the same idea without a class.
|
|
11
11
|
|
|
12
|
+
## On this page
|
|
13
|
+
|
|
14
|
+
- [The problem without it](#the-problem-without-it)
|
|
15
|
+
- [Make an independent emitter with `Emitter`](#make-an-independent-emitter-with-emitter)
|
|
16
|
+
- [Type the events (optional)](#type-the-events-optional)
|
|
17
|
+
- [Give a class the methods of a shared bus](#give-a-class-the-methods-of-a-shared-bus)
|
|
18
|
+
- [Extend the result with no class of your own](#extend-the-result-with-no-class-of-your-own)
|
|
19
|
+
- [Let the class make its bus](#let-the-class-make-its-bus)
|
|
20
|
+
- [End the subscriptions with `dispose`](#end-the-subscriptions-with-dispose)
|
|
21
|
+
- [Stream an event with a `Subject`](#stream-an-event-with-a-subject)
|
|
22
|
+
- [Keep a method of your own](#keep-a-method-of-your-own)
|
|
23
|
+
- [Subscribe a method with `@On` and `@Once`](#subscribe-a-method-with-on-and-once)
|
|
24
|
+
- [Decorate the class with `@WithBus`](#decorate-the-class-with-withbus)
|
|
25
|
+
- [Pass a method by hand](#pass-a-method-by-hand)
|
|
26
|
+
- [Type the instances (optional)](#type-the-instances-optional)
|
|
27
|
+
- [What is not done](#what-is-not-done)
|
|
28
|
+
|
|
12
29
|
## The problem without it
|
|
13
30
|
|
|
14
31
|
A class that listens to the bus keeps a reference to it, and keeps every unsubscribe function in a list to call when the object goes away. Forget one and the old object keeps handling events. `extendBus` does both for you.
|
package/docs/package.md
CHANGED
|
@@ -2,24 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
Imports are split so you only load what you use. `createBus` stays small; the rest is another entry.
|
|
4
4
|
|
|
5
|
+
## Start with these three
|
|
6
|
+
|
|
5
7
|
| Import | What is in it |
|
|
6
8
|
| ------ | ------------- |
|
|
7
9
|
| `nanoeffects` | `createBus`, the types |
|
|
8
10
|
| `nanoeffects/policies` | `latest`, `leading`, `debounce`, `throttle`, `queue` |
|
|
9
11
|
| `nanoeffects/effects` | `retry`, `timeout`, `poll`, `delay`, `take`, `all`, `allSettled`, `race`, `effectsPlugin` |
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
| `nanoeffects/
|
|
18
|
-
| `nanoeffects/
|
|
19
|
-
| `nanoeffects/
|
|
20
|
-
| `nanoeffects/
|
|
21
|
-
| `nanoeffects/
|
|
22
|
-
| `nanoeffects/
|
|
12
|
+
|
|
13
|
+
## Add the rest when you need it
|
|
14
|
+
|
|
15
|
+
Each row is optional. The "when" column says what need brings you to it.
|
|
16
|
+
|
|
17
|
+
| Import | What is in it | When |
|
|
18
|
+
| ------ | ------------- | ---- |
|
|
19
|
+
| `nanoeffects/actions` | `createAction`, `createJob`, `createModule`, `modulePlugin`, `actionsPlugin` | `emit` must return a result, or domain logic leaves the UI |
|
|
20
|
+
| `nanoeffects/match` | `matchPlugin`: `on`, `once` and `off` that take a function which picks the events | `on` must take a function that picks events |
|
|
21
|
+
| `nanoeffects/context` | `contextPlugin`: your own fields (a store, a client) on `ctx` of every task, typed from the value | a store or client must be on every task |
|
|
22
|
+
| `nanoeffects/signal` | `signalPlugin`: `emit(call, { signal })` ties the tasks of an emit to an outside `AbortSignal` | an outside `AbortSignal` (request, React Query) must stop tasks |
|
|
23
|
+
| `nanoeffects/oop` | `Emitter`, `extendBus`, `WithBus`, `On`, `Once`, `disposeBus`: the methods of a bus on a class, with the subscriptions of each instance ended by `dispose()` | you write classes, not functions |
|
|
24
|
+
| `nanoeffects/scope` | `scopePlugin`, `effect`: fields for one action, job, module or `emit` (the types `Scope`, `Effect`) | one action or module needs its own fields |
|
|
25
|
+
| `nanoeffects/redux` | `createEffectsMiddleware` | you already use Redux |
|
|
26
|
+
| `nanoeffects/observable` | `createObserve`, `Subject` | you need an Observable (RxJS) |
|
|
27
|
+
| `nanoeffects/react` | `reactPlugin`: adds the `useEmitter` hook to a bus (needs React 18 or newer) | React components |
|
|
28
|
+
| `nanoeffects/svelte` | `sveltePlugin`: adds `eventStore` to a bus (needs Svelte 5 or newer) | Svelte components |
|
|
29
|
+
| `nanoeffects/vue` | `vuePlugin`: adds `watchEmitter` and `useEventRef` to a bus (needs Vue 3.3 or newer) | Vue components |
|
|
30
|
+
| `nanoeffects/dev` | `devPlugin` (no-op in production) | you look for leaks in development |
|
|
31
|
+
| `nanoeffects/trace` | `createTracer`, the loggers and `tracePlugin` (names for the trace) | you want to see what ran and why |
|
|
23
32
|
|
|
24
33
|
The `...Plugin` names above are used with `bus.use`. [Plugins](plugins.md) lists all of them, what each returns, and the order to use them in.
|
|
25
34
|
|
package/docs/plugins.md
CHANGED
|
@@ -2,12 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
Optional. A plugin adds one thing to a bus: fields on `ctx`, a method, a registry of handlers, diagnostics. Read this page when you want to know which plugins exist, what each one gives you, and in what order to use them. Skip it for a small bus: `createBus()` alone is enough.
|
|
4
4
|
|
|
5
|
+
## On this page
|
|
6
|
+
|
|
7
|
+
- [Use a plugin](#use-a-plugin)
|
|
8
|
+
- [Pick a plugin by task](#pick-a-plugin-by-task)
|
|
9
|
+
- [Main plugins](#main-plugins)
|
|
10
|
+
- [Put a store or a client on every handler: `contextPlugin`](#put-a-store-or-a-client-on-every-handler-contextplugin)
|
|
11
|
+
- [Use `race` and `delay` without `ctx`: `effectsPlugin`](#use-race-and-delay-without-ctx-effectsplugin)
|
|
12
|
+
- [Bind or replace a handler: `actionsPlugin`](#bind-or-replace-a-handler-actionsplugin)
|
|
13
|
+
- [Mount a domain as one unit: `modulePlugin`](#mount-a-domain-as-one-unit-moduleplugin)
|
|
14
|
+
- [Extra plugins](#extra-plugins)
|
|
15
|
+
- [Give one action its own fields: `scopePlugin`](#give-one-action-its-own-fields-scopeplugin)
|
|
16
|
+
- [Tie an emit or a listener to an outside signal: `signalPlugin`](#tie-an-emit-or-a-listener-to-an-outside-signal-signalplugin)
|
|
17
|
+
- [Name the origin and the flow of a trace: `tracePlugin`](#name-the-origin-and-the-flow-of-a-trace-traceplugin)
|
|
18
|
+
- [Listen to a group of events: `matchPlugin`](#listen-to-a-group-of-events-matchplugin)
|
|
19
|
+
- [Hand the bus to a state manager: `emitterPlugin`](#hand-the-bus-to-a-state-manager-emitterplugin)
|
|
20
|
+
- [Tell the bus when a React value changes: `reactPlugin`](#tell-the-bus-when-a-react-value-changes-reactplugin)
|
|
21
|
+
- [Find leaks while you develop: `devPlugin`](#find-leaks-while-you-develop-devplugin)
|
|
22
|
+
- [Put them together](#put-them-together)
|
|
23
|
+
- [Use them in any order](#use-them-in-any-order)
|
|
24
|
+
- [See what is not a plugin](#see-what-is-not-a-plugin)
|
|
25
|
+
- [Write your own plugin](#write-your-own-plugin)
|
|
26
|
+
|
|
5
27
|
## Use a plugin
|
|
6
28
|
|
|
7
29
|
A plugin is a function of the bus. `bus.use(plugin)` calls it with the bus and returns whatever the plugin returns. Most plugins return the same bus, so you can chain them: `createBus().use(a).use(b)`. A few return something of their own (a registry, an app, a `disconnect` function): keep it.
|
|
8
30
|
|
|
9
31
|
In TypeScript keep the result of `contextPlugin` and `effectsPlugin`: the type of the bus with its new fields is the type they return (`const bus = createBus().use(contextPlugin({ store }))`).
|
|
10
32
|
|
|
33
|
+
## Pick a plugin by task
|
|
34
|
+
|
|
35
|
+
Find your need in the left column. No row fits: you need no plugin.
|
|
36
|
+
|
|
37
|
+
| I need | Use |
|
|
38
|
+
| ------ | --- |
|
|
39
|
+
| A store, client or logger on every handler | [`contextPlugin`](#put-a-store-or-a-client-on-every-handler-contextplugin) |
|
|
40
|
+
| `race`, `all` or `delay` without passing `ctx` | [`effectsPlugin`](#use-race-and-delay-without-ctx-effectsplugin) |
|
|
41
|
+
| A test that replaces a handler | [`actionsPlugin`](#bind-or-replace-a-handler-actionsplugin) |
|
|
42
|
+
| One domain mounted and unmounted as a unit | [`modulePlugin`](#mount-a-domain-as-one-unit-moduleplugin) |
|
|
43
|
+
| Fields for one action or module only | [`scopePlugin`](#give-one-action-its-own-fields-scopeplugin) |
|
|
44
|
+
| Tasks that stop when a request, socket or query is cancelled | [`signalPlugin`](#tie-an-emit-or-a-listener-to-an-outside-signal-signalplugin) |
|
|
45
|
+
| One listener for a group of events | [`matchPlugin`](#listen-to-a-group-of-events-matchplugin) |
|
|
46
|
+
| A callback for a state manager that emits | [`emitterPlugin`](#hand-the-bus-to-a-state-manager-emitterplugin) |
|
|
47
|
+
| A React component that sends its value to the bus | [`reactPlugin`](#tell-the-bus-when-a-react-value-changes-reactplugin) |
|
|
48
|
+
| Names in the trace | [`tracePlugin`](#name-the-origin-and-the-flow-of-a-trace-traceplugin) |
|
|
49
|
+
| Warnings about leaks while you develop | [`devPlugin`](#find-leaks-while-you-develop-devplugin) |
|
|
50
|
+
|
|
11
51
|
## Main plugins
|
|
12
52
|
|
|
13
53
|
These four cover what most apps need: shared dependencies, effects, handlers and modules. Start here.
|
|
@@ -33,6 +73,8 @@ const bus = createBus().use(contextPlugin({ store }));
|
|
|
33
73
|
bus.on("save", (draft, { store }) => store.put(draft));
|
|
34
74
|
```
|
|
35
75
|
|
|
76
|
+
A client that must stop with the task, such as an `axios` instance, goes in as `fromSignal((signal) => axios.create({ signal }))` from `nanoeffects/signal`: it is built from the signal of each task, when the task reads it. More in [Build a field from the signal of each task](context.md#build-a-field-from-the-signal-of-each-task).
|
|
77
|
+
|
|
36
78
|
### Use `race` and `delay` without `ctx`: `effectsPlugin`
|
|
37
79
|
|
|
38
80
|
Effects such as `race`, `all`, `take` and `delay` are bound to the task, so a handler calls `race({ … })` and not `race(ctx, { … })`. Your own functions that take `{ signal }` first work too. More in [Effects](effects.md#put-selected-effects-on-ctx).
|
|
@@ -48,7 +90,7 @@ const bus = createBus().use(effects);
|
|
|
48
90
|
|
|
49
91
|
bus.on("load", async (id, { race, delay }) => {
|
|
50
92
|
const result = await race({
|
|
51
|
-
data: (
|
|
93
|
+
data: (child) => api.get(id, child),
|
|
52
94
|
timeout: () => delay(5000),
|
|
53
95
|
});
|
|
54
96
|
});
|
|
@@ -85,7 +127,7 @@ const bus = createBus();
|
|
|
85
127
|
// modulePlugin — mounts userModule at the end of this block, or at the first use if that is earlier, and returns the app
|
|
86
128
|
const app = bus.use(modulePlugin([userModule]));
|
|
87
129
|
|
|
88
|
-
app.unmount(userModule); // its listeners, bound
|
|
130
|
+
app.unmount(userModule); // its listeners, bound actions and tasks are gone
|
|
89
131
|
```
|
|
90
132
|
|
|
91
133
|
## Extra plugins
|
|
@@ -95,7 +137,7 @@ Add one of these when you meet its need. None of them is required.
|
|
|
95
137
|
| Plugin | What it gives you | Import | `bus.use` returns |
|
|
96
138
|
| ------ | ----------------- | ------ | ----------------- |
|
|
97
139
|
| [`scopePlugin`](#give-one-action-its-own-fields-scopeplugin) | Its own fields for one action, job or module, and a way to replace them in one `emit`. | `nanoeffects/scope` | The same bus |
|
|
98
|
-
| [`signalPlugin`](#tie-an-emit-to-an-outside-signal-signalplugin) | `emit(call, { signal })` that aborts its tasks when an outside signal aborts. | `nanoeffects/signal` | The same bus, typed with the new `emit` |
|
|
140
|
+
| [`signalPlugin`](#tie-an-emit-or-a-listener-to-an-outside-signal-signalplugin) | `emit(call, { signal })` that aborts its tasks, and `on(event, handler, { signal })` that removes the listener, when an outside signal aborts. | `nanoeffects/signal` | The same bus, typed with the new `emit` |
|
|
99
141
|
| [`matchPlugin`](#listen-to-a-group-of-events-matchplugin) | `on` that listens to a group of events which a function picks. | `nanoeffects/match` | The same bus, typed with the new overloads |
|
|
100
142
|
| [`emitterPlugin`](#hand-the-bus-to-a-state-manager-emitterplugin) | A callback that emits an action, to give to a state manager. | `nanoeffects/actions` | The same bus, with `emitter` |
|
|
101
143
|
| [`tracePlugin`](#name-the-origin-and-the-flow-of-a-trace-traceplugin) | `traceId` and `traceGroupId` for a trace: the root of an emitter, a flow, a named listener. | `nanoeffects/trace` | The same bus |
|
|
@@ -111,10 +153,10 @@ import { createBus } from "nanoeffects";
|
|
|
111
153
|
import { createAction } from "nanoeffects/actions";
|
|
112
154
|
import { scopePlugin } from "nanoeffects/scope";
|
|
113
155
|
|
|
114
|
-
// scopePlugin — makes the scope of
|
|
156
|
+
// scopePlugin — makes the scope of an action work on this bus
|
|
115
157
|
const bus = createBus().use(scopePlugin());
|
|
116
158
|
|
|
117
|
-
// the last argument: fields of this
|
|
159
|
+
// the last argument: fields of this action only
|
|
118
160
|
const getUser = createAction("user/get", (id, { api }) => api.getUser(id), {
|
|
119
161
|
api,
|
|
120
162
|
});
|
|
@@ -123,25 +165,41 @@ const fakeApi = { getUser: async (id) => ({ id, name: "test" }) };
|
|
|
123
165
|
bus.emit(getUser("1"), { api: fakeApi }); // a fake api for this call only
|
|
124
166
|
```
|
|
125
167
|
|
|
126
|
-
### Tie an emit to an outside signal: `signalPlugin`
|
|
168
|
+
### Tie an emit or a listener to an outside signal: `signalPlugin`
|
|
127
169
|
|
|
128
|
-
A library such as React Query owns the request and gives you an `AbortSignal`. `emit` takes it as `{ signal }` and aborts the tasks it starts when the signal aborts, so `timeout`, `retry` and the request stop too. More in [Use nanoeffects with React Query and
|
|
170
|
+
A library such as React Query owns the request and gives you an `AbortSignal`. `emit` takes it as `{ signal }` and aborts the tasks it starts when the signal aborts, so `timeout`, `retry` and the request stop too. More in [Use nanoeffects with React Query, SWR and RTK Query](query-libraries.md).
|
|
129
171
|
|
|
130
172
|
```js
|
|
131
173
|
import { createBus } from "nanoeffects";
|
|
132
174
|
import { createAction } from "nanoeffects/actions";
|
|
133
175
|
import { signalPlugin } from "nanoeffects/signal";
|
|
134
176
|
|
|
135
|
-
// signalPlugin — emit takes { signal } for
|
|
177
|
+
// signalPlugin — emit takes { signal } for an action, a job or an action
|
|
136
178
|
const bus = createBus().use(signalPlugin());
|
|
137
179
|
|
|
138
180
|
const getUser = createAction("user/get", (id, { signal }) => api.getUser(id, { signal }));
|
|
139
181
|
|
|
140
182
|
const controller = new AbortController();
|
|
141
|
-
bus.emit(getUser("1"), { signal: controller.signal }); // runs the
|
|
183
|
+
bus.emit(getUser("1"), { signal: controller.signal }); // runs the action
|
|
142
184
|
controller.abort(); // ctx.signal of the handler aborts with it
|
|
143
185
|
```
|
|
144
186
|
|
|
187
|
+
The same signal can end a listener. `on` and `once` take `{ signal }` as their options, as `addEventListener` does, so code that has a signal keeps no `unbind` function. The listener is removed when the signal aborts; a handler that already runs is not aborted. A signal that is aborted already adds no listener.
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
import { createBus } from "nanoeffects";
|
|
191
|
+
import { signalPlugin } from "nanoeffects/signal";
|
|
192
|
+
|
|
193
|
+
// signalPlugin — on and once take { signal } in their options
|
|
194
|
+
const bus = createBus().use(signalPlugin());
|
|
195
|
+
|
|
196
|
+
const controller = new AbortController();
|
|
197
|
+
bus.on("saved", (draft) => console.log(draft), { signal: controller.signal }); // listens until the abort
|
|
198
|
+
controller.abort(); // the listener is gone: bus.get("saved") is empty
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Without the plugin the `signal` option is ignored and the listener stays. `off(event, handler)` removes a listener but does not end its link to the signal; call the function that `on` returned for that.
|
|
202
|
+
|
|
145
203
|
### Name the origin and the flow of a trace: `tracePlugin`
|
|
146
204
|
|
|
147
205
|
With a tracer on the bus, the plugin lets a trace say which subscription began a chain (`traceId` of `useEmitter` and `bus.emitter`), follow one flow by a `traceGroupId`, and show which listener ran (`bus.on(event, handler, { traceId })`). Without the plugin the options are ignored. More in [Tracing](tracing.md#show-which-subscription-began-a-chain).
|
|
@@ -254,7 +312,7 @@ import { scopePlugin } from "nanoeffects/scope";
|
|
|
254
312
|
export const bus = createBus()
|
|
255
313
|
.use(contextPlugin({ store })) // ctx.store on every task
|
|
256
314
|
.use(effectsPlugin({ race, all })) // ctx.race and ctx.all, bound to the task
|
|
257
|
-
.use(scopePlugin()) // scopes for
|
|
315
|
+
.use(scopePlugin()) // scopes for actions, jobs, modules and emit
|
|
258
316
|
.use(matchPlugin()) // bus.on takes a function that picks the events
|
|
259
317
|
.use(emitterPlugin()); // bus.emitter(creator)
|
|
260
318
|
|
|
@@ -295,7 +353,7 @@ These connect the bus to something, but are not used with `bus.use`:
|
|
|
295
353
|
- `createEffectsMiddleware(bus, { filter })` from `nanoeffects/redux` is a Redux middleware for a bus that exists. See [Stores](stores.md#wire-redux-toolkit-to-a-bus).
|
|
296
354
|
- `createBus({ trace })` takes a tracer from `nanoeffects/trace`. See [Tracing](tracing.md).
|
|
297
355
|
- `extendBus(bus, Base)` from `nanoeffects/oop` gives a class the methods of a bus, with the subscriptions of each instance ended by `dispose()`. It reads the bus as it is, so use the plugins first. See [Use the bus from a class](oop.md).
|
|
298
|
-
- `createObserve(bus)` from `nanoeffects/observable` turns an event into an Observable. See [Observable](observable.md).
|
|
356
|
+
- `createObserve(bus)` from `nanoeffects/observable` turns an event into an Observable, and `events(bus, event)` into a `for await` loop. See [Observable](observable.md).
|
|
299
357
|
|
|
300
358
|
## Write your own plugin
|
|
301
359
|
|