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.
- package/dist/{action-creator-c292nK7v.d.ts → action-creator-BrZF6hXc.d.ts} +4 -8
- package/dist/actions.d.ts +6 -6
- package/dist/actions.js +1 -1
- package/dist/{context-plugin-1pYmSQEG.d.ts → context-plugin--HkNN1ha.d.ts} +1 -1
- package/dist/dev.d.ts +9 -8
- package/dist/dev.js +1 -1
- package/dist/effects.d.ts +2 -2
- package/dist/effects.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/match.d.ts +50 -0
- package/dist/match.js +1 -0
- package/dist/{module-BlF2KSgy.d.ts → module-DqKegXHt.d.ts} +44 -19
- package/dist/notrace/{action-creator-DKG_sIia.d.ts → action-creator-DHAEgwuh.d.ts} +4 -8
- package/dist/notrace/actions.d.ts +59 -24
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/{context-plugin-Dt1IgVo8.d.ts → context-plugin-BapQkGwQ.d.ts} +1 -1
- package/dist/notrace/effects.d.ts +2 -2
- package/dist/notrace/effects.js +1 -1
- package/dist/notrace/index.d.ts +2 -2
- package/dist/notrace/{plugin-B0AXKusc.d.ts → plugin-Cxadz6uU.d.ts} +2 -2
- package/dist/notrace/policies.d.ts +1 -1
- package/dist/notrace/redux.d.ts +1 -1
- package/dist/observable.d.ts +1 -1
- package/dist/oop.d.ts +131 -0
- package/dist/oop.js +1 -0
- package/dist/{plugin-CCYlC0XS.d.ts → plugin-DkXCvsmB.d.ts} +2 -2
- package/dist/policies.d.ts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/redux.d.ts +1 -1
- package/dist/scope.d.ts +8 -9
- package/dist/scope.js +1 -1
- package/dist/trace.d.ts +1 -1
- package/dist/types-KavbvHix.d.ts +25 -0
- package/docs/README.md +2 -0
- package/docs/actions.md +80 -36
- package/docs/guide.md +142 -18
- package/docs/hmr.md +6 -5
- package/docs/migrating-from-effector.md +49 -16
- package/docs/migrating-from-rxjs.md +17 -6
- package/docs/oop.md +286 -0
- package/docs/package.md +4 -0
- package/docs/plugins.md +284 -0
- package/docs/react.md +17 -5
- package/docs/scope.md +76 -36
- package/docs/stores.md +263 -109
- package/llms-full.txt +1232 -231
- package/llms.txt +2 -0
- package/package.json +20 -2
- 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:
|
package/docs/plugins.md
ADDED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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 "./
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
80
|
-
- A function under any other key is copied as it is; a function in
|
|
81
|
-
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
226
|
-
- A scope on a command, action, job or module is typed without the events of a bus, so `take` in its
|
|
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.
|