nanoeffects 0.29.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-jeEqIKgK.d.ts → action-creator-6rlBTmH9.d.ts} +27 -2
- package/dist/actions.d.ts +3 -3
- package/dist/actions.js +1 -1
- package/dist/{bus-0uYn_kRj.js → bus-DFlmNJ6h.js} +1 -1
- package/dist/context-B5Z2RpJt.js +1 -0
- package/dist/{context-types-Bme1mzkn.d.ts → context-types-DKNc5mkS.d.ts} +1 -1
- package/dist/context.d.ts +2 -2
- 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 +1 -1
- package/dist/index.js +1 -1
- package/dist/match.d.ts +1 -1
- package/dist/{module-BWMW83qk.d.ts → module-G39MaVOt.d.ts} +2 -2
- package/dist/notrace/{action-creator-xpQ_I2h8.d.ts → action-creator-BrMSO3RP.d.ts} +1 -0
- package/dist/notrace/actions.d.ts +2 -2
- package/dist/notrace/effects.d.ts +2 -2
- package/dist/notrace/index.d.ts +1 -1
- package/dist/notrace/{plugin-6LhNx0th.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 +1 -1
- package/dist/oop.d.ts +1 -1
- package/dist/oop.js +1 -1
- package/dist/{plugin-BaErevLy.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 +2 -2
- package/dist/signal.d.ts +2 -2
- 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 +151 -2
- package/dist/trace.js +3 -1
- package/dist/{types-D5xsR37j.d.ts → types-N_cdRr1r.d.ts} +2 -2
- package/dist/vue.d.ts +1 -1
- package/dist/{with-policy-C6BIiPIi.js → with-policy-CeM1rtES.js} +1 -1
- package/dist/{with-policy-BZVRBUOm.d.ts → with-policy-CthiBp_z.d.ts} +1 -1
- package/docs/README.md +62 -31
- package/docs/actions.md +44 -27
- package/docs/cancel-stale-requests.md +2 -0
- package/docs/chat-example.md +3 -1
- package/docs/chat-widget.md +2 -0
- package/docs/context.md +321 -9
- package/docs/debounce-and-abort.md +3 -1
- package/docs/debugging-loops.md +283 -0
- package/docs/effects.md +255 -138
- package/docs/errors.md +4 -2
- package/docs/examples.md +60 -0
- package/docs/guide.md +39 -22
- package/docs/hmr.md +35 -1
- package/docs/loop-guard.md +109 -0
- package/docs/migrating-from-effector.md +4 -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 +2 -0
- package/docs/oop.md +3 -1
- package/docs/package.md +4 -1
- package/docs/pass-the-signal.md +281 -0
- package/docs/plugins.md +22 -5
- package/docs/policies.md +73 -46
- package/docs/prevent-double-submit.md +3 -1
- package/docs/query-libraries.md +3 -1
- package/docs/react.md +42 -13
- 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 +2 -0
- package/docs/server.md +2 -0
- package/docs/starter.md +2 -0
- package/docs/stores.md +2 -0
- package/docs/svelte.md +3 -1
- package/docs/tracing.md +58 -1
- package/docs/typescript.md +2 -0
- package/docs/vue.md +3 -1
- package/llms-full.txt +2008 -440
- package/llms.txt +11 -8
- package/package.json +22 -5
- package/dist/context-DtUnzC-1.js +0 -1
- package/docs/scope.md +0 -278
- package/docs/stability.md +0 -90
package/docs/effects.md
CHANGED
|
@@ -1,119 +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.
|
|
8
27
|
|
|
9
|
-
|
|
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)
|
|
28
|
+
## `retry`
|
|
18
29
|
|
|
19
|
-
|
|
30
|
+
Runs a function again after an error. Use it when a call fails now and then, for example because of the network.
|
|
20
31
|
|
|
21
|
-
|
|
32
|
+
Forms:
|
|
22
33
|
|
|
23
|
-
-
|
|
24
|
-
-
|
|
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.
|
|
25
36
|
|
|
26
|
-
|
|
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.
|
|
27
44
|
|
|
28
45
|
```js
|
|
29
46
|
import { createBus } from "nanoeffects";
|
|
30
|
-
import {
|
|
47
|
+
import { retry } from "nanoeffects/effects";
|
|
31
48
|
import { latest } from "nanoeffects/policies";
|
|
32
49
|
|
|
33
50
|
const bus = createBus();
|
|
34
51
|
const loadUser = (id, { signal }) => fetchUser(id, { signal });
|
|
35
52
|
|
|
36
|
-
//
|
|
37
|
-
const
|
|
38
|
-
// retry — up to 4 attempts with backoff
|
|
39
|
-
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, {
|
|
40
55
|
times: 4,
|
|
41
|
-
delay: (attempt) => 2 ** attempt * 100,
|
|
42
56
|
shouldRetry: (error) => !isFatal(error),
|
|
43
57
|
});
|
|
44
|
-
// latest — a newer load aborts the older retry chain
|
|
45
|
-
bus.on("load", latest(loadWithRetry));
|
|
46
58
|
|
|
47
|
-
//
|
|
48
|
-
bus.on("
|
|
59
|
+
// latest — a newer load aborts the older chain of attempts
|
|
60
|
+
bus.on("load", latest(loadWithRetry));
|
|
49
61
|
|
|
50
|
-
// ctx.run — run
|
|
62
|
+
// ctx.run — run the wrapped function as a child task (aborted with the handler)
|
|
51
63
|
bus.on("open", async (id, ctx) => {
|
|
52
|
-
const
|
|
53
|
-
const user = await ctx.run(load, id);
|
|
64
|
+
const user = await ctx.run(loadWithRetry, id);
|
|
54
65
|
ctx.emit("opened", user);
|
|
55
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();
|
|
56
76
|
|
|
57
|
-
// ctx-first form — one call inside a body
|
|
58
77
|
bus.on("load", async (id, ctx) => {
|
|
78
|
+
// retry with ctx — one call inside the body
|
|
59
79
|
const user = await retry(
|
|
60
80
|
ctx,
|
|
61
81
|
() => fetchUser(id, { signal: ctx.signal }),
|
|
62
82
|
{ times: 4 },
|
|
63
83
|
);
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
ctx.emit("ready", { user, data });
|
|
84
|
+
|
|
85
|
+
ctx.emit("loaded", user);
|
|
67
86
|
});
|
|
68
87
|
```
|
|
69
88
|
|
|
70
|
-
|
|
89
|
+
## `timeout`
|
|
71
90
|
|
|
72
|
-
|
|
91
|
+
Rejects when the work takes longer than the time you set, and aborts it. Use it when a request can hang.
|
|
73
92
|
|
|
74
|
-
|
|
93
|
+
Forms:
|
|
75
94
|
|
|
76
|
-
|
|
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.
|
|
77
97
|
|
|
78
|
-
|
|
79
|
-
- `delay` — number or `(attempt, error) => ms`.
|
|
80
|
-
- `shouldRetry(error, attempt)` — stop early.
|
|
81
|
-
- `fn` gets the attempt number starting at `1`.
|
|
82
|
-
- 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.
|
|
83
99
|
|
|
84
|
-
|
|
100
|
+
```js
|
|
101
|
+
import { createBus } from "nanoeffects";
|
|
102
|
+
import { timeout } from "nanoeffects/effects";
|
|
85
103
|
|
|
86
|
-
|
|
87
|
-
|
|
104
|
+
const bus = createBus();
|
|
105
|
+
const loadUser = (id, { signal }) => fetchUser(id, { signal });
|
|
88
106
|
|
|
89
|
-
|
|
107
|
+
// timeout — a wrapper of the handler: it must finish within 5 s
|
|
108
|
+
bus.on("load", timeout(5000, loadUser));
|
|
90
109
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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`
|
|
96
118
|
|
|
97
|
-
|
|
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.
|
|
125
|
+
|
|
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.
|
|
98
132
|
|
|
99
133
|
```js
|
|
100
134
|
import { createBus } from "nanoeffects";
|
|
101
|
-
import {
|
|
135
|
+
import { poll } from "nanoeffects/effects";
|
|
102
136
|
|
|
103
137
|
const bus = createBus();
|
|
104
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
|
+
|
|
105
142
|
bus.on("sync", async (_, ctx) => {
|
|
106
|
-
|
|
107
|
-
poll(ctx,
|
|
108
|
-
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);
|
|
109
145
|
});
|
|
110
146
|
```
|
|
111
147
|
|
|
112
|
-
|
|
148
|
+
`retry`, `timeout` and `poll` are traced: the effect is a group, and each attempt or call is a `run` child.
|
|
113
149
|
|
|
114
|
-
##
|
|
150
|
+
## `backoff`
|
|
115
151
|
|
|
116
|
-
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`.
|
|
117
153
|
|
|
118
154
|
The shortest form is a `delay` function. It needs no import:
|
|
119
155
|
|
|
@@ -153,49 +189,41 @@ const loadWithRetry = retry(loadUser, {
|
|
|
153
189
|
- `retry` counts attempts from 1. `poll` counts calls from 0, so with `poll` the first pause is `base / factor`.
|
|
154
190
|
- The result is a plain function `(attempt) => ms`, so it also fits `delay(ctx, …)`.
|
|
155
191
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
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:
|
|
159
193
|
|
|
160
194
|
```js
|
|
161
195
|
import { createBus } from "nanoeffects";
|
|
162
|
-
import {
|
|
163
|
-
|
|
164
|
-
// effectsPlugin — add race, all and delay to the context of every task
|
|
165
|
-
const effects = effectsPlugin({ race, all, delay });
|
|
166
|
-
|
|
167
|
-
// use returns the same bus, typed with the new fields
|
|
168
|
-
const bus = createBus().use(effects);
|
|
196
|
+
import { delay, poll, retry } from "nanoeffects/effects";
|
|
169
197
|
|
|
170
|
-
bus
|
|
171
|
-
// race — same as race(ctx, …)
|
|
172
|
-
const result = await race({
|
|
173
|
-
// child — the context of this starter; it has the `signal`, so the request stops with the race
|
|
174
|
-
data: (child) => fetchData(id, child),
|
|
175
|
-
// a starter gets its own child with the same bound effects, so delay(5000) stops with the race
|
|
176
|
-
timeout: ({ delay }) => delay(5000),
|
|
177
|
-
});
|
|
178
|
-
if ("timeout" in result) throw new Error("timed out");
|
|
198
|
+
const bus = createBus();
|
|
179
199
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
});
|
|
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
|
|
185
204
|
});
|
|
186
205
|
```
|
|
187
206
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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";
|
|
193
214
|
|
|
194
|
-
|
|
215
|
+
const bus = createBus();
|
|
216
|
+
|
|
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
|
+
```
|
|
195
223
|
|
|
196
|
-
|
|
224
|
+
## `cancellable`
|
|
197
225
|
|
|
198
|
-
|
|
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 }`.
|
|
199
227
|
|
|
200
228
|
```js
|
|
201
229
|
import { createBus } from "nanoeffects";
|
|
@@ -204,7 +232,7 @@ import { latest } from "nanoeffects/policies";
|
|
|
204
232
|
|
|
205
233
|
const bus = createBus();
|
|
206
234
|
|
|
207
|
-
// cancellable —
|
|
235
|
+
// cancellable — a Promise tied to the task signal; onCancel registers a cleanup
|
|
208
236
|
// latest — a newer wait aborts the older timer
|
|
209
237
|
bus.on(
|
|
210
238
|
"wait",
|
|
@@ -217,13 +245,13 @@ bus.on(
|
|
|
217
245
|
);
|
|
218
246
|
```
|
|
219
247
|
|
|
220
|
-
- 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.
|
|
221
249
|
- If the task is already aborted, the executor does not run.
|
|
222
|
-
- After the
|
|
250
|
+
- After the Promise settles, the abort listener is removed and cleanups are dropped.
|
|
223
251
|
|
|
224
|
-
##
|
|
252
|
+
## `onAbort`
|
|
225
253
|
|
|
226
|
-
`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.
|
|
227
255
|
|
|
228
256
|
```js
|
|
229
257
|
import { createBus } from "nanoeffects";
|
|
@@ -233,14 +261,14 @@ const bus = createBus();
|
|
|
233
261
|
|
|
234
262
|
bus.on("connect", (url, ctx) => {
|
|
235
263
|
const socket = new WebSocket(url);
|
|
236
|
-
// onAbort —
|
|
264
|
+
// onAbort — close the socket when this task aborts
|
|
237
265
|
onAbort(ctx, () => socket.close());
|
|
238
266
|
});
|
|
239
267
|
```
|
|
240
268
|
|
|
241
|
-
##
|
|
269
|
+
## `ctx.on` and `ctx.off`
|
|
242
270
|
|
|
243
|
-
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.
|
|
244
272
|
|
|
245
273
|
```js
|
|
246
274
|
import { createBus } from "nanoeffects";
|
|
@@ -248,15 +276,29 @@ import { createBus } from "nanoeffects";
|
|
|
248
276
|
const bus = createBus();
|
|
249
277
|
|
|
250
278
|
bus.on("watch", (_, ctx) => {
|
|
251
|
-
|
|
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);
|
|
252
284
|
});
|
|
253
285
|
```
|
|
254
286
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
- `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:
|
|
294
|
+
|
|
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`
|
|
258
300
|
|
|
259
|
-
|
|
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`.
|
|
260
302
|
|
|
261
303
|
```js
|
|
262
304
|
import { createBus } from "nanoeffects";
|
|
@@ -278,22 +320,65 @@ bus.on("login", async (user, ctx) => {
|
|
|
278
320
|
const id = await timeout(
|
|
279
321
|
ctx,
|
|
280
322
|
5000,
|
|
281
|
-
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
|
|
282
324
|
);
|
|
283
325
|
});
|
|
284
326
|
```
|
|
285
327
|
|
|
286
|
-
-
|
|
328
|
+
- It sees only events emitted after the call, takes one, and unsubscribes.
|
|
287
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.
|
|
288
330
|
- `options.predicate` skips payloads it rejects. In TypeScript, a type guard (`(payload): payload is Admin`) narrows the type of the result.
|
|
289
|
-
- `options.signal` stops the wait. Inside `race`, `all` or `timeout` you do not need it: the group already stops the
|
|
290
|
-
- 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`
|
|
335
|
+
|
|
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
|
+
```
|
|
354
|
+
|
|
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`.
|
|
356
|
+
|
|
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
|
+
```
|
|
291
376
|
|
|
292
|
-
|
|
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.
|
|
293
378
|
|
|
294
|
-
|
|
379
|
+
## `allSettled`
|
|
295
380
|
|
|
296
|
-
|
|
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.
|
|
297
382
|
|
|
298
383
|
```js
|
|
299
384
|
import { createBus } from "nanoeffects";
|
|
@@ -302,6 +387,7 @@ import { allSettled } from "nanoeffects/effects";
|
|
|
302
387
|
const bus = createBus();
|
|
303
388
|
|
|
304
389
|
bus.on("open", async (id, ctx) => {
|
|
390
|
+
// allSettled — wait for both, whatever the outcome
|
|
305
391
|
const { user, posts } = await allSettled(ctx, {
|
|
306
392
|
user: (child) => fetchUser(id, child),
|
|
307
393
|
posts: (child) => fetchPosts(id, child),
|
|
@@ -313,7 +399,9 @@ bus.on("open", async (id, ctx) => {
|
|
|
313
399
|
});
|
|
314
400
|
```
|
|
315
401
|
|
|
316
|
-
`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)`.
|
|
317
405
|
|
|
318
406
|
```js
|
|
319
407
|
import { createBus } from "nanoeffects";
|
|
@@ -322,9 +410,9 @@ import { delay, race, take } from "nanoeffects/effects";
|
|
|
322
410
|
const bus = createBus();
|
|
323
411
|
|
|
324
412
|
bus.on("save", async (draft, ctx) => {
|
|
325
|
-
// race — the first
|
|
413
|
+
// race — the first to finish wins, the other is cancelled
|
|
326
414
|
const result = await race(ctx, {
|
|
327
|
-
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
|
|
328
416
|
cancelled: take("cancel"), // take without ctx — the same, waits for the next "cancel"
|
|
329
417
|
});
|
|
330
418
|
|
|
@@ -334,9 +422,9 @@ bus.on("save", async (draft, ctx) => {
|
|
|
334
422
|
});
|
|
335
423
|
```
|
|
336
424
|
|
|
337
|
-
**`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`).
|
|
338
426
|
|
|
339
|
-
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:
|
|
340
428
|
|
|
341
429
|
```js
|
|
342
430
|
import { createBus } from "nanoeffects";
|
|
@@ -346,7 +434,7 @@ const bus = createBus();
|
|
|
346
434
|
|
|
347
435
|
bus.on("open", async (id, ctx) => {
|
|
348
436
|
const result = await race(ctx, {
|
|
349
|
-
// retry without ctx —
|
|
437
|
+
// retry without ctx — up to 3 attempts, each with the signal of the child
|
|
350
438
|
user: retry((_, { signal }) => fetchUser(id, { signal }), { times: 3 }),
|
|
351
439
|
// delay without ctx — the loser is cancelled when the other one finishes
|
|
352
440
|
waited: delay(5000),
|
|
@@ -358,9 +446,9 @@ bus.on("open", async (id, ctx) => {
|
|
|
358
446
|
});
|
|
359
447
|
```
|
|
360
448
|
|
|
361
|
-
|
|
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.
|
|
362
450
|
|
|
363
|
-
**`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:
|
|
364
452
|
|
|
365
453
|
```js
|
|
366
454
|
import { createBus } from "nanoeffects";
|
|
@@ -370,7 +458,7 @@ const bus = createBus();
|
|
|
370
458
|
|
|
371
459
|
bus.on("open", async (id, ctx) => {
|
|
372
460
|
const result = await race(ctx, {
|
|
373
|
-
// all without ctx —
|
|
461
|
+
// all without ctx — loads both, each with up to 3 attempts
|
|
374
462
|
loaded: all({
|
|
375
463
|
user: retry((_, { signal }) => fetchUser(id, { signal }), { times: 3 }),
|
|
376
464
|
posts: retry((_, { signal }) => fetchPosts(id, { signal }), { times: 3 }),
|
|
@@ -385,37 +473,66 @@ bus.on("open", async (id, ctx) => {
|
|
|
385
473
|
});
|
|
386
474
|
```
|
|
387
475
|
|
|
388
|
-
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.
|
|
477
|
+
|
|
478
|
+
## `effectsPlugin`
|
|
389
479
|
|
|
390
|
-
|
|
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)).
|
|
391
481
|
|
|
392
482
|
```js
|
|
393
483
|
import { createBus } from "nanoeffects";
|
|
394
|
-
import { all,
|
|
484
|
+
import { all, delay, effectsPlugin, race } from "nanoeffects/effects";
|
|
395
485
|
|
|
396
|
-
|
|
486
|
+
// effectsPlugin — add race, all and delay to the context of every task
|
|
487
|
+
const effects = effectsPlugin({ race, all, delay });
|
|
397
488
|
|
|
398
|
-
bus
|
|
399
|
-
|
|
400
|
-
const users = await all(
|
|
401
|
-
ctx,
|
|
402
|
-
ids.map((id) => retry(() => loadUser(id), { times: 2 })),
|
|
403
|
-
{ concurrency: 5 },
|
|
404
|
-
);
|
|
489
|
+
// use returns the same bus, typed with the new fields
|
|
490
|
+
const bus = createBus().use(effects);
|
|
405
491
|
|
|
406
|
-
|
|
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
|
+
});
|
|
407
506
|
});
|
|
408
507
|
```
|
|
409
508
|
|
|
410
|
-
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, …)`.
|
|
411
514
|
|
|
515
|
+
## Effects outside a task
|
|
412
516
|
|
|
413
|
-
|
|
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.
|
|
414
518
|
|
|
415
|
-
|
|
519
|
+
```js
|
|
520
|
+
import { delay, retry } from "nanoeffects/effects";
|
|
416
521
|
|
|
417
|
-
|
|
522
|
+
const controller = new AbortController();
|
|
418
523
|
|
|
419
|
-
|
|
524
|
+
// outside a task there is no ctx: an object with a signal is enough
|
|
525
|
+
const context = { signal: controller.signal };
|
|
420
526
|
|
|
421
|
-
|
|
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";
|