nanoeffects 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -24
- package/dist/{action-creator-D_c223C7.d.ts → action-creator-jeEqIKgK.d.ts} +73 -32
- package/dist/actions.d.ts +18 -18
- package/dist/actions.js +1 -1
- package/dist/bus-0uYn_kRj.js +1 -0
- package/dist/context-DtUnzC-1.js +1 -0
- package/dist/context-types-Bme1mzkn.d.ts +31 -0
- package/dist/context.d.ts +8 -3
- package/dist/context.js +1 -1
- package/dist/{delay-18LJ8jSk.js → delay-5OhJZZQ0.js} +1 -1
- package/dist/dev.d.ts +2 -2
- package/dist/dev.js +1 -1
- package/dist/effects.d.ts +2 -2
- package/dist/effects.js +1 -1
- package/dist/event-DVcW0mME.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-WX0Gngnt.d.ts → module-BWMW83qk.d.ts} +38 -38
- package/dist/notrace/{action-creator-Bux7Zzsl.d.ts → action-creator-xpQ_I2h8.d.ts} +73 -32
- package/dist/notrace/actions.d.ts +52 -52
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/bus-37nlVljc.js +1 -0
- 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-CATXOxtj.d.ts → plugin-6LhNx0th.d.ts} +1 -1
- package/dist/notrace/policies.d.ts +1 -1
- package/dist/notrace/policies.js +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-vvkmIIz2.d.ts → plugin-BaErevLy.d.ts} +2 -2
- package/dist/policies.d.ts +2 -2
- package/dist/policies.js +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/redux.d.ts +1 -1
- package/dist/scope.d.ts +9 -9
- package/dist/signal-field-key-CWT-3OXC.js +1 -0
- package/dist/signal.d.ts +34 -6
- package/dist/signal.js +1 -1
- package/dist/svelte.d.ts +1 -1
- package/dist/trace.d.ts +20 -5
- package/dist/trace.js +1 -1
- package/dist/{types-Bf-LX_tE.d.ts → types-D5xsR37j.d.ts} +2 -2
- package/dist/vue.d.ts +1 -1
- package/dist/{with-policy-CPjx0sLm.d.ts → with-policy-BZVRBUOm.d.ts} +1 -1
- package/dist/{with-policy-D-SQRkPh.js → with-policy-C6BIiPIi.js} +1 -1
- package/docs/README.md +14 -1
- package/docs/actions.md +28 -2
- package/docs/chat-example.md +16 -4
- package/docs/chat-widget.md +135 -0
- package/docs/context.md +33 -1
- package/docs/effects.md +20 -9
- package/docs/guide.md +21 -3
- package/docs/migrating-from-effector.md +11 -0
- package/docs/observable.md +43 -0
- package/docs/oop.md +18 -1
- package/docs/package.md +22 -13
- package/docs/plugins.md +69 -11
- package/docs/query-libraries.md +273 -5
- package/docs/react.md +12 -2
- package/docs/scope.md +14 -2
- package/docs/semantics.md +10 -0
- package/docs/server.md +3 -3
- package/docs/stability.md +2 -2
- package/docs/starter.md +3 -3
- package/docs/tracing.md +77 -9
- package/docs/typescript.md +1 -1
- package/llms-full.txt +816 -72
- package/llms.txt +8 -7
- package/package.json +6 -6
- package/skills/nanoeffects/SKILL.md +2 -2
- package/dist/bus-DQDHUN5e.js +0 -1
- package/dist/context-UcmrAIhP.js +0 -1
- package/dist/context-types-nGtzhjxD.d.ts +0 -14
- package/dist/event-0RIBGYqS.js +0 -1
- package/dist/notrace/bus-CGi2lrd5.js +0 -1
- package/dist/notrace/promise-CSLpzwSR.js +0 -1
package/docs/effects.md
CHANGED
|
@@ -4,6 +4,18 @@ Use this page for retry, timeouts, waiting, or parallel work — not as the firs
|
|
|
4
4
|
|
|
5
5
|
An abort only raises a flag. Your code decides to stop. Start with `retry` / `timeout` on a handler (see [Guide §5](guide.md#5-add-retry-and-timeout)).
|
|
6
6
|
|
|
7
|
+
## On this page
|
|
8
|
+
|
|
9
|
+
- [Wrap a handler with retry and timeout](#wrap-a-handler-with-retry-and-timeout)
|
|
10
|
+
- [Options for retry, timeout, and poll](#options-for-retry-timeout-and-poll)
|
|
11
|
+
- [Back off between retries](#back-off-between-retries)
|
|
12
|
+
- [Put selected effects on `ctx`](#put-selected-effects-on-ctx)
|
|
13
|
+
- [Tie a promise to the task signal](#tie-a-promise-to-the-task-signal)
|
|
14
|
+
- [Run cleanup on abort](#run-cleanup-on-abort)
|
|
15
|
+
- [Wait for the next event with `take`](#wait-for-the-next-event-with-take)
|
|
16
|
+
- [Run several starters together](#run-several-starters-together)
|
|
17
|
+
- [Pass `ctx` outside a task](#pass-ctx-outside-a-task)
|
|
18
|
+
|
|
7
19
|
## Wrap a handler with retry and timeout
|
|
8
20
|
|
|
9
21
|
`retry`, `timeout`, and `poll` have two forms:
|
|
@@ -49,9 +61,7 @@ bus.on("load", async (id, ctx) => {
|
|
|
49
61
|
() => fetchUser(id, { signal: ctx.signal }),
|
|
50
62
|
{ times: 4 },
|
|
51
63
|
);
|
|
52
|
-
const data = await timeout(ctx, 5000, (
|
|
53
|
-
return fetchData(id, { signal });
|
|
54
|
-
});
|
|
64
|
+
const data = await timeout(ctx, 5000, (child) => fetchData(id, child));
|
|
55
65
|
await poll(ctx, () => refresh(), 10_000);
|
|
56
66
|
ctx.emit("ready", { user, data });
|
|
57
67
|
});
|
|
@@ -160,7 +170,8 @@ const bus = createBus().use(effects);
|
|
|
160
170
|
bus.on("load", async (id, { race, all, delay }) => {
|
|
161
171
|
// race — same as race(ctx, …)
|
|
162
172
|
const result = await race({
|
|
163
|
-
|
|
173
|
+
// child — the context of this starter; it has the `signal`, so the request stops with the race
|
|
174
|
+
data: (child) => fetchData(id, child),
|
|
164
175
|
// a starter gets its own child with the same bound effects, so delay(5000) stops with the race
|
|
165
176
|
timeout: ({ delay }) => delay(5000),
|
|
166
177
|
});
|
|
@@ -168,8 +179,8 @@ bus.on("load", async (id, { race, all, delay }) => {
|
|
|
168
179
|
|
|
169
180
|
// all — same as all(ctx, …)
|
|
170
181
|
const { user, posts } = await all({
|
|
171
|
-
user: (
|
|
172
|
-
posts: (
|
|
182
|
+
user: (child) => fetchUser(id, child),
|
|
183
|
+
posts: (child) => fetchPosts(id, child),
|
|
173
184
|
});
|
|
174
185
|
});
|
|
175
186
|
```
|
|
@@ -292,8 +303,8 @@ const bus = createBus();
|
|
|
292
303
|
|
|
293
304
|
bus.on("open", async (id, ctx) => {
|
|
294
305
|
const { user, posts } = await allSettled(ctx, {
|
|
295
|
-
user: (
|
|
296
|
-
posts: (
|
|
306
|
+
user: (child) => fetchUser(id, child),
|
|
307
|
+
posts: (child) => fetchPosts(id, child),
|
|
297
308
|
});
|
|
298
309
|
|
|
299
310
|
if (user.status === "fulfilled") {
|
|
@@ -399,7 +410,7 @@ bus.on("sync", async (ids, ctx) => {
|
|
|
399
410
|
The result has the same order and shape as without the option. If one starter of `all` fails, the running ones are cancelled and the waiting ones are never called. `allSettled` goes on after a failure. When `ctx` aborts, both stop and start nothing more. A value that is not a positive integer (`0`, `-1`, `NaN`, `1.5`) throws a `RangeError` before anything starts; `Infinity` means no limit. `race` has no `concurrency`: it ends at the first result, so a starter that waited for a free slot would never run. The form without `ctx` takes no options, because a second argument would look like the form with `ctx`, so a group starter inside another group takes none; write `(child) => all(child, effects, { concurrency: 5 })` there.
|
|
400
411
|
|
|
401
412
|
|
|
402
|
-
**A starter gets the context of its own child task.** Pass `child` as the first argument to any effect, as a handler passes `ctx`: when the race ends, `child` is aborted and stops exactly this effect. The outer `ctx` is only for `race` itself. Your own API takes the signal in its options
|
|
413
|
+
**A starter gets the context of its own child task.** Pass `child` as the first argument to any effect, as a handler passes `ctx`: when the race ends, `child` is aborted and stops exactly this effect. The outer `ctx` is only for `race` itself. Your own API takes the signal in its options, and `child` has a `signal`, so pass it as the options: `(child) => fetchUser(id, child)` (`fetch` reads only `signal` from it). The same holds for `all`, `allSettled` and `timeout`.
|
|
403
414
|
|
|
404
415
|
If `ctx` is `{}` or a bare `{ signal }` (outside a task), `child` has only `signal` and `abort`, so it fits `delay` and `poll` but not `take`, which needs a bus.
|
|
405
416
|
|
package/docs/guide.md
CHANGED
|
@@ -11,6 +11,23 @@ Climb one step at a time: small bus → policies → effects → handlers and mo
|
|
|
11
11
|
|
|
12
12
|
Runnable code for every step: [`examples/guide.ts`](../examples/guide.ts).
|
|
13
13
|
|
|
14
|
+
## On this page
|
|
15
|
+
|
|
16
|
+
- [1. Create a bus](#1-create-a-bus)
|
|
17
|
+
- [Listen to every event](#listen-to-every-event)
|
|
18
|
+
- [Pick events with a function](#pick-events-with-a-function)
|
|
19
|
+
- [2. Use the handler context](#2-use-the-handler-context)
|
|
20
|
+
- [3. Abort the previous run with `latest`](#3-abort-the-previous-run-with-latest)
|
|
21
|
+
- [4. Pick another policy when overlap differs](#4-pick-another-policy-when-overlap-differs)
|
|
22
|
+
- [5. Add retry and timeout](#5-add-retry-and-timeout)
|
|
23
|
+
- [6. Run work in parallel or wait for an event](#6-run-work-in-parallel-or-wait-for-an-event)
|
|
24
|
+
- [7. Name events three ways](#7-name-events-three-ways)
|
|
25
|
+
- [8. Bind a handler to an action later](#8-bind-a-handler-to-an-action-later)
|
|
26
|
+
- [9. Mount a domain as a module](#9-mount-a-domain-as-a-module)
|
|
27
|
+
- [10. Add types when you want checking (optional)](#10-add-types-when-you-want-checking-optional)
|
|
28
|
+
- [11. Isolate errors and warn on typos](#11-isolate-errors-and-warn-on-typos)
|
|
29
|
+
- [Where next](#where-next)
|
|
30
|
+
|
|
14
31
|
## 1. Create a bus
|
|
15
32
|
|
|
16
33
|
`on` subscribes. `emit` calls the subscribers. `on` returns the unsubscribe function.
|
|
@@ -31,7 +48,7 @@ bus.emit("greet", "Bob"); // nobody listens
|
|
|
31
48
|
|
|
32
49
|
Handlers run at once, in subscription order.
|
|
33
50
|
|
|
34
|
-
`bus.once` is like `on`, but the handler runs at most once (it is removed before it is invoked). `bus.off("greet")` clears every listener of that event; `bus.off()` with no arguments clears **every** listener on the bus (including `"*"`). `ctx.off` is the same `off` as the bus’s — so `ctx.off()` also clears the whole bus.
|
|
51
|
+
`bus.once` is like `on`, but the handler runs at most once (it is removed before it is invoked). `bus.off("greet")` clears every listener of that event; `bus.off()` with no arguments clears **every** listener on the bus (including `"*"`). `ctx.off` is the same `off` as the bus’s — so `ctx.off()` also clears the whole bus. `bus.get("greet")` gives the listeners that event has right now; `bus.get("greet").length === 0` tells that nobody listens any more (`"*"` gives the any-event listeners, which a named event does not include; `bus.get()` with no name gives every listener of every event). It takes the event name as a string: for an action creator use `creator.type`.
|
|
35
52
|
|
|
36
53
|
```js
|
|
37
54
|
import { createBus } from "nanoeffects";
|
|
@@ -272,10 +289,11 @@ import { all, delay, race, take } from "nanoeffects/effects";
|
|
|
272
289
|
const bus = createBus();
|
|
273
290
|
|
|
274
291
|
// all — run together; one failure cancels the rest
|
|
292
|
+
// child — the context of that starter: it has the `signal`, so the request stops with the group
|
|
275
293
|
bus.on("search", async (id, ctx) => {
|
|
276
294
|
const { user, posts } = await all(ctx, {
|
|
277
|
-
user: (
|
|
278
|
-
posts: (
|
|
295
|
+
user: (child) => api.user(id, child),
|
|
296
|
+
posts: (child) => api.posts(id, child),
|
|
279
297
|
});
|
|
280
298
|
ctx.emit("loaded", { user, posts });
|
|
281
299
|
});
|
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
Read this page if your project uses Effector and you want to know where nanoeffects fits. They overlap in one place: events that start effects. Effector also holds state; nanoeffects does not. It maps the names, shows one effect both ways, and lists what has no equivalent.
|
|
4
4
|
|
|
5
|
+
## On this page
|
|
6
|
+
|
|
7
|
+
- [Split the work](#split-the-work)
|
|
8
|
+
- [Find the equivalent](#find-the-equivalent)
|
|
9
|
+
- [Rewrite an effect](#rewrite-an-effect)
|
|
10
|
+
- [Keep the state in Zustand](#keep-the-state-in-zustand)
|
|
11
|
+
- [Rewrite a form](#rewrite-a-form)
|
|
12
|
+
- [Replace the log with a request](#replace-the-log-with-a-request)
|
|
13
|
+
- [Use both in one app](#use-both-in-one-app)
|
|
14
|
+
- [What is different](#what-is-different)
|
|
15
|
+
|
|
5
16
|
## Split the work
|
|
6
17
|
|
|
7
18
|
| Job | Use |
|
package/docs/observable.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Use this page when your app already works with Observables or RxJS and you want a bus event as a stream. If you start from scratch, prefer `bus.on` with policies: they cancel work and show up in traces.
|
|
4
4
|
|
|
5
|
+
The same entry has `events`, which needs no stream library: it gives an event to a `for await` loop. See [Read an event with a `for await` loop](#read-an-event-with-a-for-await-loop).
|
|
6
|
+
|
|
5
7
|
`createObserve(bus)` from `nanoeffects/observable` turns a bus event into an Observable that follows the TC39 proposal (`subscribe`, `unsubscribe`, `Symbol.observable`). RxJS and other stream libraries take it as it is. Nothing here depends on them.
|
|
6
8
|
|
|
7
9
|
## Turn an event into a stream
|
|
@@ -145,6 +147,47 @@ bus.on(
|
|
|
145
147
|
|
|
146
148
|
`switchMap` drops the old inner stream. `latest` aborts the old task through `signal`, so a `fetch` that got the signal really stops.
|
|
147
149
|
|
|
150
|
+
## Read an event with a `for await` loop
|
|
151
|
+
|
|
152
|
+
Problem: a task wants to handle the payloads of one event one after another, in order, and stop when the screen closes. `take` waits for one payload, and a listener has no place for a loop.
|
|
153
|
+
|
|
154
|
+
`events(source, event, options)` from `nanoeffects/observable` returns an async iterator of the payloads. It listens from the call, so a payload that comes before the first read is kept. `source` is a bus or a task context; with a context the loop also ends when the task aborts.
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
import { createBus } from "nanoeffects";
|
|
158
|
+
import { events } from "nanoeffects/observable";
|
|
159
|
+
|
|
160
|
+
const bus = createBus();
|
|
161
|
+
const controller = new AbortController();
|
|
162
|
+
|
|
163
|
+
// events — the "query/changed" payloads one by one; keeps the last 3 while the loop is busy
|
|
164
|
+
const queries = events(bus, "query/changed", {
|
|
165
|
+
signal: controller.signal,
|
|
166
|
+
buffer: 3,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
async function watchQueries() {
|
|
170
|
+
for await (const query of queries) {
|
|
171
|
+
console.log("search for", query); // runs once per payload, in order
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
watchQueries().catch((reason) => console.log("stopped:", reason)); // the abort rejects the loop
|
|
176
|
+
bus.emit("query/changed", "cat");
|
|
177
|
+
controller.abort("screen closed"); // the loop ends with this reason
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
| Option | What it does | Default |
|
|
181
|
+
| ------ | ------------ | ------- |
|
|
182
|
+
| `signal` | Ends the loop. The waiting read rejects with the abort reason. | none |
|
|
183
|
+
| `buffer` | The most payloads kept while the loop is busy. | `Infinity` |
|
|
184
|
+
| `overflow` | Which payload goes when the buffer is full: `"drop-oldest"` or `"drop-newest"`. | `"drop-oldest"` |
|
|
185
|
+
|
|
186
|
+
- `break`, `return` and a throw inside the loop remove the listener.
|
|
187
|
+
- With no `buffer`, a loop that never reads keeps every payload. Set `buffer` for a slow loop.
|
|
188
|
+
- An event of a bus does not end, so the loop ends only by `break`, the `signal` or the task.
|
|
189
|
+
- Inside a task, `events(ctx, "query/changed")` needs no `signal`: the task ending is the end.
|
|
190
|
+
|
|
148
191
|
## Convert back to a stream or a promise
|
|
149
192
|
|
|
150
193
|
| You need | Write |
|
package/docs/oop.md
CHANGED
|
@@ -9,6 +9,23 @@ Use this page when **your domain logic lives in classes**: a service, a store or
|
|
|
9
9
|
|
|
10
10
|
If your logic is plain functions, stay with [modules](actions.md#mount-domains-with-modules): a module is the same idea without a class.
|
|
11
11
|
|
|
12
|
+
## On this page
|
|
13
|
+
|
|
14
|
+
- [The problem without it](#the-problem-without-it)
|
|
15
|
+
- [Make an independent emitter with `Emitter`](#make-an-independent-emitter-with-emitter)
|
|
16
|
+
- [Type the events (optional)](#type-the-events-optional)
|
|
17
|
+
- [Give a class the methods of a shared bus](#give-a-class-the-methods-of-a-shared-bus)
|
|
18
|
+
- [Extend the result with no class of your own](#extend-the-result-with-no-class-of-your-own)
|
|
19
|
+
- [Let the class make its bus](#let-the-class-make-its-bus)
|
|
20
|
+
- [End the subscriptions with `dispose`](#end-the-subscriptions-with-dispose)
|
|
21
|
+
- [Stream an event with a `Subject`](#stream-an-event-with-a-subject)
|
|
22
|
+
- [Keep a method of your own](#keep-a-method-of-your-own)
|
|
23
|
+
- [Subscribe a method with `@On` and `@Once`](#subscribe-a-method-with-on-and-once)
|
|
24
|
+
- [Decorate the class with `@WithBus`](#decorate-the-class-with-withbus)
|
|
25
|
+
- [Pass a method by hand](#pass-a-method-by-hand)
|
|
26
|
+
- [Type the instances (optional)](#type-the-instances-optional)
|
|
27
|
+
- [What is not done](#what-is-not-done)
|
|
28
|
+
|
|
12
29
|
## The problem without it
|
|
13
30
|
|
|
14
31
|
A class that listens to the bus keeps a reference to it, and keeps every unsubscribe function in a list to call when the object goes away. Forget one and the old object keeps handling events. `extendBus` does both for you.
|
|
@@ -310,7 +327,7 @@ export const SearchService = extendBus(bus, SearchServiceBase);
|
|
|
310
327
|
|
|
311
328
|
Things to know:
|
|
312
329
|
|
|
313
|
-
- The event is a name
|
|
330
|
+
- The event is a name or an action creator, as for `bus.on`. For a creator the payload of the method is checked against it.
|
|
314
331
|
- The second argument, `wrap`, takes the bound handler and returns the one to subscribe: `latest`, or `(handler) => debounce(300, handler)`. A policy needs it: `latest(this.search)` wraps the function without `this`, and the bound handler is what `wrap` gets.
|
|
315
332
|
- The subscriptions are made right after the constructor of `Base`. An event that is emitted inside that constructor does not reach them.
|
|
316
333
|
- A subclass inherits them. A method that a subclass overrides is subscribed only if the override is decorated.
|
package/docs/package.md
CHANGED
|
@@ -2,24 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
Imports are split so you only load what you use. `createBus` stays small; the rest is another entry.
|
|
4
4
|
|
|
5
|
+
## Start with these three
|
|
6
|
+
|
|
5
7
|
| Import | What is in it |
|
|
6
8
|
| ------ | ------------- |
|
|
7
9
|
| `nanoeffects` | `createBus`, the types |
|
|
8
10
|
| `nanoeffects/policies` | `latest`, `leading`, `debounce`, `throttle`, `queue` |
|
|
9
11
|
| `nanoeffects/effects` | `retry`, `timeout`, `poll`, `delay`, `take`, `all`, `allSettled`, `race`, `effectsPlugin` |
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
| `nanoeffects/
|
|
18
|
-
| `nanoeffects/
|
|
19
|
-
| `nanoeffects/
|
|
20
|
-
| `nanoeffects/
|
|
21
|
-
| `nanoeffects/
|
|
22
|
-
| `nanoeffects/
|
|
12
|
+
|
|
13
|
+
## Add the rest when you need it
|
|
14
|
+
|
|
15
|
+
Each row is optional. The "when" column says what need brings you to it.
|
|
16
|
+
|
|
17
|
+
| Import | What is in it | When |
|
|
18
|
+
| ------ | ------------- | ---- |
|
|
19
|
+
| `nanoeffects/actions` | `createAction`, `createJob`, `createModule`, `modulePlugin`, `actionsPlugin` | `emit` must return a result, or domain logic leaves the UI |
|
|
20
|
+
| `nanoeffects/match` | `matchPlugin`: `on`, `once` and `off` that take a function which picks the events | `on` must take a function that picks events |
|
|
21
|
+
| `nanoeffects/context` | `contextPlugin`: your own fields (a store, a client) on `ctx` of every task, typed from the value | a store or client must be on every task |
|
|
22
|
+
| `nanoeffects/signal` | `signalPlugin`: `emit(call, { signal })` ties the tasks of an emit to an outside `AbortSignal` | an outside `AbortSignal` (request, React Query) must stop tasks |
|
|
23
|
+
| `nanoeffects/oop` | `Emitter`, `extendBus`, `WithBus`, `On`, `Once`, `disposeBus`: the methods of a bus on a class, with the subscriptions of each instance ended by `dispose()` | you write classes, not functions |
|
|
24
|
+
| `nanoeffects/scope` | `scopePlugin`, `effect`: fields for one action, job, module or `emit` (the types `Scope`, `Effect`) | one action or module needs its own fields |
|
|
25
|
+
| `nanoeffects/redux` | `createEffectsMiddleware` | you already use Redux |
|
|
26
|
+
| `nanoeffects/observable` | `createObserve`, `Subject` | you need an Observable (RxJS) |
|
|
27
|
+
| `nanoeffects/react` | `reactPlugin`: adds the `useEmitter` hook to a bus (needs React 18 or newer) | React components |
|
|
28
|
+
| `nanoeffects/svelte` | `sveltePlugin`: adds `eventStore` to a bus (needs Svelte 5 or newer) | Svelte components |
|
|
29
|
+
| `nanoeffects/vue` | `vuePlugin`: adds `watchEmitter` and `useEventRef` to a bus (needs Vue 3.3 or newer) | Vue components |
|
|
30
|
+
| `nanoeffects/dev` | `devPlugin` (no-op in production) | you look for leaks in development |
|
|
31
|
+
| `nanoeffects/trace` | `createTracer`, the loggers and `tracePlugin` (names for the trace) | you want to see what ran and why |
|
|
23
32
|
|
|
24
33
|
The `...Plugin` names above are used with `bus.use`. [Plugins](plugins.md) lists all of them, what each returns, and the order to use them in.
|
|
25
34
|
|
package/docs/plugins.md
CHANGED
|
@@ -2,12 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
Optional. A plugin adds one thing to a bus: fields on `ctx`, a method, a registry of handlers, diagnostics. Read this page when you want to know which plugins exist, what each one gives you, and in what order to use them. Skip it for a small bus: `createBus()` alone is enough.
|
|
4
4
|
|
|
5
|
+
## On this page
|
|
6
|
+
|
|
7
|
+
- [Use a plugin](#use-a-plugin)
|
|
8
|
+
- [Pick a plugin by task](#pick-a-plugin-by-task)
|
|
9
|
+
- [Main plugins](#main-plugins)
|
|
10
|
+
- [Put a store or a client on every handler: `contextPlugin`](#put-a-store-or-a-client-on-every-handler-contextplugin)
|
|
11
|
+
- [Use `race` and `delay` without `ctx`: `effectsPlugin`](#use-race-and-delay-without-ctx-effectsplugin)
|
|
12
|
+
- [Bind or replace a handler: `actionsPlugin`](#bind-or-replace-a-handler-actionsplugin)
|
|
13
|
+
- [Mount a domain as one unit: `modulePlugin`](#mount-a-domain-as-one-unit-moduleplugin)
|
|
14
|
+
- [Extra plugins](#extra-plugins)
|
|
15
|
+
- [Give one action its own fields: `scopePlugin`](#give-one-action-its-own-fields-scopeplugin)
|
|
16
|
+
- [Tie an emit or a listener to an outside signal: `signalPlugin`](#tie-an-emit-or-a-listener-to-an-outside-signal-signalplugin)
|
|
17
|
+
- [Name the origin and the flow of a trace: `tracePlugin`](#name-the-origin-and-the-flow-of-a-trace-traceplugin)
|
|
18
|
+
- [Listen to a group of events: `matchPlugin`](#listen-to-a-group-of-events-matchplugin)
|
|
19
|
+
- [Hand the bus to a state manager: `emitterPlugin`](#hand-the-bus-to-a-state-manager-emitterplugin)
|
|
20
|
+
- [Tell the bus when a React value changes: `reactPlugin`](#tell-the-bus-when-a-react-value-changes-reactplugin)
|
|
21
|
+
- [Find leaks while you develop: `devPlugin`](#find-leaks-while-you-develop-devplugin)
|
|
22
|
+
- [Put them together](#put-them-together)
|
|
23
|
+
- [Use them in any order](#use-them-in-any-order)
|
|
24
|
+
- [See what is not a plugin](#see-what-is-not-a-plugin)
|
|
25
|
+
- [Write your own plugin](#write-your-own-plugin)
|
|
26
|
+
|
|
5
27
|
## Use a plugin
|
|
6
28
|
|
|
7
29
|
A plugin is a function of the bus. `bus.use(plugin)` calls it with the bus and returns whatever the plugin returns. Most plugins return the same bus, so you can chain them: `createBus().use(a).use(b)`. A few return something of their own (a registry, an app, a `disconnect` function): keep it.
|
|
8
30
|
|
|
9
31
|
In TypeScript keep the result of `contextPlugin` and `effectsPlugin`: the type of the bus with its new fields is the type they return (`const bus = createBus().use(contextPlugin({ store }))`).
|
|
10
32
|
|
|
33
|
+
## Pick a plugin by task
|
|
34
|
+
|
|
35
|
+
Find your need in the left column. No row fits: you need no plugin.
|
|
36
|
+
|
|
37
|
+
| I need | Use |
|
|
38
|
+
| ------ | --- |
|
|
39
|
+
| A store, client or logger on every handler | [`contextPlugin`](#put-a-store-or-a-client-on-every-handler-contextplugin) |
|
|
40
|
+
| `race`, `all` or `delay` without passing `ctx` | [`effectsPlugin`](#use-race-and-delay-without-ctx-effectsplugin) |
|
|
41
|
+
| A test that replaces a handler | [`actionsPlugin`](#bind-or-replace-a-handler-actionsplugin) |
|
|
42
|
+
| One domain mounted and unmounted as a unit | [`modulePlugin`](#mount-a-domain-as-one-unit-moduleplugin) |
|
|
43
|
+
| Fields for one action or module only | [`scopePlugin`](#give-one-action-its-own-fields-scopeplugin) |
|
|
44
|
+
| Tasks that stop when a request, socket or query is cancelled | [`signalPlugin`](#tie-an-emit-or-a-listener-to-an-outside-signal-signalplugin) |
|
|
45
|
+
| One listener for a group of events | [`matchPlugin`](#listen-to-a-group-of-events-matchplugin) |
|
|
46
|
+
| A callback for a state manager that emits | [`emitterPlugin`](#hand-the-bus-to-a-state-manager-emitterplugin) |
|
|
47
|
+
| A React component that sends its value to the bus | [`reactPlugin`](#tell-the-bus-when-a-react-value-changes-reactplugin) |
|
|
48
|
+
| Names in the trace | [`tracePlugin`](#name-the-origin-and-the-flow-of-a-trace-traceplugin) |
|
|
49
|
+
| Warnings about leaks while you develop | [`devPlugin`](#find-leaks-while-you-develop-devplugin) |
|
|
50
|
+
|
|
11
51
|
## Main plugins
|
|
12
52
|
|
|
13
53
|
These four cover what most apps need: shared dependencies, effects, handlers and modules. Start here.
|
|
@@ -33,6 +73,8 @@ const bus = createBus().use(contextPlugin({ store }));
|
|
|
33
73
|
bus.on("save", (draft, { store }) => store.put(draft));
|
|
34
74
|
```
|
|
35
75
|
|
|
76
|
+
A client that must stop with the task, such as an `axios` instance, goes in as `fromSignal((signal) => axios.create({ signal }))` from `nanoeffects/signal`: it is built from the signal of each task, when the task reads it. More in [Build a field from the signal of each task](context.md#build-a-field-from-the-signal-of-each-task).
|
|
77
|
+
|
|
36
78
|
### Use `race` and `delay` without `ctx`: `effectsPlugin`
|
|
37
79
|
|
|
38
80
|
Effects such as `race`, `all`, `take` and `delay` are bound to the task, so a handler calls `race({ … })` and not `race(ctx, { … })`. Your own functions that take `{ signal }` first work too. More in [Effects](effects.md#put-selected-effects-on-ctx).
|
|
@@ -48,7 +90,7 @@ const bus = createBus().use(effects);
|
|
|
48
90
|
|
|
49
91
|
bus.on("load", async (id, { race, delay }) => {
|
|
50
92
|
const result = await race({
|
|
51
|
-
data: (
|
|
93
|
+
data: (child) => api.get(id, child),
|
|
52
94
|
timeout: () => delay(5000),
|
|
53
95
|
});
|
|
54
96
|
});
|
|
@@ -85,7 +127,7 @@ const bus = createBus();
|
|
|
85
127
|
// modulePlugin — mounts userModule at the end of this block, or at the first use if that is earlier, and returns the app
|
|
86
128
|
const app = bus.use(modulePlugin([userModule]));
|
|
87
129
|
|
|
88
|
-
app.unmount(userModule); // its listeners, bound
|
|
130
|
+
app.unmount(userModule); // its listeners, bound actions and tasks are gone
|
|
89
131
|
```
|
|
90
132
|
|
|
91
133
|
## Extra plugins
|
|
@@ -95,7 +137,7 @@ Add one of these when you meet its need. None of them is required.
|
|
|
95
137
|
| Plugin | What it gives you | Import | `bus.use` returns |
|
|
96
138
|
| ------ | ----------------- | ------ | ----------------- |
|
|
97
139
|
| [`scopePlugin`](#give-one-action-its-own-fields-scopeplugin) | Its own fields for one action, job or module, and a way to replace them in one `emit`. | `nanoeffects/scope` | The same bus |
|
|
98
|
-
| [`signalPlugin`](#tie-an-emit-to-an-outside-signal-signalplugin) | `emit(call, { signal })` that aborts its tasks when an outside signal aborts. | `nanoeffects/signal` | The same bus, typed with the new `emit` |
|
|
140
|
+
| [`signalPlugin`](#tie-an-emit-or-a-listener-to-an-outside-signal-signalplugin) | `emit(call, { signal })` that aborts its tasks, and `on(event, handler, { signal })` that removes the listener, when an outside signal aborts. | `nanoeffects/signal` | The same bus, typed with the new `emit` |
|
|
99
141
|
| [`matchPlugin`](#listen-to-a-group-of-events-matchplugin) | `on` that listens to a group of events which a function picks. | `nanoeffects/match` | The same bus, typed with the new overloads |
|
|
100
142
|
| [`emitterPlugin`](#hand-the-bus-to-a-state-manager-emitterplugin) | A callback that emits an action, to give to a state manager. | `nanoeffects/actions` | The same bus, with `emitter` |
|
|
101
143
|
| [`tracePlugin`](#name-the-origin-and-the-flow-of-a-trace-traceplugin) | `traceId` and `traceGroupId` for a trace: the root of an emitter, a flow, a named listener. | `nanoeffects/trace` | The same bus |
|
|
@@ -111,10 +153,10 @@ import { createBus } from "nanoeffects";
|
|
|
111
153
|
import { createAction } from "nanoeffects/actions";
|
|
112
154
|
import { scopePlugin } from "nanoeffects/scope";
|
|
113
155
|
|
|
114
|
-
// scopePlugin — makes the scope of
|
|
156
|
+
// scopePlugin — makes the scope of an action work on this bus
|
|
115
157
|
const bus = createBus().use(scopePlugin());
|
|
116
158
|
|
|
117
|
-
// the last argument: fields of this
|
|
159
|
+
// the last argument: fields of this action only
|
|
118
160
|
const getUser = createAction("user/get", (id, { api }) => api.getUser(id), {
|
|
119
161
|
api,
|
|
120
162
|
});
|
|
@@ -123,25 +165,41 @@ const fakeApi = { getUser: async (id) => ({ id, name: "test" }) };
|
|
|
123
165
|
bus.emit(getUser("1"), { api: fakeApi }); // a fake api for this call only
|
|
124
166
|
```
|
|
125
167
|
|
|
126
|
-
### Tie an emit to an outside signal: `signalPlugin`
|
|
168
|
+
### Tie an emit or a listener to an outside signal: `signalPlugin`
|
|
127
169
|
|
|
128
|
-
A library such as React Query owns the request and gives you an `AbortSignal`. `emit` takes it as `{ signal }` and aborts the tasks it starts when the signal aborts, so `timeout`, `retry` and the request stop too. More in [Use nanoeffects with React Query and
|
|
170
|
+
A library such as React Query owns the request and gives you an `AbortSignal`. `emit` takes it as `{ signal }` and aborts the tasks it starts when the signal aborts, so `timeout`, `retry` and the request stop too. More in [Use nanoeffects with React Query, SWR and RTK Query](query-libraries.md).
|
|
129
171
|
|
|
130
172
|
```js
|
|
131
173
|
import { createBus } from "nanoeffects";
|
|
132
174
|
import { createAction } from "nanoeffects/actions";
|
|
133
175
|
import { signalPlugin } from "nanoeffects/signal";
|
|
134
176
|
|
|
135
|
-
// signalPlugin — emit takes { signal } for
|
|
177
|
+
// signalPlugin — emit takes { signal } for an action, a job or an action
|
|
136
178
|
const bus = createBus().use(signalPlugin());
|
|
137
179
|
|
|
138
180
|
const getUser = createAction("user/get", (id, { signal }) => api.getUser(id, { signal }));
|
|
139
181
|
|
|
140
182
|
const controller = new AbortController();
|
|
141
|
-
bus.emit(getUser("1"), { signal: controller.signal }); // runs the
|
|
183
|
+
bus.emit(getUser("1"), { signal: controller.signal }); // runs the action
|
|
142
184
|
controller.abort(); // ctx.signal of the handler aborts with it
|
|
143
185
|
```
|
|
144
186
|
|
|
187
|
+
The same signal can end a listener. `on` and `once` take `{ signal }` as their options, as `addEventListener` does, so code that has a signal keeps no `unbind` function. The listener is removed when the signal aborts; a handler that already runs is not aborted. A signal that is aborted already adds no listener.
|
|
188
|
+
|
|
189
|
+
```js
|
|
190
|
+
import { createBus } from "nanoeffects";
|
|
191
|
+
import { signalPlugin } from "nanoeffects/signal";
|
|
192
|
+
|
|
193
|
+
// signalPlugin — on and once take { signal } in their options
|
|
194
|
+
const bus = createBus().use(signalPlugin());
|
|
195
|
+
|
|
196
|
+
const controller = new AbortController();
|
|
197
|
+
bus.on("saved", (draft) => console.log(draft), { signal: controller.signal }); // listens until the abort
|
|
198
|
+
controller.abort(); // the listener is gone: bus.get("saved") is empty
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Without the plugin the `signal` option is ignored and the listener stays. `off(event, handler)` removes a listener but does not end its link to the signal; call the function that `on` returned for that.
|
|
202
|
+
|
|
145
203
|
### Name the origin and the flow of a trace: `tracePlugin`
|
|
146
204
|
|
|
147
205
|
With a tracer on the bus, the plugin lets a trace say which subscription began a chain (`traceId` of `useEmitter` and `bus.emitter`), follow one flow by a `traceGroupId`, and show which listener ran (`bus.on(event, handler, { traceId })`). Without the plugin the options are ignored. More in [Tracing](tracing.md#show-which-subscription-began-a-chain).
|
|
@@ -254,7 +312,7 @@ import { scopePlugin } from "nanoeffects/scope";
|
|
|
254
312
|
export const bus = createBus()
|
|
255
313
|
.use(contextPlugin({ store })) // ctx.store on every task
|
|
256
314
|
.use(effectsPlugin({ race, all })) // ctx.race and ctx.all, bound to the task
|
|
257
|
-
.use(scopePlugin()) // scopes for
|
|
315
|
+
.use(scopePlugin()) // scopes for actions, jobs, modules and emit
|
|
258
316
|
.use(matchPlugin()) // bus.on takes a function that picks the events
|
|
259
317
|
.use(emitterPlugin()); // bus.emitter(creator)
|
|
260
318
|
|
|
@@ -295,7 +353,7 @@ These connect the bus to something, but are not used with `bus.use`:
|
|
|
295
353
|
- `createEffectsMiddleware(bus, { filter })` from `nanoeffects/redux` is a Redux middleware for a bus that exists. See [Stores](stores.md#wire-redux-toolkit-to-a-bus).
|
|
296
354
|
- `createBus({ trace })` takes a tracer from `nanoeffects/trace`. See [Tracing](tracing.md).
|
|
297
355
|
- `extendBus(bus, Base)` from `nanoeffects/oop` gives a class the methods of a bus, with the subscriptions of each instance ended by `dispose()`. It reads the bus as it is, so use the plugins first. See [Use the bus from a class](oop.md).
|
|
298
|
-
- `createObserve(bus)` from `nanoeffects/observable` turns an event into an Observable. See [Observable](observable.md).
|
|
356
|
+
- `createObserve(bus)` from `nanoeffects/observable` turns an event into an Observable, and `events(bus, event)` into a `for await` loop. See [Observable](observable.md).
|
|
299
357
|
|
|
300
358
|
## Write your own plugin
|
|
301
359
|
|