nanoeffects 0.26.0 → 0.27.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/README.md +76 -23
- package/dist/{action-creator-Bpa8BUS9.d.ts → action-creator-D_c223C7.d.ts} +51 -23
- package/dist/actions.d.ts +54 -21
- package/dist/actions.js +1 -1
- package/dist/{bus-hHp-T7JV.js → bus-DQDHUN5e.js} +1 -1
- package/dist/{context-types-BKIH5a8s.d.ts → context-types-nGtzhjxD.d.ts} +1 -1
- package/dist/context.d.ts +2 -2
- package/dist/dev.d.ts +2 -2
- package/dist/effects.d.ts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/match.d.ts +1 -1
- package/dist/{module-B6rg5NIb.d.ts → module-WX0Gngnt.d.ts} +40 -57
- package/dist/notrace/{action-creator-D2CotugT.d.ts → action-creator-Bux7Zzsl.d.ts} +51 -23
- package/dist/notrace/actions.d.ts +114 -98
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/{bus-BdyvumpX.js → bus-CGi2lrd5.js} +1 -1
- package/dist/notrace/effects.d.ts +2 -2
- package/dist/notrace/index.d.ts +2 -2
- package/dist/notrace/index.js +1 -1
- package/dist/notrace/{plugin-Dz6hq8Jg.d.ts → plugin-CATXOxtj.d.ts} +1 -1
- package/dist/notrace/policies.d.ts +1 -1
- package/dist/notrace/redux.d.ts +1 -1
- package/dist/observable.d.ts +6 -6
- package/dist/oop.d.ts +2 -2
- package/dist/oop.js +1 -1
- package/dist/{plugin-D-j1DarG.d.ts → plugin-vvkmIIz2.d.ts} +2 -2
- package/dist/policies.d.ts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/redux.d.ts +1 -1
- package/dist/scope.d.ts +2 -2
- package/dist/signal.d.ts +1 -1
- package/dist/signal.js +1 -1
- package/dist/svelte.d.ts +1 -1
- package/dist/trace.d.ts +1 -1
- package/dist/{types-Bkuhnkq6.d.ts → types-Bf-LX_tE.d.ts} +2 -2
- package/dist/vue.d.ts +1 -1
- package/dist/{with-policy-BeH8wCkX.d.ts → with-policy-CPjx0sLm.d.ts} +1 -1
- package/docs/README.md +4 -4
- package/docs/actions.md +93 -52
- package/docs/chat-example.md +6 -6
- package/docs/context.md +1 -1
- package/docs/errors.md +1 -1
- package/docs/guide.md +25 -25
- package/docs/hmr.md +1 -1
- package/docs/motivation.md +10 -4
- package/docs/package.md +1 -1
- package/docs/plugins.md +23 -23
- package/docs/query-libraries.md +14 -14
- package/docs/scope.md +22 -22
- package/docs/server.md +12 -12
- package/docs/stability.md +2 -2
- package/docs/starter.md +1 -1
- package/docs/tracing.md +2 -2
- package/docs/typescript.md +16 -16
- package/llms-full.txt +230 -183
- package/llms.txt +2 -2
- package/package.json +4 -3
package/docs/actions.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Actions,
|
|
1
|
+
# Actions, jobs and modules
|
|
2
2
|
|
|
3
3
|
Use this page when **domain logic lives on the bus**, off the UI: the UI emits and listens; handlers and modules own the work.
|
|
4
4
|
|
|
@@ -10,7 +10,9 @@ A chat with two entities and three features, jobs under policies and effects, an
|
|
|
10
10
|
|
|
11
11
|
A whole project with two domains, a job and React as a thin layer: `npm create nanoeffects@latest my-app -- --template domains-ts` (see [Start a project from a template](starter.md#start-with-domains)).
|
|
12
12
|
|
|
13
|
-
## Return a value from `emit` with a
|
|
13
|
+
## Return a value from `emit` with a handler
|
|
14
|
+
|
|
15
|
+
Give `createAction` a handler, and `emit` runs it and returns its result. There is one creator for an event and for an action with work behind it; the handler is the only difference.
|
|
14
16
|
|
|
15
17
|
The bus lives in one file and its events in another, so any module can import both without a cycle.
|
|
16
18
|
|
|
@@ -24,10 +26,10 @@ export const bus = createBus();
|
|
|
24
26
|
|
|
25
27
|
```js
|
|
26
28
|
// math.js
|
|
27
|
-
import {
|
|
29
|
+
import { createAction } from "nanoeffects/actions";
|
|
28
30
|
|
|
29
|
-
//
|
|
30
|
-
export const double =
|
|
31
|
+
// createAction with a handler — emit runs the handler and returns its result
|
|
32
|
+
export const double = createAction("math/double", (value) => value * 2);
|
|
31
33
|
```
|
|
32
34
|
|
|
33
35
|
```js
|
|
@@ -37,27 +39,24 @@ import { double } from "./math";
|
|
|
37
39
|
bus.emit(double(21)); // 42
|
|
38
40
|
```
|
|
39
41
|
|
|
40
|
-
## Describe events,
|
|
42
|
+
## Describe events, handlers and jobs
|
|
41
43
|
|
|
42
44
|
`nanoeffects/actions` has the pieces to name what a bus does, apart from the code that does it.
|
|
43
45
|
|
|
44
46
|
```js
|
|
45
47
|
// user.js
|
|
46
|
-
import { createAction,
|
|
48
|
+
import { createAction, createJob } from "nanoeffects/actions";
|
|
47
49
|
|
|
48
|
-
// createAction — event with a payload (like RTK createAction)
|
|
50
|
+
// createAction — an event with a payload (like RTK createAction)
|
|
49
51
|
export const userLoaded = createAction("user/loaded");
|
|
50
52
|
export const loadFailed = createAction("user/failed");
|
|
51
53
|
|
|
52
|
-
//
|
|
53
|
-
export const getUser =
|
|
54
|
+
// createAction with a handler — emit returns what the handler returns
|
|
55
|
+
export const getUser = createAction("user/get", (id, { signal }) => {
|
|
54
56
|
return fetchUser(id, { signal });
|
|
55
57
|
});
|
|
56
58
|
|
|
57
|
-
//
|
|
58
|
-
export const sendMail = createCommand("mail/send");
|
|
59
|
-
|
|
60
|
-
// createJob — command that also emits success / fail actions
|
|
59
|
+
// createJob — an action with a handler that also emits success / fail actions
|
|
61
60
|
export const loadUser = createJob("user/load", fetchUser, {
|
|
62
61
|
success: userLoaded,
|
|
63
62
|
fail: "user/failed",
|
|
@@ -73,57 +72,99 @@ These are the events of the user domain, in a file of their own (`user.js`). The
|
|
|
73
72
|
- `userLoaded(user)` → `{ type, payload }`
|
|
74
73
|
- `userLoaded.type` — the type string
|
|
75
74
|
- `userLoaded.match(action)` — narrows an action
|
|
76
|
-
- `createAction(type,
|
|
75
|
+
- `createAction(type, handler)` — a function as the second argument is the handler; `bus.emit(action(payload))` tells the listeners, runs the handler as a task and returns its result
|
|
76
|
+
- `createAction(type, { prepare })` — build the payload from several arguments
|
|
77
|
+
- `createAction(type, { handle, prepare, ...scope })` — everything in one object: `handle` and `prepare` are taken out, and every other field is the [scope](scope.md)
|
|
77
78
|
- Works as the event of its type: `bus.emit(userLoaded(user))`, `bus.on(userLoaded, fn)`
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
A scope goes after a handler as the last argument, `createAction(type, handler, scope)`; `prepare` with a handler goes in that last object: `createAction(type, handler, { prepare, ...scope })`.
|
|
81
|
+
|
|
82
|
+
- Wrap the handler in a policy: `createAction("search", latest(fn))`.
|
|
80
83
|
|
|
81
|
-
|
|
84
|
+
An action with no handler is an event. Emitting it does not throw, and `onUnhandled` hears about it when nobody listens.
|
|
82
85
|
|
|
83
|
-
|
|
84
|
-
- `command.match(action)` narrows like an action creator’s `match`.
|
|
86
|
+
### Bind a handler later
|
|
85
87
|
|
|
86
|
-
**
|
|
88
|
+
**Problem.** The work belongs to another layer, or a test must replace it. Putting the handler in `createAction` then ties the event file to the work.
|
|
89
|
+
|
|
90
|
+
**When.** Use the registry when the handler is not known where the action is made. The action can have no handler at all; a handler bound to its type answers it. Each bus (each test) has its own registry.
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
// mail.js
|
|
94
|
+
import { createAction } from "nanoeffects/actions";
|
|
95
|
+
|
|
96
|
+
// createAction without a handler — an event; the work is bound elsewhere
|
|
97
|
+
export const sendMail = createAction("mail/send");
|
|
98
|
+
```
|
|
87
99
|
|
|
88
100
|
```js
|
|
89
101
|
// bus.js
|
|
90
102
|
import { createBus } from "nanoeffects";
|
|
91
|
-
import {
|
|
92
|
-
import { sendMail } from "./
|
|
103
|
+
import { actionsPlugin, setHandler } from "nanoeffects/actions";
|
|
104
|
+
import { sendMail } from "./mail";
|
|
93
105
|
|
|
94
106
|
export const bus = createBus();
|
|
95
107
|
|
|
96
|
-
//
|
|
97
|
-
export const
|
|
98
|
-
|
|
108
|
+
// actionsPlugin — the registry of handlers of this bus; setHandler pairs an action with its work
|
|
109
|
+
export const actions = bus.use(
|
|
110
|
+
actionsPlugin([setHandler(sendMail, smtpSend)]),
|
|
99
111
|
);
|
|
100
112
|
```
|
|
101
113
|
|
|
102
114
|
```js
|
|
103
|
-
import {
|
|
115
|
+
import { actions } from "./bus";
|
|
104
116
|
import { getUser } from "./user";
|
|
105
117
|
|
|
106
|
-
// later, e.g. in a test — latest binding wins
|
|
107
|
-
const unbind =
|
|
118
|
+
// later, e.g. in a test — the latest binding wins, a handler bound here wins over the one in createAction
|
|
119
|
+
const unbind = actions.handle(getUser, async () => fakeUser);
|
|
108
120
|
unbind();
|
|
109
121
|
```
|
|
110
122
|
|
|
111
|
-
A
|
|
123
|
+
A handler bound in the registry makes `bus.emit(sendMail(mail))` run it and return its result, the same as for an action that carries its handler. The listeners of the type are told first. With no handler bound, `sendMail` is an event again.
|
|
124
|
+
|
|
125
|
+
The registry lives in `nanoeffects/actions`, so a bus without one carries none of that code. Without a registry, `emit` runs the handler that the action carries. `bus.use(actionsPlugin())` sets `bus.actions`. A second `actionsPlugin` binds its list in that registry and returns it. There is no `actions` option in `createBus`; assign a custom runner with `bus.actions = { run }`, and add `bound(type)` to it to answer for actions that have no handler of their own.
|
|
126
|
+
|
|
127
|
+
### Say what a bound handler returns
|
|
128
|
+
|
|
129
|
+
TypeScript cannot see which handler the registry has, so the type of `emit` for an action without a handler is `void`. Tell it what the bound handler returns with `Call<Payload, Result>`. This is a promise of yours: nothing checks it against the handler.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// mail.ts
|
|
133
|
+
import type { Call } from "nanoeffects";
|
|
134
|
+
import { createAction } from "nanoeffects/actions";
|
|
135
|
+
|
|
136
|
+
// Call — payload Mail, a bound handler answers with a boolean (checker only; nothing runs differently)
|
|
137
|
+
type Mail = { to: string };
|
|
138
|
+
export const sendMail = createAction<Call<Mail, boolean>>("mail/send");
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
In an event map the same type does the job for string events, and `emit` returns `boolean`:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
// events.ts
|
|
145
|
+
import type { Call } from "nanoeffects";
|
|
146
|
+
|
|
147
|
+
// Call in the map — the payload is checked, emit returns the result (checker only)
|
|
148
|
+
export type Events = {
|
|
149
|
+
"user/getId": Call<void, string>;
|
|
150
|
+
"user/loaded": { id: string };
|
|
151
|
+
};
|
|
152
|
+
```
|
|
112
153
|
|
|
113
|
-
|
|
154
|
+
On a bus without an event map you can say it where you call: `const id = bus.emit<string>("user/getId")`. A bus with a map takes no such hint, so its names and payloads stay checked.
|
|
114
155
|
|
|
115
156
|
### Jobs
|
|
116
157
|
|
|
117
|
-
`createJob(type, handler, { pending, success, fail, cancel })` makes a
|
|
158
|
+
`createJob(type, handler, { pending, success, fail, cancel })` makes an action with a handler that also sends its outcome as actions, when nobody waits for the result. It returns the action with `loadUser.pending`, `loadUser.success`, `loadUser.fail` and `loadUser.cancel` on it.
|
|
118
159
|
|
|
119
160
|
- When the handler starts, emits `loadUser.pending(payload)`.
|
|
120
161
|
- On success, emits `loadUser.success(result)`.
|
|
121
162
|
- On failure, emits `loadUser.fail(error)`.
|
|
122
163
|
- When the task is aborted before the handler settles, emits `loadUser.cancel(reason)` instead of `success` or `fail`. After `pending`, exactly one of the three follows.
|
|
123
164
|
- `pending` / `success` / `fail` / `cancel` are a string (job makes an action creator) or an existing action creator. Missing ones become `<type>/pending`, `<type>/success`, `<type>/fail` or `<type>/cancel`.
|
|
124
|
-
- `bus.emit(loadUser(id))` returns a promise of the result. Handler errors are always handled: they go to `fail` and the promise resolves with `undefined`. For a result that must reject,
|
|
125
|
-
- Outcome actions are emitted as the task that ran the job (also for a handler bound with `
|
|
126
|
-
- A
|
|
165
|
+
- `bus.emit(loadUser(id))` returns a promise of the result. Handler errors are always handled: they go to `fail` and the promise resolves with `undefined`. For a result that must reject, give `createAction` the handler.
|
|
166
|
+
- Outcome actions are emitted as the task that ran the job (also for a handler bound with `actions.handle`). `success` and `fail` are not sent after abort; `cancel` is, at the moment of the abort.
|
|
167
|
+
- A job without a handler is not an outcome: `emit` throws `No handler for command "..."` and nothing is sent. An action without a handler is an event and does not throw.
|
|
127
168
|
- Calling the job only builds `{ type, payload }`; the handler runs on `emit`.
|
|
128
169
|
- The outcomes are events like any other: with `onUnhandled` set on the bus, it is called for a `pending` or a `cancel` that nobody listens to.
|
|
129
170
|
|
|
@@ -147,7 +188,7 @@ export const bus = createBus();
|
|
|
147
188
|
// user.js
|
|
148
189
|
import { createJob } from "nanoeffects/actions";
|
|
149
190
|
|
|
150
|
-
// createJob — a
|
|
191
|
+
// createJob — an action with a handler that also sends pending, success, fail and cancel
|
|
151
192
|
export const loadUser = createJob("user/load", fetchUser);
|
|
152
193
|
```
|
|
153
194
|
|
|
@@ -202,18 +243,18 @@ A flag is exact when calls do not overlap, or when `latest` replaces them. If se
|
|
|
202
243
|
|
|
203
244
|
## Mount domains with modules
|
|
204
245
|
|
|
205
|
-
**Problem.** A domain needs several subscriptions and
|
|
246
|
+
**Problem.** A domain needs several subscriptions and handler bindings. With plain `bus.on` / `actions.handle` you must remember every unsubscribe.
|
|
206
247
|
|
|
207
248
|
**When.** You want one mount / one unmount for that domain. Skip modules while a few top-level `bus.on` calls are enough.
|
|
208
249
|
|
|
209
|
-
`bus.use(modulePlugin(modules))` assembles an app from modules and returns `mount`, `unmount`, `isMounted`, `dispose`, `
|
|
250
|
+
`bus.use(modulePlugin(modules))` assembles an app from modules and returns `mount`, `unmount`, `isMounted`, `dispose`, `actions`, and `createModule`: the same as `createModule` below, with the context fields of the bus typed (see [TypeScript](typescript.md)).
|
|
210
251
|
|
|
211
252
|
```js
|
|
212
253
|
// user.js
|
|
213
|
-
import {
|
|
254
|
+
import { createAction } from "nanoeffects/actions";
|
|
214
255
|
|
|
215
|
-
export const getUser =
|
|
216
|
-
export const saveUser =
|
|
256
|
+
export const getUser = createAction("user/get");
|
|
257
|
+
export const saveUser = createAction("user/save", persistUser);
|
|
217
258
|
```
|
|
218
259
|
|
|
219
260
|
```js
|
|
@@ -250,7 +291,7 @@ import { app } from "./bus";
|
|
|
250
291
|
import { getUser } from "./user";
|
|
251
292
|
|
|
252
293
|
const unmount = app.mount(orderModule); // later, e.g. after a lazy import
|
|
253
|
-
app.
|
|
294
|
+
app.actions.handle(getUser, fakeUser); // in a test
|
|
254
295
|
app.dispose(); // unmounts every module
|
|
255
296
|
```
|
|
256
297
|
|
|
@@ -281,37 +322,37 @@ app.unmount(...modules); // also in reverse order
|
|
|
281
322
|
|
|
282
323
|
If the `setup` of one module throws, the modules of that call that were already mounted are unmounted again and the error is thrown, so a call mounts all of its modules or none. A module that is mounted already is skipped.
|
|
283
324
|
|
|
284
|
-
- **Unmounting undoes everything** the module did: subscriptions, bound
|
|
325
|
+
- **Unmounting undoes everything** the module did: subscriptions, bound handlers, sub-modules, and background tasks (aborted).
|
|
285
326
|
- **A module is known by its name.** Mounting a module whose name is taken replaces the mounted one (see [Hot reload](hmr.md)).
|
|
286
|
-
- **Registry.** If `bus.use(
|
|
327
|
+
- **Registry.** If `bus.use(actionsPlugin())` ran first, that registry is used; otherwise the module plugin attaches its own. Either way it is `bus.actions` and `app.actions`.
|
|
287
328
|
- A disposed app cannot mount modules.
|
|
288
329
|
- **The given modules are mounted at the end of the block that made the app**, or at the first use of the app or the bus if that comes first (the first `bus.emit`, or a call of `mount`, `unmount`, `isMounted`, `dispose`, or the read of `commands`). Nothing needs to use the app: `bus.use(modulePlugin([a, b]))` is enough to start them. The plugins that you use after `modulePlugin` in the same block still reach them, because the modules wait until that block has finished. A plugin used after an `await` can come too late: use every plugin before the first `await`. The `bus.on` calls made in between are held and made right after, so the listeners of a module stay ahead of them. See [using plugins in any order](plugins.md#use-them-in-any-order).
|
|
289
330
|
- **To mount by hand instead**, pass no modules: `modulePlugin()` starts nothing, and `app.mount(...)` starts the modules when you want.
|
|
290
331
|
- Module context = task context plus `handle`, `use`, and `spawn`:
|
|
291
|
-
- `handle` — give
|
|
292
|
-
- `use` — bring sub-modules and
|
|
332
|
+
- `handle` — give an action a handler (one that has none, or an override).
|
|
333
|
+
- `use` — bring sub-modules and actions that carry a handler; the returned function undoes that call.
|
|
293
334
|
- `spawn` — background task, aborted on unmount.
|
|
294
|
-
-
|
|
335
|
+
- An action that carries a handler keeps answering after unmount (`use` only tracks registration). Use `handle` for a handler that must live only as long as the module.
|
|
295
336
|
|
|
296
|
-
## Give
|
|
337
|
+
## Give an action or a module its own fields
|
|
297
338
|
|
|
298
|
-
|
|
339
|
+
An action with a handler can need its own client, a module its own logger. Pass a scope, a plain object whose keys go on `ctx` (`$effects` binds functions to the task), as the last argument of `createAction`, `createAction`, `createModule` and `createJob`, and turn it on with `scopePlugin`:
|
|
299
340
|
|
|
300
341
|
```js
|
|
301
342
|
// bus.js
|
|
302
343
|
import { createBus } from "nanoeffects";
|
|
303
344
|
import { scopePlugin } from "nanoeffects/scope";
|
|
304
345
|
|
|
305
|
-
// scopePlugin — makes the scope of
|
|
346
|
+
// scopePlugin — makes the scope of an action work on this bus
|
|
306
347
|
export const bus = createBus().use(scopePlugin());
|
|
307
348
|
```
|
|
308
349
|
|
|
309
350
|
```js
|
|
310
351
|
// user.js
|
|
311
|
-
import {
|
|
352
|
+
import { createAction } from "nanoeffects/actions";
|
|
312
353
|
|
|
313
|
-
// the third argument — fields of this
|
|
314
|
-
export const getUser =
|
|
354
|
+
// the third argument — fields of this action only
|
|
355
|
+
export const getUser = createAction(
|
|
315
356
|
"user/get",
|
|
316
357
|
(id, { api }) => api.getUser(id),
|
|
317
358
|
{ api },
|
package/docs/chat-example.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Use this page when your app has several features that react to each other and you want to see where each piece of logic goes. The example is a chat with rooms, a history that loads, messages that can fail to send, a search across all rooms, and new messages that arrive on their own. A small Fastify server in the example answers every request, and it misbehaves on a known schedule, so each problem on this page really happens.
|
|
4
4
|
|
|
5
|
-
It is a level D example. Start with [Actions,
|
|
5
|
+
It is a level D example. Start with [Actions, jobs and modules](actions.md) for the API, or with the `domains-ts` template in [Start a project from a template](starter.md#start-with-domains) for a smaller project.
|
|
6
6
|
|
|
7
7
|
The code is in [`examples/chat`](../examples/chat). Run it on [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/chat), or on your machine:
|
|
8
8
|
|
|
@@ -48,7 +48,7 @@ The code is in layers, and a file imports only from the layers below it. The com
|
|
|
48
48
|
|
|
49
49
|
That rule is why `show-history` exists. Selecting a room must load its history, but `room` does not know `message` and `message` does not know `room`. The feature `show-history` is the one place that knows both: it listens to `rooms/selected` and starts the job `messages/loadHistory`. The same goes for a message that arrives: `receive-messages` emits `incoming/received`, an event that the `message` entity owns and listens to.
|
|
50
50
|
|
|
51
|
-
A folder has its events (`room.ts`, which imports no bus), its work (`room-handlers.ts`), the state it keeps (`room-store.ts`) and a module (`room-module.ts`) that wires them. Another layer reaches an entity through its events and commands, never through its handlers or its store. Events and commands both live in the events file. An event (`createAction`) says that something happened, and any feature may listen: `rooms/selected` is heard by two. A command (`
|
|
51
|
+
A folder has its events (`room.ts`, which imports no bus), its work (`room-handlers.ts`), the state it keeps (`room-store.ts`) and a module (`room-module.ts`) that wires them. Another layer reaches an entity through its events and commands, never through its handlers or its store. Events and commands both live in the events file. An event (`createAction` with no handler) says that something happened, and any feature may listen: `rooms/selected` is heard by two. A command (`createAction` with a handler) is an order with exactly one handler, as `search/openResult` and `messages/mark` are. The handler is given to `createAction`, so the command works wherever it is called and can return a result, and the module binds it with `module.use` for as long as it is mounted. A job is a command that also sends its outcomes, but its handler is bound in the module (`module.handle`): a job usually runs under a policy, and the type of its outcomes is exact only when the handler is not part of the type.
|
|
52
52
|
|
|
53
53
|
The layers come from [Feature-Sliced Design](https://feature-sliced.design/), reduced to the three that an app of this size needs.
|
|
54
54
|
|
|
@@ -179,11 +179,11 @@ export const runSearch = debounce(
|
|
|
179
179
|
`take` waits for the next event of a name. Start it before the emit that causes the event, so nothing is missed:
|
|
180
180
|
|
|
181
181
|
```js
|
|
182
|
-
import {
|
|
182
|
+
import { createAction } from "nanoeffects/actions";
|
|
183
183
|
import { take } from "nanoeffects/effects";
|
|
184
184
|
|
|
185
|
-
//
|
|
186
|
-
const markMessage =
|
|
185
|
+
// createAction with a handler — an order for the message entity, with the handler that does it
|
|
186
|
+
const markMessage = createAction("messages/mark", (messageId) => console.log("mark", messageId));
|
|
187
187
|
|
|
188
188
|
// the room that is open, kept from the rooms/selected event
|
|
189
189
|
let selectedRoom = "general";
|
|
@@ -247,6 +247,6 @@ Each entity and each feature that has state keeps it in a [Zustand](https://zust
|
|
|
247
247
|
|
|
248
248
|
## Where to go next
|
|
249
249
|
|
|
250
|
-
- The API of commands, jobs and modules: [Actions,
|
|
250
|
+
- The API of commands, jobs and modules: [Actions, jobs and modules](actions.md).
|
|
251
251
|
- Policies such as `latest` and `queue`: [Policies](policies.md). Effects such as `allSettled`, `take` and `poll`: [Effects](effects.md).
|
|
252
252
|
- A smaller project with two domains: [Start with domains](starter.md#start-with-domains). Its two domains do not talk to each other, so it needs no layers.
|
package/docs/context.md
CHANGED
|
@@ -15,7 +15,7 @@ bus.on("save", (draft, { store }) => {
|
|
|
15
15
|
});
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
That is enough in JavaScript. The fields appear on handlers, `ctx.run`, `ctx.on`,
|
|
18
|
+
That is enough in JavaScript. The fields appear on handlers, `ctx.run`, `ctx.on`, actions, modules, and child tasks of effects and policies.
|
|
19
19
|
|
|
20
20
|
Library fields (`signal`, `emit`, `run`, …) win on name clashes. `context` may be a function `(task) => fields` when the value depends on the task.
|
|
21
21
|
|
package/docs/errors.md
CHANGED
|
@@ -21,7 +21,7 @@ const bus = createBus({
|
|
|
21
21
|
|
|
22
22
|
## Warn when nobody listens
|
|
23
23
|
|
|
24
|
-
A typo in an event name (`emit("serch")`) does nothing and says nothing. `onUnhandled` runs when an event is emitted and nobody listens (a listener of every event, `"*"`, counts). It is not called for
|
|
24
|
+
A typo in an event name (`emit("serch")`) does nothing and says nothing. `onUnhandled` runs when an event is emitted and nobody listens (a listener of every event, `"*"`, counts). It is not called for actions, which always have a handler to run.
|
|
25
25
|
|
|
26
26
|
Use it as a warning in development:
|
|
27
27
|
|
package/docs/guide.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Guide
|
|
2
2
|
|
|
3
|
-
Climb one step at a time: small bus → policies → effects →
|
|
3
|
+
Climb one step at a time: small bus → policies → effects → handlers and modules. Stop when your project has what it needs.
|
|
4
4
|
|
|
5
5
|
| Your project | Stop after |
|
|
6
6
|
| ------------ | ---------- |
|
|
@@ -115,7 +115,7 @@ bus.emit(userFailed("boom")); // user/failed boom
|
|
|
115
115
|
```
|
|
116
116
|
|
|
117
117
|
- `once(fn, handler)` waits for the first event that `fn` accepts. `off(fn)` removes every handler of `fn`, and `off(fn, handler)` removes one.
|
|
118
|
-
- An action creator
|
|
118
|
+
- An action creator still means that one event: only a function with no `type` is a function that picks events.
|
|
119
119
|
- The cost: the handler listens to `"*"`, so each event makes one task for each such subscription, also when the function says no, and an event that only such a subscription listens to counts as heard for `onUnhandled`. For a few known events, `on(creator, handler)` is cheaper.
|
|
120
120
|
- It works in any order with the other plugins, `devPlugin` included.
|
|
121
121
|
|
|
@@ -303,7 +303,7 @@ More in [Effects](effects.md).
|
|
|
303
303
|
|
|
304
304
|
## 7. Name events three ways
|
|
305
305
|
|
|
306
|
-
`emit` and `on` take a string, an object `{ type, payload }` (a Redux action fits), or a function with a `type` property (action creators and
|
|
306
|
+
`emit` and `on` take a string, an object `{ type, payload }` (a Redux action fits), or a function with a `type` property (action creators and actions).
|
|
307
307
|
|
|
308
308
|
From here on the bus is kept in one file and the events in files named after their domain (`move.js`, `math.js`), as in a real app: the events can then be imported anywhere without importing the bus.
|
|
309
309
|
|
|
@@ -324,10 +324,10 @@ export const moved = createAction("moved");
|
|
|
324
324
|
|
|
325
325
|
```js
|
|
326
326
|
// math.js
|
|
327
|
-
import {
|
|
327
|
+
import { createAction } from "nanoeffects/actions";
|
|
328
328
|
|
|
329
|
-
//
|
|
330
|
-
export const double =
|
|
329
|
+
// createAction with a handler — emit runs the handler and returns its result
|
|
330
|
+
export const double = createAction("math/double", (value) => value * 2);
|
|
331
331
|
```
|
|
332
332
|
|
|
333
333
|
```js
|
|
@@ -344,36 +344,36 @@ bus.on(moved, ({ x }) => console.log(x));
|
|
|
344
344
|
bus.emit(double(21)); // 42
|
|
345
345
|
```
|
|
346
346
|
|
|
347
|
-
Use strings for the plain case. Use objects when other code already builds actions.
|
|
347
|
+
Use strings for the plain case. Use objects when other code already builds actions. Give `createAction` a handler when you need a result from `emit`.
|
|
348
348
|
|
|
349
|
-
## 8.
|
|
349
|
+
## 8. Bind a handler to an action later
|
|
350
350
|
|
|
351
|
-
|
|
351
|
+
An action may carry its handler, as `double` above, or get it later from a registry — another file, or a fake in a test. An action with no handler is an event until one is bound.
|
|
352
352
|
|
|
353
353
|
```js
|
|
354
354
|
// user.js
|
|
355
|
-
import {
|
|
355
|
+
import { createAction } from "nanoeffects/actions";
|
|
356
356
|
|
|
357
|
-
export const getUser =
|
|
357
|
+
export const getUser = createAction("user/get"); // no handler yet: an event
|
|
358
358
|
```
|
|
359
359
|
|
|
360
360
|
```js
|
|
361
361
|
// bus.js
|
|
362
362
|
import { createBus } from "nanoeffects";
|
|
363
|
-
import {
|
|
363
|
+
import { actionsPlugin } from "nanoeffects/actions";
|
|
364
364
|
|
|
365
365
|
export const bus = createBus();
|
|
366
|
-
//
|
|
367
|
-
export const
|
|
366
|
+
// actionsPlugin — registry of handlers for this bus
|
|
367
|
+
export const actions = bus.use(actionsPlugin());
|
|
368
368
|
```
|
|
369
369
|
|
|
370
370
|
```js
|
|
371
|
-
import { bus,
|
|
371
|
+
import { bus, actions } from "./bus";
|
|
372
372
|
import { getUser } from "./user";
|
|
373
373
|
|
|
374
|
-
bus.emit(getUser("1")); //
|
|
374
|
+
bus.emit(getUser("1")); // nothing is bound: only an event
|
|
375
375
|
|
|
376
|
-
const unregister =
|
|
376
|
+
const unregister = actions.handle(getUser, (id) => api.user(id));
|
|
377
377
|
const user = await bus.emit(getUser("1"));
|
|
378
378
|
|
|
379
379
|
unregister();
|
|
@@ -383,7 +383,7 @@ Each bus has its own registry. A [module](#9-mount-a-domain-as-a-module) can reg
|
|
|
383
383
|
|
|
384
384
|
## 9. Mount a domain as a module
|
|
385
385
|
|
|
386
|
-
**Problem.** A feature owns several things on the bus: who answers `getUser`, who listens to `greet`, maybe
|
|
386
|
+
**Problem.** A feature owns several things on the bus: who answers `getUser`, who listens to `greet`, maybe an action that already carries its handler. With plain `bus.on` / `actions.handle` you must remember every unsubscribe when the feature goes away. Miss one and listeners pile up.
|
|
387
387
|
|
|
388
388
|
**When.** Domain logic lives on the bus, off the UI, and you need one mount / one unmount for that feature. Skip modules while a few top-level `bus.on` calls are enough.
|
|
389
389
|
|
|
@@ -391,16 +391,16 @@ Each bus has its own registry. A [module](#9-mount-a-domain-as-a-module) can reg
|
|
|
391
391
|
|
|
392
392
|
```js
|
|
393
393
|
// user.js
|
|
394
|
-
import {
|
|
394
|
+
import { createAction } from "nanoeffects/actions";
|
|
395
395
|
|
|
396
|
-
export const getUser =
|
|
396
|
+
export const getUser = createAction("user/get"); // no handler yet
|
|
397
397
|
```
|
|
398
398
|
|
|
399
399
|
```js
|
|
400
400
|
// math.js
|
|
401
|
-
import {
|
|
401
|
+
import { createAction } from "nanoeffects/actions";
|
|
402
402
|
|
|
403
|
-
export const double =
|
|
403
|
+
export const double = createAction("math/double", (value) => value * 2); // carries its handler
|
|
404
404
|
```
|
|
405
405
|
|
|
406
406
|
```js
|
|
@@ -417,7 +417,7 @@ export const greeting = createModule("greeting", (m) => {
|
|
|
417
417
|
// m.handle — this module answers getUser while mounted
|
|
418
418
|
m.handle(getUser, (id) => api.user(id));
|
|
419
419
|
|
|
420
|
-
// m.use — register
|
|
420
|
+
// m.use — register an action that already has a handler, for this module’s life
|
|
421
421
|
m.use(double);
|
|
422
422
|
});
|
|
423
423
|
```
|
|
@@ -442,8 +442,8 @@ app.unmount(greeting); // greet listener and getUser binding are gone
|
|
|
442
442
|
```
|
|
443
443
|
|
|
444
444
|
- **`m.on`** — same job as `bus.on`, scoped to the module’s lifetime.
|
|
445
|
-
- **`m.handle`** — bind a handler to
|
|
446
|
-
- **`m.use`** — bring
|
|
445
|
+
- **`m.handle`** — bind a handler to an action that has none; gone on unmount.
|
|
446
|
+
- **`m.use`** — bring an action that already carries a handler (or a sub-module). Prefer `handle` when the handler must die with the feature.
|
|
447
447
|
|
|
448
448
|
Modules also make [hot reload](hmr.md) work. Full API in [Actions](actions.md); a longer recipe in [`examples/modules.ts`](../examples/modules.ts).
|
|
449
449
|
|
package/docs/hmr.md
CHANGED
|
@@ -10,7 +10,7 @@ When a file is edited, the bundler loads its new version and runs it again, whil
|
|
|
10
10
|
|
|
11
11
|
## Replace modules by name
|
|
12
12
|
|
|
13
|
-
Mounting a module whose name is taken replaces the mounted one. The old module is unmounted first — and with it its subscriptions, bound
|
|
13
|
+
Mounting a module whose name is taken replaces the mounted one. The old module is unmounted first — and with it its subscriptions, bound handlers, sub-modules, and background tasks (their signals abort). The new version of a module file only has to mount again; nothing piles up.
|
|
14
14
|
|
|
15
15
|
- **Names must be unique.** The name means “this is the new version of that module”, so two different modules with one name replace each other. The name is also the type of the module’s tasks in the trace.
|
|
16
16
|
- **Pass `signal` to what the module does.** An unmount aborts the tasks, but a `fetch` or a loop that ignores `ctx.signal` runs on with the old code.
|
package/docs/motivation.md
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
# Why nanoeffects exists
|
|
2
2
|
|
|
3
|
-
Read this page
|
|
3
|
+
Read this page to see why the library exists and when to pick it. The main reason is to take business logic and side effects out of React, Vue or Svelte components and write them as plain `async` functions, with cancellation, retries and a context for dependencies. The second part of the page is for SDKs, core libraries and micro-frontend shells, where a shared dependency is a shared risk. It does not teach the API: start with the [Guide](guide.md).
|
|
4
4
|
|
|
5
|
-
## The
|
|
5
|
+
## The main reason: logic outside the UI
|
|
6
|
+
|
|
7
|
+
Components that hold requests, timers and races are hard to test and easy to break. The usual cure is services and pure functions outside the UI, and then every service needs the same things: to cancel work that is no longer wanted, to retry, to share dependencies, and to show what ran and why.
|
|
8
|
+
|
|
9
|
+
The common tools ask a lot: redux-saga writes this logic as generators, and RxJS as streams and operators. nanoeffects keeps ordinary `async` functions and adds one idea, the `AbortSignal`. The event bus is its heart but not its value: any emitter can send events (`nanoevents` is about 100 bytes, and there are [dozens more](https://github.com/gajus/sister#similar-libraries)). What nanoeffects adds is cancellable tasks, policies and effects, a context that works as a small container for dependencies, actions and modules to group a feature, and [tracing](tracing.md) with an adapter for Redux DevTools. It also fits what a project already has: recipes and small adapters for [stores](stores.md) (Redux Toolkit, Zustand, MobX, Pinia), an [RxJS](migrating-from-rxjs.md) bridge, and helpers for [React](react.md), [Vue](vue.md) and [Svelte](svelte.md), so nothing has to be rewritten to start.
|
|
10
|
+
|
|
11
|
+
## When the constraint is a shared library
|
|
6
12
|
|
|
7
13
|
A shared library (an SDK, a core package, a platform shell) is loaded by many apps. Whatever it depends on, the apps depend on too. State and event libraries are the worst case, because they keep **internal state and identity** (a store, a context, a class). Two copies or two versions of them do not merge: they silently stop seeing each other.
|
|
8
14
|
|
|
@@ -31,9 +37,9 @@ A library that is **not** a shared singleton cannot break this way. nanoeffects
|
|
|
31
37
|
## What the library does instead
|
|
32
38
|
|
|
33
39
|
- **No dependencies, no global state.** It is safe to ship inside an SDK next to whatever the host app runs. It does not need a `singleton` entry in the Module Federation config.
|
|
34
|
-
- **Small enough for a strict budget.** SDKs often get a few kilobytes for all third-party code. `createBus` is about 1.4 kB gzip (1.3 kB brotli); policies, effects and
|
|
40
|
+
- **Small enough for a strict budget.** SDKs often get a few kilobytes for all third-party code. `createBus` is about 1.4 kB gzip (1.3 kB brotli); policies, effects and actions are separate entries you import only when you need them. See [Package entries](package.md).
|
|
35
41
|
- **Simple first.** `on` and `emit` are the whole model. Cancellation, retry and the rest are plain functions on top. The [Guide](guide.md) and [Coming from redux-saga](migrating-from-saga.md) map the familiar names.
|
|
36
|
-
- **`emit` can return an answer.**
|
|
42
|
+
- **`emit` can return an answer.** An [action with a handler](actions.md) returns a result, so one module can ask another and wait for the reply. Events alone cannot do that, and it was the main thing missing from the Redux-based options.
|
|
37
43
|
- **Easy to debug.** Event-driven code is hard to follow: you see an effect and not the event that caused it. Every task knows its parent, so [tracing](tracing.md) shows the chain of events, with loggers for the console, the Performance panel and Redux DevTools.
|
|
38
44
|
|
|
39
45
|
## When this is not the right tool
|
package/docs/package.md
CHANGED
|
@@ -7,7 +7,7 @@ Imports are split so you only load what you use. `createBus` stays small; the re
|
|
|
7
7
|
| `nanoeffects` | `createBus`, the types |
|
|
8
8
|
| `nanoeffects/policies` | `latest`, `leading`, `debounce`, `throttle`, `queue` |
|
|
9
9
|
| `nanoeffects/effects` | `retry`, `timeout`, `poll`, `delay`, `take`, `all`, `allSettled`, `race`, `effectsPlugin` |
|
|
10
|
-
| `nanoeffects/actions` | `createAction`, `
|
|
10
|
+
| `nanoeffects/actions` | `createAction`, `createJob`, `createModule`, `modulePlugin`, `actionsPlugin` |
|
|
11
11
|
| `nanoeffects/match` | `matchPlugin`: `on`, `once` and `off` that take a function which picks the events |
|
|
12
12
|
| `nanoeffects/context` | `contextPlugin`: your own fields (a store, a client) on `ctx` of every task, typed from the value |
|
|
13
13
|
| `nanoeffects/signal` | `signalPlugin`: `emit(call, { signal })` ties the tasks of an emit to an outside `AbortSignal` |
|