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.
Files changed (58) hide show
  1. package/README.md +76 -23
  2. package/dist/{action-creator-Bpa8BUS9.d.ts → action-creator-D_c223C7.d.ts} +51 -23
  3. package/dist/actions.d.ts +54 -21
  4. package/dist/actions.js +1 -1
  5. package/dist/{bus-hHp-T7JV.js → bus-DQDHUN5e.js} +1 -1
  6. package/dist/{context-types-BKIH5a8s.d.ts → context-types-nGtzhjxD.d.ts} +1 -1
  7. package/dist/context.d.ts +2 -2
  8. package/dist/dev.d.ts +2 -2
  9. package/dist/effects.d.ts +2 -2
  10. package/dist/index.d.ts +2 -2
  11. package/dist/index.js +1 -1
  12. package/dist/match.d.ts +1 -1
  13. package/dist/{module-B6rg5NIb.d.ts → module-WX0Gngnt.d.ts} +40 -57
  14. package/dist/notrace/{action-creator-D2CotugT.d.ts → action-creator-Bux7Zzsl.d.ts} +51 -23
  15. package/dist/notrace/actions.d.ts +114 -98
  16. package/dist/notrace/actions.js +1 -1
  17. package/dist/notrace/{bus-BdyvumpX.js → bus-CGi2lrd5.js} +1 -1
  18. package/dist/notrace/effects.d.ts +2 -2
  19. package/dist/notrace/index.d.ts +2 -2
  20. package/dist/notrace/index.js +1 -1
  21. package/dist/notrace/{plugin-Dz6hq8Jg.d.ts → plugin-CATXOxtj.d.ts} +1 -1
  22. package/dist/notrace/policies.d.ts +1 -1
  23. package/dist/notrace/redux.d.ts +1 -1
  24. package/dist/observable.d.ts +6 -6
  25. package/dist/oop.d.ts +2 -2
  26. package/dist/oop.js +1 -1
  27. package/dist/{plugin-D-j1DarG.d.ts → plugin-vvkmIIz2.d.ts} +2 -2
  28. package/dist/policies.d.ts +2 -2
  29. package/dist/react.d.ts +2 -2
  30. package/dist/redux.d.ts +1 -1
  31. package/dist/scope.d.ts +2 -2
  32. package/dist/signal.d.ts +1 -1
  33. package/dist/signal.js +1 -1
  34. package/dist/svelte.d.ts +1 -1
  35. package/dist/trace.d.ts +1 -1
  36. package/dist/{types-Bkuhnkq6.d.ts → types-Bf-LX_tE.d.ts} +2 -2
  37. package/dist/vue.d.ts +1 -1
  38. package/dist/{with-policy-BeH8wCkX.d.ts → with-policy-CPjx0sLm.d.ts} +1 -1
  39. package/docs/README.md +4 -4
  40. package/docs/actions.md +93 -52
  41. package/docs/chat-example.md +6 -6
  42. package/docs/context.md +1 -1
  43. package/docs/errors.md +1 -1
  44. package/docs/guide.md +25 -25
  45. package/docs/hmr.md +1 -1
  46. package/docs/motivation.md +10 -4
  47. package/docs/package.md +1 -1
  48. package/docs/plugins.md +23 -23
  49. package/docs/query-libraries.md +14 -14
  50. package/docs/scope.md +22 -22
  51. package/docs/server.md +12 -12
  52. package/docs/stability.md +2 -2
  53. package/docs/starter.md +1 -1
  54. package/docs/tracing.md +2 -2
  55. package/docs/typescript.md +16 -16
  56. package/llms-full.txt +230 -183
  57. package/llms.txt +2 -2
  58. package/package.json +4 -3
package/docs/actions.md CHANGED
@@ -1,4 +1,4 @@
1
- # Actions, commands, jobs and modules
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 command
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 { createCommand } from "nanoeffects/actions";
29
+ import { createAction } from "nanoeffects/actions";
28
30
 
29
- // createCommand — emit runs the handler and returns its result
30
- export const double = createCommand("math/double", (value) => value * 2);
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, commands, and jobs
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, createCommand, createJob } from "nanoeffects/actions";
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
- // createCommand with a handler — emit returns what the handler returns
53
- export const getUser = createCommand("user/get", (id, { signal }) => {
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
- // createCommand without a handler — a contract; register the work elsewhere
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, prepare)` — build the payload from several arguments
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
- ### Commands
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
- Calling a command builds `{ type, payload }` like `createAction`. `bus.emit(command(payload))` (or `bus.emit(command, payload)`) tells the listeners of the command’s type, then runs its handler as a task and returns the result.
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
- - Wrap the handler in a policy: `createCommand("search", latest(fn))`.
84
- - `command.match(action)` narrows like an action creator’s `match`.
86
+ ### Bind a handler later
85
87
 
86
- **Where handlers are registered.** The command may carry a handler. The bus can also take handlers from a registry (`commandsPlugin`). A handler bound there wins over the command’s own. Each bus (each test) has its own registry.
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 { bindHandler, commandsPlugin } from "nanoeffects/actions";
92
- import { sendMail } from "./user";
103
+ import { actionsPlugin, setHandler } from "nanoeffects/actions";
104
+ import { sendMail } from "./mail";
93
105
 
94
106
  export const bus = createBus();
95
107
 
96
- // commandsPlugin — registry of handlers for this bus
97
- export const commands = bus.use(
98
- commandsPlugin([bindHandler(sendMail, smtpSend)]),
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 { commands } from "./bus";
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 = commands.handle(getUser, async () => fakeUser);
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 command with no handler anywhere throws `No handler for command "..."` from `emit` (the call itself only builds the action).
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
- The registry lives in `nanoeffects/actions`, so a bus without one carries none of that code. Without a registry, `emit` runs the command’s own handler. `bus.use(commandsPlugin())` sets `bus.commands`. A second `commandsPlugin` binds its list in that registry and returns it. There is no `commands` option in `createBus`; assign a custom runner with `bus.commands = { run }`.
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 command that also sends its outcome as actions, when nobody waits for the result. It returns the command with `loadUser.pending`, `loadUser.success`, `loadUser.fail` and `loadUser.cancel` on it.
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, use a command.
125
- - Outcome actions are emitted as the task that ran the job (also for a handler bound with `commands.handle`). `success` and `fail` are not sent after abort; `cancel` is, at the moment of the abort.
126
- - A missing handler is not an outcome: `emit` throws `No handler for command "..."` and nothing is sent.
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 command that also sends pending, success, fail and cancel
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 command bindings. With plain `bus.on` / `commands.handle` you must remember every unsubscribe.
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`, `commands`, and `createModule`: the same as `createModule` below, with the context fields of the bus typed (see [TypeScript](typescript.md)).
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 { createCommand } from "nanoeffects/actions";
254
+ import { createAction } from "nanoeffects/actions";
214
255
 
215
- export const getUser = createCommand("user/get");
216
- export const saveUser = createCommand("user/save", persistUser);
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.commands.handle(getUser, fakeUser); // in a test
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 commands, sub-modules, and background tasks (aborted).
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(commandsPlugin())` ran first, that registry is used; otherwise the module plugin attaches its own. Either way it is `bus.commands` and `app.commands`.
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 a command a handler (contract, or override).
292
- - `use` — bring sub-modules and commands that carry a handler; the returned function undoes that call.
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
- - A command 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.
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 a command, an action or a module its own fields
337
+ ## Give an action or a module its own fields
297
338
 
298
- A command 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 `createCommand`, `createAction`, `createModule` and `createJob`, and turn it on with `scopePlugin`:
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 a command work on this bus
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 { createCommand } from "nanoeffects/actions";
352
+ import { createAction } from "nanoeffects/actions";
312
353
 
313
- // the third argument — fields of this command only
314
- export const getUser = createCommand(
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 },
@@ -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, commands, 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.
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 (`createCommand`) is an order with exactly one handler, as `search/openResult` and `messages/mark` are. The handler is given to `createCommand`, 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.
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 { createCommand } from "nanoeffects/actions";
182
+ import { createAction } from "nanoeffects/actions";
183
183
  import { take } from "nanoeffects/effects";
184
184
 
185
- // createCommand — an order for the message entity, with the handler that does it
186
- const markMessage = createCommand("messages/mark", (messageId) => console.log("mark", messageId));
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, commands, jobs and modules](actions.md).
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`, commands, modules, and child tasks of effects and policies.
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 commands, which always have a handler to run.
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 → commands and modules. Stop when your project has what it needs.
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 or a command still means that one event: only a function with no `type` is a function that picks events.
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 commands).
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 { createCommand } from "nanoeffects/actions";
327
+ import { createAction } from "nanoeffects/actions";
328
328
 
329
- // createCommand — like an action, but emit runs a handler and returns its result
330
- export const double = createCommand("math/double", (value) => value * 2);
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. Use commands when you need a result from `emit`.
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. Register a command handler
349
+ ## 8. Bind a handler to an action later
350
350
 
351
- A command may carry its handler, or only be a **contract** (name + types) with the work registered elsewhere — another file, or a fake in a test.
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 { createCommand } from "nanoeffects/actions";
355
+ import { createAction } from "nanoeffects/actions";
356
356
 
357
- export const getUser = createCommand("user/get"); // contract — no handler yet
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 { commandsPlugin } from "nanoeffects/actions";
363
+ import { actionsPlugin } from "nanoeffects/actions";
364
364
 
365
365
  export const bus = createBus();
366
- // commandsPlugin — registry of handlers for this bus
367
- export const commands = bus.use(commandsPlugin());
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, commands } from "./bus";
371
+ import { bus, actions } from "./bus";
372
372
  import { getUser } from "./user";
373
373
 
374
- bus.emit(getUser("1")); // throws: No handler for command "user/get"
374
+ bus.emit(getUser("1")); // nothing is bound: only an event
375
375
 
376
- const unregister = commands.handle(getUser, (id) => api.user(id));
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 a command that already carries its handler. With plain `bus.on` / `commands.handle` you must remember every unsubscribe when the feature goes away. Miss one and listeners pile up.
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 { createCommand } from "nanoeffects/actions";
394
+ import { createAction } from "nanoeffects/actions";
395
395
 
396
- export const getUser = createCommand("user/get"); // contract — no handler yet
396
+ export const getUser = createAction("user/get"); // no handler yet
397
397
  ```
398
398
 
399
399
  ```js
400
400
  // math.js
401
- import { createCommand } from "nanoeffects/actions";
401
+ import { createAction } from "nanoeffects/actions";
402
402
 
403
- export const double = createCommand("math/double", (value) => value * 2); // carries its handler
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 a command that already has a handler, for this module’s life
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 a contract command; gone on unmount.
446
- - **`m.use`** — bring a command that already carries a handler (or a sub-module). Prefer `handle` when the handler must die with the feature.
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 commands, sub-modules, and background tasks (their signals abort). The new version of a module file only has to mount again; nothing piles up.
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.
@@ -1,8 +1,14 @@
1
1
  # Why nanoeffects exists
2
2
 
3
- Read this page if you build an SDK, a core library or a micro-frontend shell and wonder why you would pick a new event bus. It explains the constraints the library was made for. It does not teach the API: start with the [Guide](guide.md).
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 problem: a shared dependency is a shared risk
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 commands are separate entries you import only when you need them. See [Package entries](package.md).
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.** A [command](actions.md) is an event whose handler 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.
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`, `createCommand`, `createJob`, `createModule`, `modulePlugin`, `commandsPlugin` |
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` |