nanoeffects 0.28.0 → 0.30.1
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/README.md +6 -5
- package/dist/{action-creator-CcAEvUnw.d.ts → action-creator-6rlBTmH9.d.ts} +60 -33
- package/dist/actions.d.ts +18 -18
- package/dist/actions.js +1 -1
- package/dist/bus-DFlmNJ6h.js +1 -0
- package/dist/context-B5Z2RpJt.js +1 -0
- package/dist/context-types-DKNc5mkS.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 +44 -3
- package/dist/dev.js +2 -1
- package/dist/effects.d.ts +2 -2
- package/dist/effects.js +1 -1
- package/dist/emit-key-C6FL7Ewn.js +1 -0
- package/dist/guard.d.ts +59 -0
- package/dist/guard.js +1 -0
- 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-G39MaVOt.d.ts} +38 -38
- package/dist/notrace/{action-creator-DxOvVo8I.d.ts → action-creator-BrMSO3RP.d.ts} +35 -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-7GUL2EW5.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-BWNP23hr.d.ts} +2 -2
- package/dist/policies.d.ts +2 -2
- package/dist/policies.js +1 -1
- package/dist/production/dev.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/site-DLvUoXs3.js +1 -0
- package/dist/svelte.d.ts +1 -1
- package/dist/trace.d.ts +170 -6
- package/dist/trace.js +3 -1
- package/dist/{types-FV8fuvI9.d.ts → types-N_cdRr1r.d.ts} +2 -2
- package/dist/vue.d.ts +1 -1
- package/dist/{with-policy-D-SQRkPh.js → with-policy-CeM1rtES.js} +1 -1
- package/dist/{with-policy-3bhFLEXs.d.ts → with-policy-CthiBp_z.d.ts} +1 -1
- package/docs/README.md +65 -22
- package/docs/actions.md +56 -13
- package/docs/cancel-stale-requests.md +2 -0
- package/docs/chat-example.md +15 -1
- package/docs/chat-widget.md +9 -7
- package/docs/context.md +352 -8
- package/docs/debounce-and-abort.md +3 -1
- package/docs/debugging-loops.md +283 -0
- package/docs/effects.md +259 -131
- package/docs/errors.md +4 -2
- package/docs/examples.md +60 -0
- package/docs/guide.md +57 -22
- package/docs/hmr.md +35 -1
- package/docs/loop-guard.md +109 -0
- package/docs/migrating-from-effector.md +15 -2
- package/docs/migrating-from-mitt.md +6 -4
- package/docs/migrating-from-rxjs.md +2 -0
- package/docs/migrating-from-saga.md +2 -0
- package/docs/motivation.md +14 -21
- package/docs/observable.md +45 -0
- package/docs/oop.md +20 -1
- package/docs/package.md +25 -13
- package/docs/pass-the-signal.md +281 -0
- package/docs/plugins.md +90 -15
- package/docs/policies.md +73 -46
- package/docs/prevent-double-submit.md +3 -1
- package/docs/query-libraries.md +276 -6
- package/docs/react.md +50 -11
- package/docs/recipes.md +528 -0
- package/docs/ru/README.md +62 -0
- package/docs/ru/actions.md +412 -0
- package/docs/ru/cancel-stale-requests.md +106 -0
- package/docs/ru/chat-example.md +259 -0
- package/docs/ru/chat-widget.md +131 -0
- package/docs/ru/context.md +401 -0
- package/docs/ru/debounce-and-abort.md +105 -0
- package/docs/ru/debugging-loops.md +283 -0
- package/docs/ru/effects.md +538 -0
- package/docs/ru/errors.md +36 -0
- package/docs/ru/examples.md +60 -0
- package/docs/ru/guide.md +530 -0
- package/docs/ru/hmr.md +83 -0
- package/docs/ru/loop-guard.md +109 -0
- package/docs/ru/migrating-from-effector.md +339 -0
- package/docs/ru/migrating-from-mitt.md +99 -0
- package/docs/ru/migrating-from-rxjs.md +197 -0
- package/docs/ru/migrating-from-saga.md +97 -0
- package/docs/ru/motivation.md +47 -0
- package/docs/ru/observable.md +233 -0
- package/docs/ru/oop.md +431 -0
- package/docs/ru/package.md +42 -0
- package/docs/ru/pass-the-signal.md +281 -0
- package/docs/ru/plugins.md +398 -0
- package/docs/ru/policies.md +201 -0
- package/docs/ru/prevent-double-submit.md +122 -0
- package/docs/ru/query-libraries.md +392 -0
- package/docs/ru/react.md +309 -0
- package/docs/ru/recipes.md +528 -0
- package/docs/ru/semantics.md +19 -0
- package/docs/ru/server.md +204 -0
- package/docs/ru/starter.md +118 -0
- package/docs/ru/stores.md +673 -0
- package/docs/ru/svelte.md +78 -0
- package/docs/ru/tracing.md +466 -0
- package/docs/ru/typescript.md +170 -0
- package/docs/ru/vue.md +92 -0
- package/docs/semantics.md +12 -0
- package/docs/server.md +5 -3
- package/docs/starter.md +2 -0
- package/docs/stores.md +2 -0
- package/docs/svelte.md +3 -1
- package/docs/tracing.md +132 -7
- package/docs/typescript.md +3 -1
- package/docs/vue.md +3 -1
- package/llms-full.txt +2718 -545
- package/llms.txt +17 -14
- package/package.json +25 -8
- 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/scope.md +0 -266
- package/docs/stability.md +0 -90
package/docs/package.md
CHANGED
|
@@ -1,25 +1,37 @@
|
|
|
1
1
|
# Choose a package entry
|
|
2
2
|
|
|
3
|
+
> **Levels A to F.** Read first: [README](../README.md).
|
|
4
|
+
|
|
3
5
|
Imports are split so you only load what you use. `createBus` stays small; the rest is another entry.
|
|
4
6
|
|
|
7
|
+
## Start with these three
|
|
8
|
+
|
|
5
9
|
| Import | What is in it |
|
|
6
10
|
| ------ | ------------- |
|
|
7
11
|
| `nanoeffects` | `createBus`, the types |
|
|
8
12
|
| `nanoeffects/policies` | `latest`, `leading`, `debounce`, `throttle`, `queue` |
|
|
9
13
|
| `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/
|
|
14
|
+
|
|
15
|
+
## Add the rest when you need it
|
|
16
|
+
|
|
17
|
+
Each row is optional. The "when" column says what need brings you to it.
|
|
18
|
+
|
|
19
|
+
| Import | What is in it | When |
|
|
20
|
+
| ------ | ------------- | ---- |
|
|
21
|
+
| `nanoeffects/actions` | `createAction`, `createJob`, `createModule`, `modulePlugin`, `actionsPlugin` | `emit` must return a result, or domain logic leaves the UI |
|
|
22
|
+
| `nanoeffects/match` | `matchPlugin`: `on`, `once` and `off` that take a function which picks the events | `on` must take a function that picks events |
|
|
23
|
+
| `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 |
|
|
24
|
+
| `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 |
|
|
25
|
+
| `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 |
|
|
26
|
+
| `nanoeffects/scope` | `scopePlugin`, `effect`: fields for one action, job, module or `emit` (the types `Scope`, `Effect`) | one action or module needs its own fields |
|
|
27
|
+
| `nanoeffects/redux` | `createEffectsMiddleware` | you already use Redux |
|
|
28
|
+
| `nanoeffects/observable` | `createObserve`, `Subject` | you need an Observable (RxJS) |
|
|
29
|
+
| `nanoeffects/react` | `reactPlugin`: adds the `useEmitter` hook to a bus (needs React 18 or newer) | React components |
|
|
30
|
+
| `nanoeffects/svelte` | `sveltePlugin`: adds `eventStore` to a bus (needs Svelte 5 or newer) | Svelte components |
|
|
31
|
+
| `nanoeffects/vue` | `vuePlugin`: adds `watchEmitter` and `useEventRef` to a bus (needs Vue 3.3 or newer) | Vue components |
|
|
32
|
+
| `nanoeffects/dev` | `devPlugin` (no-op in production) | you look for leaks in development |
|
|
33
|
+
| `nanoeffects/trace` | `createTracer`, the loggers (`graphLogger` finds event loops) and `tracePlugin` (names for the trace) | you want to see what ran and why |
|
|
34
|
+
| `nanoeffects/guard` | `loopGuardPlugin`: refuses the emit that would pass a limit of a chain | a loop must not take the app down |
|
|
23
35
|
|
|
24
36
|
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
37
|
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# Keep the signal through a handler
|
|
2
|
+
|
|
3
|
+
> **Level B.** Read first: [Policies](policies.md#latest).
|
|
4
|
+
|
|
5
|
+
A policy such as `latest` cancels an older call by aborting its `ctx.signal`. The handler stops only if every step that waits gets that signal. One request without it keeps running, and its result arrives after the newer one. This page lists the ways the signal gets lost, what catches each one before the code runs (a linter) and while it runs (`devPlugin`), and how to fix it.
|
|
6
|
+
|
|
7
|
+
## On this page
|
|
8
|
+
|
|
9
|
+
- [Know what the signal does](#know-what-the-signal-does)
|
|
10
|
+
- [Fix a handler that loses the signal](#fix-a-handler-that-loses-the-signal)
|
|
11
|
+
- [Pass the signal when it is hard](#pass-the-signal-when-it-is-hard)
|
|
12
|
+
- [Turn on the checks](#turn-on-the-checks)
|
|
13
|
+
- [Choose which calls are checked](#choose-which-calls-are-checked)
|
|
14
|
+
- [Count lost signals in a test](#count-lost-signals-in-a-test)
|
|
15
|
+
- [Prove it with a test](#prove-it-with-a-test)
|
|
16
|
+
- [Know what the checks cannot see](#know-what-the-checks-cannot-see)
|
|
17
|
+
|
|
18
|
+
## Know what the signal does
|
|
19
|
+
|
|
20
|
+
`ctx.signal` is an `AbortSignal`. The policy aborts it when the call is cancelled (a newer call of `latest`, a `debounce` that was replaced, an unmounted module). A step stops when you give it the signal: `fetch(url, { signal: ctx.signal })`, `api.search(query, { signal: ctx.signal })`, or the whole `ctx` for an effect, `retry(ctx, ...)`. Everything else keeps running. The bus drops what a cancelled handler emits with `ctx.emit`, so the loss shows up late and quietly.
|
|
21
|
+
|
|
22
|
+
## Fix a handler that loses the signal
|
|
23
|
+
|
|
24
|
+
The two shapes you meet first, and the fix:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
import { createBus } from "nanoeffects";
|
|
28
|
+
import { latest } from "nanoeffects/policies";
|
|
29
|
+
|
|
30
|
+
const bus = createBus();
|
|
31
|
+
|
|
32
|
+
// 1. ctx is taken and the signal is lost: the request cannot stop
|
|
33
|
+
bus.on(
|
|
34
|
+
"search",
|
|
35
|
+
latest(async (query, ctx) => {
|
|
36
|
+
const items = await api.search(query);
|
|
37
|
+
ctx.emit("results", items);
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// 2. one step passes it and another does not: the first one cannot stop
|
|
42
|
+
bus.on(
|
|
43
|
+
"feed",
|
|
44
|
+
latest(async (query, ctx) => {
|
|
45
|
+
const news = await api.news(query);
|
|
46
|
+
const posts = await api.posts(query, { signal: ctx.signal });
|
|
47
|
+
ctx.emit("results", { news, posts });
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// the fix: every step that waits gets the signal
|
|
52
|
+
bus.on(
|
|
53
|
+
"feed/fixed",
|
|
54
|
+
latest(async (query, ctx) => {
|
|
55
|
+
const news = await api.news(query, { signal: ctx.signal });
|
|
56
|
+
const posts = await api.posts(query, { signal: ctx.signal });
|
|
57
|
+
ctx.emit("results", { news, posts });
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Pass the signal when it is hard
|
|
63
|
+
|
|
64
|
+
Each row is a situation where the signal is easy to lose. The right column says how to keep it. "Lint" is the rule from [`examples/lint`](https://github.com/nanoeffects/nanoeffects/tree/main/examples/lint) that flags it, and "devPlugin" is the warning at run time ([details](hmr.md#find-a-handler-that-ignores-the-signal)).
|
|
65
|
+
|
|
66
|
+
| Situation | What to do | Lint | devPlugin |
|
|
67
|
+
| --------- | ---------- | :--: | :-------: |
|
|
68
|
+
| The handler has no `ctx`: `latest(async (query) => ...)` | Take `(query, ctx)` and pass `ctx.signal` | yes | |
|
|
69
|
+
| `ctx` is taken and never used for the request | Pass `{ signal: ctx.signal }` to each request | yes | yes |
|
|
70
|
+
| One step passes the signal, another does not | Pass it to every step | yes | yes |
|
|
71
|
+
| The signal is in a variable: `const options = { signal: ctx.signal }` | Nothing, a variable that holds it counts | accepted | |
|
|
72
|
+
| Calls run together: `Promise.all([a(), b()])` | Pass it to each call, not only one | yes | |
|
|
73
|
+
| `Promise.all(ids.map((id) => load(id)))` | Pass it inside the callback | yes | |
|
|
74
|
+
| The step is inside an effect: `retry(ctx, () => api.search(q))` | The callback is not given the signal. Pass `{ signal: ctx.signal }` in it, or use the child context it gets | yes | |
|
|
75
|
+
| `timeout(ctx, 1000, (child) => api.search(q))` | Pass `child`, or `child.signal`, to the request | yes | |
|
|
76
|
+
| `ctx.run((child) => api.search(q))` | Pass `child` | yes | |
|
|
77
|
+
| A pause: `await new Promise((r) => setTimeout(r, 1000))` | Use `delay(ctx, 1000)` | yes | |
|
|
78
|
+
| A stream: `for await (const x of api.stream(q))` | Pass the signal to `api.stream` | yes | |
|
|
79
|
+
| A chain: `api.news(q).then(...)` | Pass the signal to the first call | yes | |
|
|
80
|
+
| Another signal: `AbortSignal.timeout(5000)`, or the signal of your own `AbortController` | Join it with the handler's: `AbortSignal.any([ctx.signal, AbortSignal.timeout(5000)])` | yes | |
|
|
81
|
+
| Reading a response body: `await response.json()` | Nothing, the signal of the request stops it | accepted | |
|
|
82
|
+
| A library that takes no signal (a cache, a local database) | List it in `ignore`, or check only the calls on your list with `only`. With type information the rule skips a function that takes no signal on its own | `ignore`, `only`, types | |
|
|
83
|
+
| A function takes the signal and never uses it: `search(query, options) { return fetch(url); }` | Pass the signal on inside it. Only type information finds this in the linter; a test finds it for sure | types | |
|
|
84
|
+
| The signal is wired by hand: `signal.addEventListener("abort", ...)` | Nothing, the handler is not checked step by step | accepted | |
|
|
85
|
+
| `bus.emit` in a handler | Use `ctx.emit`: only it is dropped after the handler was cancelled | yes | |
|
|
86
|
+
| Results emitted after the cancel | The handler ignored the signal. Pass it, or check `ctx.signal.aborted` | | yes |
|
|
87
|
+
| `ctx.on` after the cancel | The same | | yes |
|
|
88
|
+
|
|
89
|
+
The pause and the effect rows in code:
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
import { createBus } from "nanoeffects";
|
|
93
|
+
import { delay, retry, timeout } from "nanoeffects/effects";
|
|
94
|
+
import { latest } from "nanoeffects/policies";
|
|
95
|
+
|
|
96
|
+
const bus = createBus();
|
|
97
|
+
|
|
98
|
+
bus.on(
|
|
99
|
+
"search",
|
|
100
|
+
latest(async (query, ctx) => {
|
|
101
|
+
// delay — a pause that stops with ctx, not new Promise(setTimeout)
|
|
102
|
+
await delay(ctx, 300);
|
|
103
|
+
|
|
104
|
+
// retry — the attempts stop with ctx, but the request needs the signal too
|
|
105
|
+
const items = await retry(ctx, () => api.search(query, { signal: ctx.signal }));
|
|
106
|
+
|
|
107
|
+
// timeout — the callback gets a child context: give it to the request
|
|
108
|
+
const details = await timeout(ctx, 5000, (child) => api.details(query, child));
|
|
109
|
+
|
|
110
|
+
ctx.emit("results", { items, details });
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Calls that run together, and a signal that joins another:
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
import { createBus } from "nanoeffects";
|
|
119
|
+
import { latest } from "nanoeffects/policies";
|
|
120
|
+
|
|
121
|
+
const bus = createBus();
|
|
122
|
+
|
|
123
|
+
bus.on(
|
|
124
|
+
"feed",
|
|
125
|
+
latest(async (query, ctx) => {
|
|
126
|
+
// each call of the group gets the signal, not only the first
|
|
127
|
+
const [news, posts] = await Promise.all([
|
|
128
|
+
api.news(query, { signal: ctx.signal }),
|
|
129
|
+
api.posts(query, { signal: ctx.signal }),
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
// AbortSignal.any — stops with the handler or after 5 seconds, whichever comes first
|
|
133
|
+
const signal = AbortSignal.any([ctx.signal, AbortSignal.timeout(5000)]);
|
|
134
|
+
const details = await api.details(query, { signal });
|
|
135
|
+
|
|
136
|
+
ctx.emit("results", { news, posts, details });
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Turn on the checks
|
|
142
|
+
|
|
143
|
+
**While you write.** [`examples/lint`](https://github.com/nanoeffects/nanoeffects/tree/main/examples/lint) has a rule for ESLint and oxlint (one file, no dependency) and two GritQL plugins for Biome. Copy the file, turn the rule on, and give it the names of calls that cannot be cancelled:
|
|
144
|
+
|
|
145
|
+
<!-- docs-check: skip - a config file of the reader's project -->
|
|
146
|
+
```js
|
|
147
|
+
// eslint.config.mjs
|
|
148
|
+
import nanoeffects from "./nanoeffects-lint.mjs";
|
|
149
|
+
|
|
150
|
+
export default [
|
|
151
|
+
{
|
|
152
|
+
files: ["**/*.{js,mjs,cjs,jsx,ts,tsx}"],
|
|
153
|
+
plugins: { nanoeffects },
|
|
154
|
+
rules: {
|
|
155
|
+
"nanoeffects/policy-handler-uses-signal": [
|
|
156
|
+
"warn",
|
|
157
|
+
// calls that take no signal and are fine to wait for
|
|
158
|
+
{ ignore: ["cache.get", "storage.*"] },
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
];
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
For oxlint list the file in `jsPlugins` of `.oxlintrc.json` and use the same rule name. For Biome list the two `.grit` files in `plugins`. The Biome plugins read text, so they flag the direct steps (the first rows above) and miss the deeper ones: the calls of `Promise.all`, the callbacks of effects, `for await`, a pause, and the key of an options object.
|
|
166
|
+
|
|
167
|
+
**While you run.** `devPlugin` warns once when a handler emits, or subscribes with `ctx.on`, after its task was cancelled:
|
|
168
|
+
|
|
169
|
+
```js
|
|
170
|
+
import { createBus } from "nanoeffects";
|
|
171
|
+
import { devPlugin } from "nanoeffects/dev";
|
|
172
|
+
|
|
173
|
+
const bus = createBus();
|
|
174
|
+
|
|
175
|
+
// devPlugin — warns about a handler that kept running after it was cancelled; nothing in production
|
|
176
|
+
bus.use(devPlugin());
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Choose which calls are checked
|
|
180
|
+
|
|
181
|
+
The rule cannot tell, from the call alone, whether the function takes a signal. Three ways to tell it:
|
|
182
|
+
|
|
183
|
+
- **`ignore`** lists the calls to skip, by name: `ignore: ["cache.get", "storage.*"]`. Use it when most calls take a signal and a few cannot.
|
|
184
|
+
- **`only`** lists the calls to check, by name: `only: ["api.*", "fetch"]`. Anything else is left alone, so a call that is not on the list is never reported. Use it when you know which clients take a signal. A call on both lists is skipped, and a pause that nothing can cancel (`await new Promise(...)`) is reported whatever `only` says.
|
|
185
|
+
- **Type information** needs no list. With `@typescript-eslint/parser` and a `project`, the rule reads the function it calls. A call that does not get the signal is reported only when the function takes one (a parameter typed `AbortSignal`, or an options object with `signal`), so `cache.get(key)` is left alone. A call that passes the signal is reported when the function is in your sources and never uses it: the parameter is unused, or only other options are read from it. A signal forwarded to another function of yours is followed up to three calls deep, and a function from a library is trusted. A callee typed `any` is reported as before.
|
|
186
|
+
|
|
187
|
+
<!-- docs-check: skip - a config file of the reader's project -->
|
|
188
|
+
```js
|
|
189
|
+
// eslint.config.mjs
|
|
190
|
+
import parser from "@typescript-eslint/parser";
|
|
191
|
+
import nanoeffects from "./nanoeffects-lint.mjs";
|
|
192
|
+
|
|
193
|
+
export default [
|
|
194
|
+
{
|
|
195
|
+
files: ["**/*.{ts,tsx}"],
|
|
196
|
+
// the parser gives the rule the type checker of your project
|
|
197
|
+
languageOptions: { parser, parserOptions: { project: "./tsconfig.json" } },
|
|
198
|
+
plugins: { nanoeffects },
|
|
199
|
+
rules: { "nanoeffects/policy-handler-uses-signal": "warn" },
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
oxlint and Biome do not give a rule type information, so they use the syntax and the lists. The type-aware mode needs the `typescript` package with the compiler API (version 5 today; the native version 7 has none), as `@typescript-eslint/parser` does.
|
|
205
|
+
|
|
206
|
+
## Count lost signals in a test
|
|
207
|
+
|
|
208
|
+
`getLostSignalsCount(bus)` from `nanoeffects/dev` returns how many signals the handlers of a bus lost: each emit or `ctx.on` that a handler made after its task was cancelled. The bus needs `devPlugin`. Run the flow the way a user would, cancel it, and check that the count is 0:
|
|
209
|
+
|
|
210
|
+
```js
|
|
211
|
+
import { createBus } from "nanoeffects";
|
|
212
|
+
import { devPlugin, getLostSignalsCount, getLostSignals } from "nanoeffects/dev";
|
|
213
|
+
import { latest } from "nanoeffects/policies";
|
|
214
|
+
import { expect, test } from "vitest";
|
|
215
|
+
|
|
216
|
+
test("a newer search leaves no lost signals", async () => {
|
|
217
|
+
const bus = createBus();
|
|
218
|
+
// devPlugin — records every emit that a cancelled handler made; log hides its warnings in the test
|
|
219
|
+
bus.use(devPlugin({ log: () => {} }));
|
|
220
|
+
bus.on(
|
|
221
|
+
"search",
|
|
222
|
+
latest(async (query, ctx) => {
|
|
223
|
+
const items = await api.search(query, { signal: ctx.signal });
|
|
224
|
+
ctx.emit("results", items);
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
bus.emit("search", "a");
|
|
229
|
+
bus.emit("search", "ab"); // cancels the first call
|
|
230
|
+
|
|
231
|
+
// wait — how long to let the handlers finish before counting, in ms
|
|
232
|
+
const count = await getLostSignalsCount(bus, { wait: 50 });
|
|
233
|
+
|
|
234
|
+
expect(count).toBe(0);
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
`getLostSignals(bus)` gives the list instead of the count: `{ kind: "emit" | "on", handler, event }` for each, so a failing test says which handler and which event. Without `devPlugin` (or with `abandoned: false`) it throws and says so. In a production build both functions are empty: the count is 0.
|
|
239
|
+
|
|
240
|
+
It counts what a handler does after the cancel. A request that a handler started and that the cancel did not stop is not counted unless the handler then emits; the next section finds those.
|
|
241
|
+
|
|
242
|
+
## Prove it with a test
|
|
243
|
+
|
|
244
|
+
A linter reads code; it cannot know what a function does with the signal. A test can. The idea: replace `fetch` with a server that answers when the test lets it and stops when the request's signal aborts, run the handler, cancel it in the middle of each step, and list the requests that did not stop. No timers are involved, so the result does not depend on how fast the machine is.
|
|
245
|
+
|
|
246
|
+
<!-- docs-check: skip - a test file of the reader's project -->
|
|
247
|
+
```js
|
|
248
|
+
// search.cancel.test.js
|
|
249
|
+
import { createBus } from "nanoeffects";
|
|
250
|
+
import { latest } from "nanoeffects/policies";
|
|
251
|
+
import { expect, test } from "vitest";
|
|
252
|
+
// copy prove-cancel.js from examples/lint; it needs no package
|
|
253
|
+
import { requestsThatKeptRunning } from "./prove-cancel.js";
|
|
254
|
+
|
|
255
|
+
test("a newer search stops the older one at every step", async () => {
|
|
256
|
+
const bus = createBus();
|
|
257
|
+
bus.on(
|
|
258
|
+
"search",
|
|
259
|
+
latest(async (query, ctx) => {
|
|
260
|
+
const news = await api.news(query, { signal: ctx.signal });
|
|
261
|
+
const posts = await api.posts(query, { signal: ctx.signal });
|
|
262
|
+
ctx.emit("results", { news, posts });
|
|
263
|
+
}),
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
// for each step: start a call, cancel it with a newer one, and collect the requests that did not stop
|
|
267
|
+
const kept = await requestsThatKeptRunning((value) => bus.emit("search", value));
|
|
268
|
+
|
|
269
|
+
// empty when every request got a signal that aborted with the handler
|
|
270
|
+
expect(kept).toEqual([]);
|
|
271
|
+
});
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`kept` has the url of each request that went on, the step where the handler was cancelled, and whether the request got a signal at all (`hadSignal: false`) or one that is not the handler's (`hadSignal: true`). This finds what the linter cannot: `api.posts` that takes `{ signal }` and ignores it, an `AbortSignal.timeout` in place of the handler's signal, a request in a branch you did not think of. It checks the boundary, `fetch`; for another client (axios, a database driver) stub that client in the same way. The repository tests the helper on handlers that forward the signal, ignore it, drop it at the third step, and use a signal of their own.
|
|
275
|
+
|
|
276
|
+
## Know what the checks cannot see
|
|
277
|
+
|
|
278
|
+
- **A handler that is not written in place.** `latest(searchHandler)` and `latest(withLogging(async (query, ctx) => ...))` are not read by the linter.
|
|
279
|
+
- **A callee that ignores the signal you pass.** Without type information the rule sees that you passed it, not what the callee does with it. With it, the rule reads callees in your sources; a library is trusted. The test above checks the real requests.
|
|
280
|
+
- **A handler that never emits.** `devPlugin` sees a lost signal by an emit after the cancel; a handler that only writes elsewhere is seen by the linter alone.
|
|
281
|
+
- **Shadowed names.** The rule uses no scope analysis, so an inner variable named `signal` can hide a lost signal.
|
package/docs/plugins.md
CHANGED
|
@@ -1,13 +1,57 @@
|
|
|
1
1
|
# Choose the plugins of a bus
|
|
2
2
|
|
|
3
|
+
> **Level F.** Read first: [Guide](guide.md#1-create-a-bus) steps 1 and 2.
|
|
4
|
+
|
|
3
5
|
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
6
|
|
|
7
|
+
## On this page
|
|
8
|
+
|
|
9
|
+
- [Use a plugin](#use-a-plugin)
|
|
10
|
+
- [Pick a plugin by task](#pick-a-plugin-by-task)
|
|
11
|
+
- [Main plugins](#main-plugins)
|
|
12
|
+
- [Put a store or a client on every handler: `contextPlugin`](#put-a-store-or-a-client-on-every-handler-contextplugin)
|
|
13
|
+
- [Use `race` and `delay` without `ctx`: `effectsPlugin`](#use-race-and-delay-without-ctx-effectsplugin)
|
|
14
|
+
- [Bind or replace a handler: `actionsPlugin`](#bind-or-replace-a-handler-actionsplugin)
|
|
15
|
+
- [Mount a domain as one unit: `modulePlugin`](#mount-a-domain-as-one-unit-moduleplugin)
|
|
16
|
+
- [Extra plugins](#extra-plugins)
|
|
17
|
+
- [Give one action its own fields: `scopePlugin`](#give-one-action-its-own-fields-scopeplugin)
|
|
18
|
+
- [Tie an emit or a listener to an outside signal: `signalPlugin`](#tie-an-emit-or-a-listener-to-an-outside-signal-signalplugin)
|
|
19
|
+
- [Name the origin and the flow of a trace: `tracePlugin`](#name-the-origin-and-the-flow-of-a-trace-traceplugin)
|
|
20
|
+
- [Listen to a group of events: `matchPlugin`](#listen-to-a-group-of-events-matchplugin)
|
|
21
|
+
- [Hand the bus to a state manager: `emitterPlugin`](#hand-the-bus-to-a-state-manager-emitterplugin)
|
|
22
|
+
- [Tell the bus when a React value changes: `reactPlugin`](#tell-the-bus-when-a-react-value-changes-reactplugin)
|
|
23
|
+
- [Find leaks while you develop: `devPlugin`](#find-leaks-while-you-develop-devplugin)
|
|
24
|
+
- [Cut a loop that runs away: `loopGuardPlugin`](#cut-a-loop-that-runs-away-loopguardplugin)
|
|
25
|
+
- [Put them together](#put-them-together)
|
|
26
|
+
- [Use them in any order](#use-them-in-any-order)
|
|
27
|
+
- [See what is not a plugin](#see-what-is-not-a-plugin)
|
|
28
|
+
- [Write your own plugin](#write-your-own-plugin)
|
|
29
|
+
|
|
5
30
|
## Use a plugin
|
|
6
31
|
|
|
7
32
|
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
33
|
|
|
9
34
|
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
35
|
|
|
36
|
+
## Pick a plugin by task
|
|
37
|
+
|
|
38
|
+
Find your need in the left column. No row fits: you need no plugin.
|
|
39
|
+
|
|
40
|
+
| I need | Use |
|
|
41
|
+
| ------ | --- |
|
|
42
|
+
| A store, client or logger on every handler | [`contextPlugin`](#put-a-store-or-a-client-on-every-handler-contextplugin) |
|
|
43
|
+
| `race`, `all` or `delay` without passing `ctx` | [`effectsPlugin`](#use-race-and-delay-without-ctx-effectsplugin) |
|
|
44
|
+
| A test that replaces a handler | [`actionsPlugin`](#bind-or-replace-a-handler-actionsplugin) |
|
|
45
|
+
| One domain mounted and unmounted as a unit | [`modulePlugin`](#mount-a-domain-as-one-unit-moduleplugin) |
|
|
46
|
+
| Fields for one action or module only | [`scopePlugin`](#give-one-action-its-own-fields-scopeplugin) |
|
|
47
|
+
| Tasks that stop when a request, socket or query is cancelled | [`signalPlugin`](#tie-an-emit-or-a-listener-to-an-outside-signal-signalplugin) |
|
|
48
|
+
| One listener for a group of events | [`matchPlugin`](#listen-to-a-group-of-events-matchplugin) |
|
|
49
|
+
| A callback for a state manager that emits | [`emitterPlugin`](#hand-the-bus-to-a-state-manager-emitterplugin) |
|
|
50
|
+
| A React component that sends its value to the bus | [`reactPlugin`](#tell-the-bus-when-a-react-value-changes-reactplugin) |
|
|
51
|
+
| Names in the trace | [`tracePlugin`](#name-the-origin-and-the-flow-of-a-trace-traceplugin) |
|
|
52
|
+
| Warnings about leaks while you develop | [`devPlugin`](#find-leaks-while-you-develop-devplugin) |
|
|
53
|
+
| A loop that must not take the app down | [`loopGuardPlugin`](#cut-a-loop-that-runs-away-loopguardplugin) |
|
|
54
|
+
|
|
11
55
|
## Main plugins
|
|
12
56
|
|
|
13
57
|
These four cover what most apps need: shared dependencies, effects, handlers and modules. Start here.
|
|
@@ -16,7 +60,7 @@ These four cover what most apps need: shared dependencies, effects, handlers and
|
|
|
16
60
|
| ------ | ----------------- | ------ | ----------------- |
|
|
17
61
|
| [`contextPlugin`](#put-a-store-or-a-client-on-every-handler-contextplugin) | Your own fields (a store, a client, a logger) on `ctx` of every handler. | `nanoeffects` | The same bus, typed with the fields |
|
|
18
62
|
| [`effectsPlugin`](#use-race-and-delay-without-ctx-effectsplugin) | `race`, `all`, `delay` and your own effects on `ctx`, called without passing `ctx`. | `nanoeffects/effects` | The same bus, typed with the effects |
|
|
19
|
-
| [`actionsPlugin`](#
|
|
63
|
+
| [`actionsPlugin`](#bind-or-replace-a-handler-actionsplugin) | The handlers of actions in a registry, so a test can bind or replace one. | `nanoeffects/actions` | The registry `{ run, handle }` |
|
|
20
64
|
| [`modulePlugin`](#mount-a-domain-as-one-unit-moduleplugin) | A module: the listeners, handlers and tasks of one domain, mounted and unmounted with one call. | `nanoeffects/actions` | The app `{ mount, unmount, isMounted, dispose, actions, createModule }` |
|
|
21
65
|
|
|
22
66
|
### Put a store or a client on every handler: `contextPlugin`
|
|
@@ -33,9 +77,11 @@ const bus = createBus().use(contextPlugin({ store }));
|
|
|
33
77
|
bus.on("save", (draft, { store }) => store.put(draft));
|
|
34
78
|
```
|
|
35
79
|
|
|
80
|
+
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).
|
|
81
|
+
|
|
36
82
|
### Use `race` and `delay` without `ctx`: `effectsPlugin`
|
|
37
83
|
|
|
38
|
-
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#
|
|
84
|
+
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#effectsplugin).
|
|
39
85
|
|
|
40
86
|
```js
|
|
41
87
|
import { createBus } from "nanoeffects";
|
|
@@ -48,7 +94,7 @@ const bus = createBus().use(effects);
|
|
|
48
94
|
|
|
49
95
|
bus.on("load", async (id, { race, delay }) => {
|
|
50
96
|
const result = await race({
|
|
51
|
-
data: (
|
|
97
|
+
data: (child) => api.get(id, child),
|
|
52
98
|
timeout: () => delay(5000),
|
|
53
99
|
});
|
|
54
100
|
});
|
|
@@ -74,7 +120,7 @@ const user = await bus.emit(getUser("1")); // { id: "1" }
|
|
|
74
120
|
|
|
75
121
|
### Mount a domain as one unit: `modulePlugin`
|
|
76
122
|
|
|
77
|
-
A module groups the listeners, the bound handlers and the background tasks of one domain, and ends all of them when it is unmounted. It also makes [hot reload](hmr.md) work. More in [Modules](actions.md#
|
|
123
|
+
A module groups the listeners, the bound handlers and the background tasks of one domain, and ends all of them when it is unmounted. It also makes [hot reload](hmr.md) work. More in [Modules](actions.md#modules).
|
|
78
124
|
|
|
79
125
|
```js
|
|
80
126
|
import { createBus } from "nanoeffects";
|
|
@@ -85,7 +131,7 @@ const bus = createBus();
|
|
|
85
131
|
// modulePlugin — mounts userModule at the end of this block, or at the first use if that is earlier, and returns the app
|
|
86
132
|
const app = bus.use(modulePlugin([userModule]));
|
|
87
133
|
|
|
88
|
-
app.unmount(userModule); // its listeners, bound
|
|
134
|
+
app.unmount(userModule); // its listeners, bound actions and tasks are gone
|
|
89
135
|
```
|
|
90
136
|
|
|
91
137
|
## Extra plugins
|
|
@@ -95,26 +141,27 @@ Add one of these when you meet its need. None of them is required.
|
|
|
95
141
|
| Plugin | What it gives you | Import | `bus.use` returns |
|
|
96
142
|
| ------ | ----------------- | ------ | ----------------- |
|
|
97
143
|
| [`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` |
|
|
144
|
+
| [`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
145
|
| [`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
146
|
| [`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
147
|
| [`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 |
|
|
102
148
|
| [`reactPlugin`](#tell-the-bus-when-a-react-value-changes-reactplugin) | A React hook that emits when a value of a component changes. | `nanoeffects/react` | The same bus, with `useEmitter` |
|
|
103
149
|
| [`devPlugin`](#find-leaks-while-you-develop-devplugin) | Warnings about listeners that pile up, and messages about modules, while you develop. | `nanoeffects/dev` | A function that disconnects it |
|
|
150
|
+
| [`loopGuardPlugin`](#cut-a-loop-that-runs-away-loopguardplugin) | Refuses the emit that would pass a limit of a chain, so a loop ends. | `nanoeffects/guard` | The same bus |
|
|
104
151
|
|
|
105
152
|
### Give one action its own fields: `scopePlugin`
|
|
106
153
|
|
|
107
|
-
An action with a handler, an action, a job or a module can carry its own fields and effects, and one `emit` can replace them. A test gives a fake to one call. More in [Scopes](
|
|
154
|
+
An action with a handler, an action, a job or a module can carry its own fields and effects, and one `emit` can replace them. A test gives a fake to one call. More in [Scopes](context.md#scope-fields-for-an-action-a-module-and-a-call).
|
|
108
155
|
|
|
109
156
|
```js
|
|
110
157
|
import { createBus } from "nanoeffects";
|
|
111
158
|
import { createAction } from "nanoeffects/actions";
|
|
112
159
|
import { scopePlugin } from "nanoeffects/scope";
|
|
113
160
|
|
|
114
|
-
// scopePlugin — makes the scope of
|
|
161
|
+
// scopePlugin — makes the scope of an action work on this bus
|
|
115
162
|
const bus = createBus().use(scopePlugin());
|
|
116
163
|
|
|
117
|
-
// the last argument: fields of this
|
|
164
|
+
// the last argument: fields of this action only
|
|
118
165
|
const getUser = createAction("user/get", (id, { api }) => api.getUser(id), {
|
|
119
166
|
api,
|
|
120
167
|
});
|
|
@@ -123,25 +170,41 @@ const fakeApi = { getUser: async (id) => ({ id, name: "test" }) };
|
|
|
123
170
|
bus.emit(getUser("1"), { api: fakeApi }); // a fake api for this call only
|
|
124
171
|
```
|
|
125
172
|
|
|
126
|
-
### Tie an emit to an outside signal: `signalPlugin`
|
|
173
|
+
### Tie an emit or a listener to an outside signal: `signalPlugin`
|
|
127
174
|
|
|
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
|
|
175
|
+
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
176
|
|
|
130
177
|
```js
|
|
131
178
|
import { createBus } from "nanoeffects";
|
|
132
179
|
import { createAction } from "nanoeffects/actions";
|
|
133
180
|
import { signalPlugin } from "nanoeffects/signal";
|
|
134
181
|
|
|
135
|
-
// signalPlugin — emit takes { signal } for
|
|
182
|
+
// signalPlugin — emit takes { signal } for an action or a job
|
|
136
183
|
const bus = createBus().use(signalPlugin());
|
|
137
184
|
|
|
138
185
|
const getUser = createAction("user/get", (id, { signal }) => api.getUser(id, { signal }));
|
|
139
186
|
|
|
140
187
|
const controller = new AbortController();
|
|
141
|
-
bus.emit(getUser("1"), { signal: controller.signal }); // runs the
|
|
188
|
+
bus.emit(getUser("1"), { signal: controller.signal }); // runs the action
|
|
142
189
|
controller.abort(); // ctx.signal of the handler aborts with it
|
|
143
190
|
```
|
|
144
191
|
|
|
192
|
+
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.
|
|
193
|
+
|
|
194
|
+
```js
|
|
195
|
+
import { createBus } from "nanoeffects";
|
|
196
|
+
import { signalPlugin } from "nanoeffects/signal";
|
|
197
|
+
|
|
198
|
+
// signalPlugin — on and once take { signal } in their options
|
|
199
|
+
const bus = createBus().use(signalPlugin());
|
|
200
|
+
|
|
201
|
+
const controller = new AbortController();
|
|
202
|
+
bus.on("saved", (draft) => console.log(draft), { signal: controller.signal }); // listens until the abort
|
|
203
|
+
controller.abort(); // the listener is gone: bus.get("saved") is empty
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
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.
|
|
207
|
+
|
|
145
208
|
### Name the origin and the flow of a trace: `tracePlugin`
|
|
146
209
|
|
|
147
210
|
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).
|
|
@@ -233,6 +296,18 @@ const disconnect = bus.use(devPlugin());
|
|
|
233
296
|
disconnect(); // stops the diagnostics
|
|
234
297
|
```
|
|
235
298
|
|
|
299
|
+
### Cut a loop that runs away: `loopGuardPlugin`
|
|
300
|
+
|
|
301
|
+
Two handlers that emit each other's events never stop, and a loop of microtasks freezes the page. The plugin follows the chain of emits and refuses the emit that would repeat one event too often or make the chain too long. It needs no tracer, so it can stay on in production. More in [Cut a loop](loop-guard.md).
|
|
302
|
+
|
|
303
|
+
```js
|
|
304
|
+
import { createBus } from "nanoeffects";
|
|
305
|
+
import { loopGuardPlugin } from "nanoeffects/guard";
|
|
306
|
+
|
|
307
|
+
// loopGuardPlugin — refuses the emit that would put one event in a chain 50 times
|
|
308
|
+
const bus = createBus().use(loopGuardPlugin({ maxRepeats: 50 }));
|
|
309
|
+
```
|
|
310
|
+
|
|
236
311
|
## Put them together
|
|
237
312
|
|
|
238
313
|
One bus with every plugin that does not need React, in a file of its own. The events of the app stay in other files. The plugins work in any order; the next section says what that means for modules.
|
|
@@ -254,7 +329,7 @@ import { scopePlugin } from "nanoeffects/scope";
|
|
|
254
329
|
export const bus = createBus()
|
|
255
330
|
.use(contextPlugin({ store })) // ctx.store on every task
|
|
256
331
|
.use(effectsPlugin({ race, all })) // ctx.race and ctx.all, bound to the task
|
|
257
|
-
.use(scopePlugin()) // scopes for
|
|
332
|
+
.use(scopePlugin()) // scopes for actions, jobs, modules and emit
|
|
258
333
|
.use(matchPlugin()) // bus.on takes a function that picks the events
|
|
259
334
|
.use(emitterPlugin()); // bus.emitter(creator)
|
|
260
335
|
|
|
@@ -295,7 +370,7 @@ These connect the bus to something, but are not used with `bus.use`:
|
|
|
295
370
|
- `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
371
|
- `createBus({ trace })` takes a tracer from `nanoeffects/trace`. See [Tracing](tracing.md).
|
|
297
372
|
- `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).
|
|
373
|
+
- `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
374
|
|
|
300
375
|
## Write your own plugin
|
|
301
376
|
|