nanoeffects 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/{action-creator-Bd4TBZgl.d.ts → action-creator-BrZF6hXc.d.ts} +0 -4
  2. package/dist/actions.d.ts +3 -3
  3. package/dist/actions.js +1 -1
  4. package/dist/{context-plugin-BVNKqkIx.d.ts → context-plugin--HkNN1ha.d.ts} +1 -1
  5. package/dist/dev.d.ts +9 -8
  6. package/dist/dev.js +1 -1
  7. package/dist/effects.d.ts +2 -2
  8. package/dist/effects.js +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/match.d.ts +50 -0
  11. package/dist/match.js +1 -0
  12. package/dist/{module-DZ5ngUrM.d.ts → module-DqKegXHt.d.ts} +43 -18
  13. package/dist/notrace/{action-creator-D-y_-xRS.d.ts → action-creator-DHAEgwuh.d.ts} +0 -4
  14. package/dist/notrace/actions.d.ts +52 -17
  15. package/dist/notrace/actions.js +1 -1
  16. package/dist/notrace/{context-plugin-DDfsaErA.d.ts → context-plugin-BapQkGwQ.d.ts} +1 -1
  17. package/dist/notrace/effects.d.ts +2 -2
  18. package/dist/notrace/effects.js +1 -1
  19. package/dist/notrace/index.d.ts +2 -2
  20. package/dist/notrace/{plugin-CDAngHwg.d.ts → plugin-Cxadz6uU.d.ts} +2 -2
  21. package/dist/notrace/policies.d.ts +1 -1
  22. package/dist/notrace/redux.d.ts +1 -1
  23. package/dist/observable.d.ts +1 -1
  24. package/dist/oop.d.ts +131 -0
  25. package/dist/oop.js +1 -0
  26. package/dist/{plugin-DQNPBcxx.d.ts → plugin-DkXCvsmB.d.ts} +2 -2
  27. package/dist/policies.d.ts +1 -1
  28. package/dist/react.d.ts +1 -1
  29. package/dist/redux.d.ts +1 -1
  30. package/dist/scope.d.ts +5 -6
  31. package/dist/trace.d.ts +1 -1
  32. package/dist/{types-BJ0fzV1p.d.ts → types-KavbvHix.d.ts} +13 -3
  33. package/docs/README.md +1 -0
  34. package/docs/actions.md +79 -35
  35. package/docs/guide.md +142 -18
  36. package/docs/hmr.md +6 -5
  37. package/docs/migrating-from-effector.md +49 -16
  38. package/docs/migrating-from-rxjs.md +17 -6
  39. package/docs/oop.md +286 -0
  40. package/docs/package.md +2 -0
  41. package/docs/plugins.md +227 -51
  42. package/docs/react.md +17 -5
  43. package/docs/scope.md +60 -22
  44. package/docs/stores.md +263 -109
  45. package/llms-full.txt +1152 -267
  46. package/llms.txt +1 -0
  47. package/package.json +20 -2
package/docs/plugins.md CHANGED
@@ -1,54 +1,202 @@
1
1
  # Choose the plugins of a bus
2
2
 
3
- Optional. A plugin adds one thing to a bus: fields on `ctx`, a method, a registry of handlers, diagnostics. Read this page when you want to know which plugins exist, what each one returns, and in what order to use them. Skip it for a small bus: `createBus()` alone is enough.
3
+ Optional. A plugin adds one thing to a bus: fields on `ctx`, a method, a registry of handlers, diagnostics. Read this page when you want to know which plugins exist, what each one gives you, and in what order to use them. Skip it for a small bus: `createBus()` alone is enough.
4
4
 
5
5
  ## Use a plugin
6
6
 
7
- A plugin is a function of the bus. `bus.use(plugin)` calls it with the bus and returns whatever the plugin returns.
7
+ A plugin is a function of the bus. `bus.use(plugin)` calls it with the bus and returns whatever the plugin returns. Most plugins return the same bus, so you can chain them: `createBus().use(a).use(b)`. A few return something of their own (a registry, an app, a `disconnect` function): keep it.
8
+
9
+ In TypeScript keep the result of `contextPlugin` and `effectsPlugin`: the type of the bus with its new fields is the type they return (`const bus = createBus().use(contextPlugin({ store }))`).
10
+
11
+ ## Main plugins
12
+
13
+ These four cover what most apps need: shared dependencies, effects, commands and modules. Start here.
14
+
15
+ | Plugin | What it gives you | Import | `bus.use` returns |
16
+ | ------ | ----------------- | ------ | ----------------- |
17
+ | [`contextPlugin`](#put-a-store-or-a-client-on-every-handler-contextplugin) | Your own fields (a store, a client, a logger) on `ctx` of every handler. | `nanoeffects` | The same bus, typed with the fields |
18
+ | [`effectsPlugin`](#use-race-and-delay-without-ctx-effectsplugin) | `race`, `all`, `delay` and your own effects on `ctx`, called without passing `ctx`. | `nanoeffects/effects` | The same bus, typed with the effects |
19
+ | [`commandsPlugin`](#let-a-test-replace-a-handler-commandsplugin) | The handlers of commands in a registry, so a test can replace one. | `nanoeffects/actions` | The registry `{ run, handle }` |
20
+ | [`modulePlugin`](#mount-a-domain-as-one-unit-moduleplugin) | A module: the listeners, handlers and tasks of one domain, mounted and unmounted with one call. | `nanoeffects/actions` | The app `{ mount, unmount, isMounted, dispose, commands, createModule }` |
21
+
22
+ ### Put a store or a client on every handler: `contextPlugin`
23
+
24
+ Every task gets the same fields on `ctx`: a store, an API client, a logger. The types come from the values. More in [Context fields](context.md).
25
+
26
+ ```js
27
+ import { contextPlugin, createBus } from "nanoeffects";
28
+
29
+ // contextPlugin — every handler gets ctx.store
30
+ const bus = createBus().use(contextPlugin({ store }));
31
+
32
+ bus.on("save", (draft, { store }) => store.put(draft));
33
+ ```
34
+
35
+ ### Use `race` and `delay` without `ctx`: `effectsPlugin`
36
+
37
+ Effects such as `race`, `all`, `take` and `delay` are bound to the task, so a handler calls `race({ … })` and not `race(ctx, { … })`. Your own functions that take `{ signal }` first work too. More in [Effects](effects.md#put-selected-effects-on-ctx).
8
38
 
9
39
  ```js
10
40
  import { createBus } from "nanoeffects";
41
+ import { delay, effectsPlugin, race } from "nanoeffects/effects";
11
42
 
12
- // your own plugin: a function of the bus that returns what you want back
13
- const countPlugin = (bus) => {
14
- let count = 0;
15
- const unbind = bus.on("*", () => {
16
- count += 1;
43
+ // effectsPlugin — race and delay are bound to the task of each handler
44
+ const bus = createBus().use(effectsPlugin({ race, delay }));
45
+
46
+ bus.on("load", async (id, { race, delay }) => {
47
+ const result = await race({
48
+ data: ({ signal }) => api.get(id, { signal }),
49
+ timeout: () => delay(5000),
17
50
  });
18
- return { count: () => count, stop: unbind };
19
- };
51
+ });
52
+ ```
20
53
 
54
+ ### Let a test replace a handler: `commandsPlugin`
55
+
56
+ Commands get their handlers from a registry that belongs to the bus. A command can be only a contract, and a test binds a fake. More in [Actions](actions.md#describe-events-commands-and-jobs).
57
+
58
+ ```js
59
+ import { createBus } from "nanoeffects";
60
+ import { commandsPlugin, createCommand } from "nanoeffects/actions";
61
+
62
+ const getUser = createCommand("user/get"); // a contract: no handler yet
21
63
  const bus = createBus();
22
- // use — calls countPlugin(bus) and returns its result
23
- const counter = bus.use(countPlugin);
24
64
 
25
- bus.emit("greet", "Ada");
26
- console.log(counter.count()); // 1
27
- counter.stop();
65
+ // commandsPlugin — the registry of handlers of this bus
66
+ const commands = bus.use(commandsPlugin());
67
+
68
+ commands.handle(getUser, async (id) => ({ id })); // the latest binding wins
69
+ const user = await bus.emit(getUser("1")); // { id: "1" }
70
+ ```
71
+
72
+ ### Mount a domain as one unit: `modulePlugin`
73
+
74
+ A module groups the listeners, the bound commands and the background tasks of one domain, and ends all of them when it is unmounted. It also makes [hot reload](hmr.md) work. More in [Modules](actions.md#mount-domains-with-modules).
75
+
76
+ ```js
77
+ import { createBus } from "nanoeffects";
78
+ import { modulePlugin } from "nanoeffects/actions";
79
+
80
+ const bus = createBus();
81
+
82
+ // modulePlugin — mounts userModule on the first use of the app or the bus, and returns the app
83
+ const app = bus.use(modulePlugin([userModule]));
84
+
85
+ app.unmount(userModule); // its listeners, bound commands and tasks are gone
86
+ ```
87
+
88
+ ## Extra plugins
89
+
90
+ Add one of these when you meet its need. None of them is required.
91
+
92
+ | Plugin | What it gives you | Import | `bus.use` returns |
93
+ | ------ | ----------------- | ------ | ----------------- |
94
+ | [`scopePlugin`](#give-one-command-its-own-fields-scopeplugin) | Its own fields for one command, action, job or module, and a way to replace them in one `emit`. | `nanoeffects/scope` | The same bus |
95
+ | [`matchPlugin`](#listen-to-a-group-of-events-matchplugin) | `on` that listens to a group of events which a function picks. | `nanoeffects/match` | The same bus, typed with the new overloads |
96
+ | [`emitterPlugin`](#hand-the-bus-to-a-state-manager-emitterplugin) | A callback that emits an action, to give to a state manager. | `nanoeffects/actions` | The same bus, with `emitter` |
97
+ | [`reactPlugin`](#tell-the-bus-when-a-react-value-changes-reactplugin) | A React hook that emits when a value of a component changes. | `nanoeffects/react` | The same bus, with `useEmitter` |
98
+ | [`devPlugin`](#find-leaks-while-you-develop-devplugin) | Warnings about listeners that pile up, and messages about modules, while you develop. | `nanoeffects/dev` | A function that disconnects it |
99
+
100
+ ### Give one command its own fields: `scopePlugin`
101
+
102
+ A command, an action, a job or a module can carry its own fields and effects, and one `emit` can replace them. A test gives a fake to one call. More in [Scopes](scope.md).
103
+
104
+ ```js
105
+ import { createBus } from "nanoeffects";
106
+ import { createCommand } from "nanoeffects/actions";
107
+ import { scopePlugin } from "nanoeffects/scope";
108
+
109
+ // scopePlugin — makes the scope of a command work on this bus
110
+ const bus = createBus().use(scopePlugin());
111
+
112
+ // the last argument: fields of this command only
113
+ const getUser = createCommand("user/get", (id, { api }) => api.getUser(id), {
114
+ api,
115
+ });
116
+
117
+ const fakeApi = { getUser: async (id) => ({ id, name: "test" }) };
118
+ bus.emit(getUser("1"), { api: fakeApi }); // a fake api for this call only
28
119
  ```
29
120
 
30
- The bus keeps no list of plugins. A plugin that was used cannot be taken out by the bus; it returns its own `stop` or `disconnect` when it needs one.
121
+ ### Listen to a group of events: `matchPlugin`
31
122
 
32
- ## See every plugin
123
+ `on`, `once` and `off` take a function that picks the events, such as every `user/*` event. A type guard narrows the payload. The subscription listens to every event, so for a few known events `on(creator, handler)` is cheaper. More in the [Guide](guide.md#pick-events-with-a-function).
33
124
 
34
- | Plugin | Import | It adds | `bus.use` returns | Page |
35
- | ------ | ------ | ------- | ----------------- | ---- |
36
- | `contextPlugin` | `nanoeffects` | Fields on `ctx` of every task | The same bus, typed with the fields | [Context fields](context.md) |
37
- | `effectsPlugin` | `nanoeffects/effects` | `race`, `all`, `take`, `delay` and your own effects on `ctx`, bound to the task | The same bus, typed with the effects | [Effects](effects.md#put-selected-effects-on-ctx) |
38
- | `scopePlugin` | `nanoeffects/scope` | Fields and effects for one command, action, job, module or `emit` | The same bus | [Scopes](scope.md) |
39
- | `commandsPlugin` | `nanoeffects/actions` | The registry of command handlers, `bus.commands` | The registry `{ run, handle }` | [Actions](actions.md#describe-events-commands-and-jobs) |
40
- | `modulePlugin` | `nanoeffects/actions` | Modules that mount and unmount as one unit | The app `{ mount, unmount, isMounted, dispose, commands, createModule }` | [Modules](actions.md#mount-domains-with-modules) |
41
- | `emitterPlugin` | `nanoeffects/actions` | `bus.emitter(creator)`, a callback that emits an action | The same bus, with `emitter` | [Stores](stores.md#emit-from-a-state-manager) |
42
- | `reactPlugin` | `nanoeffects/react` | `bus.useEmitter(event, value)`, a React hook | The same bus, with `useEmitter` | [React and stores](stores.md#react-sharing-a-value-across-components) |
43
- | `devPlugin` | `nanoeffects/dev` | Warnings about listeners that pile up, and messages about modules | A function that disconnects it | [Hot reload](hmr.md#report-leaks-with-devplugin) |
125
+ ```js
126
+ import { createBus } from "nanoeffects";
127
+ import { matchPlugin } from "nanoeffects/match";
128
+
129
+ // matchPlugin — on takes a function that picks the events
130
+ const bus = createBus().use(matchPlugin());
131
+
132
+ bus.on(
133
+ (action) => action.type.startsWith("user/"),
134
+ (payload, ctx) => console.log(ctx.type, payload),
135
+ );
136
+ ```
137
+
138
+ ### Hand the bus to a state manager: `emitterPlugin`
139
+
140
+ `bus.emitter(creator)` is a callback that emits an action. A state manager takes it as its subscription callback, so a change of the store becomes an event. More in [Stores](stores.md#emit-from-a-state-manager).
141
+
142
+ ```js
143
+ import { atom } from "nanostores";
144
+ import { createBus } from "nanoeffects";
145
+ import { createAction, emitterPlugin } from "nanoeffects/actions";
146
+
147
+ // emitterPlugin — adds bus.emitter
148
+ const bus = createBus().use(emitterPlugin());
149
+ const nameChanged = createAction("name/changed");
150
+ const $name = atom("");
151
+
152
+ $name.listen(bus.emitter(nameChanged)); // every change of $name emits nameChanged
153
+ ```
154
+
155
+ ### Tell the bus when a React value changes: `reactPlugin`
156
+
157
+ `useEmitter(event, value)` emits after a render commits with a new value. It does not emit on mount, and not twice under StrictMode. It needs React 18 or newer. More in [Stores](stores.md#react-sharing-a-value-across-components).
158
+
159
+ ```jsx
160
+ import { useState } from "react";
161
+ import { createBus } from "nanoeffects";
162
+ import { reactPlugin } from "nanoeffects/react";
163
+
164
+ // reactPlugin — adds bus.useEmitter; destructuring gives a free hook
165
+ const bus = createBus().use(reactPlugin());
166
+ const { useEmitter } = bus;
167
+
168
+ function Profile() {
169
+ const [name, setName] = useState("");
170
+ useEmitter("name/changed", name); // emits after a render with a new name
171
+ return <input value={name} onChange={(event) => setName(event.target.value)} />;
172
+ }
173
+ ```
174
+
175
+ ### Find leaks while you develop: `devPlugin`
176
+
177
+ It warns about listeners that were added and never unbound, and reports the modules that are mounted, replaced and unmounted. In a production build `nanoeffects/dev` is empty, so the call can stay. More in [Hot reload](hmr.md#report-leaks-with-devplugin).
178
+
179
+ ```js
180
+ import { createBus } from "nanoeffects";
181
+ import { modulePlugin } from "nanoeffects/actions";
182
+ import { devPlugin } from "nanoeffects/dev";
183
+
184
+ const bus = createBus();
185
+ bus.use(modulePlugin([userModule]));
186
+
187
+ // devPlugin — warnings and messages in the console while you develop;
188
+ // it finds the app of modulePlugin on the bus, whichever you use first
189
+ const disconnect = bus.use(devPlugin());
44
190
 
45
- "The same bus" means you can keep writing `bus` and ignore the result. Keep the result of `contextPlugin` and `effectsPlugin` in TypeScript, though: the type of the bus with its new fields is the type they return (`const bus = createBus().use(contextPlugin({ store }))`).
191
+ disconnect(); // stops the diagnostics
192
+ ```
46
193
 
47
194
  ## Put them together
48
195
 
49
- One bus with every plugin that does not need React. The order is explained in the next section.
196
+ One bus with every plugin that does not need React, in a file of its own. The events of the app stay in other files. The plugins work in any order; the next section says what that means for modules.
50
197
 
51
198
  ```js
199
+ // bus.js
52
200
  import { contextPlugin, createBus } from "nanoeffects";
53
201
  import {
54
202
  commandsPlugin,
@@ -57,47 +205,45 @@ import {
57
205
  } from "nanoeffects/actions";
58
206
  import { devPlugin } from "nanoeffects/dev";
59
207
  import { all, effectsPlugin, race } from "nanoeffects/effects";
208
+ import { matchPlugin } from "nanoeffects/match";
60
209
  import { scopePlugin } from "nanoeffects/scope";
61
210
 
62
- const bus = createBus()
211
+ export const bus = createBus()
63
212
  .use(contextPlugin({ store })) // ctx.store on every task
64
213
  .use(effectsPlugin({ race, all })) // ctx.race and ctx.all, bound to the task
65
214
  .use(scopePlugin()) // scopes for commands, actions, jobs, modules and emit
215
+ .use(matchPlugin()) // bus.on takes a function that picks the events
66
216
  .use(emitterPlugin()); // bus.emitter(creator)
67
217
 
68
218
  // commandsPlugin — the registry of command handlers; keep it to bind handlers later
69
- const commands = bus.use(commandsPlugin());
219
+ export const commands = bus.use(commandsPlugin());
70
220
 
71
- // modulePlugin — mounts userModule now; keep app to mount and unmount later
72
- const app = bus.use(modulePlugin([userModule]));
221
+ // modulePlugin — mounts userModule on the first use; keep app to mount and unmount later
222
+ export const app = bus.use(modulePlugin([userModule]));
73
223
 
74
224
  // devPlugin — reports leaks and modules while you develop; keep disconnect to remove it
75
- const disconnect = bus.use(devPlugin({ app }));
225
+ export const disconnect = bus.use(devPlugin());
76
226
  ```
77
227
 
78
- ## Follow the order
228
+ ## Use them in any order
229
+
230
+ The plugins give the same result in any order. Use them as they read best, and put the app of `modulePlugin` where you keep it.
79
231
 
80
- Most plugins do not care about each other. These pairs do:
232
+ Two things happen because modules are mounted late:
81
233
 
82
- | Rule | Why |
83
- | ---- | --- |
84
- | `scopePlugin` after `effectsPlugin` | `effectsPlugin` assigns its effects on `ctx`, so one used later would replace an effect with the same name that a scope put there. |
85
- | `scopePlugin` before `modulePlugin` | `modulePlugin` mounts the modules when it is used, and a module with a scope needs the plugin by then. |
86
- | `commandsPlugin` before `modulePlugin`, if you bind handlers yourself | `modulePlugin` uses the registry that exists. Without one it makes its own, which is `app.commands`. |
87
- | `modulePlugin` before `devPlugin({ app })` | `devPlugin` reports the modules of the `app` that `modulePlugin` returned. |
88
- | `emitterPlugin` and `reactPlugin` anywhere | They only call `bus.emit`, which is read when you call them. |
234
+ - **Modules start on the first use.** `modulePlugin([userModule])` mounts `userModule` when the app or the bus is first used: the first `bus.emit`, or the first call of `app.mount`, `app.unmount`, `app.isMounted`, `app.dispose` or the read of `app.commands`. By then every plugin is on the bus, so a module and the tasks it spawns have the fields, effects and scopes of the plugins you used after `modulePlugin`, and `m.on(fn, handler)` works with `matchPlugin`.
235
+ - **Listeners keep their place.** A `bus.on` after `bus.use(modulePlugin(...))` is held until the modules are mounted, then made, in the order you wrote them. The listeners of a module therefore stay ahead of the ones registered after the plugin, and behind the ones registered before it. The function `bus.on` returns works before and after.
89
236
 
90
- Two more things the checker enforces. A name that is already a context field of the bus is a type error when `contextPlugin` or `effectsPlugin` adds it again. `contextPlugin` does not replace a field that is on `ctx` already, so a scope wins over it in any order.
237
+ Other plugins do not need an order for another reason:
91
238
 
92
- ## Choose by need
239
+ - `commandsPlugin` binds its list in the registry that the bus has, so it can come before or after `modulePlugin`. A custom `bus.commands = { run }` is replaced.
240
+ - `effectsPlugin` and `contextPlugin` do not replace a field that is on `ctx` already, so a scope wins in any order.
241
+ - `devPlugin()` finds the app that `modulePlugin` put on the bus (`bus.app`). `devPlugin({ app })` still works.
242
+ - `matchPlugin` and `devPlugin` both wrap `bus.on`; `devPlugin` counts a function that picks events under `"*"`, and its `disconnect` leaves a wrapper that was put on top of it.
93
243
 
94
- - **A store, a client or a logger on every handler.** `contextPlugin`. Or `createBus({ context })` when you do not need the types from the value.
95
- - **`race`, `all` or your own effect without passing `ctx`.** `effectsPlugin`.
96
- - **Fields for one command, or a fake for one test call.** `scopePlugin`.
97
- - **A request with a result, or handlers that a test can replace.** `commandsPlugin`. Add `modulePlugin` when the domain needs a mount and an unmount.
98
- - **A callback for a state manager, such as `$name.listen(...)`.** `emitterPlugin`.
99
- - **Tell the bus when a value of a React component changes.** `reactPlugin`.
100
- - **Find listeners that were never unbound, or see modules reload.** `devPlugin`. In a production build `nanoeffects/dev` is empty, so the call can stay.
244
+ A name that is already a context field of the bus is a type error when `contextPlugin` or `effectsPlugin` adds it again.
245
+
246
+ A test puts every plugin on a bus in many orders and checks what each one does (`src/tests/plugin-matrix.test.ts`). An order that breaks something is a bug.
101
247
 
102
248
  ## See what is not a plugin
103
249
 
@@ -105,4 +251,34 @@ These connect the bus to something, but are not used with `bus.use`:
105
251
 
106
252
  - `createEffectsMiddleware(bus, { filter })` from `nanoeffects/redux` is a Redux middleware for a bus that exists. See [Stores](stores.md#wire-redux-toolkit-to-a-bus).
107
253
  - `createBus({ trace })` takes a tracer from `nanoeffects/trace`. See [Tracing](tracing.md).
254
+ - `extendBus(bus, Base)` from `nanoeffects/oop` gives a class the methods of a bus, with the subscriptions of each instance ended by `dispose()`. It reads the bus as it is, so use the plugins first. See [Use the bus from a class](oop.md).
108
255
  - `createObserve(bus)` from `nanoeffects/observable` turns an event into an Observable. See [Observable](observable.md).
256
+
257
+ ## Write your own plugin
258
+
259
+ A plugin is a function of the bus that returns what you want back. The bus keeps no list of plugins, so a plugin that was used cannot be taken out by the bus; it returns its own `stop` or `disconnect` when it needs one.
260
+
261
+ ```js
262
+ // count-plugin.js
263
+ // your own plugin: a function of the bus that returns what you want back
264
+ export const countPlugin = (bus) => {
265
+ let count = 0;
266
+ const unbind = bus.on("*", () => {
267
+ count += 1;
268
+ });
269
+ return { count: () => count, stop: unbind };
270
+ };
271
+ ```
272
+
273
+ ```js
274
+ import { createBus } from "nanoeffects";
275
+ import { countPlugin } from "./count-plugin";
276
+
277
+ const bus = createBus();
278
+ // use — calls countPlugin(bus) and returns its result
279
+ const counter = bus.use(countPlugin);
280
+
281
+ bus.emit("greet", "Ada");
282
+ console.log(counter.count()); // 1
283
+ counter.stop();
284
+ ```
package/docs/react.md CHANGED
@@ -16,19 +16,26 @@ Each has a policy or an effect: `latest`, `leading`, `retry`. They live in one p
16
16
 
17
17
  ## Wire the bus once
18
18
 
19
- Put the bus and its handlers in their own file. Create it once, outside any component: a bus created in the body of a component would be a new bus, without subscribers, on every render.
19
+ Keep the bus in a file of its own, and the handlers of the form in another. Create the bus once, outside any component: a bus created in the body of a component would be a new bus, without subscribers, on every render.
20
20
 
21
21
  ```js
22
- // registration.js
22
+ // bus.js
23
23
  import { createBus } from "nanoeffects";
24
+
25
+ export const bus = createBus(); // the bus: created once, at the top of the module
26
+ ```
27
+
28
+ The handlers know the bus and the bus knows nothing of them. They register when `registration.js` is imported, once, when the app starts:
29
+
30
+ ```js
31
+ // registration.js
24
32
  import { retry } from "nanoeffects/effects";
25
33
  import { debounce, latest, leading } from "nanoeffects/policies";
34
+ import { bus } from "./bus";
26
35
 
27
36
  // fetch rejects with a TypeError when the network fails; an HTTP error is a plain Error
28
37
  const isNetworkError = (error) => error instanceof TypeError;
29
38
 
30
- export const bus = createBus(); // the bus: created once, at the top of the module
31
-
32
39
  // debounce — wait 300 ms of quiet; latest — abort the check of the previous name
33
40
  bus.on(
34
41
  "username/typed",
@@ -66,12 +73,15 @@ The handlers know nothing about React. A test can emit `username/typed` and read
66
73
 
67
74
  ## Connect the form
68
75
 
76
+ <!-- docs-check: continue -->
77
+
69
78
  The component keeps what it shows in `useState`. It subscribes in `useEffect` and emits events from its handlers. `bus.on` returns the function that unsubscribes; returning it from the effect removes the listener on unmount.
70
79
 
71
80
  ```jsx
72
81
  // SignUpForm.jsx
73
82
  import { useEffect, useState } from "react";
74
- import { bus } from "./registration"; // the bus of the previous snippet
83
+ import { bus } from "./bus"; // the bus of the previous snippet
84
+ import "./registration"; // registers the handlers; the app does this once, when it starts
75
85
 
76
86
  export function SignUpForm() {
77
87
  const [name, setName] = useState("");
@@ -134,6 +144,8 @@ In development, React StrictMode runs the effect twice (subscribe, unsubscribe,
134
144
 
135
145
  ## Stop running work when the form closes
136
146
 
147
+ <!-- docs-check: continue -->
148
+
137
149
  Unsubscribing stops the component from hearing results, but a request that has started keeps running. To cancel it, the component emits `form/closed` when it unmounts, and each handler that should stop listens for it with `ctx.on`:
138
150
 
139
151
  ```js
package/docs/scope.md CHANGED
@@ -11,27 +11,29 @@ You give a scope to the thing that uses it. A call can replace it. Nothing else
11
11
  Scopes are a plugin, so a bus that does not use them pays nothing.
12
12
 
13
13
  ```js
14
+ // bus.js
14
15
  import { createBus } from "nanoeffects";
15
16
  import { scopePlugin } from "nanoeffects/scope";
16
17
 
17
18
  // scopePlugin — makes the scopes below work on this bus; returns the same bus
18
- const bus = createBus().use(scopePlugin());
19
+ export const bus = createBus().use(scopePlugin());
19
20
  ```
20
21
 
22
+ The bus is in one file and the commands and 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.
23
+
21
24
  - Without `scopePlugin` a scope is **ignored**: no error and no warning. If a field is missing from `ctx`, check this first.
22
- - Use it **after** `effectsPlugin` (an effect it adds later would be assigned over an effect of a scope with the same name) and **before** `modulePlugin` (it mounts the modules when it is used). `contextPlugin` and `createBus({ context })` can come in any order: a scope wins over them.
25
+ - Use it in any order with the other plugins: a scope wins over `effectsPlugin`, `contextPlugin` and `createBus({ context })`.
23
26
 
24
27
  ## Give a command its own fields
25
28
 
29
+ <!-- docs-check: continue -->
30
+
26
31
  ```js
27
- import { createBus } from "nanoeffects";
32
+ // user.js
28
33
  import { createCommand } from "nanoeffects/actions";
29
- import { scopePlugin } from "nanoeffects/scope";
30
-
31
- const bus = createBus().use(scopePlugin());
32
34
 
33
35
  // createCommand — third argument is the scope of this command
34
- const getUser = createCommand(
36
+ export const getUser = createCommand(
35
37
  "user/get",
36
38
  (id, { api, cache }) => {
37
39
  // api and cache are on ctx for this command only
@@ -39,6 +41,11 @@ const getUser = createCommand(
39
41
  },
40
42
  { api, cache: new Map() },
41
43
  );
44
+ ```
45
+
46
+ ```js
47
+ import { bus } from "./bus";
48
+ import { getUser } from "./user";
42
49
 
43
50
  const user = await bus.emit(getUser("1")); // the handler has api and cache
44
51
  ```
@@ -50,18 +57,15 @@ The listeners of `"user/get"` get the same fields when the command is emitted, b
50
57
  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.
51
58
 
52
59
  ```js
53
- import { createBus } from "nanoeffects";
60
+ // feed.js
54
61
  import { createCommand } from "nanoeffects/actions";
55
62
  import { delay, race } from "nanoeffects/effects";
56
- import { scopePlugin } from "nanoeffects/scope";
57
-
58
- const bus = createBus().use(scopePlugin());
59
63
 
60
64
  // your own effect: first parameter is the task (only its signal is used)
61
65
  const fetchJson = (ctx, url) =>
62
66
  fetch(url, { signal: ctx.signal }).then((response) => response.json());
63
67
 
64
- const loadFeed = createCommand(
68
+ export const loadFeed = createCommand(
65
69
  "feed/load",
66
70
  (url, { fetchJson, race, delay }) =>
67
71
  // race, delay, fetchJson — bound to this task: no ctx argument
@@ -72,6 +76,19 @@ const loadFeed = createCommand(
72
76
  // $effects — built-in and your own functions share one object
73
77
  { $effects: { fetchJson, race, delay } },
74
78
  );
79
+ ```
80
+
81
+ ```js
82
+ // bus.js
83
+ import { createBus } from "nanoeffects";
84
+ import { scopePlugin } from "nanoeffects/scope";
85
+
86
+ export const bus = createBus().use(scopePlugin());
87
+ ```
88
+
89
+ ```js
90
+ import { bus } from "./bus";
91
+ import { loadFeed } from "./feed";
75
92
 
76
93
  const result = await bus.emit(loadFeed("/feed"));
77
94
  ```
@@ -85,14 +102,27 @@ const result = await bus.emit(loadFeed("/feed"));
85
102
  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.
86
103
 
87
104
  ```js
105
+ // bus.js
88
106
  import { createBus } from "nanoeffects";
89
- import { createCommand } from "nanoeffects/actions";
90
107
  import { scopePlugin } from "nanoeffects/scope";
91
108
 
92
- const bus = createBus().use(scopePlugin());
93
- const getUser = createCommand("user/get", (id, { api }) => api.getUser(id), {
94
- api,
95
- });
109
+ export const bus = createBus().use(scopePlugin());
110
+ ```
111
+
112
+ ```js
113
+ // user.js
114
+ import { createCommand } from "nanoeffects/actions";
115
+
116
+ export const getUser = createCommand(
117
+ "user/get",
118
+ (id, { api }) => api.getUser(id),
119
+ { api },
120
+ );
121
+ ```
122
+
123
+ ```js
124
+ import { bus } from "./bus";
125
+ import { getUser } from "./user";
96
126
 
97
127
  // the real api for this call
98
128
  await bus.emit(getUser("1"));
@@ -112,23 +142,30 @@ Rules for the second argument:
112
142
  ## Give an action, a job or a module its own fields
113
143
 
114
144
  ```js
115
- import { createAction, createJob, createModule } from "nanoeffects/actions";
145
+ // user.js
146
+ import { createAction, createJob } from "nanoeffects/actions";
116
147
 
117
148
  // createAction — the listeners of this action's calls get the fields
118
- const userLoaded = createAction("user/loaded", { store });
149
+ export const userLoaded = createAction("user/loaded", { store });
119
150
 
120
151
  // createJob — the scope is the fourth argument, after the outcomes;
121
152
  // the handler and the listeners of the outcome get its fields
122
- const loadUser = createJob(
153
+ export const loadUser = createJob(
123
154
  "user/load",
124
155
  fetchUser,
125
156
  { success: userLoaded, fail: "user/failed" },
126
157
  { logger },
127
158
  );
159
+ ```
160
+
161
+ ```js
162
+ // audit-module.js
163
+ import { createModule } from "nanoeffects/actions";
164
+ import { userLoaded } from "./user";
128
165
 
129
166
  // createModule — third argument; everything the module owns gets the fields:
130
167
  // m.on listeners, m.handle handlers, m.spawn tasks
131
- const auditModule = createModule(
168
+ export const auditModule = createModule(
132
169
  "audit",
133
170
  (m) => {
134
171
  m.on(userLoaded, (user, { logger }) => logger.info(user.id));
@@ -161,13 +198,14 @@ In `$effects` a function loses its first parameter, and TypeScript cannot keep a
161
198
  `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.
162
199
 
163
200
  ```js
201
+ // pick.js
164
202
  import { createCommand } from "nanoeffects/actions";
165
203
  import { effect } from "nanoeffects/scope";
166
204
 
167
205
  // effect — marks the function, so a scope binds it to the task; the editor keeps its generic
168
206
  const first = effect((ctx, items) => items[0]);
169
207
 
170
- const pick = createCommand("pick", (items, { first }) => first(items), {
208
+ export const pick = createCommand("pick", (items, { first }) => first(items), {
171
209
  first, // a plain key of the scope, not an entry of `$effects`
172
210
  });
173
211
  ```