nanoeffects 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -55
- package/dist/{action-Bdfp-Wvd.d.ts → action-creator-DOWcbXpy.d.ts} +79 -73
- package/dist/actions.d.ts +75 -6
- package/dist/actions.js +1 -1
- package/dist/bus-BAs9x7IQ.js +1 -0
- package/dist/{context-DlA80GOA.js → context-CK0ZomWt.js} +1 -1
- package/dist/context-plugin-B0sLWOxR.d.ts +34 -0
- package/dist/dev.d.ts +2 -2
- package/dist/effects.d.ts +153 -56
- package/dist/effects.js +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/{module-I1ixyJzx.d.ts → module-DCJl-qfi.d.ts} +24 -9
- package/dist/notrace/{action-Bdfp-Wvd.d.ts → action-creator-DOWcbXpy.d.ts} +79 -73
- package/dist/notrace/actions.d.ts +97 -13
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/bus-Bdk0YyFQ.js +1 -0
- package/dist/notrace/{context-yAWj0BFZ.js → context-CctVmHLC.js} +1 -1
- package/dist/notrace/context-plugin-B0sLWOxR.d.ts +34 -0
- package/dist/notrace/{delay-Cgvn16oY.js → delay-B_W1tnkS.js} +1 -1
- package/dist/notrace/effects.d.ts +153 -56
- package/dist/notrace/effects.js +1 -1
- package/dist/notrace/index.d.ts +3 -2
- package/dist/notrace/index.js +1 -1
- package/dist/notrace/policies.d.ts +1 -1
- package/dist/notrace/policies.js +1 -1
- package/dist/notrace/redux.d.ts +18 -20
- package/dist/notrace/redux.js +1 -1
- package/dist/observable.d.ts +1 -1
- package/dist/policies.d.ts +1 -1
- package/dist/policies.js +1 -1
- package/dist/react.d.ts +50 -0
- package/dist/react.js +1 -0
- package/dist/redux.d.ts +18 -20
- package/dist/redux.js +1 -1
- package/dist/trace.d.ts +1 -1
- package/docs/README.md +30 -9
- package/docs/actions.md +117 -41
- package/docs/context.md +48 -7
- package/docs/effects.md +229 -73
- package/docs/errors.md +23 -10
- package/docs/guide.md +238 -129
- package/docs/hmr.md +25 -13
- package/docs/migrating-from-effector.md +293 -0
- package/docs/migrating-from-rxjs.md +127 -0
- package/docs/migrating-from-saga.md +93 -0
- package/docs/observable.md +11 -9
- package/docs/package.md +24 -0
- package/docs/policies.md +93 -30
- package/docs/react.md +157 -0
- package/docs/semantics.md +5 -3
- package/docs/stores.md +488 -25
- package/docs/tracing.md +69 -54
- package/docs/typescript.md +153 -0
- package/llms-full.txt +2645 -0
- package/llms.txt +48 -0
- package/package.json +39 -9
- package/dist/bus-CRnlwmQ4.js +0 -1
- package/dist/notrace/bus-Dx0l9X6h.js +0 -1
- package/dist/notrace/event-0RIBGYqS.js +0 -1
package/docs/effects.md
CHANGED
|
@@ -1,20 +1,199 @@
|
|
|
1
1
|
# Effects and cancellation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use this page for retry, timeouts, waiting, or parallel work — not as the first page for a small bus. Pick **one** helper for your job; the rest is reference.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
An abort only raises a flag. Your code decides to stop. Start with `retry` / `timeout` on a handler (see [Guide §5](guide.md#5-add-retry-and-timeout)).
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Wrap a handler with retry and timeout
|
|
8
|
+
|
|
9
|
+
`retry`, `timeout`, and `poll` have two forms:
|
|
10
|
+
|
|
11
|
+
- **Task wrapper** — take a function `(payload, ctx) => result` and return one. Pass them into `on` next to policies (no `ctx` in their arguments).
|
|
12
|
+
- **Call inside a body** — take `ctx` first.
|
|
13
|
+
|
|
14
|
+
The first argument tells them apart: a function (`retry`), a number (`timeout`), or a number/function (`poll`) is a wrapper; an object is `ctx`.
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
import { createBus } from "nanoeffects";
|
|
18
|
+
import { poll, retry, timeout } from "nanoeffects/effects";
|
|
19
|
+
import { latest } from "nanoeffects/policies";
|
|
20
|
+
|
|
21
|
+
const bus = createBus();
|
|
22
|
+
const loadUser = (id, { signal }) => fetchUser(id, { signal });
|
|
23
|
+
|
|
24
|
+
// timeout — one try must finish within 5 s
|
|
25
|
+
const loadOnce = timeout(5000, loadUser);
|
|
26
|
+
// retry — up to 4 attempts with backoff
|
|
27
|
+
const loadWithRetry = retry(loadOnce, {
|
|
28
|
+
times: 4,
|
|
29
|
+
delay: (attempt) => 2 ** attempt * 100,
|
|
30
|
+
shouldRetry: (error) => !isFatal(error),
|
|
31
|
+
});
|
|
32
|
+
// latest — a newer load aborts the older retry chain
|
|
33
|
+
bus.on("load", latest(loadWithRetry));
|
|
34
|
+
|
|
35
|
+
// poll — call refresh every 10 s until the task aborts
|
|
36
|
+
bus.on("watch", poll(10_000, (_, { signal }) => refresh({ signal })));
|
|
37
|
+
|
|
38
|
+
// ctx.run — run a wrapped Task as a child (aborted with the handler)
|
|
39
|
+
bus.on("open", async (id, ctx) => {
|
|
40
|
+
const load = retry(timeout(5000, loadUser));
|
|
41
|
+
const user = await ctx.run(load, id);
|
|
42
|
+
ctx.emit("opened", user);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ctx-first form — one call inside a body
|
|
46
|
+
bus.on("load", async (id, ctx) => {
|
|
47
|
+
const user = await retry(
|
|
48
|
+
ctx,
|
|
49
|
+
() => fetchUser(id, { signal: ctx.signal }),
|
|
50
|
+
{ times: 4 },
|
|
51
|
+
);
|
|
52
|
+
const data = await timeout(ctx, 5000, ({ signal }) => {
|
|
53
|
+
return fetchData(id, { signal });
|
|
54
|
+
});
|
|
55
|
+
await poll(ctx, () => refresh(), 10_000);
|
|
56
|
+
ctx.emit("ready", { user, data });
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Use the wrapper for a whole handler or a whole `ctx.run`. Use the `ctx` form for one call in the middle of a body.
|
|
61
|
+
|
|
62
|
+
Each attempt or call of a wrapper gets its own child context. Abort of the caller reaches it. Abort of one attempt (a timeout) does not reach the caller. The attempt number is passed only in the `ctx` form.
|
|
63
|
+
|
|
64
|
+
### Options for retry, timeout, and poll
|
|
65
|
+
|
|
66
|
+
**`retry`**
|
|
67
|
+
|
|
68
|
+
- `times` — total attempts (default `3`).
|
|
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.
|
|
73
|
+
|
|
74
|
+
**`timeout`**
|
|
75
|
+
|
|
76
|
+
- Error is a `DOMException` named `TimeoutError` (like `AbortSignal.timeout`).
|
|
77
|
+
- Rejects even if the starter ignores its signal.
|
|
78
|
+
|
|
79
|
+
**`poll`** (like patronum `interval`)
|
|
80
|
+
|
|
81
|
+
- First call after `ms`. `leading: true` also calls at once.
|
|
82
|
+
- Next ticks keep a steady rate whatever the duration of `fn`, so a slow `fn` can overlap the next call.
|
|
83
|
+
- `waitForFn: true` starts each pause when the call ends — calls never overlap.
|
|
84
|
+
- `fn` gets the call number from `0`.
|
|
85
|
+
- An error from `fn` stops polling and rejects. An abort ends it normally and clears the timer.
|
|
86
|
+
|
|
87
|
+
A pause can be a function wherever a time is given. For retries with a cap and jitter, see [Back off between retries](#back-off-between-retries):
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
import { createBus } from "nanoeffects";
|
|
91
|
+
import { delay, poll, retry } from "nanoeffects/effects";
|
|
92
|
+
|
|
93
|
+
const bus = createBus();
|
|
94
|
+
|
|
95
|
+
bus.on("sync", async (_, ctx) => {
|
|
96
|
+
retry(ctx, load, { times: 5, delay: (attempt) => 2 ** attempt * 100 }); // 200, 400, 800, …
|
|
97
|
+
poll(ctx, refresh, (call) => Math.min(1000 * 2 ** call, 60_000)); // slows to once a minute
|
|
98
|
+
await delay(ctx, () => 200 + Math.random() * 200); // 200–400 ms
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
All three are traced: the effect is a group (`retry`, `timeout`, `poll`) and each attempt or call is a `run` child.
|
|
103
|
+
|
|
104
|
+
## Back off between retries
|
|
105
|
+
|
|
106
|
+
Use this 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. Make the pause grow with each attempt, and add jitter so clients spread out.
|
|
107
|
+
|
|
108
|
+
The shortest form is a `delay` function. It needs no import:
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
import { retry } from "nanoeffects/effects";
|
|
112
|
+
|
|
113
|
+
const loadWithRetry = retry(loadUser, {
|
|
114
|
+
times: 5,
|
|
115
|
+
delay: (attempt) => 2 ** attempt * 100, // 200, 400, 800, 1600 ms
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
When you also want a cap and jitter, build the function with `backoff`:
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
import { backoff, retry } from "nanoeffects/effects";
|
|
123
|
+
|
|
124
|
+
const loadWithRetry = retry(loadUser, {
|
|
125
|
+
times: 5,
|
|
126
|
+
// backoff — pause 200, 400, 800, … ms, never over 5 s, a random part of it
|
|
127
|
+
delay: backoff({ base: 200, max: 5000, jitter: true }),
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
| Option | Meaning |
|
|
132
|
+
| ------ | ------- |
|
|
133
|
+
| `base` | pause before the second attempt, in ms (default `100`) |
|
|
134
|
+
| `factor` | each pause is this many times longer (default `2`) |
|
|
135
|
+
| `max` | longest pause, in ms (default none) |
|
|
136
|
+
| `jitter` | `true` picks a random pause from 0 up to the computed one (default `false`) |
|
|
137
|
+
|
|
138
|
+
- With `jitter: true` a pause can be close to 0, so treat `base` as a scale, not a minimum.
|
|
139
|
+
- `retry` counts attempts from 1. `poll` counts calls from 0, so with `poll` the first pause is `base / factor`.
|
|
140
|
+
- The result is a plain function `(attempt) => ms`, so it also fits `delay(ctx, …)`.
|
|
141
|
+
|
|
142
|
+
## Put selected effects on `ctx`
|
|
143
|
+
|
|
144
|
+
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)).
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
import { createBus } from "nanoeffects";
|
|
148
|
+
import { all, delay, effectsPlugin, race } from "nanoeffects/effects";
|
|
149
|
+
|
|
150
|
+
// effectsPlugin — add race, all and delay to the context of every task; returns the same bus
|
|
151
|
+
const bus = createBus().use(effectsPlugin({ race, all, delay }));
|
|
152
|
+
|
|
153
|
+
bus.on("load", async (id, { race, all, delay }) => {
|
|
154
|
+
// race — same as race(ctx, …)
|
|
155
|
+
const result = await race({
|
|
156
|
+
data: ({ signal }) => fetchData(id, { signal }),
|
|
157
|
+
timeout: () => delay(5000), // delay — same as delay(ctx, 5000)
|
|
158
|
+
});
|
|
159
|
+
if ("timeout" in result) throw new Error("timed out");
|
|
160
|
+
|
|
161
|
+
// all — same as all(ctx, …)
|
|
162
|
+
const { user, posts } = await all({
|
|
163
|
+
user: ({ signal }) => fetchUser(id, { signal }),
|
|
164
|
+
posts: ({ signal }) => fetchPosts(id, { signal }),
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- Supported: `race`, `all`, `allSettled`, `take`, `delay`. The key is the effect name.
|
|
170
|
+
- `retry`, `timeout` and `poll` are not supported: bound to the task they would look like their handler wrappers.
|
|
171
|
+
- 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`.
|
|
172
|
+
- Free forms keep working: `race(ctx, …)`.
|
|
173
|
+
|
|
174
|
+
## Tie a promise to the task signal
|
|
175
|
+
|
|
176
|
+
`delay(ctx, ms)` waits and rejects on abort (`ms` can be a function).
|
|
177
|
+
|
|
178
|
+
`cancellable(ctx, executor)` is a promise tied to the task signal. It works like `new Promise`, but the executor gets `{ resolve, reject, onCancel }`.
|
|
179
|
+
|
|
180
|
+
```js
|
|
181
|
+
import { createBus } from "nanoeffects";
|
|
8
182
|
import { cancellable } from "nanoeffects/effects";
|
|
183
|
+
import { latest } from "nanoeffects/policies";
|
|
9
184
|
|
|
185
|
+
const bus = createBus();
|
|
186
|
+
|
|
187
|
+
// cancellable — promise tied to the task signal; onCancel for cleanups
|
|
188
|
+
// latest — a newer wait aborts the older timer
|
|
10
189
|
bus.on(
|
|
11
190
|
"wait",
|
|
12
|
-
latest((_, ctx) =>
|
|
13
|
-
cancellable(ctx, ({ resolve, onCancel }) => {
|
|
191
|
+
latest((_, ctx) => {
|
|
192
|
+
return cancellable(ctx, ({ resolve, onCancel }) => {
|
|
14
193
|
const timer = setTimeout(resolve, 5000);
|
|
15
194
|
onCancel(() => clearTimeout(timer));
|
|
16
|
-
})
|
|
17
|
-
),
|
|
195
|
+
});
|
|
196
|
+
}),
|
|
18
197
|
);
|
|
19
198
|
```
|
|
20
199
|
|
|
@@ -22,116 +201,93 @@ bus.on(
|
|
|
22
201
|
- If the task is already aborted, the executor does not run.
|
|
23
202
|
- After the promise settles, the abort listener is removed and cleanups are dropped.
|
|
24
203
|
|
|
25
|
-
|
|
204
|
+
## Run cleanup on abort
|
|
205
|
+
|
|
206
|
+
`onAbort(ctx, fn)` runs `fn(reason)` on abort (at once if already aborted) and returns a function that unsubscribes.
|
|
26
207
|
|
|
27
|
-
```
|
|
208
|
+
```js
|
|
209
|
+
import { createBus } from "nanoeffects";
|
|
28
210
|
import { onAbort } from "nanoeffects/effects";
|
|
29
211
|
|
|
212
|
+
const bus = createBus();
|
|
213
|
+
|
|
30
214
|
bus.on("connect", (url, ctx) => {
|
|
31
215
|
const socket = new WebSocket(url);
|
|
216
|
+
// onAbort — run cleanup when this task aborts
|
|
32
217
|
onAbort(ctx, () => socket.close());
|
|
33
218
|
});
|
|
34
219
|
```
|
|
35
220
|
|
|
36
|
-
##
|
|
37
|
-
|
|
38
|
-
`allSettled(ctx, effects)` is `all` that waits for every starter and never fails because of one: each result is `{ status: "fulfilled", value }` or `{ status: "rejected", reason }`, in the shape of the input (object or array). Nothing is cancelled when a starter fails. Only an abort of `ctx` rejects it.
|
|
221
|
+
## Wait for the next event with `take`
|
|
39
222
|
|
|
40
|
-
|
|
41
|
-
const { user, posts } = await allSettled(ctx, {
|
|
42
|
-
user: ({ signal }) => fetchUser(id, { signal }),
|
|
43
|
-
posts: ({ signal }) => fetchPosts(id, { signal }),
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
if (user.status === "fulfilled") render(user.value);
|
|
47
|
-
```
|
|
223
|
+
A task context has `on` and `off`, like the bus. A subscription made with `ctx.on` ends when the task aborts:
|
|
48
224
|
|
|
49
|
-
|
|
225
|
+
```js
|
|
226
|
+
import { createBus } from "nanoeffects";
|
|
50
227
|
|
|
51
|
-
|
|
228
|
+
const bus = createBus();
|
|
52
229
|
|
|
53
|
-
```ts
|
|
54
230
|
bus.on("watch", (_, ctx) => {
|
|
55
231
|
ctx.on("tick", (n) => console.log(n)); // gone when this task aborts
|
|
56
232
|
});
|
|
57
233
|
```
|
|
58
234
|
|
|
59
|
-
- The returned function unsubscribes sooner. After
|
|
60
|
-
-
|
|
61
|
-
- `ctx.off
|
|
235
|
+
- The returned function unsubscribes sooner. After abort, `ctx.on` does nothing.
|
|
236
|
+
- Under `latest`, subscriptions of a superseded run end with that run.
|
|
237
|
+
- `ctx.off(event)` clears every handler of the event on the bus, not only the task’s.
|
|
62
238
|
|
|
63
239
|
`take(ctx, event, options?)` waits for the next `event` and resolves with its payload:
|
|
64
240
|
|
|
65
|
-
```
|
|
241
|
+
```js
|
|
242
|
+
import { createBus } from "nanoeffects";
|
|
66
243
|
import { take, timeout } from "nanoeffects/effects";
|
|
67
244
|
|
|
245
|
+
const bus = createBus();
|
|
246
|
+
|
|
68
247
|
bus.on("login", async (user, ctx) => {
|
|
69
248
|
const reason = await take(ctx, "logout"); // resolves on the next logout
|
|
70
249
|
cleanup(user, reason);
|
|
71
250
|
|
|
72
251
|
// wait for a matching event, but not longer than 5 s
|
|
73
252
|
const id = await timeout(ctx, 5000, ({ signal }) =>
|
|
74
|
-
take(ctx, "saved", { signal, predicate: (
|
|
253
|
+
take(ctx, "saved", { signal, predicate: (savedId) => savedId === draft.id }),
|
|
75
254
|
);
|
|
76
255
|
});
|
|
77
256
|
```
|
|
78
257
|
|
|
79
|
-
-
|
|
80
|
-
- `options.predicate` skips payloads it rejects
|
|
81
|
-
-
|
|
82
|
-
|
|
83
|
-
## retry, timeout, poll
|
|
258
|
+
- Sees only events emitted after the call, takes one, and unsubscribes.
|
|
259
|
+
- `options.predicate` skips payloads it rejects. In TypeScript, a type guard (`(payload): payload is Admin`) narrows the type of the result.
|
|
260
|
+
- `options.signal` stops the wait (how a `race` or `timeout` starter cancels it).
|
|
261
|
+
- On abort of the task or of `options.signal`, unsubscribes and rejects with the abort reason.
|
|
84
262
|
|
|
85
|
-
|
|
263
|
+
## Run several starters together
|
|
86
264
|
|
|
87
|
-
|
|
88
|
-
import { poll, retry, timeout } from "nanoeffects/effects";
|
|
265
|
+
`all(ctx, effects)` runs starters together. One failure cancels the rest. Shape of the input (object or array) is preserved in the result.
|
|
89
266
|
|
|
90
|
-
|
|
267
|
+
`allSettled(ctx, effects)` waits for every starter and never fails because of one. Each result is `{ status: "fulfilled", value }` or `{ status: "rejected", reason }`. Nothing is cancelled when a starter fails. Only an abort of `ctx` rejects it.
|
|
91
268
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
latest(
|
|
96
|
-
retry(timeout(5000, loadUser), {
|
|
97
|
-
times: 4,
|
|
98
|
-
delay: (attempt) => 2 ** attempt * 100,
|
|
99
|
-
shouldRetry: (error) => !isFatal(error),
|
|
100
|
-
}),
|
|
101
|
-
),
|
|
102
|
-
);
|
|
269
|
+
```js
|
|
270
|
+
import { createBus } from "nanoeffects";
|
|
271
|
+
import { allSettled } from "nanoeffects/effects";
|
|
103
272
|
|
|
104
|
-
|
|
105
|
-
bus.on("watch", poll(10_000, (_, { signal }) => refresh({ signal })));
|
|
273
|
+
const bus = createBus();
|
|
106
274
|
|
|
107
|
-
// a wrapped Task runs as a child of a handler with ctx.run: the only place where ctx is written
|
|
108
275
|
bus.on("open", async (id, ctx) => {
|
|
109
|
-
const user = await ctx
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
276
|
+
const { user, posts } = await allSettled(ctx, {
|
|
277
|
+
user: ({ signal }) => fetchUser(id, { signal }),
|
|
278
|
+
posts: ({ signal }) => fetchPosts(id, { signal }),
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
if (user.status === "fulfilled") {
|
|
282
|
+
render(user.value);
|
|
283
|
+
}
|
|
117
284
|
});
|
|
118
285
|
```
|
|
119
286
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
- `retry`: `times` is the total number of attempts (default 3), `delay` is a number or `(attempt, error) => ms`, `shouldRetry(error, attempt)` can stop early. `fn` gets the attempt number, starting at 1. On abort it rejects with the abort reason.
|
|
123
|
-
- `timeout`: the error is a `DOMException` named `TimeoutError`, like `AbortSignal.timeout`. It rejects even if the starter ignores its signal.
|
|
124
|
-
- `poll`: like patronum `interval`. The first call comes after `ms` (`leading: true` also calls at once), and the next ticks keep a steady rate whatever the duration of `fn`, so a slow `fn` can overlap with the next call; `waitForFn: true` starts each pause when the call ends instead, so calls never overlap. `fn` gets the call number from 0. An error from `fn` stops polling and rejects; an abort ends it normally and clears the timer.
|
|
125
|
-
- A pause can be a function wherever a time is given, for backoff or jitter: `retry`'s `delay` gets `(attempt, error)`, `poll`'s `ms` gets the number of the call the pause leads to, and `delay` itself takes a function called once when it starts:
|
|
126
|
-
|
|
127
|
-
```ts
|
|
128
|
-
retry(ctx, load, { times: 5, delay: (attempt) => 2 ** attempt * 100 }); // 200, 400, 800, ...
|
|
129
|
-
poll(ctx, refresh, (call) => Math.min(1000 * 2 ** call, 60_000)); // slows down to once a minute
|
|
130
|
-
await delay(ctx, () => 200 + Math.random() * 200); // 200 to 400 ms
|
|
131
|
-
```
|
|
287
|
+
`race(ctx, effects)` settles with the first starter that finishes and cancels the others.
|
|
132
288
|
|
|
133
|
-
|
|
289
|
+
## Pass `ctx` outside a task
|
|
134
290
|
|
|
135
|
-
|
|
291
|
+
Effects that wait inside a body take `ctx` first: `delay`, `all`, `allSettled`, `race`, `cancellable`, `onAbort`, `take`, and the `ctx` forms of `retry` / `timeout` / `poll`. Outside a task, pass `{}` or any object with a `signal`.
|
|
136
292
|
|
|
137
293
|
For `fetch` and other APIs that take a signal, pass `ctx.signal`. A timeout without `race`: `AbortSignal.any([ctx.signal, AbortSignal.timeout(5000)])`.
|
package/docs/errors.md
CHANGED
|
@@ -1,21 +1,34 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Handle errors and unheard events
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Defaults match a small bus. Set these when you want isolation or typo warnings.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Isolate failing handlers
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
`emit` is a plain loop over the handlers (like nanoevents). If a handler throws, the remaining handlers are skipped and the error reaches the caller of `emit`. A rejected promise of an async handler is left unhandled.
|
|
8
|
+
|
|
9
|
+
Pass `onError` to isolate handlers instead. Then every handler runs. Thrown errors and rejections of async handlers (including those wrapped in `latest`, `debounce`, and the like) go to `onError`. An `onError` that throws is logged to `console.error`.
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { createBus } from "nanoeffects";
|
|
13
|
+
|
|
14
|
+
// createBus — onError isolates failing handlers so others still run
|
|
15
|
+
const bus = createBus({
|
|
16
|
+
onError: (error, { type, payload }) => {
|
|
17
|
+
report(error, type, payload);
|
|
18
|
+
},
|
|
10
19
|
});
|
|
11
20
|
```
|
|
12
21
|
|
|
13
|
-
##
|
|
22
|
+
## Warn when nobody listens
|
|
23
|
+
|
|
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 commands, which always have a handler to run.
|
|
25
|
+
|
|
26
|
+
Use it as a warning in development:
|
|
14
27
|
|
|
15
|
-
|
|
28
|
+
```js
|
|
29
|
+
import { createBus } from "nanoeffects";
|
|
16
30
|
|
|
17
|
-
|
|
18
|
-
const bus = createBus<Events>({
|
|
31
|
+
const bus = createBus({
|
|
19
32
|
onUnhandled: import.meta.env.DEV
|
|
20
33
|
? ({ type }) => console.warn(`Nobody listens to "${type}"`)
|
|
21
34
|
: void 0,
|