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/plugins.md CHANGED
@@ -10,14 +10,14 @@ In TypeScript keep the result of `contextPlugin` and `effectsPlugin`: the type o
10
10
 
11
11
  ## Main plugins
12
12
 
13
- These four cover what most apps need: shared dependencies, effects, commands and modules. Start here.
13
+ These four cover what most apps need: shared dependencies, effects, handlers and modules. Start here.
14
14
 
15
15
  | Plugin | What it gives you | Import | `bus.use` returns |
16
16
  | ------ | ----------------- | ------ | ----------------- |
17
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
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 }` |
19
+ | [`actionsPlugin`](#let-a-test-replace-a-handler-commandsplugin) | The handlers of actions in a registry, so a test can bind or 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, actions, createModule }` |
21
21
 
22
22
  ### Put a store or a client on every handler: `contextPlugin`
23
23
 
@@ -54,27 +54,27 @@ bus.on("load", async (id, { race, delay }) => {
54
54
  });
55
55
  ```
56
56
 
57
- ### Let a test replace a handler: `commandsPlugin`
57
+ ### Bind or replace a handler: `actionsPlugin`
58
58
 
59
- 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).
59
+ Handlers of actions can live in a registry that belongs to the bus. An action can have no handler, and a test or another layer binds one. More in [Actions](actions.md#bind-a-handler-later).
60
60
 
61
61
  ```js
62
62
  import { createBus } from "nanoeffects";
63
- import { commandsPlugin, createCommand } from "nanoeffects/actions";
63
+ import { actionsPlugin, createAction } from "nanoeffects/actions";
64
64
 
65
- const getUser = createCommand("user/get"); // a contract: no handler yet
65
+ const getUser = createAction("user/get"); // no handler yet: an event
66
66
  const bus = createBus();
67
67
 
68
- // commandsPlugin — the registry of handlers of this bus
69
- const commands = bus.use(commandsPlugin());
68
+ // actionsPlugin — the registry of handlers of this bus
69
+ const actions = bus.use(actionsPlugin());
70
70
 
71
- commands.handle(getUser, async (id) => ({ id })); // the latest binding wins
71
+ actions.handle(getUser, async (id) => ({ id })); // the latest binding wins
72
72
  const user = await bus.emit(getUser("1")); // { id: "1" }
73
73
  ```
74
74
 
75
75
  ### Mount a domain as one unit: `modulePlugin`
76
76
 
77
- 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).
77
+ A module groups the listeners, the bound handlers 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).
78
78
 
79
79
  ```js
80
80
  import { createBus } from "nanoeffects";
@@ -94,7 +94,7 @@ Add one of these when you meet its need. None of them is required.
94
94
 
95
95
  | Plugin | What it gives you | Import | `bus.use` returns |
96
96
  | ------ | ----------------- | ------ | ----------------- |
97
- | [`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 |
97
+ | [`scopePlugin`](#give-one-action-its-own-fields-scopeplugin) | Its own fields for one action, job or module, and a way to replace them in one `emit`. | `nanoeffects/scope` | The same bus |
98
98
  | [`signalPlugin`](#tie-an-emit-to-an-outside-signal-signalplugin) | `emit(call, { signal })` that aborts its tasks when an outside signal aborts. | `nanoeffects/signal` | The same bus, typed with the new `emit` |
99
99
  | [`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 |
100
100
  | [`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` |
@@ -102,20 +102,20 @@ Add one of these when you meet its need. None of them is required.
102
102
  | [`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` |
103
103
  | [`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 |
104
104
 
105
- ### Give one command its own fields: `scopePlugin`
105
+ ### Give one action its own fields: `scopePlugin`
106
106
 
107
- 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).
107
+ An action with a handler, 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).
108
108
 
109
109
  ```js
110
110
  import { createBus } from "nanoeffects";
111
- import { createCommand } from "nanoeffects/actions";
111
+ import { createAction } from "nanoeffects/actions";
112
112
  import { scopePlugin } from "nanoeffects/scope";
113
113
 
114
114
  // scopePlugin — makes the scope of a command work on this bus
115
115
  const bus = createBus().use(scopePlugin());
116
116
 
117
117
  // the last argument: fields of this command only
118
- const getUser = createCommand("user/get", (id, { api }) => api.getUser(id), {
118
+ const getUser = createAction("user/get", (id, { api }) => api.getUser(id), {
119
119
  api,
120
120
  });
121
121
 
@@ -129,13 +129,13 @@ A library such as React Query owns the request and gives you an `AbortSignal`. `
129
129
 
130
130
  ```js
131
131
  import { createBus } from "nanoeffects";
132
- import { createCommand } from "nanoeffects/actions";
132
+ import { createAction } from "nanoeffects/actions";
133
133
  import { signalPlugin } from "nanoeffects/signal";
134
134
 
135
135
  // signalPlugin — emit takes { signal } for a command, a job or an action
136
136
  const bus = createBus().use(signalPlugin());
137
137
 
138
- const getUser = createCommand("user/get", (id, { signal }) => api.getUser(id, { signal }));
138
+ const getUser = createAction("user/get", (id, { signal }) => api.getUser(id, { signal }));
139
139
 
140
140
  const controller = new AbortController();
141
141
  bus.emit(getUser("1"), { signal: controller.signal }); // runs the command
@@ -242,7 +242,7 @@ One bus with every plugin that does not need React, in a file of its own. The ev
242
242
  import { createBus } from "nanoeffects";
243
243
  import { contextPlugin } from "nanoeffects/context";
244
244
  import {
245
- commandsPlugin,
245
+ actionsPlugin,
246
246
  emitterPlugin,
247
247
  modulePlugin,
248
248
  } from "nanoeffects/actions";
@@ -258,8 +258,8 @@ export const bus = createBus()
258
258
  .use(matchPlugin()) // bus.on takes a function that picks the events
259
259
  .use(emitterPlugin()); // bus.emitter(creator)
260
260
 
261
- // commandsPlugin — the registry of command handlers; keep it to bind handlers later
262
- export const commands = bus.use(commandsPlugin());
261
+ // actionsPlugin — the registry of action handlers; keep it to bind handlers later
262
+ export const actions = bus.use(actionsPlugin());
263
263
 
264
264
  // modulePlugin — mounts userModule at the end of this block; keep app to mount and unmount later
265
265
  export const app = bus.use(modulePlugin([userModule]));
@@ -274,12 +274,12 @@ The plugins give the same result in any order. Use them as they read best, and p
274
274
 
275
275
  Two things happen because modules are mounted late:
276
276
 
277
- - **Modules start at the end of the block.** `modulePlugin([userModule])` mounts `userModule` when the synchronous block that made it has finished, or earlier if the app or the bus is used first: 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 used in the same block 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`. A plugin used after an `await` can come too late: use every plugin before the first `await`. With no module, `modulePlugin()` starts nothing until the first use.
277
+ - **Modules start at the end of the block.** `modulePlugin([userModule])` mounts `userModule` when the synchronous block that made it has finished, or earlier if the app or the bus is used first: the first `bus.emit`, or the first call of `app.mount`, `app.unmount`, `app.isMounted`, `app.dispose` or the read of `app.actions`. By then every plugin used in the same block 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`. A plugin used after an `await` can come too late: use every plugin before the first `await`. With no module, `modulePlugin()` starts nothing until the first use.
278
278
  - **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.
279
279
 
280
280
  Other plugins do not need an order for another reason:
281
281
 
282
- - `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.
282
+ - `actionsPlugin` binds its list in the registry that the bus has, so it can come before or after `modulePlugin`. A custom `bus.actions = { run }` is replaced.
283
283
  - `effectsPlugin` and `contextPlugin` do not replace a field that is on `ctx` already, so a scope wins in any order.
284
284
  - `devPlugin()` finds the app that `modulePlugin` put on the bus (`bus.app`). `devPlugin({ app })` still works.
285
285
  - `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.
@@ -1,6 +1,6 @@
1
1
  # Use nanoeffects with React Query and SWR
2
2
 
3
- Read this page if you fetch data with [React Query](https://tanstack.com/query) (TanStack Query) or [SWR](https://swr.vercel.app) and want `retry`, `timeout` and the abort of nanoeffects inside the request. It shows one command that both libraries call.
3
+ Read this page if you fetch data with [React Query](https://tanstack.com/query) (TanStack Query) or [SWR](https://swr.vercel.app) and want `retry`, `timeout` and the abort of nanoeffects inside the request. It shows one action that both libraries call.
4
4
 
5
5
  You need a React project, `nanoeffects`, and one of the two libraries. `api` below is your own client: replace it with your requests.
6
6
 
@@ -16,9 +16,9 @@ The libraries and nanoeffects do different jobs, so each keeps its own.
16
16
 
17
17
  `isLoading` and `data` come from the library, not from `emit`. `await bus.emit(...)` resolves once, with the result, so it cannot report that the request is still running.
18
18
 
19
- ## Make the command
19
+ ## Make the action
20
20
 
21
- A command is the request itself. It lives in files that know nothing about React: the bus, the command with its handler, and the component.
21
+ An action with a handler is the request itself. It lives in files that know nothing about React: the bus, the action with its handler, and the component.
22
22
 
23
23
  ```js
24
24
  // bus.js
@@ -34,11 +34,11 @@ The handler tries the request up to three times and gives up after five seconds
34
34
 
35
35
  ```js
36
36
  // user.js
37
- import { createCommand } from "nanoeffects/actions";
37
+ import { createAction } from "nanoeffects/actions";
38
38
  import { retry, timeout } from "nanoeffects/effects";
39
39
 
40
- // createCommand — a command with a handler: emit returns what the handler returns
41
- export const getUser = createCommand("user/get", async (id, ctx) => {
40
+ // createAction with a handler — emit returns what the handler returns
41
+ export const getUser = createAction("user/get", async (id, ctx) => {
42
42
  // one try: the request, with the signal of timeout so it can stop it
43
43
  const load = ({ signal }) => api.getUser(id, { signal });
44
44
  // timeout — fail the try after 5 s
@@ -73,13 +73,13 @@ export function Profile({ id }) {
73
73
  }
74
74
  ```
75
75
 
76
- The command rejects when all tries fail, so React Query shows the `error`. React Query counts its own `retry` too: set `retry: false` in `useQuery` when the handler already retries, or the tries multiply.
76
+ The action rejects when all tries fail, so React Query shows the `error`. React Query counts its own `retry` too: set `retry: false` in `useQuery` when the handler already retries, or the tries multiply.
77
77
 
78
78
  ## Call it from SWR
79
79
 
80
80
  <!-- docs-check: continue -->
81
81
 
82
- SWR does not pass a signal to the fetcher, so there is nothing to give to `emit`. The command still brings `retry` and `timeout`.
82
+ SWR does not pass a signal to the fetcher, so there is nothing to give to `emit`. The action still brings `retry` and `timeout`.
83
83
 
84
84
  ```jsx
85
85
  // Profile.jsx
@@ -103,7 +103,7 @@ export function Profile({ id }) {
103
103
 
104
104
  <!-- docs-check: continue -->
105
105
 
106
- A command gives its result only to the caller. If a store or another module must react to a loaded user, the handler can say so:
106
+ An action with a handler gives its result only to the caller. If a store or another module must react to a loaded user, the handler can say so:
107
107
 
108
108
  ```js
109
109
  // user-events.js
@@ -113,14 +113,14 @@ import { createAction } from "nanoeffects/actions";
113
113
  export const userLoaded = createAction("user/loaded");
114
114
  ```
115
115
 
116
- Send it from the handler with `ctx.emit(userLoaded(user))`. Do not replace the command with `createJob` here: a job handles the error itself and resolves with `undefined`, so React Query would never see the failure. Use a job when nothing waits for the result and the app shows a loading state from the bus, see [Show a loading state with a job](actions.md#show-a-loading-state-with-a-job).
116
+ Send it from the handler with `ctx.emit(userLoaded(user))`. Do not replace the action with `createJob` here: a job handles the error itself and resolves with `undefined`, so React Query would never see the failure. Use a job when nothing waits for the result and the app shows a loading state from the bus, see [Show a loading state with a job](actions.md#show-a-loading-state-with-a-job).
117
117
 
118
118
  ## Know the limits
119
119
 
120
- - **Without the plugin, `{ signal }` is ignored.** The command runs, but the abort does not reach it. Call `bus.use(signalPlugin())` once, where the bus is made.
120
+ - **Without the plugin, `{ signal }` is ignored.** The action runs, but the abort does not reach it. Call `bus.use(signalPlugin())` once, where the bus is made.
121
121
  - **Only the call forms take it.** `bus.emit(getUser(id), { signal })` works; `bus.emit("user/get", id, { signal })` does not.
122
- - **A handler that returns without a promise is finished with the call**, so a signal that aborts later does not reach it. Commands that fetch are async, so this only matters for a sync handler that sets up a callback.
123
- - **Give each request its own signal for an event.** For a command or a job the link ends when it settles. For `bus.emit(action, { signal })` it stays until the signal aborts.
124
- - **The signal reaches the tasks that this `emit` starts**, with the listeners of the command's type and the `ctx.run` children. A separate `bus.emit` that the handler makes is not linked: pass `{ signal: ctx.signal }` to it.
122
+ - **A handler that returns without a promise is finished with the call**, so a signal that aborts later does not reach it. Actions that fetch are async, so this only matters for a sync handler that sets up a callback.
123
+ - **Give each request its own signal for an event.** For an action with a handler or a job the link ends when it settles. For `bus.emit(action, { signal })` it stays until the signal aborts.
124
+ - **The signal reaches the tasks that this `emit` starts**, with the listeners of the action's type and the `ctx.run` children. A separate `bus.emit` that the handler makes is not linked: pass `{ signal: ctx.signal }` to it.
125
125
 
126
126
  A whole small app, with a search, a filter, pages and a detail panel, is in the [Rick and Morty example](../examples/rickandmorty).
package/docs/scope.md CHANGED
@@ -1,6 +1,6 @@
1
- # Pin context fields to a command, an action, a module or a call
1
+ # Pin context fields to an action, a module or a call
2
2
 
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.
3
+ Optional, for domain logic. Use it when one action 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
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
 
@@ -19,21 +19,21 @@ import { scopePlugin } from "nanoeffects/scope";
19
19
  export const bus = createBus().use(scopePlugin());
20
20
  ```
21
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.
22
+ The bus is in one file and the 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
23
 
24
24
  - Without `scopePlugin` a scope is **ignored**: no error and no warning. If a field is missing from `ctx`, check this first.
25
25
  - Use it in any order with the other plugins: a scope wins over `effectsPlugin`, `contextPlugin` and `createBus({ context })`.
26
26
 
27
- ## Give a command its own fields
27
+ ## Give an action its own fields
28
28
 
29
29
  <!-- docs-check: continue -->
30
30
 
31
31
  ```js
32
32
  // user.js
33
- import { createCommand } from "nanoeffects/actions";
33
+ import { createAction } from "nanoeffects/actions";
34
34
 
35
- // createCommand — third argument is the scope of this command
36
- export const getUser = createCommand(
35
+ // createAction — third argument is the scope of this action
36
+ export const getUser = createAction(
37
37
  "user/get",
38
38
  (id, { api, cache }) => {
39
39
  // api and cache are on ctx for this command only
@@ -50,7 +50,7 @@ import { getUser } from "./user";
50
50
  const user = await bus.emit(getUser("1")); // the handler has api and cache
51
51
  ```
52
52
 
53
- The listeners of `"user/get"` get the same fields when the command is emitted, because the fields belong to the call.
53
+ The listeners of `"user/get"` get the same fields when the action is emitted, because the fields belong to the call.
54
54
 
55
55
  ## Bind an effect to the task
56
56
 
@@ -58,14 +58,14 @@ An effect is a function with `{ signal }` as its first parameter. In `$effects`
58
58
 
59
59
  ```js
60
60
  // feed.js
61
- import { createCommand } from "nanoeffects/actions";
61
+ import { createAction } from "nanoeffects/actions";
62
62
  import { delay, race } from "nanoeffects/effects";
63
63
 
64
64
  // your own effect: first parameter is the task (only its signal is used)
65
65
  const fetchJson = (ctx, url) =>
66
66
  fetch(url, { signal: ctx.signal }).then((response) => response.json());
67
67
 
68
- export const loadFeed = createCommand(
68
+ export const loadFeed = createAction(
69
69
  "feed/load",
70
70
  (url, { fetchJson, race, delay }) =>
71
71
  // race, delay, fetchJson — bound to this task: no ctx argument
@@ -111,9 +111,9 @@ export const bus = createBus().use(scopePlugin());
111
111
 
112
112
  ```js
113
113
  // user.js
114
- import { createCommand } from "nanoeffects/actions";
114
+ import { createAction } from "nanoeffects/actions";
115
115
 
116
- export const getUser = createCommand(
116
+ export const getUser = createAction(
117
117
  "user/get",
118
118
  (id, { api }) => api.getUser(id),
119
119
  { api },
@@ -134,7 +134,7 @@ await bus.emit(getUser("1"), { api: fakeApi });
134
134
 
135
135
  Rules for the second argument:
136
136
 
137
- - It works with an action or a command call (`emit(getUser("1"), scope)`, `ctx.emit(loaded(user), scope)`).
137
+ - It works with the call of an action (`emit(getUser("1"), scope)`, `ctx.emit(loaded(user), scope)`).
138
138
  - It does not work with the string form: in `emit("user/get", payload)` the second argument is the payload.
139
139
  - It can replace `$effects` too: `{ $effects: { delay: instantDelay } }`.
140
140
  - Fields of the library (`signal`, `emit`, …) in it are ignored.
@@ -199,13 +199,13 @@ In `$effects` a function loses its first parameter, and TypeScript cannot keep a
199
199
 
200
200
  ```js
201
201
  // pick.js
202
- import { createCommand } from "nanoeffects/actions";
202
+ import { createAction } from "nanoeffects/actions";
203
203
  import { effect } from "nanoeffects/scope";
204
204
 
205
205
  // effect — marks the function, so a scope binds it to the task; the editor keeps its generic
206
206
  const first = effect((ctx, items) => items[0]);
207
207
 
208
- export const pick = createCommand("pick", (items, { first }) => first(items), {
208
+ export const pick = createAction("pick", (items, { first }) => first(items), {
209
209
  first, // a plain key of the scope, not an entry of `$effects`
210
210
  });
211
211
  ```
@@ -215,7 +215,7 @@ Call `effect` where you declare the function, as above, and not inline in the sc
215
215
  `Effect<F>` (TypeScript): declare how the effect looks **after** it is bound, and keep it in `$effects`.
216
216
 
217
217
  ```ts
218
- import { createCommand } from "nanoeffects/actions";
218
+ import { createAction } from "nanoeffects/actions";
219
219
  import type { Effect } from "nanoeffects/effects";
220
220
 
221
221
  // First — checker-only: how a handler calls the effect (no ctx)
@@ -224,7 +224,7 @@ type First = <T>(items: T[]) => T;
224
224
  // Effect<First> — the function takes the task first and is typed as First once bound
225
225
  const first: Effect<First> = (ctx, items) => items[0];
226
226
 
227
- const pick = createCommand(
227
+ const pick = createAction(
228
228
  "pick",
229
229
  (id: string, { first }) => first([id]), // string
230
230
  { $effects: { first } },
@@ -235,14 +235,14 @@ const pick = createCommand(
235
235
 
236
236
  ## Type the fields (optional)
237
237
 
238
- A handler of a command, job or module with a scope sees the fields of the scope in `ctx`, typed from the values you gave. You write no `WithContext`.
238
+ A handler of an action, job or module with a scope sees the fields of the scope in `ctx`, typed from the values you gave. You write no `WithContext`.
239
239
 
240
240
  ```ts
241
- import { createCommand } from "nanoeffects/actions";
241
+ import { createAction } from "nanoeffects/actions";
242
242
 
243
243
  type User = { id: string; name: string };
244
244
 
245
- const getUser = createCommand(
245
+ const getUser = createAction(
246
246
  "user/get",
247
247
  async (id: string, ctx) => {
248
248
  ctx.cache; // Map<string, User>, from the scope below
@@ -260,7 +260,7 @@ const getUser = createCommand(
260
260
  ## Limits
261
261
 
262
262
  - No plugin, no scope: see [Turn scopes on](#turn-scopes-on).
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.
263
+ - The fields of the bus are not in the handler of an action made with `createAction(…, scope)` unless you type `ctx` yourself; the fields of the scope are.
264
264
  - The string form of `emit` takes no scope.
265
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.
266
+ - A scope on an action, job or module is typed without the events of a bus, so `take` in its `$effects` takes any event name.
package/docs/server.md CHANGED
@@ -16,9 +16,9 @@ The fix is one `AbortController` for each unit of work that the client owns, and
16
16
  | A WebSocket | the socket connects | the socket closes |
17
17
  | A queued job | the job is accepted | the server shuts down |
18
18
 
19
- ## Make the bus and the command
19
+ ## Make the bus and the action
20
20
 
21
- The bus and the command live in files that know nothing about HTTP, so the same command can run in a route, a test or a worker.
21
+ The bus and the action live in files that know nothing about HTTP, so the same action can run in a route, a test or a worker.
22
22
 
23
23
  ```js
24
24
  // bus.js
@@ -30,15 +30,15 @@ import { signalPlugin } from "nanoeffects/signal";
30
30
  export const bus = createBus().use(signalPlugin());
31
31
  ```
32
32
 
33
- The command loads a user. It tries the request up to three times, and gives up on each try after five seconds. `ctx.signal` is the signal of the task: it aborts when the controller of the request aborts.
33
+ The action loads a user. It tries the request up to three times, and gives up on each try after five seconds. `ctx.signal` is the signal of the task: it aborts when the controller of the request aborts.
34
34
 
35
35
  ```js
36
36
  // user.js
37
- import { createCommand } from "nanoeffects/actions";
37
+ import { createAction } from "nanoeffects/actions";
38
38
  import { retry, timeout } from "nanoeffects/effects";
39
39
 
40
- // createCommand — a command with a handler: emit returns what the handler returns
41
- export const getUser = createCommand("user/get", async (id, ctx) => {
40
+ // createAction — a command with a handler: emit returns what the handler returns
41
+ export const getUser = createAction("user/get", async (id, ctx) => {
42
42
  // one try: the request, with the signal of timeout so it can stop it
43
43
  const load = ({ signal }) => api.getUser(id, { signal });
44
44
  // timeout — fail the try after 5 s
@@ -91,7 +91,7 @@ app.get("/users/:id", async (request, reply) => {
91
91
  });
92
92
  ```
93
93
 
94
- After an abort the command rejects with the abort reason. The `catch` above ignores it when the connection is gone, because there is nobody to answer.
94
+ After an abort the action rejects with the abort reason. The `catch` above ignores it when the connection is gone, because there is nobody to answer.
95
95
 
96
96
  ## Stop the server
97
97
 
@@ -123,17 +123,17 @@ The route above fills `inFlight` and empties it in the `close` listener. The [ap
123
123
 
124
124
  A socket lasts minutes and carries several tasks (a subscription, a heartbeat), so the controller belongs to the socket and every `emit` for it gets the same signal. Closing the socket stops all of them.
125
125
 
126
- First the command. It sends a price now and then every second, until its task is aborted.
126
+ First the action. It sends a price now and then every second, until its task is aborted.
127
127
 
128
128
  ```js
129
129
  // prices.js
130
- import { createCommand } from "nanoeffects/actions";
130
+ import { createAction } from "nanoeffects/actions";
131
131
  import { poll } from "nanoeffects/effects";
132
132
  import { latest } from "nanoeffects/policies";
133
133
 
134
- // createCommand — a command with a handler
134
+ // createAction — a command with a handler
135
135
  // latest — a new subscription stops the old one; key — but only the old one of the same socket
136
- export const watchPrices = createCommand(
136
+ export const watchPrices = createAction(
137
137
  "prices/watch",
138
138
  latest(
139
139
  async (call, ctx) => {
@@ -147,7 +147,7 @@ export const watchPrices = createCommand(
147
147
  );
148
148
  ```
149
149
 
150
- Without the `key`, one policy serves the whole command, and a new subscription on one socket would stop the subscription on another.
150
+ Without the `key`, one policy serves the whole action, and a new subscription on one socket would stop the subscription on another.
151
151
 
152
152
  Then the route. Each socket gets an id and a controller.
153
153
 
package/docs/stability.md CHANGED
@@ -11,7 +11,7 @@ A stable entry follows the rules below. An unstable entry can change in any mino
11
11
  | `nanoeffects` | Stable | `createBus` and the types of a bus |
12
12
  | `nanoeffects/effects` | Stable | `retry`, `timeout`, `poll`, `take`, `delay`, `all`, `allSettled`, `race`, `backoff`, `cancellable`, `effectsPlugin` |
13
13
  | `nanoeffects/policies` | Stable | `latest`, `leading`, `debounce`, `throttle`, `queue`, `withPolicy` |
14
- | `nanoeffects/actions` | Stable | `createAction`, `createCommand`, `createJob`, `createModule` and their plugins |
14
+ | `nanoeffects/actions` | Stable | `createAction`, `createJob`, `createModule` and their plugins |
15
15
  | `nanoeffects/trace` | Stable: the contract | `Tracer`, `Trace`, `TraceEvent`, `TraceLogger`, `Phase`, `EffectName` and `tracePlugin` |
16
16
  | `nanoeffects/trace` | Unstable: the tools | `createTracer`, `consoleLogger`, `performanceLogger`, `createTraceRecorder`, `reduxDevtools` and their option types |
17
17
  | `nanoeffects/scope` | Unstable | `scopePlugin`, `effect` |
@@ -54,7 +54,7 @@ A deprecated export keeps working and is removed only in a major release. `RaceS
54
54
  | ---- | --------- |
55
55
  | Node.js, the library | 18, 20, 22 and 24 (a smoke test runs the built package on each) |
56
56
  | Node.js, the dev tools and the trace in a browser | 22 and newer |
57
- | TypeScript | The types are written and checked with TypeScript 7. TypeScript 5.9 is used for the Vue tools and for the test of the exports. Older versions are not tested. |
57
+ | TypeScript | 5.9 and 7. The built declaration files are type-checked with TypeScript 5.9 in CI, and the source with TypeScript 7. Older versions are not tested. |
58
58
  | React | 18 or newer (`nanoeffects/react`) |
59
59
  | Vue | 3.3 or newer (`nanoeffects/vue`) |
60
60
  | Svelte | 5 or newer (`nanoeffects/svelte`) |
package/docs/starter.md CHANGED
@@ -100,7 +100,7 @@ npm create nanoeffects@latest my-app -- --template domains-ts
100
100
 
101
101
  For a bigger example, a chat in which parts react to each other, see [Build a chat with entities, features, modules and a small server](chat-example.md). It has more layers than `domains-ts`: when domains must react to each other, it keeps them from importing each other by splitting them into entities and features.
102
102
 
103
- The commands, jobs and modules themselves are explained in [Actions, commands, jobs and modules](actions.md). The template keeps found products in a Context provider with one `useEventState`, and the order state in a store of its own: `useEventState` is [not a store](react.md#show-the-last-value-of-an-event-as-state).
103
+ The commands, jobs and modules themselves are explained in [Actions, jobs and modules](actions.md). The template keeps found products in a Context provider with one `useEventState`, and the order state in a store of its own: `useEventState` is [not a store](react.md#show-the-last-value-of-an-event-as-state).
104
104
 
105
105
  ## Read the files
106
106
 
package/docs/tracing.md CHANGED
@@ -138,7 +138,7 @@ Give the flow a `traceGroupId` in the scope of the command, the job or the actio
138
138
 
139
139
  ```js
140
140
  import { createBus } from "nanoeffects";
141
- import { createCommand } from "nanoeffects/actions";
141
+ import { createAction } from "nanoeffects/actions";
142
142
  import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
143
143
 
144
144
  // createTracer: sends each trace event to the logger; consoleLogger: prints one line each
@@ -149,7 +149,7 @@ const bus = createBus({ trace })
149
149
  .use(tracePlugin());
150
150
 
151
151
  // the scope names the flow; traceGroupId is for the tracer, not a field of ctx
152
- const pay = createCommand("checkout/pay", async () => {}, {
152
+ const pay = createAction("checkout/pay", async () => {}, {
153
153
  traceGroupId: "checkout",
154
154
  });
155
155
  ```
@@ -1,20 +1,20 @@
1
1
  # Use nanoeffects in a TypeScript app with many files
2
2
 
3
- Read this page if your app is in TypeScript and the commands and modules live in different files. It shows how to write a command so that TypeScript infers its payload and result, and what to add when the handlers need the fields of your bus (`ctx.store`, the effects of `effectsPlugin`). For one file or for JavaScript you do not need it.
3
+ Read this page if your app is in TypeScript and the handlers and modules live in different files. It shows how to write an action with a handler so that TypeScript infers its payload and result, and what to add when the handlers need the fields of your bus (`ctx.store`, the effects of `effectsPlugin`). For one file or for JavaScript you do not need it.
4
4
 
5
- ## Write a command with its handler
5
+ ## Write an action with its handler
6
6
 
7
- Give `createCommand` the handler and write nothing else. The payload comes from the annotated parameter, the result from the body. You do not write a generic.
7
+ Give `createAction` the handler and write nothing else. The payload comes from the annotated parameter, the result from the body. You do not write a generic.
8
8
 
9
9
  <!-- docs-check: skip - a file of examples/typescript, type-checked there -->
10
10
  ```ts
11
11
  // user.ts
12
- import { createCommand, createModule } from "nanoeffects/actions";
12
+ import { createAction, createModule } from "nanoeffects/actions";
13
13
 
14
14
  export type User = { id: string; name: string };
15
15
 
16
16
  // payload: string, from `id: string`; result: Promise<User>, from the body
17
- export const getUser = createCommand(
17
+ export const getUser = createAction(
18
18
  "user/get",
19
19
  async (id: string, { signal }) => {
20
20
  const response = await fetch(`/api/users/${id}`, { signal });
@@ -56,11 +56,11 @@ A wrong payload at the call (`getUser(1)`) is an error. Annotate the parameters,
56
56
 
57
57
  ## Give `ctx` the fields of your bus
58
58
 
59
- Types of a bus live on the bus. `contextPlugin({ store })` and `effectsPlugin({ race })` make `ctx.store` and `ctx.race` known to the handlers of **that bus**. A command made in another file does not see the bus, so its `ctx` has no `store`:
59
+ Types of a bus live on the bus. `contextPlugin({ store })` and `effectsPlugin({ race })` make `ctx.store` and `ctx.race` known to the handlers of **that bus**. An action with a handler made in another file does not see the bus, so its `ctx` has no `store`:
60
60
 
61
61
  <!-- docs-check: skip - shows the error on purpose -->
62
62
  ```ts
63
- export const getUser = createCommand("user/get", (id: string, ctx) => {
63
+ export const getUser = createAction("user/get", (id: string, ctx) => {
64
64
  ctx.store; // error: Property 'store' does not exist
65
65
  });
66
66
  ```
@@ -93,7 +93,7 @@ export type AppContext = Context<AppEvents>; // ctx of a handler of this bus: ha
93
93
  | Type | What it is | Why you name it |
94
94
  | ---- | ---------- | --------------- |
95
95
  | `EventsOf<typeof bus>` | the event map of the bus, with the context fields of the plugins (and of `WithContext`, if you use it) | the source of `AppContext` |
96
- | `Context<AppEvents>` | the `ctx` that a handler of the bus gets | annotate the `ctx` of a handler given to `createCommand` |
96
+ | `Context<AppEvents>` | the `ctx` that a handler of the bus gets | annotate the `ctx` of a handler given to `createAction` |
97
97
 
98
98
  These types change nothing at runtime. They only tell the checker what the bus already does.
99
99
 
@@ -102,10 +102,10 @@ Annotate `ctx` with `AppContext` where the handler reads a field of the bus, and
102
102
  <!-- docs-check: skip - a file of examples/typescript-context, type-checked there -->
103
103
  ```ts
104
104
  // user.ts
105
- import { createCommand } from "nanoeffects/actions";
105
+ import { createAction } from "nanoeffects/actions";
106
106
  import { type AppContext, createModule } from "./bus";
107
107
 
108
- export const getUser = createCommand(
108
+ export const getUser = createAction(
109
109
  "user/get",
110
110
  (id: string, { store, signal }: AppContext) => store.fetchUser(id, { signal }),
111
111
  );
@@ -145,14 +145,14 @@ Pass `ctx` (or `{ signal }`) when you call it. The effects of `nanoeffects/effec
145
145
 
146
146
  ## Keep the contract apart from the code
147
147
 
148
- When the file with the command must not import the code that implements it, make the command without a handler. Here you write the payload and the result once, as generics, and the handler goes to a module, where it needs no annotation:
148
+ When the file with the action must not import the code that implements it, make the action without a handler. Here you write the payload and the result once, as a `Call`, and the handler goes to a module, where it needs no annotation. `Call` is a promise of yours: nothing checks it against the handler, so keep the handler typed from it in the module:
149
149
 
150
150
  <!-- docs-check: skip - fragment of the file above -->
151
151
  ```ts
152
- export const getUser = createCommand<string, Promise<User>>("user/get"); // the contract
152
+ export const getUser = createAction<Call<string, Promise<User>>>("user/get"); // the contract
153
153
 
154
154
  createModule("user", (m) => {
155
- // payload and result come from the command, ctx from the app: no annotation
155
+ // payload and result come from the Call, ctx from the app: no annotation
156
156
  m.handle(getUser, (id, { store, signal }) => store.fetchUser(id, { signal }));
157
157
  });
158
158
  ```
@@ -161,8 +161,8 @@ A wrong payload, a wrong field of `ctx` and a wrong result are errors.
161
161
 
162
162
  ## Check your setup
163
163
 
164
- - **A field is missing on `ctx`.** The `ctx` of a handler given to `createCommand` is not annotated: add `: AppContext`. A handler written inline in `m.handle` or `m.on` of a module made with the `createModule` of `bus.ts` does not need it. The default `ctx` has no fields of your bus.
165
- - **`EventsOf<typeof bus>` is `never`.** `bus` is not a bus. `bus.use(commandsPlugin())` and `bus.use(modulePlugin(...))` return the registry or the app, not the bus: keep the bus in its own constant (`bus`, then `app = bus.use(modulePlugin())`).
164
+ - **A field is missing on `ctx`.** The `ctx` of a handler given to `createAction` is not annotated: add `: AppContext`. A handler written inline in `m.handle` or `m.on` of a module made with the `createModule` of `bus.ts` does not need it. The default `ctx` has no fields of your bus.
165
+ - **`EventsOf<typeof bus>` is `never`.** `bus` is not a bus. `bus.use(actionsPlugin())` and `bus.use(modulePlugin(...))` return the registry or the app, not the bus: keep the bus in its own constant (`bus`, then `app = bus.use(modulePlugin())`).
166
166
  - **`effectsPlugin` is not in the type.** `use` must come before you read `typeof bus`. Chain `createBus(...).use(effectsPlugin(...))` in one binding.
167
167
  - **A policy loses the types.** It does not: `latest(handler)` keeps the payload, the result and the context of `handler`.
168
- - **Do not pass the context as a generic of `createCommand`.** It is the fourth generic, after the payload, the result and the type string, so you would have to write all four. Annotate `ctx` with `AppContext`, or put the handler in a module made with the `createModule` of `bus.ts`.
168
+ - **Do not pass the context as a generic of `createAction`.** It is the fourth generic of the handler form, after the payload, the result and the type string, so you would have to write all four. Annotate `ctx` with `AppContext`, or put the handler in a module made with the `createModule` of `bus.ts`.