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.
Files changed (84) hide show
  1. package/README.md +26 -24
  2. package/dist/{action-creator-D_c223C7.d.ts → action-creator-jeEqIKgK.d.ts} +73 -32
  3. package/dist/actions.d.ts +18 -18
  4. package/dist/actions.js +1 -1
  5. package/dist/bus-0uYn_kRj.js +1 -0
  6. package/dist/context-DtUnzC-1.js +1 -0
  7. package/dist/context-types-Bme1mzkn.d.ts +31 -0
  8. package/dist/context.d.ts +8 -3
  9. package/dist/context.js +1 -1
  10. package/dist/{delay-18LJ8jSk.js → delay-5OhJZZQ0.js} +1 -1
  11. package/dist/dev.d.ts +2 -2
  12. package/dist/dev.js +1 -1
  13. package/dist/effects.d.ts +2 -2
  14. package/dist/effects.js +1 -1
  15. package/dist/event-DVcW0mME.js +1 -0
  16. package/dist/index.d.ts +2 -2
  17. package/dist/index.js +1 -1
  18. package/dist/match.d.ts +3 -3
  19. package/dist/match.js +1 -1
  20. package/dist/{module-WX0Gngnt.d.ts → module-BWMW83qk.d.ts} +38 -38
  21. package/dist/notrace/{action-creator-Bux7Zzsl.d.ts → action-creator-xpQ_I2h8.d.ts} +73 -32
  22. package/dist/notrace/actions.d.ts +52 -52
  23. package/dist/notrace/actions.js +1 -1
  24. package/dist/notrace/bus-37nlVljc.js +1 -0
  25. package/dist/notrace/effects.d.ts +2 -2
  26. package/dist/notrace/index.d.ts +2 -2
  27. package/dist/notrace/index.js +1 -1
  28. package/dist/notrace/{plugin-CATXOxtj.d.ts → plugin-6LhNx0th.d.ts} +1 -1
  29. package/dist/notrace/policies.d.ts +1 -1
  30. package/dist/notrace/policies.js +1 -1
  31. package/dist/notrace/redux.d.ts +1 -1
  32. package/dist/observable.d.ts +43 -1
  33. package/dist/observable.js +1 -1
  34. package/dist/onAbort-BgMfspK4.js +1 -0
  35. package/dist/oop.d.ts +3 -3
  36. package/dist/oop.js +1 -1
  37. package/dist/{plugin-vvkmIIz2.d.ts → plugin-BaErevLy.d.ts} +2 -2
  38. package/dist/policies.d.ts +2 -2
  39. package/dist/policies.js +1 -1
  40. package/dist/react.d.ts +2 -2
  41. package/dist/react.js +1 -1
  42. package/dist/redux.d.ts +1 -1
  43. package/dist/scope.d.ts +9 -9
  44. package/dist/signal-field-key-CWT-3OXC.js +1 -0
  45. package/dist/signal.d.ts +34 -6
  46. package/dist/signal.js +1 -1
  47. package/dist/svelte.d.ts +1 -1
  48. package/dist/trace.d.ts +20 -5
  49. package/dist/trace.js +1 -1
  50. package/dist/{types-Bf-LX_tE.d.ts → types-D5xsR37j.d.ts} +2 -2
  51. package/dist/vue.d.ts +1 -1
  52. package/dist/{with-policy-CPjx0sLm.d.ts → with-policy-BZVRBUOm.d.ts} +1 -1
  53. package/dist/{with-policy-D-SQRkPh.js → with-policy-C6BIiPIi.js} +1 -1
  54. package/docs/README.md +14 -1
  55. package/docs/actions.md +28 -2
  56. package/docs/chat-example.md +16 -4
  57. package/docs/chat-widget.md +135 -0
  58. package/docs/context.md +33 -1
  59. package/docs/effects.md +20 -9
  60. package/docs/guide.md +21 -3
  61. package/docs/migrating-from-effector.md +11 -0
  62. package/docs/observable.md +43 -0
  63. package/docs/oop.md +18 -1
  64. package/docs/package.md +22 -13
  65. package/docs/plugins.md +69 -11
  66. package/docs/query-libraries.md +273 -5
  67. package/docs/react.md +12 -2
  68. package/docs/scope.md +14 -2
  69. package/docs/semantics.md +10 -0
  70. package/docs/server.md +3 -3
  71. package/docs/stability.md +2 -2
  72. package/docs/starter.md +3 -3
  73. package/docs/tracing.md +77 -9
  74. package/docs/typescript.md +1 -1
  75. package/llms-full.txt +816 -72
  76. package/llms.txt +8 -7
  77. package/package.json +6 -6
  78. package/skills/nanoeffects/SKILL.md +2 -2
  79. package/dist/bus-DQDHUN5e.js +0 -1
  80. package/dist/context-UcmrAIhP.js +0 -1
  81. package/dist/context-types-nGtzhjxD.d.ts +0 -14
  82. package/dist/event-0RIBGYqS.js +0 -1
  83. package/dist/notrace/bus-CGi2lrd5.js +0 -1
  84. 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, ({ signal }) => {
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
- data: ({ signal }) => fetchData(id, { signal }),
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: ({ signal }) => fetchUser(id, { signal }),
172
- posts: ({ signal }) => fetchPosts(id, { signal }),
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: ({ signal }) => fetchUser(id, { signal }),
296
- posts: ({ signal }) => fetchPosts(id, { signal }),
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: `({ signal }) => fetchUser(id, { signal })`. The same holds for `all`, `allSettled` and `timeout`.
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: ({ signal }) => api.user(id, { signal }),
278
- posts: ({ signal }) => api.posts(id, { signal }),
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 |
@@ -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, an action creator or a command, as for `bus.on`. For a creator or a command the payload of the method is checked against it.
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
- | `nanoeffects/actions` | `createAction`, `createJob`, `createModule`, `modulePlugin`, `actionsPlugin` |
11
- | `nanoeffects/match` | `matchPlugin`: `on`, `once` and `off` that take a function which picks the events |
12
- | `nanoeffects/context` | `contextPlugin`: your own fields (a store, a client) on `ctx` of every task, typed from the value |
13
- | `nanoeffects/signal` | `signalPlugin`: `emit(call, { signal })` ties the tasks of an emit to an outside `AbortSignal` |
14
- | `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()` |
15
- | `nanoeffects/scope` | `scopePlugin`, `effect`: fields for one command, action, job, module or `emit` (the types `Scope`, `Effect`) |
16
- | `nanoeffects/redux` | `createEffectsMiddleware` |
17
- | `nanoeffects/observable` | `createObserve`, `Subject` |
18
- | `nanoeffects/react` | `reactPlugin`: adds the `useEmitter` hook to a bus (needs React 18 or newer) |
19
- | `nanoeffects/svelte` | `sveltePlugin`: adds `eventStore` to a bus (needs Svelte 5 or newer) |
20
- | `nanoeffects/vue` | `vuePlugin`: adds `watchEmitter` and `useEventRef` to a bus (needs Vue 3.3 or newer) |
21
- | `nanoeffects/dev` | `devPlugin` (no-op in production) |
22
- | `nanoeffects/trace` | `createTracer`, the loggers and `tracePlugin` (names for the trace) |
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: ({ signal }) => api.get(id, { signal }),
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 commands and tasks are gone
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 a command work on this bus
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 command only
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 SWR](query-libraries.md).
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 a command, a job or an action
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 command
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 commands, actions, jobs, modules and emit
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