nanoeffects 0.28.0 → 0.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +6 -5
  2. package/dist/{action-creator-CcAEvUnw.d.ts → action-creator-6rlBTmH9.d.ts} +60 -33
  3. package/dist/actions.d.ts +18 -18
  4. package/dist/actions.js +1 -1
  5. package/dist/bus-DFlmNJ6h.js +1 -0
  6. package/dist/context-B5Z2RpJt.js +1 -0
  7. package/dist/context-types-DKNc5mkS.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 +44 -3
  12. package/dist/dev.js +2 -1
  13. package/dist/effects.d.ts +2 -2
  14. package/dist/effects.js +1 -1
  15. package/dist/emit-key-C6FL7Ewn.js +1 -0
  16. package/dist/guard.d.ts +59 -0
  17. package/dist/guard.js +1 -0
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.js +1 -1
  20. package/dist/match.d.ts +3 -3
  21. package/dist/match.js +1 -1
  22. package/dist/{module-C_oEXw1n.d.ts → module-G39MaVOt.d.ts} +38 -38
  23. package/dist/notrace/{action-creator-DxOvVo8I.d.ts → action-creator-BrMSO3RP.d.ts} +35 -32
  24. package/dist/notrace/actions.d.ts +52 -52
  25. package/dist/notrace/actions.js +1 -1
  26. package/dist/notrace/{bus-DmKn4SDD.js → bus-37nlVljc.js} +1 -1
  27. package/dist/notrace/effects.d.ts +2 -2
  28. package/dist/notrace/index.d.ts +2 -2
  29. package/dist/notrace/index.js +1 -1
  30. package/dist/notrace/{plugin-lJjjd-SN.d.ts → plugin-7GUL2EW5.d.ts} +1 -1
  31. package/dist/notrace/policies.d.ts +1 -1
  32. package/dist/notrace/redux.d.ts +1 -1
  33. package/dist/observable.d.ts +43 -1
  34. package/dist/observable.js +1 -1
  35. package/dist/onAbort-BgMfspK4.js +1 -0
  36. package/dist/oop.d.ts +3 -3
  37. package/dist/oop.js +1 -1
  38. package/dist/{plugin-CuvIgjnX.d.ts → plugin-BWNP23hr.d.ts} +2 -2
  39. package/dist/policies.d.ts +2 -2
  40. package/dist/policies.js +1 -1
  41. package/dist/production/dev.js +1 -1
  42. package/dist/react.d.ts +2 -2
  43. package/dist/react.js +1 -1
  44. package/dist/redux.d.ts +1 -1
  45. package/dist/scope.d.ts +9 -9
  46. package/dist/signal-field-key-CWT-3OXC.js +1 -0
  47. package/dist/signal.d.ts +34 -6
  48. package/dist/signal.js +1 -1
  49. package/dist/site-DLvUoXs3.js +1 -0
  50. package/dist/svelte.d.ts +1 -1
  51. package/dist/trace.d.ts +170 -6
  52. package/dist/trace.js +3 -1
  53. package/dist/{types-FV8fuvI9.d.ts → types-N_cdRr1r.d.ts} +2 -2
  54. package/dist/vue.d.ts +1 -1
  55. package/dist/{with-policy-D-SQRkPh.js → with-policy-CeM1rtES.js} +1 -1
  56. package/dist/{with-policy-3bhFLEXs.d.ts → with-policy-CthiBp_z.d.ts} +1 -1
  57. package/docs/README.md +65 -22
  58. package/docs/actions.md +56 -13
  59. package/docs/cancel-stale-requests.md +2 -0
  60. package/docs/chat-example.md +15 -1
  61. package/docs/chat-widget.md +9 -7
  62. package/docs/context.md +352 -8
  63. package/docs/debounce-and-abort.md +3 -1
  64. package/docs/debugging-loops.md +283 -0
  65. package/docs/effects.md +259 -131
  66. package/docs/errors.md +4 -2
  67. package/docs/examples.md +60 -0
  68. package/docs/guide.md +57 -22
  69. package/docs/hmr.md +35 -1
  70. package/docs/loop-guard.md +109 -0
  71. package/docs/migrating-from-effector.md +15 -2
  72. package/docs/migrating-from-mitt.md +6 -4
  73. package/docs/migrating-from-rxjs.md +2 -0
  74. package/docs/migrating-from-saga.md +2 -0
  75. package/docs/motivation.md +14 -21
  76. package/docs/observable.md +45 -0
  77. package/docs/oop.md +20 -1
  78. package/docs/package.md +25 -13
  79. package/docs/pass-the-signal.md +281 -0
  80. package/docs/plugins.md +90 -15
  81. package/docs/policies.md +73 -46
  82. package/docs/prevent-double-submit.md +3 -1
  83. package/docs/query-libraries.md +276 -6
  84. package/docs/react.md +50 -11
  85. package/docs/recipes.md +528 -0
  86. package/docs/ru/README.md +62 -0
  87. package/docs/ru/actions.md +412 -0
  88. package/docs/ru/cancel-stale-requests.md +106 -0
  89. package/docs/ru/chat-example.md +259 -0
  90. package/docs/ru/chat-widget.md +131 -0
  91. package/docs/ru/context.md +401 -0
  92. package/docs/ru/debounce-and-abort.md +105 -0
  93. package/docs/ru/debugging-loops.md +283 -0
  94. package/docs/ru/effects.md +538 -0
  95. package/docs/ru/errors.md +36 -0
  96. package/docs/ru/examples.md +60 -0
  97. package/docs/ru/guide.md +530 -0
  98. package/docs/ru/hmr.md +83 -0
  99. package/docs/ru/loop-guard.md +109 -0
  100. package/docs/ru/migrating-from-effector.md +339 -0
  101. package/docs/ru/migrating-from-mitt.md +99 -0
  102. package/docs/ru/migrating-from-rxjs.md +197 -0
  103. package/docs/ru/migrating-from-saga.md +97 -0
  104. package/docs/ru/motivation.md +47 -0
  105. package/docs/ru/observable.md +233 -0
  106. package/docs/ru/oop.md +431 -0
  107. package/docs/ru/package.md +42 -0
  108. package/docs/ru/pass-the-signal.md +281 -0
  109. package/docs/ru/plugins.md +398 -0
  110. package/docs/ru/policies.md +201 -0
  111. package/docs/ru/prevent-double-submit.md +122 -0
  112. package/docs/ru/query-libraries.md +392 -0
  113. package/docs/ru/react.md +309 -0
  114. package/docs/ru/recipes.md +528 -0
  115. package/docs/ru/semantics.md +19 -0
  116. package/docs/ru/server.md +204 -0
  117. package/docs/ru/starter.md +118 -0
  118. package/docs/ru/stores.md +673 -0
  119. package/docs/ru/svelte.md +78 -0
  120. package/docs/ru/tracing.md +466 -0
  121. package/docs/ru/typescript.md +170 -0
  122. package/docs/ru/vue.md +92 -0
  123. package/docs/semantics.md +12 -0
  124. package/docs/server.md +5 -3
  125. package/docs/starter.md +2 -0
  126. package/docs/stores.md +2 -0
  127. package/docs/svelte.md +3 -1
  128. package/docs/tracing.md +132 -7
  129. package/docs/typescript.md +3 -1
  130. package/docs/vue.md +3 -1
  131. package/llms-full.txt +2718 -545
  132. package/llms.txt +17 -14
  133. package/package.json +25 -8
  134. package/skills/nanoeffects/SKILL.md +2 -2
  135. package/dist/bus-DlpEluzd.js +0 -1
  136. package/dist/context-UcmrAIhP.js +0 -1
  137. package/dist/context-types-hCKgU8sC.d.ts +0 -14
  138. package/docs/scope.md +0 -266
  139. package/docs/stability.md +0 -90
@@ -0,0 +1,60 @@
1
+ # Examples
2
+
3
+ > **Levels A to F.** Read first: [README](README.md#the-ladder-from-a-to-f).
4
+
5
+ Examples to run in the browser and to read. Every folder lives in `examples/` of the repository and is type-checked with the project. Two of them are explained step by step: [Build a chat](chat-example.md) and [Build a chat widget](chat-widget.md).
6
+
7
+ ## Try it in the browser
8
+
9
+ All three are the same search box: type fast and only one request starts (`debounce`); type again while a request runs and the older one is aborted (`latest`). StackBlitz runs them with no install.
10
+
11
+ | Folder | What is in it |
12
+ | ------ | ------------- |
13
+ | [`playground`](../examples/playground) · [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/playground) | Plain DOM and one file: the smallest setup. The page lists the requests, so the abort is visible. |
14
+ | [`playground-react`](../examples/playground-react) · [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/playground-react) | The same search in a React component, with the bus outside React. |
15
+ | [`playground-devtools`](../examples/playground-devtools) · [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/playground-devtools) | The same search with its emits and stopped calls sent to Redux DevTools. |
16
+
17
+ ## Bigger projects
18
+
19
+ React and TypeScript projects that show where logic goes when an app has several features.
20
+
21
+ | Folder | What is in it |
22
+ | ------ | ------------- |
23
+ | [`chat`](../examples/chat) · [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/chat) | Rooms, history under `latest`, ordered sends under `queue` and `retry`, search under `allSettled`, polling with `poll`. Explained in [Build a chat](chat-example.md). |
24
+ | [`pokedex`](../examples/pokedex) · [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/pokedex) | Real requests to PokeAPI: a search with `debounce`, `latest`, `timeout` and `retry`, abilities with `allSettled`. |
25
+ | [`spells`](../examples/spells) · [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/spells) | The smallest example: a search in one file, one action that carries its handler. |
26
+ | [`chat-widget`](../examples/chat-widget) · [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/chat-widget) | A chat behind a launcher button: opening mounts one module, closing unmounts it. Explained in [Build a chat widget](chat-widget.md). |
27
+ | [`counter`](../examples/counter) · [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/counter) | The counter of the redux-saga tutorial, without a store and with Redux Toolkit. |
28
+ | [`rickandmorty`](../examples/rickandmorty) · [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/rickandmorty) | Characters with React Query: requests are actions that get its abort signal. |
29
+ | [`trades`](../examples/trades) · [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/trades) | A live stream over a WebSocket: `retry`, `latest` and `throttle`. |
30
+
31
+ ## Backend examples
32
+
33
+ Small [Fastify](https://fastify.dev) apps with no database. They need Node 18 or newer: run `npm install` and `npm start` in the folder. More in [Stop work on a server](server.md).
34
+
35
+ | Folder | What is in it |
36
+ | ------ | ------------- |
37
+ | [`api-server`](../examples/api-server) | A server over PokeAPI: `timeout`, `retry`, `allSettled`, and an abort when the client leaves. |
38
+ | [`job-worker`](../examples/job-worker) | A queue worker: `queue`, `retry`, `timeout`, counts from the outcomes of a job. |
39
+ | [`ws-server`](../examples/ws-server) | A WebSocket server: one controller per socket, `latest` with a `key`. |
40
+
41
+ ## Recipes
42
+
43
+ Each file is one job, written as a short program with a comment on every nanoeffects call.
44
+
45
+ | File | Job | Docs |
46
+ | ---- | --- | ---- |
47
+ | [`search.ts`](../examples/search.ts) | A search box that waits for quiet and aborts the older request. | [Policies](policies.md) |
48
+ | [`backoff.ts`](../examples/backoff.ts) | Retry a flaky request with a growing pause. | [Effects](effects.md#backoff) |
49
+ | [`registration-form.ts`](../examples/registration-form.ts) | A sign-up form: a username check that does not race. | [React form](react.md) |
50
+ | [`scope.ts`](../examples/scope.ts) | An action with its own effects and a test that replaces one. | [Working with context](context.md) |
51
+ | [`modules.ts`](../examples/modules.ts) | Domain logic with no UI, mounted and unmounted as one unit. | [Actions](actions.md) |
52
+ | [`redux.ts`](../examples/redux.ts) | Redux Toolkit actions that also reach the bus. | [State managers](stores.md) |
53
+ | [`guide.ts`](../examples/guide.ts) | The code of every step of the guide. | [Guide](guide.md) |
54
+
55
+ ## TypeScript projects
56
+
57
+ | Folder | What is in it |
58
+ | ------ | ------------- |
59
+ | [`typescript`](../examples/typescript) | Name the event map once and use it in modules and actions. |
60
+ | [`typescript-context`](../examples/typescript-context) | Give `ctx` the fields of your bus and type them from one place. |
package/docs/guide.md CHANGED
@@ -1,16 +1,35 @@
1
1
  # Guide
2
2
 
3
- Climb one step at a time: small bus → policies → effects → handlers and modules. Stop when your project has what it needs.
3
+ > **Levels A to D.** Read first: [README](../README.md) (install and the first example).
4
4
 
5
- | Your project | Stop after |
6
- | ------------ | ---------- |
7
- | Widget / prototype | §1–2 |
8
- | SPA with async UI | §3–4 |
9
- | Unreliable network / long flows | §5–6 |
10
- | Domain logic off the UI | §7–9 |
5
+ Climb one step at a time: small bus → policies → effects → handlers and modules. Stop when your project has what it needs. Install the package first: `npm install nanoeffects` (details in the [README](../README.md#install)). The levels A to D are described in the [map of the docs](README.md#the-ladder-from-a-to-f).
6
+
7
+ | Level | Your project | Stop after |
8
+ | ----- | ------------ | ---------- |
9
+ | A | Widget / prototype | §1–2 |
10
+ | B | SPA with async UI | §3–4 |
11
+ | C | Unreliable network / long flows | §5–6 |
12
+ | D | Domain logic off the UI | §7–9 |
11
13
 
12
14
  Runnable code for every step: [`examples/guide.ts`](../examples/guide.ts).
13
15
 
16
+ ## On this page
17
+
18
+ - [1. Create a bus](#1-create-a-bus)
19
+ - [Listen to every event](#listen-to-every-event)
20
+ - [Pick events with a function](#pick-events-with-a-function)
21
+ - [2. Use the handler context](#2-use-the-handler-context)
22
+ - [3. Abort the previous run with `latest`](#3-abort-the-previous-run-with-latest)
23
+ - [4. Pick another policy when overlap differs](#4-pick-another-policy-when-overlap-differs)
24
+ - [5. Add retry and timeout](#5-add-retry-and-timeout)
25
+ - [6. Run work in parallel or wait for an event](#6-run-work-in-parallel-or-wait-for-an-event)
26
+ - [7. Name events three ways](#7-name-events-three-ways)
27
+ - [8. Replace a handler on the fly](#8-replace-a-handler-on-the-fly)
28
+ - [9. Mount a domain as a module](#9-mount-a-domain-as-a-module)
29
+ - [10. Add types when you want checking (optional)](#10-add-types-when-you-want-checking-optional)
30
+ - [11. Isolate errors and warn on typos](#11-isolate-errors-and-warn-on-typos)
31
+ - [Where next](#where-next)
32
+
14
33
  ## 1. Create a bus
15
34
 
16
35
  `on` subscribes. `emit` calls the subscribers. `on` returns the unsubscribe function.
@@ -31,7 +50,12 @@ bus.emit("greet", "Bob"); // nobody listens
31
50
 
32
51
  Handlers run at once, in subscription order.
33
52
 
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. `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`.
53
+ Four more calls cover most of what you need from the bus itself:
54
+
55
+ - `bus.once(event, handler)` is like `on`, but the handler runs at most once (it is removed before it is invoked).
56
+ - `bus.off("greet")` clears every listener of that event.
57
+ - `bus.off()` with no arguments clears **every** listener on the bus, `"*"` included. `ctx.off` is the same `off` as the bus's, so `ctx.off()` also clears the whole bus.
58
+ - `bus.get("greet")` gives the listeners that event has right now, so `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
59
 
36
60
  ```js
37
61
  import { createBus } from "nanoeffects";
@@ -49,6 +73,8 @@ bus.off(); // clear every subscription
49
73
 
50
74
  ### Listen to every event
51
75
 
76
+ You can skip this and the next section on a first read.
77
+
52
78
  `bus.on("*", fn)` runs for every event of the bus. It is for logging, debugging, or forwarding events elsewhere. The handler gets the payload and the task context as usual, and `ctx.type` is the name of the event.
53
79
 
54
80
  ```js
@@ -71,7 +97,7 @@ unbind();
71
97
 
72
98
  ### Pick events with a function
73
99
 
74
- To listen to a group of events, such as every `user/*` event, `"*"` and a check of `ctx.type` do the job but leave the check to you. `matchPlugin` lets `on` take a function that picks the events. The function gets `{ type, payload }` of each event, and the handler runs when it returns true.
100
+ This is for level B and later. To listen to a group of events, such as every `user/*` event, `"*"` and a check of `ctx.type` do the job but leave the check to you. `matchPlugin` lets `on` take a function that picks the events. The function gets `{ type, payload }` of each event, and the handler runs when it returns true.
75
101
 
76
102
  <!-- docs-check: new -->
77
103
 
@@ -217,7 +243,7 @@ const searchLatest = latest(async (query, { signal }) => {
217
243
  bus.on("search", debounce(300, searchLatest));
218
244
  ```
219
245
 
220
- Full options: [Policies](policies.md). No bus? A policy also works as a plain function: [Use a policy without a bus](policies.md#use-a-policy-without-a-bus).
246
+ Full options: [Policies](policies.md). No bus? A policy also works as a plain function: [`withPolicy`: call a policy outside a bus](policies.md#withpolicy-call-a-policy-outside-a-bus).
221
247
 
222
248
  ## 5. Add retry and timeout
223
249
 
@@ -272,10 +298,11 @@ import { all, delay, race, take } from "nanoeffects/effects";
272
298
  const bus = createBus();
273
299
 
274
300
  // all — run together; one failure cancels the rest
301
+ // child — the context of that request: it has the `signal`, so the request stops with the group
275
302
  bus.on("search", async (id, ctx) => {
276
303
  const { user, posts } = await all(ctx, {
277
- user: ({ signal }) => api.user(id, { signal }),
278
- posts: ({ signal }) => api.posts(id, { signal }),
304
+ user: (child) => api.user(id, child),
305
+ posts: (child) => api.posts(id, child),
279
306
  });
280
307
  ctx.emit("loaded", { user, posts });
281
308
  });
@@ -346,9 +373,11 @@ bus.emit(double(21)); // 42
346
373
 
347
374
  Use strings for the plain case. Use objects when other code already builds actions. Give `createAction` a handler when you need a result from `emit`.
348
375
 
349
- ## 8. Bind a handler to an action later
376
+ ## 8. Replace a handler on the fly
350
377
 
351
- An action may carry its handler, as `double` above, or get it later from a registry — another file, or a fake in a test. An action with no handler is an event until one is bound.
378
+ **Problem.** The handler of an action is in the file of the feature, but a test needs a fake, or another layer owns the work. If the handler sits in `createAction`, the event file is tied to the work.
379
+
380
+ **What to do.** Create the action without a handler: it is an event. Bind the real handler with `setHandler` when the bus is made. Later, `actions.handle` binds a newer handler that wins until you remove it.
352
381
 
353
382
  ```js
354
383
  // user.js
@@ -360,26 +389,32 @@ export const getUser = createAction("user/get"); // no handler yet: an event
360
389
  ```js
361
390
  // bus.js
362
391
  import { createBus } from "nanoeffects";
363
- import { actionsPlugin } from "nanoeffects/actions";
392
+ import { actionsPlugin, setHandler } from "nanoeffects/actions";
393
+ import { getUser } from "./user";
364
394
 
365
395
  export const bus = createBus();
366
- // actionsPlugin — registry of handlers for this bus
367
- export const actions = bus.use(actionsPlugin());
396
+
397
+ // setHandler — binds the real handler to the action
398
+ const realUser = setHandler(getUser, (id) => api.user(id));
399
+
400
+ // actionsPlugin — the registry of bound handlers of this bus
401
+ export const actions = bus.use(actionsPlugin([realUser]));
368
402
  ```
369
403
 
370
404
  ```js
371
405
  import { bus, actions } from "./bus";
372
406
  import { getUser } from "./user";
373
407
 
374
- bus.emit(getUser("1")); // nothing is bound: only an event
408
+ const user = await bus.emit(getUser("1")); // the real handler answers
375
409
 
376
- const unregister = actions.handle(getUser, (id) => api.user(id));
377
- const user = await bus.emit(getUser("1"));
410
+ // actions.handle — a newer binding wins, for example a fake in a test
411
+ const restore = actions.handle(getUser, (id) => ({ id, name: "test" }));
412
+ const fake = await bus.emit(getUser("1")); // the fake answers
378
413
 
379
- unregister();
414
+ restore(); // the real handler answers again
380
415
  ```
381
416
 
382
- Each bus has its own registry. A [module](#9-mount-a-domain-as-a-module) can register handlers and remove them on unmount. More in [Actions](actions.md).
417
+ Each bus has its own registry. A [module](#9-mount-a-domain-as-a-module) can bind handlers and remove them on unmount. More in [Actions](actions.md#bind-a-handler-later).
383
418
 
384
419
  ## 9. Mount a domain as a module
385
420
 
package/docs/hmr.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Hot reload and development diagnostics
2
2
 
3
+ > **Level F.** Read first: [Actions, jobs and modules](actions.md).
4
+
3
5
  Use this page in development with modules and hot reload. Skip until you mount domains with `modulePlugin`.
4
6
 
5
7
  When a file is edited, the bundler loads its new version and runs it again, while the page and the rest of the code stay alive. For a bus that can go wrong in three ways:
@@ -37,11 +39,43 @@ bus.use(devPlugin());
37
39
 
38
40
  - **Listeners that pile up.** When more than `maxListeners` (default `10`) listeners of one event were added and not unsubscribed, one warning names the event and the count. The count follows the function that `on` returns; `bus.off(event, fn)` is not seen.
39
41
  - **Modules.** For the app of `modulePlugin` (or the `app` option) it logs `module "user" mounted`, `replaced` when a module of the same name was mounted, and `unmounted`. Only what goes through `app.mount` and `app.unmount` is reported, not sub-modules.
40
- - **Options.** `leaks: false` and `modules: false` switch a diagnostic off; `maxListeners` sets the limit; `log` receives the messages instead of the console.
42
+ - **Where a named listener was made.** For a listener with a `traceId`, it notes the file and the line of the `bus.on` call, while the listener is subscribed. [`graphLogger`](debugging-loops.md#read-a-report) shows it in a loop report, so you can open the handler that closes the loop.
43
+ - **Handlers that ignore the signal.** When a handler emits after its task was aborted (a newer call of `latest`, an unmounted module), the bus drops that emit. One warning then names the handler and the event: the handler did not stop when it was cancelled, usually because `ctx.signal` never reached the request. [Details](#find-a-handler-that-ignores-the-signal)
44
+ - **Options.** `abandoned: false`, `leaks: false` and `modules: false` switch a diagnostic off; `maxListeners` sets the limit; `log` receives the messages instead of the console.
41
45
  - **Disconnect.** `bus.use(devPlugin(...))` returns the function that stops the diagnostics and puts `bus.on` and `app.mount` back.
42
46
 
43
47
  It changes no behavior: it wraps `bus.on`, `app.mount`, and `app.unmount` and adds messages.
44
48
 
49
+ ### Find a handler that ignores the signal
50
+
51
+ A policy such as `latest` cancels the older call by aborting its `ctx.signal`. A handler that does not pass the signal to its request keeps running, and its result arrives after the newer one. The policy drops that result, so nothing looks wrong until the handler does something else with it.
52
+
53
+ ```js
54
+ import { createBus } from "nanoeffects";
55
+ import { latest } from "nanoeffects/policies";
56
+
57
+ const bus = createBus();
58
+
59
+ bus.on(
60
+ "search",
61
+ latest(async (query, ctx) => {
62
+ // the signal is not passed, so an older request is not cancelled
63
+ const items = await fetch(`/api?q=${query}`).then((response) => response.json());
64
+ ctx.emit("results", items);
65
+ }),
66
+ );
67
+ ```
68
+
69
+ With `devPlugin`, the second `search` aborts the first call. When the first call then emits `results`, the bus drops the emit and the plugin warns once:
70
+
71
+ ```text
72
+ [nanoeffects] the handler of "search" emitted "results" after its task was aborted, so the emit was dropped. The handler kept running after it was cancelled: pass ctx.signal to the request (fetch(url, { signal: ctx.signal })) or give ctx to an effect (retry(ctx, ...)).
73
+ ```
74
+
75
+ The check lives in the plugin only: the policies and the bus carry no code for it, and in production `nanoeffects/dev` is empty. It sees the handlers that emit or subscribe with `ctx.on` after the cancel; a handler that never does is not seen.
76
+
77
+ A linter finds the same mistake before the code runs, and much more of it: a handler with no `ctx`, a step that does not get the signal while another does, the calls of `Promise.all`, the callbacks of `retry` and `timeout`, a pause, a stream, `bus.emit` in a handler. [Keep the signal through a handler](pass-the-signal.md) lists every situation, what catches it and how to fix it, and shows the setup for ESLint, oxlint and Biome.
78
+
45
79
  ### Keep the import in production
46
80
 
47
81
  `nanoeffects/dev` is a no-op in a production build. `package.json` maps it to an empty build under the `production` export condition (Vite and webpack set that in production mode), so `bus.use(devPlugin())` can stay without a guard: it touches nothing and costs a few bytes.
@@ -0,0 +1,109 @@
1
+ # Cut a loop with `loopGuardPlugin`
2
+
3
+ > **Level F.** Read first: [Plugins](plugins.md#use-a-plugin). To find a loop before you cut it, see [Find and cut event loops](debugging-loops.md).
4
+
5
+ Two handlers that emit each other's events never stop. A loop made of `await` and timers burns CPU and network. A loop made of microtasks freezes the page, because the browser never gets a turn to draw. A report cannot stop any of them. `loopGuardPlugin` can: it follows the chain of emits and refuses the emit that would pass a limit.
6
+
7
+ Use it as a safety net, also in production. It needs no tracer and changes nothing while a chain stays under the limits. It is opt-in: a bus that does not use it behaves as before.
8
+
9
+ ## On this page
10
+
11
+ - [Cut a loop](#cut-a-loop)
12
+ - [Choose what happens at the limit](#choose-what-happens-at-the-limit)
13
+ - [Limit the emits that come from outside](#limit-the-emits-that-come-from-outside)
14
+ - [Leave a recursion out](#leave-a-recursion-out)
15
+ - [Know what it does not do](#know-what-it-does-not-do)
16
+
17
+ ## Cut a loop
18
+
19
+ The plugin counts how many times the same event is in the chain of emits that led to an emit. The chain follows `ctx.emit`, also across `await`, timers and microtasks. An emit that would pass a limit is refused, and the loop ends there. Other chains go on, and the next emit from outside starts a chain at zero.
20
+
21
+ ```js
22
+ // bus.js
23
+ import { createBus } from "nanoeffects";
24
+ import { loopGuardPlugin } from "nanoeffects/guard";
25
+
26
+ // loopGuardPlugin — refuses the emit that would pass a limit of a chain
27
+ export const bus = createBus().use(
28
+ loopGuardPlugin({
29
+ maxRepeats: 50, // one event may be in a chain 50 times
30
+ maxDepth: 1000, // a chain may be 1000 emits long, whatever the names are
31
+ }),
32
+ );
33
+ ```
34
+
35
+ | Option | Default | Cuts |
36
+ | ------ | ------- | ---- |
37
+ | `maxRepeats` | 50 | The same event in one chain too often: a loop. |
38
+ | `maxDepth` | 1000 | A chain that is too long, also when every event has a new name (`step/${n}`). |
39
+ | `maxPerSecond` | none | Emits of one event from outside any handler that come too fast ([below](#limit-the-emits-that-come-from-outside)). |
40
+ | `ignore` | none | Event names that the repeat count skips ([below](#leave-a-recursion-out)). |
41
+
42
+ ## Choose what happens at the limit
43
+
44
+ `onLimit` says what the refused emit does. Either way the plugin reports the finding once for each path. The report has the path of the loop and never the payload.
45
+
46
+ ```js
47
+ // bus.js
48
+ import { createBus } from "nanoeffects";
49
+ import { LoopError, loopGuardPlugin } from "nanoeffects/guard";
50
+
51
+ export const bus = createBus().use(
52
+ loopGuardPlugin({
53
+ onLimit: "drop", // "drop" (default): the emit is not sent; "throw": it throws a LoopError
54
+ // report — replaces the default console.error
55
+ report: (finding) => console.error(finding.limit, finding.path.join(" → ")),
56
+ }),
57
+ );
58
+
59
+ // with onLimit: "throw", the emit that was refused throws, and the error names the path
60
+ try {
61
+ bus.emit("cart/changed");
62
+ } catch (error) {
63
+ if (error instanceof LoopError) {
64
+ console.log(error.limit, error.path);
65
+ }
66
+ }
67
+ ```
68
+
69
+ - **`"drop"`** is the default. The emit returns as if nobody listened, so no new error appears in an async handler.
70
+ - **`"throw"`** shows the loop with a stack. In an async handler the error becomes a rejection: handle it with `onError`.
71
+
72
+ A loop that is cut can leave the state of the app half done: the handlers that ran have done their part, the ones after the cut have not. Fix the loop, and keep the guard as a net.
73
+
74
+ ## Limit the emits that come from outside
75
+
76
+ A component that emits on every render has no chain: each emit comes from outside any handler. `maxPerSecond` counts them for each event.
77
+
78
+ ```js
79
+ // bus.js
80
+ import { createBus } from "nanoeffects";
81
+ import { loopGuardPlugin } from "nanoeffects/guard";
82
+
83
+ // maxPerSecond — over 100 emits of one event in a second from outside are refused
84
+ export const bus = createBus().use(loopGuardPlugin({ maxPerSecond: 100 }));
85
+ ```
86
+
87
+ It is off by default, because a burst of many emits can be right (a stream of messages).
88
+
89
+ ## Leave a recursion out
90
+
91
+ A chain that repeats on purpose is cut too. A handler that emits `tree/visit` for each level of a tree, or one that emits `tick` again after a timer for hours, passes `maxRepeats`. List its event in `ignore`. `maxDepth` still applies to it.
92
+
93
+ ```js
94
+ // bus.js
95
+ import { createBus } from "nanoeffects";
96
+ import { loopGuardPlugin } from "nanoeffects/guard";
97
+
98
+ export const bus = createBus().use(
99
+ loopGuardPlugin({
100
+ ignore: ["tree/visit"], // names, or a function of the name
101
+ }),
102
+ );
103
+ ```
104
+
105
+ ## Know what it does not do
106
+
107
+ - **It does not abort the chain.** It refuses one emit, at the edge where the limit is passed. The handlers that already run, run to the end.
108
+ - **It follows `ctx.emit`.** A `bus.emit` called from inside a handler is not part of the chain: it starts a new one with the counts at zero. Use `ctx.emit` inside handlers.
109
+ - **It does not find a loop.** To see the path of a loop, and loops that go through components, use `graphLogger` ([Find and cut event loops](debugging-loops.md)).
@@ -1,7 +1,20 @@
1
1
  # Coming from Effector
2
2
 
3
+ > **Level E, for readers who know Effector.** Read first: [Guide](guide.md#1-create-a-bus) steps 1 to 3.
4
+
3
5
  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
6
 
7
+ ## On this page
8
+
9
+ - [Split the work](#split-the-work)
10
+ - [Find the equivalent](#find-the-equivalent)
11
+ - [Rewrite an effect](#rewrite-an-effect)
12
+ - [Keep the state in Zustand](#keep-the-state-in-zustand)
13
+ - [Rewrite a form](#rewrite-a-form)
14
+ - [Replace the log with a request](#replace-the-log-with-a-request)
15
+ - [Use both in one app](#use-both-in-one-app)
16
+ - [What is different](#what-is-different)
17
+
5
18
  ## Split the work
6
19
 
7
20
  | Job | Use |
@@ -20,7 +33,7 @@ nanoeffects has no store. `createStore` can be replaced by any store you like, f
20
33
  | event call `loaded(user)` | `bus.emit("loaded", user)` |
21
34
  | `createEffect(handler)` | a handler: `bus.on(event, handler)` |
22
35
  | effect with `done` and `fail` | [`createJob`](actions.md): emits the success and fail actions |
23
- | `effect.pending` | `job.pending`, `job.success`, `job.fail` and `job.cancel`: set and clear the flag in your store ([Show a loading state](actions.md#show-a-loading-state-with-a-job)) |
36
+ | `effect.pending` | `job.pending`, `job.success`, `job.fail` and `job.cancel`: set and clear the flag in your store ([Show a loading state](actions.md#the-state-of-a-job)) |
24
37
  | `sample({ clock, fn, target })` | `bus.on(clock, handler)` and `ctx.emit(target, …)` |
25
38
  | `sample({ source: $store, clock })` | read the store from a [context field](context.md) in the handler |
26
39
  | `sample({ filter })` | an `if` in the handler |
@@ -318,7 +331,7 @@ bus.on(
318
331
  ## What is different
319
332
 
320
333
  - **No state.** There is no `createStore`, `combine`, `restore`, `$store.map`. The bus holds listeners only. Take a store such as Zustand for state ([Stores](stores.md)).
321
- - **`pending` is an action, not a store.** `createJob` sends `pending` when the handler starts, and `success`, `fail` or `cancel` when it ends; you keep the flag in your store ([Show a loading state](actions.md#show-a-loading-state-with-a-job)). Policies report to the job, so the flag also follows `latest`, `leading`, `debounce`, `throttle` and `queue`; that section says when to count instead. A running task is also visible through [Tracing](tracing.md).
334
+ - **`pending` is an action, not a store.** `createJob` sends `pending` when the handler starts, and `success`, `fail` or `cancel` when it ends; you keep the flag in your store ([Show a loading state](actions.md#the-state-of-a-job)). Policies report to the job, so the flag also follows `latest`, `leading`, `debounce`, `throttle` and `queue`; that section says when to count instead. A running task is also visible through [Tracing](tracing.md).
322
335
  - **Cancellation is the default tool.** An Effector effect runs every call to the end. Here `latest` and `ctx.signal` stop what is no longer needed.
323
336
  - **Events are matched by name, not by identity.** An event is a string or a `createAction` creator, not a unit that you import and call. Two modules agree on the name.
324
337
  - **No graph.** `sample` and `forward` build a static graph. A bus subscribes handlers at runtime and the call order is the order of `on`.
@@ -1,6 +1,8 @@
1
- # Coming from mitt, nanoevents or EventEmitter
1
+ # Coming from mitt and nanoevents
2
2
 
3
- Read this page if your app uses a small event emitter (`mitt`, `nanoevents`, Node's `EventEmitter`, `tiny-emitter`) and its handlers start to do async work: requests, timers, retries. It maps the calls, shows what you gain, and shows what to check before you switch.
3
+ > **Level A, for readers who know mitt or nanoevents.** Read first: nothing: the bus is the same idea.
4
+
5
+ Read this page if your app uses a small event emitter (`mitt`, `nanoevents`, `tiny-emitter`) and its handlers start to do async work: requests, timers, retries. It maps the calls, shows what you gain, and shows what to check before you switch.
4
6
 
5
7
  ## When to switch
6
8
 
@@ -15,8 +17,8 @@ If handlers stay synchronous, keep your emitter. nanoeffects costs about 1.4 kB
15
17
 
16
18
  ## Find the equivalent
17
19
 
18
- | mitt / EventEmitter | nanoeffects |
19
- | ------------------- | ----------- |
20
+ | mitt / nanoevents | nanoeffects |
21
+ | ----------------- | ----------- |
20
22
  | `emitter.on("a", handler)` | `bus.on("a", handler)` |
21
23
  | `emitter.off("a", handler)` | call the function that `bus.on` returned, or `bus.off("a")` for all handlers of `a` |
22
24
  | `emitter.emit("a", payload)` | `bus.emit("a", payload)` |
@@ -1,5 +1,7 @@
1
1
  # Use nanoeffects next to RxJS
2
2
 
3
+ > **Level E, for readers who know RxJS.** Read first: [Guide](guide.md#1-create-a-bus) steps 1 to 3.
4
+
3
5
  Read this page if your project uses RxJS and you wonder what nanoeffects takes over. It does not replace RxJS. The two split the work: RxJS shapes streams of values, nanoeffects runs the side effects that events start. It maps the operators, shows a search box and a polling loop both ways, and lists what stays in RxJS.
4
6
 
5
7
  ## Split the work
@@ -1,5 +1,7 @@
1
1
  # Coming from redux-saga
2
2
 
3
+ > **Level E, for readers who know redux-saga.** Read first: [Guide](guide.md#1-create-a-bus) steps 1 to 3.
4
+
3
5
  Read this page if you know redux-saga and want to find the same job in nanoeffects. It maps the names, shows one saga rewritten, and lists what has no equivalent. It does not teach the library: start with the [Guide](guide.md).
4
6
 
5
7
  ## Find the equivalent
@@ -1,16 +1,22 @@
1
1
  # Why nanoeffects exists
2
2
 
3
- Read this page to see why the library exists and when to pick it. The main reason is to take business logic and side effects out of React, Vue or Svelte components and write them as plain `async` functions, with cancellation, retries and a context for dependencies. The second part of the page is for SDKs, core libraries and micro-frontend shells, where a shared dependency is a shared risk. It does not teach the API: start with the [Guide](guide.md).
3
+ > **Level A.** Read first: [README](../README.md).
4
4
 
5
- ## The main reason: logic outside the UI
5
+ Read this page to see why the library exists. It moves business logic and side effects out of React, Vue or Svelte components into plain `async` functions, with cancellation, retries and a context for dependencies. It does not teach the API: start with the [Guide](guide.md).
6
+
7
+ ## Moving logic out of the UI
6
8
 
7
9
  Components that hold requests, timers and races are hard to test and easy to break. The usual cure is services and pure functions outside the UI, and then every service needs the same things: to cancel work that is no longer wanted, to retry, to share dependencies, and to show what ran and why.
8
10
 
9
- The common tools ask a lot: redux-saga writes this logic as generators, and RxJS as streams and operators. nanoeffects keeps ordinary `async` functions and adds one idea, the `AbortSignal`. The event bus is its heart but not its value: any emitter can send events (`nanoevents` is about 100 bytes, and there are [dozens more](https://github.com/gajus/sister#similar-libraries)). What nanoeffects adds is cancellable tasks, policies and effects, a context that works as a small container for dependencies, actions and modules to group a feature, and [tracing](tracing.md) with an adapter for Redux DevTools. It also fits what a project already has: recipes and small adapters for [stores](stores.md) (Redux Toolkit, Zustand, MobX, Pinia), an [RxJS](migrating-from-rxjs.md) bridge, and helpers for [React](react.md), [Vue](vue.md) and [Svelte](svelte.md), so nothing has to be rewritten to start.
11
+ The common tools ask a lot: redux-saga writes this logic as generators, and RxJS as streams and operators. nanoeffects keeps ordinary `async` functions and adds one idea, the `AbortSignal`.
12
+
13
+ Another way to take logic out of the UI is a dependency injection (DI) container, such as the one in Angular or in any DI library. nanoeffects has no container, but it can do dependency injection at the level of the contract: a handler takes what it needs from `ctx`, and the bus decides what to give. See [Context fields](context.md).
10
14
 
11
- ## When the constraint is a shared library
15
+ The event bus is the heart of the library but not its value: any emitter can send events (`nanoevents` is about 100 bytes, and there are [dozens more](https://github.com/gajus/sister#similar-libraries)). What nanoeffects adds is cancellable tasks, policies and effects, a context that works as a small container for dependencies, actions and modules to group a feature, and [tracing](tracing.md) with an adapter for Redux DevTools. It also fits what a project already has: recipes and small adapters for [stores](stores.md) (Redux Toolkit, Zustand, MobX, Pinia), an [RxJS](migrating-from-rxjs.md) bridge, and helpers for [React](react.md), [Vue](vue.md) and [Svelte](svelte.md), so nothing has to be rewritten to start.
12
16
 
13
- A shared library (an SDK, a core package, a platform shell) is loaded by many apps. Whatever it depends on, the apps depend on too. State and event libraries are the worst case, because they keep **internal state and identity** (a store, a context, a class). Two copies or two versions of them do not merge: they silently stop seeing each other.
17
+ ## Micro-frontends
18
+
19
+ In micro-frontends every app can bring its own versions of the same libraries. State and event libraries are the worst case, because they keep **internal state and identity** (a store, a context, a class). Two copies or two versions of them do not merge: they silently stop seeing each other. Different versions and copies also do more than work out of step: they load more code and use more resources than one agreed copy would. So the goal is a project that is as simple as possible, with as few shared dependencies that hold state as possible.
14
20
 
15
21
  Module Federation makes this visible. Each remote can bring its own copy unless every team agrees on a `singleton` and a version range. The [Nx guide on library versions](https://nx.dev/docs/kb/manage-library-versions-with-module-federation) lists the result: libraries with internal state conflict, and a missing method in one version breaks callers written for another.
16
22
 
@@ -23,25 +29,12 @@ Real cases from the libraries nanoeffects was compared with:
23
29
  | Effector | A library that uses `effector-react/scope` and is bundled into another project gets two instances. Its `Provider` is not seen by `useStore` of the app: "no scope found". | [effector#727](https://github.com/effector/effector/issues/727) |
24
30
  | RxJS | Two copies give two `Observable` classes. Types of one version do not fit the other, and operators added to one copy are missing on the other. The advice is "one version in `node_modules`". | [RxJS#2984](https://github.com/ReactiveX/rxjs/issues/2984), [neo4j-javascript-driver#978](https://github.com/neo4j/neo4j-javascript-driver/issues/978) |
25
31
 
26
- A library that is **not** a shared singleton cannot break this way. nanoeffects has no dependencies and no module-level state: a bus is an object you create. Two copies, or two versions, of the package in one page are two independent buses. Nothing is registered globally, no `Provider` is needed, and the host app does not have to pick a version for you.
27
-
28
- ## Why not the alternatives
32
+ nanoeffects has no dependencies and no module-level state: a bus is an object you create. Two copies, or two versions, of the package in one page are two independent buses. Nothing is registered globally, no `Provider` is needed, and the host app does not have to pick a version for you.
29
33
 
30
- | Option | Why it did not fit |
31
- | ------ | ------------------ |
32
- | Redux (with sagas) | A store and a provider shared by every consumer, which is the risk above. `redux-saga` also has no cheap way to get an answer back from an event: effects talk through the store. |
33
- | Effector | A graph with global identity. A version or scope mismatch between the SDK and the app reaches the whole infrastructure. |
34
- | Zustand | Good state, but it is a store, not a way to run and cancel work. It is the natural place to hold state next to nanoeffects: see [Stores](stores.md). |
35
- | RxJS | Flexible, but learning its ecosystem (operators, subjects, schedulers) is a high entry cost for every developer who touches the SDK. The package is also not small: [Bundlephobia](https://bundlephobia.com/package/rxjs) reports 17.3 kB minified and gzipped for 7.8.2. |
36
-
37
- ## What the library does instead
34
+ ## What the library does
38
35
 
39
36
  - **No dependencies, no global state.** It is safe to ship inside an SDK next to whatever the host app runs. It does not need a `singleton` entry in the Module Federation config.
40
37
  - **Small enough for a strict budget.** SDKs often get a few kilobytes for all third-party code. `createBus` is about 1.4 kB gzip (1.3 kB brotli); policies, effects and actions are separate entries you import only when you need them. See [Package entries](package.md).
41
- - **Simple first.** `on` and `emit` are the whole model. Cancellation, retry and the rest are plain functions on top. The [Guide](guide.md) and [Coming from redux-saga](migrating-from-saga.md) map the familiar names.
38
+ - **Easy to start.** `on` and `emit` are the whole model. Cancellation, retry and the rest are plain functions on top. The [Guide](guide.md) and [Coming from redux-saga](migrating-from-saga.md) map the familiar names.
42
39
  - **`emit` can return an answer.** An [action with a handler](actions.md) returns a result, so one module can ask another and wait for the reply. Events alone cannot do that, and it was the main thing missing from the Redux-based options.
43
40
  - **Easy to debug.** Event-driven code is hard to follow: you see an effect and not the event that caused it. Every task knows its parent, so [tracing](tracing.md) shows the chain of events, with loggers for the console, the Performance panel and Redux DevTools.
44
-
45
- ## When this is not the right tool
46
-
47
- nanoeffects holds no state. If you need a store, keep Redux, Zustand or Effector and let the bus run the async work: [Stores](stores.md). If you already have streams everywhere, keep RxJS: [Use nanoeffects next to RxJS](migrating-from-rxjs.md).
@@ -1,7 +1,11 @@
1
1
  # Turn events into Observables
2
2
 
3
+ > **Level F.** Read first: [Guide](guide.md#1-create-a-bus) step 1.
4
+
3
5
  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
6
 
7
+ 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).
8
+
5
9
  `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
10
 
7
11
  ## Turn an event into a stream
@@ -145,6 +149,47 @@ bus.on(
145
149
 
146
150
  `switchMap` drops the old inner stream. `latest` aborts the old task through `signal`, so a `fetch` that got the signal really stops.
147
151
 
152
+ ## Read an event with a `for await` loop
153
+
154
+ 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.
155
+
156
+ `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.
157
+
158
+ ```js
159
+ import { createBus } from "nanoeffects";
160
+ import { events } from "nanoeffects/observable";
161
+
162
+ const bus = createBus();
163
+ const controller = new AbortController();
164
+
165
+ // events — the "query/changed" payloads one by one; keeps the last 3 while the loop is busy
166
+ const queries = events(bus, "query/changed", {
167
+ signal: controller.signal,
168
+ buffer: 3,
169
+ });
170
+
171
+ async function watchQueries() {
172
+ for await (const query of queries) {
173
+ console.log("search for", query); // runs once per payload, in order
174
+ }
175
+ }
176
+
177
+ watchQueries().catch((reason) => console.log("stopped:", reason)); // the abort rejects the loop
178
+ bus.emit("query/changed", "cat");
179
+ controller.abort("screen closed"); // the loop ends with this reason
180
+ ```
181
+
182
+ | Option | What it does | Default |
183
+ | ------ | ------------ | ------- |
184
+ | `signal` | Ends the loop. The waiting read rejects with the abort reason. | none |
185
+ | `buffer` | The most payloads kept while the loop is busy. | `Infinity` |
186
+ | `overflow` | Which payload goes when the buffer is full: `"drop-oldest"` or `"drop-newest"`. | `"drop-oldest"` |
187
+
188
+ - `break`, `return` and a throw inside the loop remove the listener.
189
+ - With no `buffer`, a loop that never reads keeps every payload. Set `buffer` for a slow loop.
190
+ - An event of a bus does not end, so the loop ends only by `break`, the `signal` or the task.
191
+ - Inside a task, `events(ctx, "query/changed")` needs no `signal`: the task ending is the end.
192
+
148
193
  ## Convert back to a stream or a promise
149
194
 
150
195
  | You need | Write |
package/docs/oop.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Use the bus from a class
2
2
 
3
+ > **Level D.** Read first: [Guide](guide.md#1-create-a-bus) step 1.
4
+
3
5
  Use this page when **your domain logic lives in classes**: a service, a store or a controller that needs the bus inside its methods. There are two ways to give a class the methods of a bus, and both remember what each instance subscribed to, so `dispose()` ends it all:
4
6
 
5
7
  | You want | Use |
@@ -7,7 +9,24 @@ Use this page when **your domain logic lives in classes**: a service, a store or
7
9
  | every `new Service()` to be an independent emitter | `extends Emitter` |
8
10
  | the instances to share a bus, or to use plugins | `extends extendBus(bus)` |
9
11
 
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.
12
+ If your logic is plain functions, stay with [modules](actions.md#modules): a module is the same idea without a class.
13
+
14
+ ## On this page
15
+
16
+ - [The problem without it](#the-problem-without-it)
17
+ - [Make an independent emitter with `Emitter`](#make-an-independent-emitter-with-emitter)
18
+ - [Type the events (optional)](#type-the-events-optional)
19
+ - [Give a class the methods of a shared bus](#give-a-class-the-methods-of-a-shared-bus)
20
+ - [Extend the result with no class of your own](#extend-the-result-with-no-class-of-your-own)
21
+ - [Let the class make its bus](#let-the-class-make-its-bus)
22
+ - [End the subscriptions with `dispose`](#end-the-subscriptions-with-dispose)
23
+ - [Stream an event with a `Subject`](#stream-an-event-with-a-subject)
24
+ - [Keep a method of your own](#keep-a-method-of-your-own)
25
+ - [Subscribe a method with `@On` and `@Once`](#subscribe-a-method-with-on-and-once)
26
+ - [Decorate the class with `@WithBus`](#decorate-the-class-with-withbus)
27
+ - [Pass a method by hand](#pass-a-method-by-hand)
28
+ - [Type the instances (optional)](#type-the-instances-optional)
29
+ - [What is not done](#what-is-not-done)
11
30
 
12
31
  ## The problem without it
13
32