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/effects.md
CHANGED
|
@@ -1,109 +1,155 @@
|
|
|
1
1
|
# Effects and cancellation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Level C.** Read first: [Policies](policies.md).
|
|
4
|
+
|
|
5
|
+
Use this page for retry, timeouts, waiting, or parallel work — not as the first page for a small bus. Pick **one** effect for your job; the rest is reference.
|
|
4
6
|
|
|
5
7
|
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
8
|
|
|
7
|
-
##
|
|
9
|
+
## Effects
|
|
10
|
+
|
|
11
|
+
Every effect below has its own section with its forms, options and an example. `ctx` is the context of the task.
|
|
12
|
+
|
|
13
|
+
- [`retry`](#retry) — repeat a call after an error.
|
|
14
|
+
- [`timeout`](#timeout) — stop a call that takes too long.
|
|
15
|
+
- [`poll`](#poll) — call a function at an interval.
|
|
16
|
+
- [`backoff`](#backoff) — make the pause between retries grow.
|
|
17
|
+
- [`delay`](#delay) — wait for a time; an abort ends the wait.
|
|
18
|
+
- [`cancellable`](#cancellable) — a Promise tied to the task signal, for callbacks and timers.
|
|
19
|
+
- [`onAbort`](#onabort) — run code when the task is aborted.
|
|
20
|
+
- [`ctx.on` and `ctx.off`](#ctxon-and-ctxoff) — subscribe to the bus inside a task.
|
|
21
|
+
- [`take`](#take) — wait for the next event inside a handler.
|
|
22
|
+
- [`all`](#all) — run several calls together; one failure cancels the rest.
|
|
23
|
+
- [`allSettled`](#allsettled) — wait for all of them, whatever the outcome.
|
|
24
|
+
- [`race`](#race) — take the first result and cancel the rest.
|
|
25
|
+
- [`effectsPlugin`](#effectsplugin) — short forms of effects: call them without `ctx`.
|
|
26
|
+
- [Effects outside a task](#effects-outside-a-task) — call effects where there is no handler.
|
|
27
|
+
|
|
28
|
+
## `retry`
|
|
29
|
+
|
|
30
|
+
Runs a function again after an error. Use it when a call fails now and then, for example because of the network.
|
|
8
31
|
|
|
9
|
-
|
|
32
|
+
Forms:
|
|
10
33
|
|
|
11
|
-
-
|
|
12
|
-
-
|
|
34
|
+
- `retry(fn, options?)` — a wrapper of a handler: takes a function `(payload, ctx) => result` and returns one. Pass it to `on` next to policies.
|
|
35
|
+
- `retry(ctx, fn, options?)` — a call inside the body of a handler.
|
|
13
36
|
|
|
14
|
-
|
|
37
|
+
Options:
|
|
38
|
+
|
|
39
|
+
- `times` — total attempts. It is `3` by default.
|
|
40
|
+
- `delay` — a number or a function `(attempt, error) => ms`. For a growing pause see [`backoff`](#backoff).
|
|
41
|
+
- `shouldRetry(error, attempt)` — return `false` to stop early.
|
|
42
|
+
- `fn` gets the attempt number, starting at `1`.
|
|
43
|
+
- On abort, `retry` rejects with the abort reason.
|
|
15
44
|
|
|
16
45
|
```js
|
|
17
46
|
import { createBus } from "nanoeffects";
|
|
18
|
-
import {
|
|
47
|
+
import { retry } from "nanoeffects/effects";
|
|
19
48
|
import { latest } from "nanoeffects/policies";
|
|
20
49
|
|
|
21
50
|
const bus = createBus();
|
|
22
51
|
const loadUser = (id, { signal }) => fetchUser(id, { signal });
|
|
23
52
|
|
|
24
|
-
//
|
|
25
|
-
const
|
|
26
|
-
// retry — up to 4 attempts with backoff
|
|
27
|
-
const loadWithRetry = retry(loadOnce, {
|
|
53
|
+
// retry — a wrapper of the handler: up to 4 attempts, but not after a fatal error
|
|
54
|
+
const loadWithRetry = retry(loadUser, {
|
|
28
55
|
times: 4,
|
|
29
|
-
delay: (attempt) => 2 ** attempt * 100,
|
|
30
56
|
shouldRetry: (error) => !isFatal(error),
|
|
31
57
|
});
|
|
32
|
-
// latest — a newer load aborts the older retry chain
|
|
33
|
-
bus.on("load", latest(loadWithRetry));
|
|
34
58
|
|
|
35
|
-
//
|
|
36
|
-
bus.on("
|
|
59
|
+
// latest — a newer load aborts the older chain of attempts
|
|
60
|
+
bus.on("load", latest(loadWithRetry));
|
|
37
61
|
|
|
38
|
-
// ctx.run — run
|
|
62
|
+
// ctx.run — run the wrapped function as a child task (aborted with the handler)
|
|
39
63
|
bus.on("open", async (id, ctx) => {
|
|
40
|
-
const
|
|
41
|
-
const user = await ctx.run(load, id);
|
|
64
|
+
const user = await ctx.run(loadWithRetry, id);
|
|
42
65
|
ctx.emit("opened", user);
|
|
43
66
|
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Use the wrapper for a whole handler or a whole `ctx.run`. Each attempt gets its own child context: an abort of the caller reaches it, and an abort of one attempt does not reach the caller. For one call in the middle of a body use the form with `ctx`:
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
import { createBus } from "nanoeffects";
|
|
73
|
+
import { retry } from "nanoeffects/effects";
|
|
74
|
+
|
|
75
|
+
const bus = createBus();
|
|
44
76
|
|
|
45
|
-
// ctx-first form — one call inside a body
|
|
46
77
|
bus.on("load", async (id, ctx) => {
|
|
78
|
+
// retry with ctx — one call inside the body
|
|
47
79
|
const user = await retry(
|
|
48
80
|
ctx,
|
|
49
81
|
() => fetchUser(id, { signal: ctx.signal }),
|
|
50
82
|
{ times: 4 },
|
|
51
83
|
);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
});
|
|
55
|
-
await poll(ctx, () => refresh(), 10_000);
|
|
56
|
-
ctx.emit("ready", { user, data });
|
|
84
|
+
|
|
85
|
+
ctx.emit("loaded", user);
|
|
57
86
|
});
|
|
58
87
|
```
|
|
59
88
|
|
|
60
|
-
|
|
89
|
+
## `timeout`
|
|
61
90
|
|
|
62
|
-
|
|
91
|
+
Rejects when the work takes longer than the time you set, and aborts it. Use it when a request can hang.
|
|
63
92
|
|
|
64
|
-
|
|
93
|
+
Forms:
|
|
65
94
|
|
|
66
|
-
|
|
95
|
+
- `timeout(ms, handler)` — a wrapper of a handler.
|
|
96
|
+
- `timeout(ctx, ms, fn)` — a call inside a body; `fn` gets the context of its own child task.
|
|
67
97
|
|
|
68
|
-
|
|
69
|
-
- `delay` — number or `(attempt, error) => ms`.
|
|
70
|
-
- `shouldRetry(error, attempt)` — stop early.
|
|
71
|
-
- `fn` gets the attempt number starting at `1`.
|
|
72
|
-
- On abort, rejects with the abort reason.
|
|
98
|
+
The error is a `DOMException` named `TimeoutError` (like `AbortSignal.timeout`). `timeout` rejects even if the function ignores its signal.
|
|
73
99
|
|
|
74
|
-
|
|
100
|
+
```js
|
|
101
|
+
import { createBus } from "nanoeffects";
|
|
102
|
+
import { timeout } from "nanoeffects/effects";
|
|
75
103
|
|
|
76
|
-
|
|
77
|
-
|
|
104
|
+
const bus = createBus();
|
|
105
|
+
const loadUser = (id, { signal }) => fetchUser(id, { signal });
|
|
78
106
|
|
|
79
|
-
|
|
107
|
+
// timeout — a wrapper of the handler: it must finish within 5 s
|
|
108
|
+
bus.on("load", timeout(5000, loadUser));
|
|
80
109
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
110
|
+
bus.on("open", async (id, ctx) => {
|
|
111
|
+
// timeout with ctx — one call inside the body; child carries the signal of the request
|
|
112
|
+
const data = await timeout(ctx, 5000, (child) => fetchData(id, child));
|
|
113
|
+
ctx.emit("opened", data);
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## `poll`
|
|
118
|
+
|
|
119
|
+
Calls a function again and again at an interval, until the task is aborted. Use it to refresh data every N seconds. It is like `interval` in patronum.
|
|
120
|
+
|
|
121
|
+
Forms:
|
|
122
|
+
|
|
123
|
+
- `poll(ms, fn, options?)` — a wrapper of a handler.
|
|
124
|
+
- `poll(ctx, fn, ms)` — a call inside a body.
|
|
86
125
|
|
|
87
|
-
|
|
126
|
+
Options:
|
|
127
|
+
|
|
128
|
+
- The first call happens after `ms`. `leading: true` also calls at once.
|
|
129
|
+
- The next ticks keep a steady rate whatever the duration of `fn`, so a slow `fn` can overlap the next call. `waitForFn: true` starts each pause when the call ends, so calls never overlap.
|
|
130
|
+
- `fn` gets the call number, starting at `0`.
|
|
131
|
+
- An error from `fn` stops polling and rejects. An abort ends it normally and clears the timer.
|
|
88
132
|
|
|
89
133
|
```js
|
|
90
134
|
import { createBus } from "nanoeffects";
|
|
91
|
-
import {
|
|
135
|
+
import { poll } from "nanoeffects/effects";
|
|
92
136
|
|
|
93
137
|
const bus = createBus();
|
|
94
138
|
|
|
139
|
+
// poll — a wrapper of the handler: call refresh every 10 s until the task aborts
|
|
140
|
+
bus.on("watch", poll(10_000, (_, { signal }) => refresh({ signal })));
|
|
141
|
+
|
|
95
142
|
bus.on("sync", async (_, ctx) => {
|
|
96
|
-
|
|
97
|
-
poll(ctx,
|
|
98
|
-
await delay(ctx, () => 200 + Math.random() * 200); // 200–400 ms
|
|
143
|
+
// poll with ctx — the same inside a body
|
|
144
|
+
await poll(ctx, () => refresh(), 10_000);
|
|
99
145
|
});
|
|
100
146
|
```
|
|
101
147
|
|
|
102
|
-
|
|
148
|
+
`retry`, `timeout` and `poll` are traced: the effect is a group, and each attempt or call is a `run` child.
|
|
103
149
|
|
|
104
|
-
##
|
|
150
|
+
## `backoff`
|
|
105
151
|
|
|
106
|
-
Use
|
|
152
|
+
Makes the pause between retries grow, with a cap and jitter. Use it when a server or network fails under load: retrying at once, or at the same fixed pause, sends every client back at the same moment and can keep the server down. `backoff(options)` returns a function `(attempt) => ms`.
|
|
107
153
|
|
|
108
154
|
The shortest form is a `delay` function. It needs no import:
|
|
109
155
|
|
|
@@ -143,48 +189,41 @@ const loadWithRetry = retry(loadUser, {
|
|
|
143
189
|
- `retry` counts attempts from 1. `poll` counts calls from 0, so with `poll` the first pause is `base / factor`.
|
|
144
190
|
- The result is a plain function `(attempt) => ms`, so it also fits `delay(ctx, …)`.
|
|
145
191
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
If handlers call the same helpers often, put them on the task once. A handler then destructures them like `signal`, with no `ctx` argument. Only the effects you pass are imported, so the rest stay out of the bundle ([package entries](package.md)).
|
|
192
|
+
A pause can be a function wherever a time is given:
|
|
149
193
|
|
|
150
194
|
```js
|
|
151
195
|
import { createBus } from "nanoeffects";
|
|
152
|
-
import {
|
|
153
|
-
|
|
154
|
-
// effectsPlugin — add race, all and delay to the context of every task
|
|
155
|
-
const effects = effectsPlugin({ race, all, delay });
|
|
156
|
-
|
|
157
|
-
// use returns the same bus, typed with the new fields
|
|
158
|
-
const bus = createBus().use(effects);
|
|
196
|
+
import { delay, poll, retry } from "nanoeffects/effects";
|
|
159
197
|
|
|
160
|
-
bus
|
|
161
|
-
// race — same as race(ctx, …)
|
|
162
|
-
const result = await race({
|
|
163
|
-
data: ({ signal }) => fetchData(id, { signal }),
|
|
164
|
-
// a starter gets its own child with the same bound effects, so delay(5000) stops with the race
|
|
165
|
-
timeout: ({ delay }) => delay(5000),
|
|
166
|
-
});
|
|
167
|
-
if ("timeout" in result) throw new Error("timed out");
|
|
198
|
+
const bus = createBus();
|
|
168
199
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
});
|
|
200
|
+
bus.on("sync", async (_, ctx) => {
|
|
201
|
+
retry(ctx, load, { times: 5, delay: (attempt) => 2 ** attempt * 100 }); // 200, 400, 800, …
|
|
202
|
+
poll(ctx, refresh, (call) => Math.min(1000 * 2 ** call, 60_000)); // slows to once a minute
|
|
203
|
+
await delay(ctx, () => 200 + Math.random() * 200); // 200–400 ms
|
|
174
204
|
});
|
|
175
205
|
```
|
|
176
206
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
207
|
+
## `delay`
|
|
208
|
+
|
|
209
|
+
Waits for the given time and rejects on abort. A plain `setTimeout` would go on after the task is aborted; `delay` ends the wait at once and clears the timer. `delay(ctx, ms)`, where `ms` can be a function.
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
import { createBus } from "nanoeffects";
|
|
213
|
+
import { delay } from "nanoeffects/effects";
|
|
214
|
+
|
|
215
|
+
const bus = createBus();
|
|
182
216
|
|
|
183
|
-
|
|
217
|
+
bus.on("saved", async (_, ctx) => {
|
|
218
|
+
// delay — wait 3 s; an abort ends the wait at once and clears the timer
|
|
219
|
+
await delay(ctx, 3000);
|
|
220
|
+
ctx.emit("toast/hidden");
|
|
221
|
+
});
|
|
222
|
+
```
|
|
184
223
|
|
|
185
|
-
|
|
224
|
+
## `cancellable`
|
|
186
225
|
|
|
187
|
-
|
|
226
|
+
A Promise tied to the task signal. Use it for APIs that work with callbacks, timers or listeners: the signal cannot stop them by itself, so `cancellable` rejects the Promise on abort and runs your cleanup. `cancellable(ctx, executor)` works like `new Promise`, but the executor gets `{ resolve, reject, onCancel }`.
|
|
188
227
|
|
|
189
228
|
```js
|
|
190
229
|
import { createBus } from "nanoeffects";
|
|
@@ -193,7 +232,7 @@ import { latest } from "nanoeffects/policies";
|
|
|
193
232
|
|
|
194
233
|
const bus = createBus();
|
|
195
234
|
|
|
196
|
-
// cancellable —
|
|
235
|
+
// cancellable — a Promise tied to the task signal; onCancel registers a cleanup
|
|
197
236
|
// latest — a newer wait aborts the older timer
|
|
198
237
|
bus.on(
|
|
199
238
|
"wait",
|
|
@@ -206,13 +245,13 @@ bus.on(
|
|
|
206
245
|
);
|
|
207
246
|
```
|
|
208
247
|
|
|
209
|
-
- If the task aborts before the
|
|
248
|
+
- If the task aborts before the Promise settles, cleanups run (last registered first) and the Promise rejects with the abort reason.
|
|
210
249
|
- If the task is already aborted, the executor does not run.
|
|
211
|
-
- After the
|
|
250
|
+
- After the Promise settles, the abort listener is removed and cleanups are dropped.
|
|
212
251
|
|
|
213
|
-
##
|
|
252
|
+
## `onAbort`
|
|
214
253
|
|
|
215
|
-
`onAbort(ctx, fn)` runs `fn(reason)` on abort (at once if already aborted) and returns a function that unsubscribes.
|
|
254
|
+
Runs code when the task is aborted: a hook of the life of the task. A task may be aborted because the user left or a newer call replaced it, and whatever it opened (a socket, a timer, a listener) must be closed then. `onAbort(ctx, fn)` runs `fn(reason)` on abort (at once if the task is already aborted) and returns a function that unsubscribes.
|
|
216
255
|
|
|
217
256
|
```js
|
|
218
257
|
import { createBus } from "nanoeffects";
|
|
@@ -222,14 +261,14 @@ const bus = createBus();
|
|
|
222
261
|
|
|
223
262
|
bus.on("connect", (url, ctx) => {
|
|
224
263
|
const socket = new WebSocket(url);
|
|
225
|
-
// onAbort —
|
|
264
|
+
// onAbort — close the socket when this task aborts
|
|
226
265
|
onAbort(ctx, () => socket.close());
|
|
227
266
|
});
|
|
228
267
|
```
|
|
229
268
|
|
|
230
|
-
##
|
|
269
|
+
## `ctx.on` and `ctx.off`
|
|
231
270
|
|
|
232
|
-
A task context has `on` and `off`, like the bus
|
|
271
|
+
Subscribes to the bus inside a task. A task context has `on` and `off`, like the bus, and a subscription made with `ctx.on` ends when the task aborts.
|
|
233
272
|
|
|
234
273
|
```js
|
|
235
274
|
import { createBus } from "nanoeffects";
|
|
@@ -237,15 +276,29 @@ import { createBus } from "nanoeffects";
|
|
|
237
276
|
const bus = createBus();
|
|
238
277
|
|
|
239
278
|
bus.on("watch", (_, ctx) => {
|
|
240
|
-
|
|
279
|
+
// gone when this task aborts
|
|
280
|
+
const stop = ctx.on("tick", (n) => console.log(n));
|
|
281
|
+
|
|
282
|
+
// or end it sooner yourself: stop() unsubscribes
|
|
283
|
+
ctx.on("watch/closed", stop);
|
|
241
284
|
});
|
|
242
285
|
```
|
|
243
286
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
- `ctx.
|
|
287
|
+
Be careful with it:
|
|
288
|
+
|
|
289
|
+
- **A task that finishes by itself is not aborted**, so its `ctx.on` subscription stays. When the work is done, unsubscribe with the function that `ctx.on` returns, or call `ctx.abort()`.
|
|
290
|
+
- To wait for **one** event, use [`take`](#take): it unsubscribes by itself.
|
|
291
|
+
- For listeners that live as long as a feature, use a [module](actions.md#modules): `unmount` removes all of them.
|
|
292
|
+
|
|
293
|
+
Also:
|
|
247
294
|
|
|
248
|
-
|
|
295
|
+
- After an abort, `ctx.on` does nothing.
|
|
296
|
+
- Under `latest`, the subscriptions of a superseded run end with that run.
|
|
297
|
+
- `ctx.off(event)` clears every handler of the event on the bus, not only the ones of this task.
|
|
298
|
+
|
|
299
|
+
## `take`
|
|
300
|
+
|
|
301
|
+
Waits for the next event of the bus inside a handler and resolves with its payload. `take(ctx, event, options?)`. The `event` is the name of an event or an action creator, as for `bus.on`.
|
|
249
302
|
|
|
250
303
|
```js
|
|
251
304
|
import { createBus } from "nanoeffects";
|
|
@@ -267,22 +320,65 @@ bus.on("login", async (user, ctx) => {
|
|
|
267
320
|
const id = await timeout(
|
|
268
321
|
ctx,
|
|
269
322
|
5000,
|
|
270
|
-
take("saved", { predicate: (savedId) => savedId === draft.id }), // take without ctx
|
|
323
|
+
take("saved", { predicate: (savedId) => savedId === draft.id }), // take without ctx — a ready-made function for the group
|
|
271
324
|
);
|
|
272
325
|
});
|
|
273
326
|
```
|
|
274
327
|
|
|
275
|
-
-
|
|
328
|
+
- It sees only events emitted after the call, takes one, and unsubscribes.
|
|
276
329
|
- With an action creator the result has the payload type of the creator. A creator made without a payload type (`createAction("saved")`) takes it from the event map of the bus.
|
|
277
330
|
- `options.predicate` skips payloads it rejects. In TypeScript, a type guard (`(payload): payload is Admin`) narrows the type of the result.
|
|
278
|
-
- `options.signal` stops the wait. Inside `race`, `all` or `timeout` you do not need it: the group already stops the
|
|
279
|
-
- On abort of the task or of `options.signal`, unsubscribes and rejects with the abort reason.
|
|
331
|
+
- `options.signal` stops the wait. Inside `race`, `all` or `timeout` you do not need it: the group already stops the wait, so write `take("saved")` without options.
|
|
332
|
+
- On abort of the task or of `options.signal`, it unsubscribes and rejects with the abort reason.
|
|
333
|
+
|
|
334
|
+
## `all`
|
|
280
335
|
|
|
281
|
-
|
|
336
|
+
Runs several calls together; one failure cancels the rest. Use it to load several things at once. `all(ctx, effects)` takes an object or an array of functions, and the result keeps that shape.
|
|
337
|
+
|
|
338
|
+
```js
|
|
339
|
+
import { createBus } from "nanoeffects";
|
|
340
|
+
import { all } from "nanoeffects/effects";
|
|
341
|
+
|
|
342
|
+
const bus = createBus();
|
|
343
|
+
|
|
344
|
+
bus.on("open", async (id, ctx) => {
|
|
345
|
+
// all — run both requests together; one failure cancels the other. child is the context of its own request
|
|
346
|
+
const { user, posts } = await all(ctx, {
|
|
347
|
+
user: (child) => fetchUser(id, child),
|
|
348
|
+
posts: (child) => fetchPosts(id, child),
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
ctx.emit("opened", { user, posts });
|
|
352
|
+
});
|
|
353
|
+
```
|
|
282
354
|
|
|
283
|
-
`
|
|
355
|
+
Every function of the group gets `child`, the context of its own child task. Pass `child` as the first argument to any effect, as a handler passes `ctx`: when the group ends or fails, `child` is aborted and stops exactly this effect. If your API takes the signal in its options, `child` has a `signal`, so pass it as the options: `(child) => fetchUser(id, child)` (`fetch` reads only `signal` from it). The same holds for `allSettled`, `race` and `timeout`.
|
|
284
356
|
|
|
285
|
-
|
|
357
|
+
**Limit how many run at once.** `all` and `allSettled` start everything together. Over a long list that is a burst of requests. Pass `{ concurrency }` as the third argument and at most that many run at a time; the next one starts when a running one settles:
|
|
358
|
+
|
|
359
|
+
```js
|
|
360
|
+
import { createBus } from "nanoeffects";
|
|
361
|
+
import { all, retry } from "nanoeffects/effects";
|
|
362
|
+
|
|
363
|
+
const bus = createBus();
|
|
364
|
+
|
|
365
|
+
bus.on("sync", async (ids, ctx) => {
|
|
366
|
+
// all with concurrency — 5 loads at a time, results in the order of ids
|
|
367
|
+
const users = await all(
|
|
368
|
+
ctx,
|
|
369
|
+
ids.map((id) => retry(() => loadUser(id), { times: 2 })),
|
|
370
|
+
{ concurrency: 5 },
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
render(users);
|
|
374
|
+
});
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
The result has the same order and shape as without the option. If one call of `all` fails, the running ones are cancelled and the waiting ones are never started. `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 call that waited for a free slot would never run.
|
|
378
|
+
|
|
379
|
+
## `allSettled`
|
|
380
|
+
|
|
381
|
+
Waits for every call and never fails because of one. Use it when a partial answer is better than none. `allSettled(ctx, effects)`. Each result is `{ status: "fulfilled", value }` or `{ status: "rejected", reason }`. Nothing is cancelled when a call fails; only an abort of `ctx` rejects it.
|
|
286
382
|
|
|
287
383
|
```js
|
|
288
384
|
import { createBus } from "nanoeffects";
|
|
@@ -291,9 +387,10 @@ import { allSettled } from "nanoeffects/effects";
|
|
|
291
387
|
const bus = createBus();
|
|
292
388
|
|
|
293
389
|
bus.on("open", async (id, ctx) => {
|
|
390
|
+
// allSettled — wait for both, whatever the outcome
|
|
294
391
|
const { user, posts } = await allSettled(ctx, {
|
|
295
|
-
user: (
|
|
296
|
-
posts: (
|
|
392
|
+
user: (child) => fetchUser(id, child),
|
|
393
|
+
posts: (child) => fetchPosts(id, child),
|
|
297
394
|
});
|
|
298
395
|
|
|
299
396
|
if (user.status === "fulfilled") {
|
|
@@ -302,7 +399,9 @@ bus.on("open", async (id, ctx) => {
|
|
|
302
399
|
});
|
|
303
400
|
```
|
|
304
401
|
|
|
305
|
-
`race
|
|
402
|
+
## `race`
|
|
403
|
+
|
|
404
|
+
Settles with the first call that finishes and cancels the others. Use it to take the fastest answer or to cut a wait short. `race(ctx, effects)`.
|
|
306
405
|
|
|
307
406
|
```js
|
|
308
407
|
import { createBus } from "nanoeffects";
|
|
@@ -311,9 +410,9 @@ import { delay, race, take } from "nanoeffects/effects";
|
|
|
311
410
|
const bus = createBus();
|
|
312
411
|
|
|
313
412
|
bus.on("save", async (draft, ctx) => {
|
|
314
|
-
// race — the first
|
|
413
|
+
// race — the first to finish wins, the other is cancelled
|
|
315
414
|
const result = await race(ctx, {
|
|
316
|
-
waited: delay(1000), // delay without ctx — a
|
|
415
|
+
waited: delay(1000), // delay without ctx — a ready-made function for the group: race gives it its own child
|
|
317
416
|
cancelled: take("cancel"), // take without ctx — the same, waits for the next "cancel"
|
|
318
417
|
});
|
|
319
418
|
|
|
@@ -323,9 +422,9 @@ bus.on("save", async (draft, ctx) => {
|
|
|
323
422
|
});
|
|
324
423
|
```
|
|
325
424
|
|
|
326
|
-
**`delay` and `take` without `ctx` are ready-made
|
|
425
|
+
**`delay` and `take` without `ctx` are ready-made functions for a group.** In a group, `delay(1000)` and `take("cancel")` do what `(child) => delay(child, 1000)` and `(child) => take(child, "cancel")` do: the group calls them with the child, so the wait stops with the group. Use this form inside `race`, `all`, `allSettled` and `timeout`; use `delay(ctx, 1000)` in plain handler code. They wait only when a group calls them: `await delay(1000)` on its own does not wait, because it is a function, not a Promise. In TypeScript, one kept in a variable outside a group does not know the events of your bus; name them yourself: `const cancelled: StarterOf<Events> = take("cancel")` (`StarterOf` is exported by `nanoeffects/effects`).
|
|
327
426
|
|
|
328
|
-
The wrapper form of `retry`, `timeout` and `poll`
|
|
427
|
+
The wrapper form of `retry`, `timeout` and `poll` works in a group too: the group calls it with the child, and the function you wrap gets `undefined` as its payload and the child as its context. Write the data it needs into a closure:
|
|
329
428
|
|
|
330
429
|
```js
|
|
331
430
|
import { createBus } from "nanoeffects";
|
|
@@ -335,7 +434,7 @@ const bus = createBus();
|
|
|
335
434
|
|
|
336
435
|
bus.on("open", async (id, ctx) => {
|
|
337
436
|
const result = await race(ctx, {
|
|
338
|
-
// retry without ctx —
|
|
437
|
+
// retry without ctx — up to 3 attempts, each with the signal of the child
|
|
339
438
|
user: retry((_, { signal }) => fetchUser(id, { signal }), { times: 3 }),
|
|
340
439
|
// delay without ctx — the loser is cancelled when the other one finishes
|
|
341
440
|
waited: delay(5000),
|
|
@@ -347,9 +446,9 @@ bus.on("open", async (id, ctx) => {
|
|
|
347
446
|
});
|
|
348
447
|
```
|
|
349
448
|
|
|
350
|
-
|
|
449
|
+
So `retry`, `timeout` and `poll` have three call forms: with `ctx`, as a Task for `bus.on` and `ctx.run`, and inside a group. The arguments tell them apart: a context first is the `ctx` form; a function called with a payload and a context is a Task; the same function called by a group with one argument, its child, is the group form. This does not depend on how many parameters your function declares.
|
|
351
450
|
|
|
352
|
-
**`all`, `allSettled` and `race` without `ctx`
|
|
451
|
+
**`all`, `allSettled` and `race` without `ctx` also work in a group**, so a group nests in another group with no wrapper. Load two things, but give up after five seconds:
|
|
353
452
|
|
|
354
453
|
```js
|
|
355
454
|
import { createBus } from "nanoeffects";
|
|
@@ -359,7 +458,7 @@ const bus = createBus();
|
|
|
359
458
|
|
|
360
459
|
bus.on("open", async (id, ctx) => {
|
|
361
460
|
const result = await race(ctx, {
|
|
362
|
-
// all without ctx —
|
|
461
|
+
// all without ctx — loads both, each with up to 3 attempts
|
|
363
462
|
loaded: all({
|
|
364
463
|
user: retry((_, { signal }) => fetchUser(id, { signal }), { times: 3 }),
|
|
365
464
|
posts: retry((_, { signal }) => fetchPosts(id, { signal }), { times: 3 }),
|
|
@@ -374,37 +473,66 @@ bus.on("open", async (id, ctx) => {
|
|
|
374
473
|
});
|
|
375
474
|
```
|
|
376
475
|
|
|
377
|
-
One argument is the short form and two arguments are the form with `ctx`: `all(effects)` returns a
|
|
476
|
+
One argument is the short form and two arguments are the form with `ctx`: `all(effects)` returns a function for a group, `all(ctx, effects)` runs the group. The inner group stops with the outer one and cancels its own calls. Passing only a context, `all(ctx)`, is a mistake and throws a `TypeError` at once. The form without `ctx` takes no options, because a second argument would look like the form with `ctx`; write `(child) => all(child, effects, { concurrency: 5 })` there. In TypeScript, a group kept in a variable outside a group needs a type: `StarterOf<Events>`, or `StarterOf<Events, Result>` when you need its value.
|
|
378
477
|
|
|
379
|
-
|
|
478
|
+
## `effectsPlugin`
|
|
479
|
+
|
|
480
|
+
Short forms of effects: `effectsPlugin(effects)` puts chosen effects on `ctx`, so a handler calls them without passing `ctx` around. Use it when many handlers call the same helpers, to cut the boilerplate. A handler destructures them like `signal`. Only the effects you pass are imported, so the rest stay out of the bundle ([package entries](package.md)).
|
|
380
481
|
|
|
381
482
|
```js
|
|
382
483
|
import { createBus } from "nanoeffects";
|
|
383
|
-
import { all,
|
|
484
|
+
import { all, delay, effectsPlugin, race } from "nanoeffects/effects";
|
|
384
485
|
|
|
385
|
-
|
|
486
|
+
// effectsPlugin — add race, all and delay to the context of every task
|
|
487
|
+
const effects = effectsPlugin({ race, all, delay });
|
|
386
488
|
|
|
387
|
-
bus
|
|
388
|
-
|
|
389
|
-
const users = await all(
|
|
390
|
-
ctx,
|
|
391
|
-
ids.map((id) => retry(() => loadUser(id), { times: 2 })),
|
|
392
|
-
{ concurrency: 5 },
|
|
393
|
-
);
|
|
489
|
+
// use returns the same bus, typed with the new fields
|
|
490
|
+
const bus = createBus().use(effects);
|
|
394
491
|
|
|
395
|
-
|
|
492
|
+
bus.on("load", async (id, { race, all, delay }) => {
|
|
493
|
+
// race — same as race(ctx, …)
|
|
494
|
+
const result = await race({
|
|
495
|
+
data: (child) => fetchData(id, child),
|
|
496
|
+
// a group function gets its own child with the same bound effects, so delay(5000) stops with the race
|
|
497
|
+
timeout: ({ delay }) => delay(5000),
|
|
498
|
+
});
|
|
499
|
+
if ("timeout" in result) throw new Error("timed out");
|
|
500
|
+
|
|
501
|
+
// all — same as all(ctx, …)
|
|
502
|
+
const { user, posts } = await all({
|
|
503
|
+
user: (child) => fetchUser(id, child),
|
|
504
|
+
posts: (child) => fetchPosts(id, child),
|
|
505
|
+
});
|
|
396
506
|
});
|
|
397
507
|
```
|
|
398
508
|
|
|
399
|
-
The
|
|
509
|
+
- The key is the name the effect gets on `ctx`. `race`, `all`, `allSettled`, `take` and `delay` are typed by hand.
|
|
510
|
+
- Your own function works too if its first parameter is `{ signal }` (an *effect*): `effectsPlugin({ fetchJson })` gives `ctx.fetchJson(url)`. TypeScript removes the first parameter from its type; for a generic function see [keep a generic effect generic](context.md#keep-a-generic-effect-generic).
|
|
511
|
+
- Leave `retry`, `timeout` and `poll` out: bound to the task they would look like their handler wrappers.
|
|
512
|
+
- Fields from `createBus({ context })` stay. TypeScript rejects an effect whose name is already a context field of the bus, including one added by an earlier `effectsPlugin`.
|
|
513
|
+
- The free forms keep working: `race(ctx, …)`.
|
|
400
514
|
|
|
515
|
+
## Effects outside a task
|
|
401
516
|
|
|
402
|
-
|
|
517
|
+
Effects take `ctx` first. Inside a handler it is the `ctx` you already have. Outside a task (in a plain function, a script or a test) there is no `ctx`: pass an object with a `signal`, or `{}` if you never abort.
|
|
403
518
|
|
|
404
|
-
|
|
519
|
+
```js
|
|
520
|
+
import { delay, retry } from "nanoeffects/effects";
|
|
405
521
|
|
|
406
|
-
|
|
522
|
+
const controller = new AbortController();
|
|
407
523
|
|
|
408
|
-
|
|
524
|
+
// outside a task there is no ctx: an object with a signal is enough
|
|
525
|
+
const context = { signal: controller.signal };
|
|
409
526
|
|
|
410
|
-
|
|
527
|
+
// the effects take it as ctx; fetchUser gets the same signal, so the request itself stops too
|
|
528
|
+
const user = await retry(
|
|
529
|
+
context,
|
|
530
|
+
() => fetchUser("1", { signal: controller.signal }),
|
|
531
|
+
{ times: 3 },
|
|
532
|
+
);
|
|
533
|
+
|
|
534
|
+
await delay(context, 500);
|
|
535
|
+
|
|
536
|
+
// stops the pending retries and waits
|
|
537
|
+
controller.abort();
|
|
538
|
+
```
|
package/docs/errors.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Handle errors and unheard events
|
|
2
2
|
|
|
3
|
+
> **Level B.** Read first: [Guide](guide.md#1-create-a-bus) steps 1 and 2.
|
|
4
|
+
|
|
3
5
|
Defaults match a small bus. Set these when you want isolation or typo warnings.
|
|
4
6
|
|
|
5
7
|
## Isolate failing handlers
|
|
@@ -21,9 +23,9 @@ const bus = createBus({
|
|
|
21
23
|
|
|
22
24
|
## Warn when nobody listens
|
|
23
25
|
|
|
24
|
-
A typo in an event name (`emit("serch")`) does nothing and says nothing. `onUnhandled` runs when an event is emitted and nobody listens (a listener of every event, `"*"`, counts). It is not called for
|
|
26
|
+
A typo in an event name (`emit("serch")`) does nothing and says nothing. `onUnhandled` runs when an event is emitted and nobody listens (a listener of every event, `"*"`, counts). It is not called for an action that has a handler to run. An action without a handler is only an event, so it is called for it.
|
|
25
27
|
|
|
26
|
-
Use it as a warning in development:
|
|
28
|
+
Use it as a warning in development (`import.meta.env.DEV` is the Vite flag; use your own check in another bundler):
|
|
27
29
|
|
|
28
30
|
```js
|
|
29
31
|
import { createBus } from "nanoeffects";
|