nanoeffects 0.6.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 (49) hide show
  1. package/dist/{action-creator-c292nK7v.d.ts → action-creator-BrZF6hXc.d.ts} +4 -8
  2. package/dist/actions.d.ts +6 -6
  3. package/dist/actions.js +1 -1
  4. package/dist/{context-plugin-1pYmSQEG.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-BlF2KSgy.d.ts → module-DqKegXHt.d.ts} +44 -19
  13. package/dist/notrace/{action-creator-DKG_sIia.d.ts → action-creator-DHAEgwuh.d.ts} +4 -8
  14. package/dist/notrace/actions.d.ts +59 -24
  15. package/dist/notrace/actions.js +1 -1
  16. package/dist/notrace/{context-plugin-Dt1IgVo8.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-B0AXKusc.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-CCYlC0XS.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 +8 -9
  31. package/dist/scope.js +1 -1
  32. package/dist/trace.d.ts +1 -1
  33. package/dist/types-KavbvHix.d.ts +25 -0
  34. package/docs/README.md +2 -0
  35. package/docs/actions.md +80 -36
  36. package/docs/guide.md +142 -18
  37. package/docs/hmr.md +6 -5
  38. package/docs/migrating-from-effector.md +49 -16
  39. package/docs/migrating-from-rxjs.md +17 -6
  40. package/docs/oop.md +286 -0
  41. package/docs/package.md +4 -0
  42. package/docs/plugins.md +284 -0
  43. package/docs/react.md +17 -5
  44. package/docs/scope.md +76 -36
  45. package/docs/stores.md +263 -109
  46. package/llms-full.txt +1232 -231
  47. package/llms.txt +2 -0
  48. package/package.json +20 -2
  49. package/dist/types-mLaAx-EZ.d.ts +0 -15
package/docs/package.md CHANGED
@@ -8,6 +8,8 @@ Imports are split so you only load what you use. `createBus` stays small; the re
8
8
  | `nanoeffects/policies` | `latest`, `leading`, `debounce`, `throttle`, `queue` |
9
9
  | `nanoeffects/effects` | `retry`, `timeout`, `poll`, `delay`, `take`, `all`, `allSettled`, `race`, `effectsPlugin` |
10
10
  | `nanoeffects/actions` | `createAction`, `createCommand`, `createJob`, `createModule`, `modulePlugin`, `commandsPlugin` |
11
+ | `nanoeffects/match` | `matchPlugin`: `on`, `once` and `off` that take a function which picks the events |
12
+ | `nanoeffects/oop` | `extendBus`, `WithBus`, `On`, `Once`, `disposeBus`: the methods of a bus on a class, with the subscriptions of each instance ended by `dispose()` |
11
13
  | `nanoeffects/scope` | `scopePlugin`, `effect`: fields for one command, action, job, module or `emit` (the types `Scope`, `Effect`) |
12
14
  | `nanoeffects/redux` | `createEffectsMiddleware` |
13
15
  | `nanoeffects/observable` | `createObserve` |
@@ -15,6 +17,8 @@ Imports are split so you only load what you use. `createBus` stays small; the re
15
17
  | `nanoeffects/dev` | `devPlugin` (no-op in production) |
16
18
  | `nanoeffects/trace` | `createTracer` and the loggers |
17
19
 
20
+ The `...Plugin` names above are used with `bus.use`. [Plugins](plugins.md) lists all of them, what each returns, and the order to use them in.
21
+
18
22
  ## Give the docs to an AI assistant
19
23
 
20
24
  The package ships two plain-text files for assistants that write code with it:
@@ -0,0 +1,284 @@
1
+ # Choose the plugins of a bus
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 gives you, and in what order to use them. Skip it for a small bus: `createBus()` alone is enough.
4
+
5
+ ## Use a plugin
6
+
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).
38
+
39
+ ```js
40
+ import { createBus } from "nanoeffects";
41
+ import { delay, effectsPlugin, race } from "nanoeffects/effects";
42
+
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),
50
+ });
51
+ });
52
+ ```
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
63
+ const bus = createBus();
64
+
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
119
+ ```
120
+
121
+ ### Listen to a group of events: `matchPlugin`
122
+
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).
124
+
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());
190
+
191
+ disconnect(); // stops the diagnostics
192
+ ```
193
+
194
+ ## Put them together
195
+
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.
197
+
198
+ ```js
199
+ // bus.js
200
+ import { contextPlugin, createBus } from "nanoeffects";
201
+ import {
202
+ commandsPlugin,
203
+ emitterPlugin,
204
+ modulePlugin,
205
+ } from "nanoeffects/actions";
206
+ import { devPlugin } from "nanoeffects/dev";
207
+ import { all, effectsPlugin, race } from "nanoeffects/effects";
208
+ import { matchPlugin } from "nanoeffects/match";
209
+ import { scopePlugin } from "nanoeffects/scope";
210
+
211
+ export const bus = createBus()
212
+ .use(contextPlugin({ store })) // ctx.store on every task
213
+ .use(effectsPlugin({ race, all })) // ctx.race and ctx.all, bound to the task
214
+ .use(scopePlugin()) // scopes for commands, actions, jobs, modules and emit
215
+ .use(matchPlugin()) // bus.on takes a function that picks the events
216
+ .use(emitterPlugin()); // bus.emitter(creator)
217
+
218
+ // commandsPlugin — the registry of command handlers; keep it to bind handlers later
219
+ export const commands = bus.use(commandsPlugin());
220
+
221
+ // modulePlugin — mounts userModule on the first use; keep app to mount and unmount later
222
+ export const app = bus.use(modulePlugin([userModule]));
223
+
224
+ // devPlugin — reports leaks and modules while you develop; keep disconnect to remove it
225
+ export const disconnect = bus.use(devPlugin());
226
+ ```
227
+
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.
231
+
232
+ Two things happen because modules are mounted late:
233
+
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.
236
+
237
+ Other plugins do not need an order for another reason:
238
+
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.
243
+
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.
247
+
248
+ ## See what is not a plugin
249
+
250
+ These connect the bus to something, but are not used with `bus.use`:
251
+
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).
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).
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  Optional, for domain logic. Use it when one command 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.
4
4
 
5
- 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)`.
5
+ 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)`.
6
6
 
7
7
  You give a scope to the thing that uses it. A call can replace it. Nothing else changes: handlers still get `(payload, ctx)`.
8
8
 
@@ -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
  ```
@@ -47,21 +54,18 @@ The listeners of `"user/get"` get the same fields when the command is emitted, b
47
54
 
48
55
  ## Bind an effect to the task
49
56
 
50
- 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.
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
@@ -69,30 +73,56 @@ const loadFeed = createCommand(
69
73
  feed: () => fetchJson(url),
70
74
  timeout: () => delay(5000),
71
75
  }),
72
- // effects — built-in and your own functions share one object
73
- { effects: { fetchJson, race, delay } },
76
+ // $effects — built-in and your own functions share one object
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
  ```
78
95
 
79
- - Put in `effects` only functions that take `ctx` first. A plain function (a formatter, a logger) belongs in `context`.
80
- - A function under any other key is copied as it is; a function in `effects` is bound.
81
- - `effects` is the one name a field of the scope cannot have.
96
+ - Put in `$effects` only functions that take `ctx` first. A plain function (a formatter, a logger) belongs in `context`.
97
+ - A function under any other key is copied as it is; a function in `$effects` is bound.
98
+ - `$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.
82
99
 
83
100
  ## Replace a field for one call
84
101
 
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"));
@@ -106,29 +136,36 @@ Rules for the second argument:
106
136
 
107
137
  - It works with an action or a command call (`emit(getUser("1"), scope)`, `ctx.emit(loaded(user), scope)`).
108
138
  - It does not work with the string form: in `emit("user/get", payload)` the second argument is the payload.
109
- - It can replace `effects` too: `{ effects: { delay: instantDelay } }`.
139
+ - It can replace `$effects` too: `{ $effects: { delay: instantDelay } }`.
110
140
  - Fields of the library (`signal`, `emit`, …) in it are ignored.
111
141
 
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));
@@ -137,6 +174,8 @@ const auditModule = createModule(
137
174
  );
138
175
  ```
139
176
 
177
+ 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.
178
+
140
179
  A module scope does not leak: a listener registered outside the module does not see it.
141
180
 
142
181
  ## Which value wins
@@ -154,25 +193,26 @@ The fields of the library (`signal`, `abort`, `emit`, `on`, `off`, `run`, `type`
154
193
 
155
194
  ## Keep a generic effect generic
156
195
 
157
- 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.
196
+ 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.
158
197
 
159
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.
160
199
 
161
200
  ```js
201
+ // pick.js
162
202
  import { createCommand } from "nanoeffects/actions";
163
203
  import { effect } from "nanoeffects/scope";
164
204
 
165
205
  // effect — marks the function, so a scope binds it to the task; the editor keeps its generic
166
206
  const first = effect((ctx, items) => items[0]);
167
207
 
168
- const pick = createCommand("pick", (items, { first }) => first(items), {
169
- first, // a plain key of the scope, not an entry of effects
208
+ export const pick = createCommand("pick", (items, { first }) => first(items), {
209
+ first, // a plain key of the scope, not an entry of `$effects`
170
210
  });
171
211
  ```
172
212
 
173
213
  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.
174
214
 
175
- `Effect<F>` (TypeScript): declare how the effect looks **after** it is bound, and keep it in `effects`.
215
+ `Effect<F>` (TypeScript): declare how the effect looks **after** it is bound, and keep it in `$effects`.
176
216
 
177
217
  ```ts
178
218
  import { createCommand } from "nanoeffects/actions";
@@ -187,7 +227,7 @@ const first: Effect<First> = (ctx, items) => items[0];
187
227
  const pick = createCommand(
188
228
  "pick",
189
229
  (id: string, { first }) => first([id]), // string
190
- { effects: { first } },
230
+ { $effects: { first } },
191
231
  );
192
232
  ```
193
233
 
@@ -222,5 +262,5 @@ const getUser = createCommand(
222
262
  - No plugin, no scope: see [Turn scopes on](#turn-scopes-on).
223
263
  - The fields of the bus are not in the handler of a command made with `createCommand(…, scope)` unless you type `ctx` yourself; the fields of the scope are.
224
264
  - The string form of `emit` takes no scope.
225
- - `effect(fn)` works only as a value of a scope, not as an entry of `effects`.
226
- - A scope on a command, action, job or module is typed without the events of a bus, so `take` in its `effects` takes any event name.
265
+ - `effect(fn)` works only as a value of a scope, not as an entry of `$effects`.
266
+ - A scope on a command, action, job or module is typed without the events of a bus, so `take` in its `$effects` takes any event name.