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
package/docs/actions.md CHANGED
@@ -1,6 +1,23 @@
1
1
  # Actions, jobs and modules
2
2
 
3
- Use this page when **domain logic lives on the bus**, off the UI: the UI emits and listens; handlers and modules own the work.
3
+ > **Level D.** Read first: [Guide](guide.md#1-create-a-bus) steps 1 to 6.
4
+
5
+ When a project has a lot of business logic, you need to split it. One way is to split it by domains, or entities. In a shop, the domains could be products and product categories. Then the code that belongs to the actions of one domain goes into one file (`product.js`), its handlers into another, and the module that mounts them into a third. A domain can then be mounted, replaced or unmounted without touching the others. For the shop the files could look like this:
6
+
7
+ ```text
8
+ src/
9
+ bus.js
10
+ product/
11
+ product.js
12
+ product-handlers.js
13
+ product-module.js
14
+ category/
15
+ category.js
16
+ category-handlers.js
17
+ category-module.js
18
+ ```
19
+
20
+ The examples below show the content of such files.
4
21
 
5
22
  If you only need `on` / `emit` or policies, stay in the [Guide](guide.md) until you need a result from `emit` or a mountable domain.
6
23
 
@@ -10,11 +27,37 @@ A chat with two entities and three features, jobs under policies and effects, an
10
27
 
11
28
  A whole project with two domains, a job and React as a thin layer: `npm create nanoeffects@latest my-app -- --template domains-ts` (see [Start a project from a template](starter.md#start-with-domains)).
12
29
 
13
- ## Return a value from `emit` with a handler
30
+ ## Action, job and module
31
+
32
+ Four forms, one idea each. Take the first row that fits.
33
+
34
+ | Form | What it is | Use it when |
35
+ | ---- | ---------- | ----------- |
36
+ | `createAction(type)` | An action with no handler: a name and a payload. | The UI or a handler tells that something happened. |
37
+ | `createAction(type, handler)` | An action with a handler: `emit` runs the handler and returns its result. | You want the result of the handler from `emit`. |
38
+ | `createJob(type, handler)` | An action with a handler that returns its result as the actions `success` and `fail`, and also sends `pending` and `cancel`. | You want the result of the work of a handler and, if you need it, to subscribe to the state of that work. |
39
+ | `createModule(name, setup)` | A group of actions and jobs with their handlers, mounted and unmounted with one call. | One domain must start and stop as a unit. |
40
+
41
+ Start with actions. Add a handler when you need a result, a job when you need a status, a module when you need a unit.
42
+
43
+ ## On this page
44
+
45
+ - [Action, job and module](#action-job-and-module)
46
+ - [Get the result of `emit`](#get-the-result-of-emit)
47
+ - [Actions, handlers and jobs](#actions-handlers-and-jobs)
48
+ - [Actions](#actions)
49
+ - [Bind a handler later](#bind-a-handler-later)
50
+ - [The result type of a bound handler](#the-result-type-of-a-bound-handler)
51
+ - [Jobs](#jobs)
52
+ - [The state of a job](#the-state-of-a-job)
53
+ - [Modules](#modules)
54
+ - [Extend the context of actions and modules](#extend-the-context-of-actions-and-modules)
55
+
56
+ ## Get the result of `emit`
14
57
 
15
- Give `createAction` a handler, and `emit` runs it and returns its result. There is one creator for an event and for an action with work behind it; the handler is the only difference.
58
+ When you want an answer from a handler at the `emit` call, give `createAction` the handler. A handler is a function `(payload, ctx) => result` that runs when the action is emitted. `emit` runs it and returns its result. There is one creator for an action with and without a handler; the handler is the only difference.
16
59
 
17
- The bus lives in one file and its events in another, so any module can import both without a cycle.
60
+ The bus lives in one file and its actions in another, so any module can import both without a cycle.
18
61
 
19
62
  ```js
20
63
  // bus.js
@@ -39,7 +82,7 @@ import { double } from "./math";
39
82
  bus.emit(double(21)); // 42
40
83
  ```
41
84
 
42
- ## Describe events, handlers and jobs
85
+ ## Actions, handlers and jobs
43
86
 
44
87
  `nanoeffects/actions` has the pieces to name what a bus does, apart from the code that does it.
45
88
 
@@ -74,7 +117,7 @@ These are the events of the user domain, in a file of their own (`user.js`). The
74
117
  - `userLoaded.match(action)` — narrows an action
75
118
  - `createAction(type, handler)` — a function as the second argument is the handler; `bus.emit(action(payload))` tells the listeners, runs the handler as a task and returns its result
76
119
  - `createAction(type, { prepare })` — build the payload from several arguments
77
- - `createAction(type, { handle, prepare, ...scope })` — everything in one object: `handle` and `prepare` are taken out, and every other field is the [scope](scope.md)
120
+ - `createAction(type, { handle, prepare, ...scope })` — everything in one object: `handle` and `prepare` are taken out, and every other field is the [scope](context.md#scope-fields-for-an-action-a-module-and-a-call)
78
121
  - Works as the event of its type: `bus.emit(userLoaded(user))`, `bus.on(userLoaded, fn)`
79
122
 
80
123
  A scope goes after a handler as the last argument, `createAction(type, handler, scope)`; `prepare` with a handler goes in that last object: `createAction(type, handler, { prepare, ...scope })`.
@@ -124,7 +167,7 @@ A handler bound in the registry makes `bus.emit(sendMail(mail))` run it and retu
124
167
 
125
168
  The registry lives in `nanoeffects/actions`, so a bus without one carries none of that code. Without a registry, `emit` runs the handler that the action carries. `bus.use(actionsPlugin())` sets `bus.actions`. A second `actionsPlugin` binds its list in that registry and returns it. There is no `actions` option in `createBus`; assign a custom runner with `bus.actions = { run }`, and add `bound(type)` to it to answer for actions that have no handler of their own.
126
169
 
127
- ### Say what a bound handler returns
170
+ ### The result type of a bound handler
128
171
 
129
172
  TypeScript cannot see which handler the registry has, so the type of `emit` for an action without a handler is `void`. Tell it what the bound handler returns with `Call<Payload, Result>`. This is a promise of yours: nothing checks it against the handler.
130
173
 
@@ -164,11 +207,11 @@ On a bus without an event map you can say it where you call: `const id = bus.emi
164
207
  - `pending` / `success` / `fail` / `cancel` are a string (job makes an action creator) or an existing action creator. Missing ones become `<type>/pending`, `<type>/success`, `<type>/fail` or `<type>/cancel`.
165
208
  - `bus.emit(loadUser(id))` returns a promise of the result. Handler errors are always handled: they go to `fail` and the promise resolves with `undefined`. For a result that must reject, give `createAction` the handler.
166
209
  - Outcome actions are emitted as the task that ran the job (also for a handler bound with `actions.handle`). `success` and `fail` are not sent after abort; `cancel` is, at the moment of the abort.
167
- - A job without a handler is not an outcome: `emit` throws `No handler for command "..."` and nothing is sent. An action without a handler is an event and does not throw.
210
+ - A job without a handler is not an outcome: `emit` throws `No handler for action "..."` and nothing is sent. An action without a handler is an event and does not throw.
168
211
  - Calling the job only builds `{ type, payload }`; the handler runs on `emit`.
169
212
  - The outcomes are events like any other: with `onUnhandled` set on the bus, it is called for a `pending` or a `cancel` that nobody listens to.
170
213
 
171
- ## Show a loading state with a job
214
+ ## The state of a job
172
215
 
173
216
  **Problem.** A spinner needs a flag that is `true` while a request runs. Setting it by hand in the handler (`loading = true`, then `try` / `finally`) breaks when the task is aborted: `ctx.emit` does nothing after an abort, so a flag that is cleared through the bus stays `true`.
174
217
 
@@ -241,7 +284,7 @@ bus.emit(loadUser("1"));
241
284
 
242
285
  A flag is exact when calls do not overlap, or when `latest` replaces them. If several runs can go at once (`queue` with `concurrency` above 1, or a job without a policy called many times), keep a counter: add one on `pending` and take one off on `success`, `fail` and `cancel`. A policy that you write yourself, or that you hide in your own function, does not report to the job; `pending` is then sent at the call, as for a job with no policy.
243
286
 
244
- ## Mount domains with modules
287
+ ## Modules
245
288
 
246
289
  **Problem.** A domain needs several subscriptions and handler bindings. With plain `bus.on` / `actions.handle` you must remember every unsubscribe.
247
290
 
@@ -334,9 +377,9 @@ If the `setup` of one module throws, the modules of that call that were already
334
377
  - `spawn` — background task, aborted on unmount.
335
378
  - An action that carries a handler keeps answering after unmount (`use` only tracks registration). Use `handle` for a handler that must live only as long as the module.
336
379
 
337
- ## Give an action or a module its own fields
380
+ ## Extend the context of actions and modules
338
381
 
339
- An action with a handler can need its own client, a module its own logger. Pass a scope, a plain object whose keys go on `ctx` (`$effects` binds functions to the task), as the last argument of `createAction`, `createAction`, `createModule` and `createJob`, and turn it on with `scopePlugin`:
382
+ An action with a handler can need its own client, a module its own logger. Pass a scope, a plain object whose keys go on `ctx` (`$effects` binds functions to the task), as the last argument of `createAction`, `createModule` and `createJob`, and turn it on with `scopePlugin`:
340
383
 
341
384
  ```js
342
385
  // bus.js
@@ -368,4 +411,4 @@ const fakeApi = { getUser: async (id) => ({ id, name: "test" }) };
368
411
  bus.emit(getUser("1"), { api: fakeApi });
369
412
  ```
370
413
 
371
- The handler gets the fields typed. Without `scopePlugin` the scope is ignored. The full picture, with jobs, actions, modules and which value wins, is in [scopes](scope.md).
414
+ The handler gets the fields typed. Without `scopePlugin` the scope is ignored. The full picture, with jobs, actions, modules and which value wins, is in [scopes](context.md#scope-fields-for-an-action-a-module-and-a-call).
@@ -1,5 +1,7 @@
1
1
  # Cancel stale requests and fix race conditions
2
2
 
3
+ > **Level B.** Read first: the bus from the [README](../README.md).
4
+
3
5
  Read this page if an older response can overwrite a newer one: a search box that shows the results of the previous query, a filter that flickers, a form that submits twice. It shows the cause, the fix with `latest`, and how to add a pause, a retry and a timeout. It needs only the bus from the [README](../README.md).
4
6
 
5
7
  ## See the problem
@@ -1,5 +1,7 @@
1
1
  # Build a chat with entities, features, modules and a small server
2
2
 
3
+ > **Level D.** Read first: [Actions, jobs and modules](actions.md).
4
+
3
5
  Use this page when your app has several features that react to each other and you want to see where each piece of logic goes. The example is a chat with rooms, a history that loads, messages that can fail to send, a search across all rooms, and new messages that arrive on their own. A small Fastify server in the example answers every request, and it misbehaves on a known schedule, so each problem on this page really happens.
4
6
 
5
7
  It is a level D example. Start with [Actions, jobs and modules](actions.md) for the API, or with the `domains-ts` template in [Start a project from a template](starter.md#start-with-domains) for a smaller project.
@@ -14,6 +16,18 @@ npm run dev
14
16
 
15
17
  `npm run dev` starts the server and Vite together. Start it with `CHAOS=off npm run dev` to turn the failures off.
16
18
 
19
+ ## On this page
20
+
21
+ - [Read how the server fails](#read-how-the-server-fails)
22
+ - [See how the files are split](#see-how-the-files-are-split)
23
+ - [Load a history that must not race](#load-a-history-that-must-not-race)
24
+ - [Send a message that can fail](#send-a-message-that-can-fail)
25
+ - [Search where one room is slow](#search-where-one-room-is-slow)
26
+ - [Wait for a room before marking a result](#wait-for-a-room-before-marking-a-result)
27
+ - [Poll for new messages](#poll-for-new-messages)
28
+ - [Know where each value lives](#know-where-each-value-lives)
29
+ - [Where to go next](#where-to-go-next)
30
+
17
31
  ## Read how the server fails
18
32
 
19
33
  The server keeps its data in memory. These rules are on purpose, and they are the same every run, so you can follow each section below by doing what the table says:
@@ -76,7 +90,7 @@ import { latest } from "nanoeffects/policies";
76
90
  export const fetchHistory = latest((roomId, { signal }) => api.loadHistory(roomId, { signal }));
77
91
  ```
78
92
 
79
- The module gives the job its work and keeps the state from the outcomes, so an aborted load clears the flag too (see [Show a loading state with a job](actions.md#show-a-loading-state-with-a-job)):
93
+ The module gives the job its work and keeps the state from the outcomes, so an aborted load clears the flag too (see [Show a loading state with a job](actions.md#the-state-of-a-job)):
80
94
 
81
95
  ```js
82
96
  // message-module.js
@@ -1,5 +1,7 @@
1
1
  # Build a chat widget that listens only while it is open
2
2
 
3
+ > **Level D.** Read first: [Guide](guide.md#1-create-a-bus) steps 1 to 3.
4
+
3
5
  Use this page when a site has a chat with a bot behind a launcher button, and the chat must cost nothing while it is closed. Closed means no subscriptions on the bus, no polling, no keyboard listener, and no request still running. Opening turns all of it on, and closing turns all of it off in one call.
4
6
 
5
7
  The code is in [`examples/chat-widget`](../examples/chat-widget): plain DOM and TypeScript, a fake bot in the browser, no server. Run it on [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/chat-widget), or on your machine:
@@ -30,7 +32,7 @@ It is a level D example: the logic lives in one module, and the page only mounts
30
32
  | `src/chat-handlers.ts` | `askBot` (`timeout` and `retry` inside) and `watchOperator` (`poll`): plain `async` functions that know nothing about the page. |
31
33
  | `src/chat-module.ts` | The list of what is alive while the chat is open. |
32
34
  | `src/panel.ts` | The view: shows the lines, sends the texts, returns one function that removes it. |
33
- | `src/main.ts` | The launcher: the only file that knows both the module and the panel. |
35
+ | `src/main.ts` | The launcher: the only file that knows both the module and the panel; it listens for `closeRequested` only while the chat is open. |
34
36
 
35
37
  ## Bind everything in one module
36
38
 
@@ -80,7 +82,7 @@ Three details that make the cleanup complete:
80
82
 
81
83
  ## Open and close from the launcher
82
84
 
83
- `app.mount` returns the function that unmounts the module. The launcher puts it together with the one that removes the panel.
85
+ `app.mount` returns the function that unmounts the module. `bus.on` returns the one that unsubscribes. The launcher puts them together with the one that removes the panel into a single `close`.
84
86
 
85
87
  <!-- docs-check: skip - the other files of the example are in examples/chat-widget -->
86
88
  ```js
@@ -96,7 +98,10 @@ let close = null; // set while the chat is open
96
98
  const open = () => {
97
99
  const stopModule = app.mount(chatModule); // subscriptions, polling and Escape start
98
100
  const stopPanel = renderPanel(document.body);
101
+ // the module emits closeRequested on Escape; nothing does while the chat is closed
102
+ const stopEscape = bus.on(closeRequested, () => close?.());
99
103
  close = () => {
104
+ stopEscape();
100
105
  stopPanel();
101
106
  stopModule(); // everything the module started ends, a running request included
102
107
  close = null;
@@ -110,18 +115,15 @@ button.addEventListener("click", () => {
110
115
  open();
111
116
  }
112
117
  });
113
-
114
- // the launcher stays subscribed: it must hear Escape to close the chat
115
- bus.on(closeRequested, () => close?.());
116
118
  ```
117
119
 
118
- The launcher's own `bus.on` is the only subscription that lives while the chat is closed, and it does nothing then: `close` is `null`.
120
+ While the chat is closed the launcher holds no subscription at all: it only has the click listener of the button, which is a DOM listener, not a listener of the bus.
119
121
 
120
122
  ## Check that closing cleans up
121
123
 
122
124
  1. Open the chat and send a text. The bot answers after about a second. Every third question fails, and the retry asks again.
123
125
  2. Send a text and close the chat before the answer arrives. No line appears, also when you open the chat again.
124
- 3. Look at the box "Listeners on the bus": it shows `bus.get(event).length` for four events and updates when the chat opens or closes. Closed: only `chat/closeRequested` has a listener (the launcher). Open: `chat/sent`, `chat/messageAdded` and `chat/typing` have one each as well. That is the proof that nothing is left subscribed.
126
+ 3. Look at the box "Listeners on the bus": it shows `bus.get(event).length` for four events and updates when the chat opens or closes. Closed: every event shows `0`, and so does "all events". Open: each of the four has one listener, and "all events" is `4`. That is the proof that nothing is left subscribed.
125
127
  4. Wait 10 s with the chat closed. The operator is not polled: no line from the operator appears.
126
128
  5. Press `Escape` with the chat closed. Nothing happens. Open the chat and press it again: it closes.
127
129
  6. Open the chat again. The old lines are back, and the polling runs.
package/docs/context.md CHANGED
@@ -1,8 +1,31 @@
1
- # Put your own fields on every task
1
+ # Working with context
2
2
 
3
- Optional: put a store, client, or logger on every task. Skip until many handlers need the same dependency.
3
+ > **Level D.** Read first: [Guide](guide.md#1-create-a-bus).
4
4
 
5
- ## Pass values at runtime
5
+ Optional: put a store, client or logger on every task, or give fields to one action, module or call. Skip until many handlers need the same dependency.
6
+
7
+ ## On this page
8
+
9
+ - [Context fields](#context-fields)
10
+ - [Pass values at runtime](#pass-values-at-runtime)
11
+ - [Type the fields (optional)](#type-the-fields-optional)
12
+ - [Build a field from the signal of each task](#build-a-field-from-the-signal-of-each-task)
13
+ - [Scope: fields for an action, a module and a call](#scope-fields-for-an-action-a-module-and-a-call)
14
+ - [Turn scopes on](#turn-scopes-on)
15
+ - [Give an action its own fields](#give-an-action-its-own-fields)
16
+ - [Bind an effect to the task](#bind-an-effect-to-the-task)
17
+ - [Replace a field for one call](#replace-a-field-for-one-call)
18
+ - [Give an action, a job or a module its own fields](#give-an-action-a-job-or-a-module-its-own-fields)
19
+ - [Which value wins](#which-value-wins)
20
+ - [Keep a generic effect generic](#keep-a-generic-effect-generic)
21
+ - [Type the fields of a scope](#type-the-fields-of-a-scope)
22
+ - [Limits](#limits)
23
+
24
+ ## Context fields
25
+
26
+ ### Pass values at runtime
27
+
28
+ <!-- docs-check: new -->
6
29
 
7
30
  ```js
8
31
  import { createBus } from "nanoeffects";
@@ -15,15 +38,17 @@ bus.on("save", (draft, { store }) => {
15
38
  });
16
39
  ```
17
40
 
18
- That is enough in JavaScript. The fields appear on handlers, `ctx.run`, `ctx.on`, actions, modules, and child tasks of effects and policies.
41
+ Here `store` is your own object: a store, an API client, a logger, anything. That is enough in JavaScript. The fields appear on handlers, `ctx.run`, `ctx.on`, actions, modules, and child tasks of effects and policies.
19
42
 
20
43
  Library fields (`signal`, `emit`, `run`, …) win on name clashes. `context` may be a function `(task) => fields` when the value depends on the task.
21
44
 
22
- To get `race`, `all`, `take` and the like on every task without passing `ctx`, see [`effectsPlugin`](effects.md#put-selected-effects-on-ctx).
45
+ To get `race`, `all`, `take` and the like on every task without passing `ctx`, see [`effectsPlugin`](effects.md#effectsplugin).
23
46
 
24
- To give fields to **one** command, action, job or module, or to replace them for one `emit`, see [scopes](scope.md).
47
+ To give fields to **one** action, job or module, or to replace them for one `emit`, see [scopes](context.md#scope-fields-for-an-action-a-module-and-a-call).
25
48
 
26
- ## Type the fields (optional)
49
+ ### Type the fields (optional)
50
+
51
+ <!-- docs-check: new -->
27
52
 
28
53
  In TypeScript, `contextPlugin` (from `nanoeffects/context`) puts the fields on every task of **this** bus and takes their types from the value you pass. The event map holds only events. It does not wire anything across the app — another bus can have different fields or none.
29
54
 
@@ -45,7 +70,41 @@ bus.on("save", (draft, { store }) => store.put(draft));
45
70
  - A name that the bus already has is a type error. The fields of the library (`signal`, `emit`, `run`, …) win at runtime, and a field that the context already has is not replaced.
46
71
  - The plugin returns the same bus, so keep the result of the chain: `const bus = createBus().use(contextPlugin({ store }))`.
47
72
 
48
- ### Name the fields in the event map instead
73
+ ### Build a field from the signal of each task
74
+
75
+ <!-- docs-check: new -->
76
+
77
+ Problem: an HTTP client or a database handle should stop when the task is aborted. Without help, every handler passes `ctx.signal` to it by hand, and one forgotten call leaves a request running.
78
+
79
+ `fromSignal(make)` from `nanoeffects/signal` marks a field of `contextPlugin` as a value that is built from the signal of the task. It does not run `make` for the bus: it runs it for each task that reads the field, once, with the `ctx.signal` of that task.
80
+
81
+ ```js
82
+ import { createBus } from "nanoeffects";
83
+ import { contextPlugin } from "nanoeffects/context";
84
+ import { fromSignal } from "nanoeffects/signal";
85
+ import axios from "axios";
86
+
87
+ // contextPlugin — every task gets these fields
88
+ // fromSignal — http is made from the signal of the task that reads ctx.http
89
+ const bus = createBus().use(
90
+ contextPlugin({
91
+ logger: console, // a plain field: the same object for every task
92
+ http: fromSignal((signal) => axios.create({ signal })),
93
+ }),
94
+ );
95
+
96
+ bus.on("user/open", async (id, ctx) => {
97
+ const response = await ctx.http.get(`/users/${id}`); // aborts with the task
98
+ ctx.logger.log(response.data);
99
+ });
100
+ ```
101
+
102
+ - `make` runs at the first read, so a handler that never uses `ctx.http` pays nothing for it.
103
+ - A task that `ctx.run` starts builds its own value from its own signal, so aborting the child stops only the child's requests.
104
+ - The field is not copied by `{ ...ctx }` and `Object.keys(ctx)` does not list it. Read `ctx.http` first if you copy a context.
105
+ - Without `contextPlugin` a `fromSignal` marker is only an object.
106
+
107
+ #### Name the fields in the event map instead
49
108
 
50
109
  `createBus<Events>({ context: { store } })` also types the fields, but through the event map: `WithContext` is checker-only and says which fields the handlers of this bus have. Use it when you cannot call `use`, for example for the bus that `createEffectsMiddleware` makes.
51
110
 
@@ -57,3 +116,288 @@ type Events = { save: Draft } & WithContext<{ store: Store }>;
57
116
 
58
117
  const bus = createBus<Events>({ context: { store } });
59
118
  ```
119
+
120
+ ## Scope: fields for an action, a module and a call
121
+
122
+ Optional, for domain logic. Use it when one action needs its own client, one module its own logger, or one test its own fake, and a field on the whole bus ([context fields](context.md)) is too wide.
123
+
124
+ A **scope** is a plain object. Every key goes on `ctx` as it is (`cache`, `api`, `logger`), except one: `$effects`. Its functions take `{ signal }` as the first parameter, like `retry(ctx, fn)`, and go on `ctx` bound to the task, so you call `ctx.fetchJson(url)` instead of `fetchJson(ctx, url)`.
125
+
126
+ You give a scope to the thing that uses it. A call can replace it. Nothing else changes: handlers still get `(payload, ctx)`.
127
+
128
+ ### Turn scopes on
129
+
130
+ <!-- docs-check: new -->
131
+
132
+ Scopes are a plugin, so a bus that does not use them pays nothing.
133
+
134
+ ```js
135
+ // bus.js
136
+ import { createBus } from "nanoeffects";
137
+ import { scopePlugin } from "nanoeffects/scope";
138
+
139
+ // scopePlugin — makes the scopes below work on this bus; returns the same bus
140
+ export const bus = createBus().use(scopePlugin());
141
+ ```
142
+
143
+ The bus is in one file and the actions in files named after their domain, so the next example imports `bus` from here. Each section after it keeps its own `bus.js` and domain file, as if it were an app of its own.
144
+
145
+ - Without `scopePlugin` a scope is **ignored**: no error and no warning. If a field is missing from `ctx`, check this first.
146
+ - Use it in any order with the other plugins: a scope wins over `effectsPlugin`, `contextPlugin` and `createBus({ context })`.
147
+
148
+ ### Give an action its own fields
149
+
150
+ <!-- docs-check: new -->
151
+
152
+ <!-- docs-check: continue -->
153
+
154
+ ```js
155
+ // user.js
156
+ import { createAction } from "nanoeffects/actions";
157
+
158
+ // createAction — third argument is the scope of this action
159
+ export const getUser = createAction(
160
+ "user/get",
161
+ (id, { api, cache }) => {
162
+ // api and cache are on ctx for this action only
163
+ return cache.get(id) ?? api.getUser(id);
164
+ },
165
+ { api, cache: new Map() },
166
+ );
167
+ ```
168
+
169
+ ```js
170
+ import { bus } from "./bus";
171
+ import { getUser } from "./user";
172
+
173
+ const user = await bus.emit(getUser("1")); // the handler has api and cache
174
+ ```
175
+
176
+ The listeners of `"user/get"` get the same fields when the action is emitted, because the fields belong to the call.
177
+
178
+ ### Bind an effect to the task
179
+
180
+ <!-- docs-check: new -->
181
+
182
+ An effect is a function with `{ signal }` as its first parameter. In `$effects` it is bound to the task, so it stops with the task.
183
+
184
+ ```js
185
+ // feed.js
186
+ import { createAction } from "nanoeffects/actions";
187
+ import { delay, race } from "nanoeffects/effects";
188
+
189
+ // your own effect: first parameter is the task (only its signal is used)
190
+ const fetchJson = (ctx, url) =>
191
+ fetch(url, { signal: ctx.signal }).then((response) => response.json());
192
+
193
+ export const loadFeed = createAction(
194
+ "feed/load",
195
+ (url, { fetchJson, race, delay }) =>
196
+ // race, delay, fetchJson — bound to this task: no ctx argument
197
+ race({
198
+ feed: () => fetchJson(url),
199
+ timeout: () => delay(5000),
200
+ }),
201
+ // $effects — built-in and your own functions share one object
202
+ { $effects: { fetchJson, race, delay } },
203
+ );
204
+ ```
205
+
206
+ ```js
207
+ // bus.js
208
+ import { createBus } from "nanoeffects";
209
+ import { scopePlugin } from "nanoeffects/scope";
210
+
211
+ export const bus = createBus().use(scopePlugin());
212
+ ```
213
+
214
+ ```js
215
+ import { bus } from "./bus";
216
+ import { loadFeed } from "./feed";
217
+
218
+ const result = await bus.emit(loadFeed("/feed"));
219
+ ```
220
+
221
+ - Put in `$effects` only functions that take `ctx` first. A plain function (a formatter, a logger) belongs in `context`.
222
+ - A function under any other key is copied as it is; a function in `$effects` is bound.
223
+ - `$effects` is the one name with a meaning. A field called `effects` is an ordinary field: `{ effects: { retries: 3 } }` puts that object on `ctx`, and nothing in it is bound.
224
+
225
+ ### Replace a field for one call
226
+
227
+ <!-- docs-check: new -->
228
+
229
+ The second argument of `emit` is a scope for the tasks of **that** call. It is the usual way to give a test a fake.
230
+
231
+ ```js
232
+ // bus.js
233
+ import { createBus } from "nanoeffects";
234
+ import { scopePlugin } from "nanoeffects/scope";
235
+
236
+ export const bus = createBus().use(scopePlugin());
237
+ ```
238
+
239
+ ```js
240
+ // user.js
241
+ import { createAction } from "nanoeffects/actions";
242
+
243
+ export const getUser = createAction(
244
+ "user/get",
245
+ (id, { api }) => api.getUser(id),
246
+ { api },
247
+ );
248
+ ```
249
+
250
+ ```js
251
+ import { bus } from "./bus";
252
+ import { getUser } from "./user";
253
+
254
+ // the real api for this call
255
+ await bus.emit(getUser("1"));
256
+
257
+ // the second argument — a fake api, for this call only
258
+ const fakeApi = { getUser: async (id) => ({ id, name: "test" }) };
259
+ await bus.emit(getUser("1"), { api: fakeApi });
260
+ ```
261
+
262
+ Rules for the second argument:
263
+
264
+ - It works with the call of an action (`emit(getUser("1"), scope)`, `ctx.emit(loaded(user), scope)`).
265
+ - It does not work with the string form: in `emit("user/get", payload)` the second argument is the payload.
266
+ - It can replace `$effects` too: `{ $effects: { delay: instantDelay } }`.
267
+ - Fields of the library (`signal`, `emit`, …) in it are ignored.
268
+
269
+ ### Give an action, a job or a module its own fields
270
+
271
+ <!-- docs-check: new -->
272
+
273
+ ```js
274
+ // user.js
275
+ import { createAction, createJob } from "nanoeffects/actions";
276
+
277
+ // createAction — the listeners of this action's calls get the fields
278
+ export const userLoaded = createAction("user/loaded", { store });
279
+
280
+ // createJob — the scope is the fourth argument, after the outcomes;
281
+ // the handler and the listeners of the outcome get its fields
282
+ export const loadUser = createJob(
283
+ "user/load",
284
+ fetchUser,
285
+ { success: userLoaded, fail: "user/failed" },
286
+ { logger },
287
+ );
288
+ ```
289
+
290
+ ```js
291
+ // audit-module.js
292
+ import { createModule } from "nanoeffects/actions";
293
+ import { userLoaded } from "./user";
294
+
295
+ // createModule — third argument; everything the module owns gets the fields:
296
+ // m.on listeners, m.handle handlers, m.spawn tasks
297
+ export const auditModule = createModule(
298
+ "audit",
299
+ (m) => {
300
+ m.on(userLoaded, (user, { logger }) => logger.info(user.id));
301
+ },
302
+ { logger },
303
+ );
304
+ ```
305
+
306
+ If the outcome action (`success` or `fail`) has a scope of its own, its listeners see the job's fields with the action's own fields over them; the handler of the job sees only the job's.
307
+
308
+ A module scope does not leak: a listener registered outside the module does not see it.
309
+
310
+ ### Which value wins
311
+
312
+ <!-- docs-check: new -->
313
+
314
+ When several places set the same field, the strongest one wins:
315
+
316
+ | Strength | Where it is set |
317
+ | -------- | --------------- |
318
+ | 1 (strongest) | the second argument of `emit` |
319
+ | 2 | the scope of the action or job |
320
+ | 3 | the scope of the module that owns the handler |
321
+ | 4 (weakest) | the bus: `createBus({ context })`, `contextPlugin`, `effectsPlugin` |
322
+
323
+ The fields of the library (`signal`, `abort`, `emit`, `on`, `off`, `run`, `type`, `payload`) are never replaced. The name `traceGroupId` is reserved: the tracer reads it, and it is not put on `ctx` ([Follow one flow](tracing.md#follow-one-flow-with-a-tracegroupid)). A child task (`ctx.run`, an attempt of `retry`) has the fields of its parent.
324
+
325
+ ### Keep a generic effect generic
326
+
327
+ <!-- docs-check: new -->
328
+
329
+ In `$effects` a function loses its first parameter, and TypeScript cannot keep a generic through that step: `<T>(items: T[]) => T` becomes `(items: unknown[]) => unknown`. JavaScript code is not affected. If you want the exact type, use one of two ways.
330
+
331
+ `effect(fn)`: mark the function where you declare it, then put it in the scope like any other value. No annotation, so a JavaScript editor shows the exact hint too.
332
+
333
+ ```js
334
+ // pick.js
335
+ import { createAction } from "nanoeffects/actions";
336
+ import { effect } from "nanoeffects/scope";
337
+
338
+ // effect — marks the function, so a scope binds it to the task; the editor keeps its generic
339
+ const first = effect((ctx, items) => items[0]);
340
+
341
+ export const pick = createAction("pick", (items, { first }) => first(items), {
342
+ first, // a plain key of the scope, not an entry of `$effects`
343
+ });
344
+ ```
345
+
346
+ Call `effect` where you declare the function, as above, and not inline in the scope: TypeScript does not infer the scope from a generic call written there.
347
+
348
+ `Effect<F>` (TypeScript): declare how the effect looks **after** it is bound, and keep it in `$effects`.
349
+
350
+ ```ts
351
+ import { createAction } from "nanoeffects/actions";
352
+ import type { Effect } from "nanoeffects/effects";
353
+
354
+ // First — checker-only: how a handler calls the effect (no ctx)
355
+ type First = <T>(items: T[]) => T;
356
+
357
+ // Effect<First> — the function takes the task first and is typed as First once bound
358
+ const first: Effect<First> = (ctx, items) => items[0];
359
+
360
+ const pick = createAction(
361
+ "pick",
362
+ (id: string, { first }) => first([id]), // string
363
+ { $effects: { first } },
364
+ );
365
+ ```
366
+
367
+ `Effect<F>` and `effect` change what the checker sees. At run time the function is the same.
368
+
369
+ ### Type the fields of a scope
370
+
371
+ <!-- docs-check: new -->
372
+
373
+ A handler of an action, job or module with a scope sees the fields of the scope in `ctx`, typed from the values you gave. You write no `WithContext`.
374
+
375
+ ```ts
376
+ import { createAction } from "nanoeffects/actions";
377
+
378
+ type User = { id: string; name: string };
379
+
380
+ const getUser = createAction(
381
+ "user/get",
382
+ async (id: string, ctx) => {
383
+ ctx.cache; // Map<string, User>, from the scope below
384
+ return ctx.cache.get(id);
385
+ },
386
+ { cache: new Map<string, User>() },
387
+ );
388
+ ```
389
+
390
+ - A field of a scope replaces a field of the same name from the bus in the type too.
391
+ - An override in `emit` must match the type of the field it replaces: `emit(getUser("1"), { cache: 1 })` is an error.
392
+ - A field that no scope and no bus declares has no type to check against, so `emit(call, { unknown: 1 })` is an error in TypeScript.
393
+ - `bus.on(creator, fn)` for an action with a scope types `ctx` with its fields.
394
+
395
+ ### Limits
396
+
397
+ <!-- docs-check: new -->
398
+
399
+ - No plugin, no scope: see [Turn scopes on](#turn-scopes-on).
400
+ - The fields of the bus are not in the handler of an action made with `createAction(…, scope)` unless you type `ctx` yourself; the fields of the scope are.
401
+ - The string form of `emit` takes no scope.
402
+ - `effect(fn)` works only as a value of a scope, not as an entry of `$effects`.
403
+ - A scope on an action, job or module is typed without the events of a bus, so `take` in its `$effects` takes any event name.
@@ -1,4 +1,6 @@
1
- # Debounce a request and abort the previous one
1
+ # Pause between requests
2
+
3
+ > **Level B.** Read first: [Policies](policies.md).
2
4
 
3
5
  Read this page if you debounce user input (a search box, an autosave, a resize handler) and you also want the request in flight to stop when a new one starts. `debounce` alone waits for a pause. It does not cancel a request that already started. This page shows the two policies together, the order to put them in, and when to use `throttle` instead.
4
6