nanoeffects 0.25.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 (60) 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-B5V47MZ0.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-ks-oNq5n.d.ts → plugin-CATXOxtj.d.ts} +10 -9
  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-B6UQyoVd.d.ts → plugin-vvkmIIz2.d.ts} +11 -10
  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-DWjyeii7.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 +5 -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/effects.md +2 -2
  44. package/docs/errors.md +1 -1
  45. package/docs/guide.md +25 -25
  46. package/docs/hmr.md +1 -1
  47. package/docs/motivation.md +10 -4
  48. package/docs/oop.md +2 -0
  49. package/docs/package.md +1 -1
  50. package/docs/plugins.md +23 -23
  51. package/docs/query-libraries.md +14 -14
  52. package/docs/scope.md +22 -22
  53. package/docs/server.md +12 -12
  54. package/docs/stability.md +90 -0
  55. package/docs/starter.md +1 -1
  56. package/docs/tracing.md +2 -2
  57. package/docs/typescript.md +16 -16
  58. package/llms-full.txt +326 -183
  59. package/llms.txt +3 -2
  60. package/package.json +4 -3
package/llms-full.txt CHANGED
@@ -17,7 +17,7 @@ Source: docs/guide.md
17
17
 
18
18
  # Guide
19
19
 
20
- Climb one step at a time: small bus → policies → effects → commands and modules. Stop when your project has what it needs.
20
+ Climb one step at a time: small bus → policies → effects → handlers and modules. Stop when your project has what it needs.
21
21
 
22
22
  | Your project | Stop after |
23
23
  | ------------ | ---------- |
@@ -132,7 +132,7 @@ bus.emit(userFailed("boom")); // user/failed boom
132
132
  ```
133
133
 
134
134
  - `once(fn, handler)` waits for the first event that `fn` accepts. `off(fn)` removes every handler of `fn`, and `off(fn, handler)` removes one.
135
- - An action creator or a command still means that one event: only a function with no `type` is a function that picks events.
135
+ - An action creator still means that one event: only a function with no `type` is a function that picks events.
136
136
  - 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.
137
137
  - It works in any order with the other plugins, `devPlugin` included.
138
138
 
@@ -320,7 +320,7 @@ More in [Effects](effects.md).
320
320
 
321
321
  ## 7. Name events three ways
322
322
 
323
- `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).
323
+ `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).
324
324
 
325
325
  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.
326
326
 
@@ -341,10 +341,10 @@ export const moved = createAction("moved");
341
341
 
342
342
  ```js
343
343
  // math.js
344
- import { createCommand } from "nanoeffects/actions";
344
+ import { createAction } from "nanoeffects/actions";
345
345
 
346
- // createCommand — like an action, but emit runs a handler and returns its result
347
- export const double = createCommand("math/double", (value) => value * 2);
346
+ // createAction with a handler — emit runs the handler and returns its result
347
+ export const double = createAction("math/double", (value) => value * 2);
348
348
  ```
349
349
 
350
350
  ```js
@@ -361,36 +361,36 @@ bus.on(moved, ({ x }) => console.log(x));
361
361
  bus.emit(double(21)); // 42
362
362
  ```
363
363
 
364
- Use strings for the plain case. Use objects when other code already builds actions. Use commands when you need a result from `emit`.
364
+ 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`.
365
365
 
366
- ## 8. Register a command handler
366
+ ## 8. Bind a handler to an action later
367
367
 
368
- 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.
368
+ 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.
369
369
 
370
370
  ```js
371
371
  // user.js
372
- import { createCommand } from "nanoeffects/actions";
372
+ import { createAction } from "nanoeffects/actions";
373
373
 
374
- export const getUser = createCommand("user/get"); // contract — no handler yet
374
+ export const getUser = createAction("user/get"); // no handler yet: an event
375
375
  ```
376
376
 
377
377
  ```js
378
378
  // bus.js
379
379
  import { createBus } from "nanoeffects";
380
- import { commandsPlugin } from "nanoeffects/actions";
380
+ import { actionsPlugin } from "nanoeffects/actions";
381
381
 
382
382
  export const bus = createBus();
383
- // commandsPlugin — registry of handlers for this bus
384
- export const commands = bus.use(commandsPlugin());
383
+ // actionsPlugin — registry of handlers for this bus
384
+ export const actions = bus.use(actionsPlugin());
385
385
  ```
386
386
 
387
387
  ```js
388
- import { bus, commands } from "./bus";
388
+ import { bus, actions } from "./bus";
389
389
  import { getUser } from "./user";
390
390
 
391
- bus.emit(getUser("1")); // throws: No handler for command "user/get"
391
+ bus.emit(getUser("1")); // nothing is bound: only an event
392
392
 
393
- const unregister = commands.handle(getUser, (id) => api.user(id));
393
+ const unregister = actions.handle(getUser, (id) => api.user(id));
394
394
  const user = await bus.emit(getUser("1"));
395
395
 
396
396
  unregister();
@@ -400,7 +400,7 @@ Each bus has its own registry. A [module](#9-mount-a-domain-as-a-module) can reg
400
400
 
401
401
  ## 9. Mount a domain as a module
402
402
 
403
- **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.
403
+ **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.
404
404
 
405
405
  **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.
406
406
 
@@ -408,16 +408,16 @@ Each bus has its own registry. A [module](#9-mount-a-domain-as-a-module) can reg
408
408
 
409
409
  ```js
410
410
  // user.js
411
- import { createCommand } from "nanoeffects/actions";
411
+ import { createAction } from "nanoeffects/actions";
412
412
 
413
- export const getUser = createCommand("user/get"); // contract — no handler yet
413
+ export const getUser = createAction("user/get"); // no handler yet
414
414
  ```
415
415
 
416
416
  ```js
417
417
  // math.js
418
- import { createCommand } from "nanoeffects/actions";
418
+ import { createAction } from "nanoeffects/actions";
419
419
 
420
- export const double = createCommand("math/double", (value) => value * 2); // carries its handler
420
+ export const double = createAction("math/double", (value) => value * 2); // carries its handler
421
421
  ```
422
422
 
423
423
  ```js
@@ -434,7 +434,7 @@ export const greeting = createModule("greeting", (m) => {
434
434
  // m.handle — this module answers getUser while mounted
435
435
  m.handle(getUser, (id) => api.user(id));
436
436
 
437
- // m.use — register a command that already has a handler, for this module’s life
437
+ // m.use — register an action that already has a handler, for this module’s life
438
438
  m.use(double);
439
439
  });
440
440
  ```
@@ -459,8 +459,8 @@ app.unmount(greeting); // greet listener and getUser binding are gone
459
459
  ```
460
460
 
461
461
  - **`m.on`** — same job as `bus.on`, scoped to the module’s lifetime.
462
- - **`m.handle`** — bind a handler to a contract command; gone on unmount.
463
- - **`m.use`** — bring a command that already carries a handler (or a sub-module). Prefer `handle` when the handler must die with the feature.
462
+ - **`m.handle`** — bind a handler to an action that has none; gone on unmount.
463
+ - **`m.use`** — bring an action that already carries a handler (or a sub-module). Prefer `handle` when the handler must die with the feature.
464
464
 
465
465
  Modules also make [hot reload](hmr.md) work. Full API in [Actions](actions.md); a longer recipe in [`examples/modules.ts`](../examples/modules.ts).
466
466
 
@@ -617,7 +617,7 @@ npm create nanoeffects@latest my-app -- --template domains-ts
617
617
 
618
618
  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.
619
619
 
620
- 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).
620
+ 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).
621
621
 
622
622
  ## Read the files
623
623
 
@@ -643,9 +643,15 @@ Source: docs/motivation.md
643
643
 
644
644
  # Why nanoeffects exists
645
645
 
646
- 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).
646
+ 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).
647
+
648
+ ## The main reason: logic outside the UI
649
+
650
+ 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.
647
651
 
648
- ## The problem: a shared dependency is a shared risk
652
+ 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.
653
+
654
+ ## When the constraint is a shared library
649
655
 
650
656
  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.
651
657
 
@@ -674,9 +680,9 @@ A library that is **not** a shared singleton cannot break this way. nanoeffects
674
680
  ## What the library does instead
675
681
 
676
682
  - **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.
677
- - **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).
683
+ - **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).
678
684
  - **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.
679
- - **`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.
685
+ - **`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.
680
686
  - **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.
681
687
 
682
688
  ## When this is not the right tool
@@ -695,7 +701,7 @@ Imports are split so you only load what you use. `createBus` stays small; the re
695
701
  | `nanoeffects` | `createBus`, the types |
696
702
  | `nanoeffects/policies` | `latest`, `leading`, `debounce`, `throttle`, `queue` |
697
703
  | `nanoeffects/effects` | `retry`, `timeout`, `poll`, `delay`, `take`, `all`, `allSettled`, `race`, `effectsPlugin` |
698
- | `nanoeffects/actions` | `createAction`, `createCommand`, `createJob`, `createModule`, `modulePlugin`, `commandsPlugin` |
704
+ | `nanoeffects/actions` | `createAction`, `createJob`, `createModule`, `modulePlugin`, `actionsPlugin` |
699
705
  | `nanoeffects/match` | `matchPlugin`: `on`, `once` and `off` that take a function which picks the events |
700
706
  | `nanoeffects/context` | `contextPlugin`: your own fields (a store, a client) on `ctx` of every task, typed from the value |
701
707
  | `nanoeffects/signal` | `signalPlugin`: `emit(call, { signal })` ties the tasks of an emit to an outside `AbortSignal` |
@@ -721,6 +727,100 @@ The package ships plain-text files for assistants that write code with it:
721
727
 
722
728
  Find them in `node_modules/nanoeffects/`, or load `llms-full.txt` from a CDN such as `https://unpkg.com/nanoeffects/llms-full.txt`. All match the installed version.
723
729
 
730
+ ---
731
+ Source: docs/stability.md
732
+
733
+ # Know what is stable
734
+
735
+ Use this page to decide what you can rely on. It says which imports are stable, what counts as a breaking change, which versions are supported, and which call forms stay.
736
+
737
+ ## See which entries are stable
738
+
739
+ A stable entry follows the rules below. An unstable entry can change in any minor release; every change is written in the [CHANGELOG](../CHANGELOG.md), and nothing promises a deprecation period first.
740
+
741
+ | Import | Status | Note |
742
+ | ------ | ------ | ---- |
743
+ | `nanoeffects` | Stable | `createBus` and the types of a bus |
744
+ | `nanoeffects/effects` | Stable | `retry`, `timeout`, `poll`, `take`, `delay`, `all`, `allSettled`, `race`, `backoff`, `cancellable`, `effectsPlugin` |
745
+ | `nanoeffects/policies` | Stable | `latest`, `leading`, `debounce`, `throttle`, `queue`, `withPolicy` |
746
+ | `nanoeffects/actions` | Stable | `createAction`, `createJob`, `createModule` and their plugins |
747
+ | `nanoeffects/trace` | Stable: the contract | `Tracer`, `Trace`, `TraceEvent`, `TraceLogger`, `Phase`, `EffectName` and `tracePlugin` |
748
+ | `nanoeffects/trace` | Unstable: the tools | `createTracer`, `consoleLogger`, `performanceLogger`, `createTraceRecorder`, `reduxDevtools` and their option types |
749
+ | `nanoeffects/scope` | Unstable | `scopePlugin`, `effect` |
750
+ | `nanoeffects/match` | Unstable | `matchPlugin` |
751
+ | `nanoeffects/signal` | Unstable | `signalPlugin` |
752
+ | `nanoeffects/context` | Unstable | `contextPlugin` |
753
+ | `nanoeffects/oop` | Unstable | `Emitter`, `extendBus`, `On`, `Once`, `WithBus`, `disposeBus` |
754
+ | `nanoeffects/observable` | Unstable | `createObserve`, `Subject` |
755
+ | `nanoeffects/redux` | Unstable | `createEffectsMiddleware` |
756
+ | `nanoeffects/react` | Unstable | `reactPlugin`, `usePolicy` |
757
+ | `nanoeffects/svelte` | Unstable | `sveltePlugin` |
758
+ | `nanoeffects/vue` | Unstable | `vuePlugin` |
759
+ | `nanoeffects/dev` | Unstable | `devPlugin` |
760
+
761
+ The adapters (`react`, `vue`, `svelte`, `redux`) are unstable because they follow the APIs of other libraries and have had little use. They can become stable after the library has been in use for a while.
762
+
763
+ ## Know what is a breaking change
764
+
765
+ For a stable entry, these changes are breaking:
766
+
767
+ - A change in what a call does at run time.
768
+ - A change in a type that stops code that compiled before from compiling.
769
+ - A removed or renamed export.
770
+
771
+ A stricter type that only finds mistakes in code that was already wrong is not breaking. It ships in a minor release with a note in the CHANGELOG.
772
+
773
+ These are not covered by the promise:
774
+
775
+ - Any unstable entry.
776
+ - The text of the messages of `nanoeffects/dev`.
777
+ - The payload details of trace events. The shape of an event (`id`, `parentId`, `rootId`, `type`, `effect`, `phase`) is covered; the contents of `payload` are not.
778
+
779
+ Before version 1.0.0, a breaking change ships in a minor release under `### Breaking` in the CHANGELOG. From 1.0.0 it ships only in a major release.
780
+
781
+ A deprecated export keeps working and is removed only in a major release. `RaceStarter` is deprecated: use `StarterOf` instead.
782
+
783
+ ## Check the supported versions
784
+
785
+ | What | Supported |
786
+ | ---- | --------- |
787
+ | Node.js, the library | 18, 20, 22 and 24 (a smoke test runs the built package on each) |
788
+ | Node.js, the dev tools and the trace in a browser | 22 and newer |
789
+ | 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. |
790
+ | React | 18 or newer (`nanoeffects/react`) |
791
+ | Vue | 3.3 or newer (`nanoeffects/vue`) |
792
+ | Svelte | 5 or newer (`nanoeffects/svelte`) |
793
+
794
+ The library has no dependencies. React, Vue and Svelte are optional peer dependencies of their own entries only.
795
+
796
+ ## Rely on these call forms
797
+
798
+ `retry`, `timeout` and `poll` have three call forms, and all three stay:
799
+
800
+ ```js
801
+ import { createBus } from "nanoeffects";
802
+ import { race, retry } from "nanoeffects/effects";
803
+
804
+ const bus = createBus();
805
+
806
+ bus.on("open", async (id, ctx) => {
807
+ // with ctx: you pass the context, the function gets the signal
808
+ const user = await retry(ctx, ({ signal }) => fetchUser(id, { signal }), { times: 3 });
809
+
810
+ // as a starter: a group (race, all, allSettled, timeout) calls it with its child
811
+ const result = await race(ctx, {
812
+ user: retry((_, { signal }) => fetchUser(id, { signal }), { times: 3 }),
813
+ });
814
+ });
815
+
816
+ // as a Task: a function of (payload, ctx), which bus.on and ctx.run call
817
+ bus.on("reload", retry((id, { signal }) => fetchUser(id, { signal }), { times: 3 }));
818
+ ```
819
+
820
+ The forms are told apart by the arguments: a context first is the form with `ctx`; a function called with a payload and a context is the Task; the same function called by a group with one argument, its child, is the starter. The rule is part of the contract and does not depend on how many parameters your function declares.
821
+
822
+ `all` and `allSettled` take options only in the form with `ctx`, `all(ctx, effects, { concurrency })`. The form without `ctx` has one argument, so a second one would look like the `ctx` form. `race` takes no `concurrency`: it ends at the first result, so a starter that waited for a free slot would never run. See [Effects](effects.md#run-several-starters-together).
823
+
724
824
  ---
725
825
  Source: docs/plugins.md
726
826
 
@@ -736,14 +836,14 @@ In TypeScript keep the result of `contextPlugin` and `effectsPlugin`: the type o
736
836
 
737
837
  ## Main plugins
738
838
 
739
- These four cover what most apps need: shared dependencies, effects, commands and modules. Start here.
839
+ These four cover what most apps need: shared dependencies, effects, handlers and modules. Start here.
740
840
 
741
841
  | Plugin | What it gives you | Import | `bus.use` returns |
742
842
  | ------ | ----------------- | ------ | ----------------- |
743
843
  | [`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 |
744
844
  | [`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 |
745
- | [`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 }` |
746
- | [`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 }` |
845
+ | [`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 }` |
846
+ | [`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 }` |
747
847
 
748
848
  ### Put a store or a client on every handler: `contextPlugin`
749
849
 
@@ -780,27 +880,27 @@ bus.on("load", async (id, { race, delay }) => {
780
880
  });
781
881
  ```
782
882
 
783
- ### Let a test replace a handler: `commandsPlugin`
883
+ ### Bind or replace a handler: `actionsPlugin`
784
884
 
785
- 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).
885
+ 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).
786
886
 
787
887
  ```js
788
888
  import { createBus } from "nanoeffects";
789
- import { commandsPlugin, createCommand } from "nanoeffects/actions";
889
+ import { actionsPlugin, createAction } from "nanoeffects/actions";
790
890
 
791
- const getUser = createCommand("user/get"); // a contract: no handler yet
891
+ const getUser = createAction("user/get"); // no handler yet: an event
792
892
  const bus = createBus();
793
893
 
794
- // commandsPlugin — the registry of handlers of this bus
795
- const commands = bus.use(commandsPlugin());
894
+ // actionsPlugin — the registry of handlers of this bus
895
+ const actions = bus.use(actionsPlugin());
796
896
 
797
- commands.handle(getUser, async (id) => ({ id })); // the latest binding wins
897
+ actions.handle(getUser, async (id) => ({ id })); // the latest binding wins
798
898
  const user = await bus.emit(getUser("1")); // { id: "1" }
799
899
  ```
800
900
 
801
901
  ### Mount a domain as one unit: `modulePlugin`
802
902
 
803
- 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).
903
+ 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).
804
904
 
805
905
  ```js
806
906
  import { createBus } from "nanoeffects";
@@ -820,7 +920,7 @@ Add one of these when you meet its need. None of them is required.
820
920
 
821
921
  | Plugin | What it gives you | Import | `bus.use` returns |
822
922
  | ------ | ----------------- | ------ | ----------------- |
823
- | [`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 |
923
+ | [`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 |
824
924
  | [`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` |
825
925
  | [`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 |
826
926
  | [`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` |
@@ -828,20 +928,20 @@ Add one of these when you meet its need. None of them is required.
828
928
  | [`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` |
829
929
  | [`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 |
830
930
 
831
- ### Give one command its own fields: `scopePlugin`
931
+ ### Give one action its own fields: `scopePlugin`
832
932
 
833
- 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).
933
+ 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).
834
934
 
835
935
  ```js
836
936
  import { createBus } from "nanoeffects";
837
- import { createCommand } from "nanoeffects/actions";
937
+ import { createAction } from "nanoeffects/actions";
838
938
  import { scopePlugin } from "nanoeffects/scope";
839
939
 
840
940
  // scopePlugin — makes the scope of a command work on this bus
841
941
  const bus = createBus().use(scopePlugin());
842
942
 
843
943
  // the last argument: fields of this command only
844
- const getUser = createCommand("user/get", (id, { api }) => api.getUser(id), {
944
+ const getUser = createAction("user/get", (id, { api }) => api.getUser(id), {
845
945
  api,
846
946
  });
847
947
 
@@ -855,13 +955,13 @@ A library such as React Query owns the request and gives you an `AbortSignal`. `
855
955
 
856
956
  ```js
857
957
  import { createBus } from "nanoeffects";
858
- import { createCommand } from "nanoeffects/actions";
958
+ import { createAction } from "nanoeffects/actions";
859
959
  import { signalPlugin } from "nanoeffects/signal";
860
960
 
861
961
  // signalPlugin — emit takes { signal } for a command, a job or an action
862
962
  const bus = createBus().use(signalPlugin());
863
963
 
864
- const getUser = createCommand("user/get", (id, { signal }) => api.getUser(id, { signal }));
964
+ const getUser = createAction("user/get", (id, { signal }) => api.getUser(id, { signal }));
865
965
 
866
966
  const controller = new AbortController();
867
967
  bus.emit(getUser("1"), { signal: controller.signal }); // runs the command
@@ -968,7 +1068,7 @@ One bus with every plugin that does not need React, in a file of its own. The ev
968
1068
  import { createBus } from "nanoeffects";
969
1069
  import { contextPlugin } from "nanoeffects/context";
970
1070
  import {
971
- commandsPlugin,
1071
+ actionsPlugin,
972
1072
  emitterPlugin,
973
1073
  modulePlugin,
974
1074
  } from "nanoeffects/actions";
@@ -984,8 +1084,8 @@ export const bus = createBus()
984
1084
  .use(matchPlugin()) // bus.on takes a function that picks the events
985
1085
  .use(emitterPlugin()); // bus.emitter(creator)
986
1086
 
987
- // commandsPlugin — the registry of command handlers; keep it to bind handlers later
988
- export const commands = bus.use(commandsPlugin());
1087
+ // actionsPlugin — the registry of action handlers; keep it to bind handlers later
1088
+ export const actions = bus.use(actionsPlugin());
989
1089
 
990
1090
  // modulePlugin — mounts userModule at the end of this block; keep app to mount and unmount later
991
1091
  export const app = bus.use(modulePlugin([userModule]));
@@ -1000,12 +1100,12 @@ The plugins give the same result in any order. Use them as they read best, and p
1000
1100
 
1001
1101
  Two things happen because modules are mounted late:
1002
1102
 
1003
- - **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.
1103
+ - **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.
1004
1104
  - **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.
1005
1105
 
1006
1106
  Other plugins do not need an order for another reason:
1007
1107
 
1008
- - `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.
1108
+ - `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.
1009
1109
  - `effectsPlugin` and `contextPlugin` do not replace a field that is on `ctx` already, so a scope wins in any order.
1010
1110
  - `devPlugin()` finds the app that `modulePlugin` put on the bus (`bus.app`). `devPlugin({ app })` still works.
1011
1111
  - `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.
@@ -1943,7 +2043,7 @@ bus.on("open", async (id, ctx) => {
1943
2043
  });
1944
2044
  ```
1945
2045
 
1946
- The same call with a payload and a context, `(payload, ctx)`, is the Task that `bus.on` and `ctx.run` use, and it does not change.
2046
+ The same call with a payload and a context, `(payload, ctx)`, is the Task that `bus.on` and `ctx.run` use, and it does not change. `retry`, `timeout` and `poll` therefore have three call forms: with `ctx`, as a Task, and as a starter. All three stay; [Stability](stability.md#rely-on-these-call-forms) says how they are told apart.
1947
2047
 
1948
2048
  **`all`, `allSettled` and `race` without `ctx` are starters too**, so a group nests in another group with no wrapper. Load two things, but give up after five seconds:
1949
2049
 
@@ -1992,7 +2092,7 @@ bus.on("sync", async (ids, ctx) => {
1992
2092
  });
1993
2093
  ```
1994
2094
 
1995
- The result has the same order and shape as without the option. If one starter of `all` fails, the running ones are cancelled and the waiting ones are never called. `allSettled` goes on after a failure. When `ctx` aborts, both stop and start nothing more. A value that is not a positive integer (`0`, `-1`, `NaN`, `1.5`) throws a `RangeError` before anything starts; `Infinity` means no limit. `race` has no `concurrency`: it ends at the first result, so a starter that waited for a free slot would never run. A group starter inside another group takes no options; write `(child) => all(child, effects, { concurrency: 5 })` there.
2095
+ The result has the same order and shape as without the option. If one starter of `all` fails, the running ones are cancelled and the waiting ones are never called. `allSettled` goes on after a failure. When `ctx` aborts, both stop and start nothing more. A value that is not a positive integer (`0`, `-1`, `NaN`, `1.5`) throws a `RangeError` before anything starts; `Infinity` means no limit. `race` has no `concurrency`: it ends at the first result, so a starter that waited for a free slot would never run. The form without `ctx` takes no options, because a second argument would look like the form with `ctx`, so a group starter inside another group takes none; write `(child) => all(child, effects, { concurrency: 5 })` there.
1996
2096
 
1997
2097
 
1998
2098
  **A starter gets the context of its own child task.** Pass `child` as the first argument to any effect, as a handler passes `ctx`: when the race ends, `child` is aborted and stops exactly this effect. The outer `ctx` is only for `race` itself. Your own API takes the signal in its options: `({ signal }) => fetchUser(id, { signal })`. The same holds for `all`, `allSettled` and `timeout`.
@@ -2031,7 +2131,7 @@ const bus = createBus({
2031
2131
 
2032
2132
  ## Warn when nobody listens
2033
2133
 
2034
- 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.
2134
+ 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.
2035
2135
 
2036
2136
  Use it as a warning in development:
2037
2137
 
@@ -2048,7 +2148,7 @@ const bus = createBus({
2048
2148
  ---
2049
2149
  Source: docs/actions.md
2050
2150
 
2051
- # Actions, commands, jobs and modules
2151
+ # Actions, jobs and modules
2052
2152
 
2053
2153
  Use this page when **domain logic lives on the bus**, off the UI: the UI emits and listens; handlers and modules own the work.
2054
2154
 
@@ -2060,7 +2160,9 @@ A chat with two entities and three features, jobs under policies and effects, an
2060
2160
 
2061
2161
  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)).
2062
2162
 
2063
- ## Return a value from `emit` with a command
2163
+ ## Return a value from `emit` with a handler
2164
+
2165
+ 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.
2064
2166
 
2065
2167
  The bus lives in one file and its events in another, so any module can import both without a cycle.
2066
2168
 
@@ -2074,10 +2176,10 @@ export const bus = createBus();
2074
2176
 
2075
2177
  ```js
2076
2178
  // math.js
2077
- import { createCommand } from "nanoeffects/actions";
2179
+ import { createAction } from "nanoeffects/actions";
2078
2180
 
2079
- // createCommand — emit runs the handler and returns its result
2080
- export const double = createCommand("math/double", (value) => value * 2);
2181
+ // createAction with a handler — emit runs the handler and returns its result
2182
+ export const double = createAction("math/double", (value) => value * 2);
2081
2183
  ```
2082
2184
 
2083
2185
  ```js
@@ -2087,27 +2189,24 @@ import { double } from "./math";
2087
2189
  bus.emit(double(21)); // 42
2088
2190
  ```
2089
2191
 
2090
- ## Describe events, commands, and jobs
2192
+ ## Describe events, handlers and jobs
2091
2193
 
2092
2194
  `nanoeffects/actions` has the pieces to name what a bus does, apart from the code that does it.
2093
2195
 
2094
2196
  ```js
2095
2197
  // user.js
2096
- import { createAction, createCommand, createJob } from "nanoeffects/actions";
2198
+ import { createAction, createJob } from "nanoeffects/actions";
2097
2199
 
2098
- // createAction — event with a payload (like RTK createAction)
2200
+ // createAction — an event with a payload (like RTK createAction)
2099
2201
  export const userLoaded = createAction("user/loaded");
2100
2202
  export const loadFailed = createAction("user/failed");
2101
2203
 
2102
- // createCommand with a handler — emit returns what the handler returns
2103
- export const getUser = createCommand("user/get", (id, { signal }) => {
2204
+ // createAction with a handler — emit returns what the handler returns
2205
+ export const getUser = createAction("user/get", (id, { signal }) => {
2104
2206
  return fetchUser(id, { signal });
2105
2207
  });
2106
2208
 
2107
- // createCommand without a handler — a contract; register the work elsewhere
2108
- export const sendMail = createCommand("mail/send");
2109
-
2110
- // createJob — command that also emits success / fail actions
2209
+ // createJob — an action with a handler that also emits success / fail actions
2111
2210
  export const loadUser = createJob("user/load", fetchUser, {
2112
2211
  success: userLoaded,
2113
2212
  fail: "user/failed",
@@ -2123,57 +2222,99 @@ These are the events of the user domain, in a file of their own (`user.js`). The
2123
2222
  - `userLoaded(user)` → `{ type, payload }`
2124
2223
  - `userLoaded.type` — the type string
2125
2224
  - `userLoaded.match(action)` — narrows an action
2126
- - `createAction(type, prepare)` — build the payload from several arguments
2225
+ - `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
2226
+ - `createAction(type, { prepare })` — build the payload from several arguments
2227
+ - `createAction(type, { handle, prepare, ...scope })` — everything in one object: `handle` and `prepare` are taken out, and every other field is the [scope](scope.md)
2127
2228
  - Works as the event of its type: `bus.emit(userLoaded(user))`, `bus.on(userLoaded, fn)`
2128
2229
 
2129
- ### Commands
2230
+ 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 })`.
2130
2231
 
2131
- 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.
2232
+ - Wrap the handler in a policy: `createAction("search", latest(fn))`.
2132
2233
 
2133
- - Wrap the handler in a policy: `createCommand("search", latest(fn))`.
2134
- - `command.match(action)` narrows like an action creator’s `match`.
2234
+ An action with no handler is an event. Emitting it does not throw, and `onUnhandled` hears about it when nobody listens.
2235
+
2236
+ ### Bind a handler later
2237
+
2238
+ **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.
2239
+
2240
+ **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.
2241
+
2242
+ ```js
2243
+ // mail.js
2244
+ import { createAction } from "nanoeffects/actions";
2135
2245
 
2136
- **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.
2246
+ // createAction without a handler — an event; the work is bound elsewhere
2247
+ export const sendMail = createAction("mail/send");
2248
+ ```
2137
2249
 
2138
2250
  ```js
2139
2251
  // bus.js
2140
2252
  import { createBus } from "nanoeffects";
2141
- import { bindHandler, commandsPlugin } from "nanoeffects/actions";
2142
- import { sendMail } from "./user";
2253
+ import { actionsPlugin, setHandler } from "nanoeffects/actions";
2254
+ import { sendMail } from "./mail";
2143
2255
 
2144
2256
  export const bus = createBus();
2145
2257
 
2146
- // commandsPlugin — registry of handlers for this bus
2147
- export const commands = bus.use(
2148
- commandsPlugin([bindHandler(sendMail, smtpSend)]),
2258
+ // actionsPlugin — the registry of handlers of this bus; setHandler pairs an action with its work
2259
+ export const actions = bus.use(
2260
+ actionsPlugin([setHandler(sendMail, smtpSend)]),
2149
2261
  );
2150
2262
  ```
2151
2263
 
2152
2264
  ```js
2153
- import { commands } from "./bus";
2265
+ import { actions } from "./bus";
2154
2266
  import { getUser } from "./user";
2155
2267
 
2156
- // later, e.g. in a test — latest binding wins
2157
- const unbind = commands.handle(getUser, async () => fakeUser);
2268
+ // later, e.g. in a test — the latest binding wins, a handler bound here wins over the one in createAction
2269
+ const unbind = actions.handle(getUser, async () => fakeUser);
2158
2270
  unbind();
2159
2271
  ```
2160
2272
 
2161
- A command with no handler anywhere throws `No handler for command "..."` from `emit` (the call itself only builds the action).
2273
+ 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.
2274
+
2275
+ 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.
2276
+
2277
+ ### Say what a bound handler returns
2278
+
2279
+ 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.
2280
+
2281
+ ```ts
2282
+ // mail.ts
2283
+ import type { Call } from "nanoeffects";
2284
+ import { createAction } from "nanoeffects/actions";
2285
+
2286
+ // Call — payload Mail, a bound handler answers with a boolean (checker only; nothing runs differently)
2287
+ type Mail = { to: string };
2288
+ export const sendMail = createAction<Call<Mail, boolean>>("mail/send");
2289
+ ```
2290
+
2291
+ In an event map the same type does the job for string events, and `emit` returns `boolean`:
2162
2292
 
2163
- 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 }`.
2293
+ ```ts
2294
+ // events.ts
2295
+ import type { Call } from "nanoeffects";
2296
+
2297
+ // Call in the map — the payload is checked, emit returns the result (checker only)
2298
+ export type Events = {
2299
+ "user/getId": Call<void, string>;
2300
+ "user/loaded": { id: string };
2301
+ };
2302
+ ```
2303
+
2304
+ 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.
2164
2305
 
2165
2306
  ### Jobs
2166
2307
 
2167
- `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.
2308
+ `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.
2168
2309
 
2169
2310
  - When the handler starts, emits `loadUser.pending(payload)`.
2170
2311
  - On success, emits `loadUser.success(result)`.
2171
2312
  - On failure, emits `loadUser.fail(error)`.
2172
2313
  - 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.
2173
2314
  - `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`.
2174
- - `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.
2175
- - 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.
2176
- - A missing handler is not an outcome: `emit` throws `No handler for command "..."` and nothing is sent.
2315
+ - `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.
2316
+ - 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.
2317
+ - 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.
2177
2318
  - Calling the job only builds `{ type, payload }`; the handler runs on `emit`.
2178
2319
  - 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.
2179
2320
 
@@ -2197,7 +2338,7 @@ export const bus = createBus();
2197
2338
  // user.js
2198
2339
  import { createJob } from "nanoeffects/actions";
2199
2340
 
2200
- // createJob — a command that also sends pending, success, fail and cancel
2341
+ // createJob — an action with a handler that also sends pending, success, fail and cancel
2201
2342
  export const loadUser = createJob("user/load", fetchUser);
2202
2343
  ```
2203
2344
 
@@ -2252,18 +2393,18 @@ A flag is exact when calls do not overlap, or when `latest` replaces them. If se
2252
2393
 
2253
2394
  ## Mount domains with modules
2254
2395
 
2255
- **Problem.** A domain needs several subscriptions and command bindings. With plain `bus.on` / `commands.handle` you must remember every unsubscribe.
2396
+ **Problem.** A domain needs several subscriptions and handler bindings. With plain `bus.on` / `actions.handle` you must remember every unsubscribe.
2256
2397
 
2257
2398
  **When.** You want one mount / one unmount for that domain. Skip modules while a few top-level `bus.on` calls are enough.
2258
2399
 
2259
- `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)).
2400
+ `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)).
2260
2401
 
2261
2402
  ```js
2262
2403
  // user.js
2263
- import { createCommand } from "nanoeffects/actions";
2404
+ import { createAction } from "nanoeffects/actions";
2264
2405
 
2265
- export const getUser = createCommand("user/get");
2266
- export const saveUser = createCommand("user/save", persistUser);
2406
+ export const getUser = createAction("user/get");
2407
+ export const saveUser = createAction("user/save", persistUser);
2267
2408
  ```
2268
2409
 
2269
2410
  ```js
@@ -2300,7 +2441,7 @@ import { app } from "./bus";
2300
2441
  import { getUser } from "./user";
2301
2442
 
2302
2443
  const unmount = app.mount(orderModule); // later, e.g. after a lazy import
2303
- app.commands.handle(getUser, fakeUser); // in a test
2444
+ app.actions.handle(getUser, fakeUser); // in a test
2304
2445
  app.dispose(); // unmounts every module
2305
2446
  ```
2306
2447
 
@@ -2331,37 +2472,37 @@ app.unmount(...modules); // also in reverse order
2331
2472
 
2332
2473
  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.
2333
2474
 
2334
- - **Unmounting undoes everything** the module did: subscriptions, bound commands, sub-modules, and background tasks (aborted).
2475
+ - **Unmounting undoes everything** the module did: subscriptions, bound handlers, sub-modules, and background tasks (aborted).
2335
2476
  - **A module is known by its name.** Mounting a module whose name is taken replaces the mounted one (see [Hot reload](hmr.md)).
2336
- - **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`.
2477
+ - **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`.
2337
2478
  - A disposed app cannot mount modules.
2338
2479
  - **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).
2339
2480
  - **To mount by hand instead**, pass no modules: `modulePlugin()` starts nothing, and `app.mount(...)` starts the modules when you want.
2340
2481
  - Module context = task context plus `handle`, `use`, and `spawn`:
2341
- - `handle` — give a command a handler (contract, or override).
2342
- - `use` — bring sub-modules and commands that carry a handler; the returned function undoes that call.
2482
+ - `handle` — give an action a handler (one that has none, or an override).
2483
+ - `use` — bring sub-modules and actions that carry a handler; the returned function undoes that call.
2343
2484
  - `spawn` — background task, aborted on unmount.
2344
- - 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.
2485
+ - 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.
2345
2486
 
2346
- ## Give a command, an action or a module its own fields
2487
+ ## Give an action or a module its own fields
2347
2488
 
2348
- 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`:
2489
+ 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`:
2349
2490
 
2350
2491
  ```js
2351
2492
  // bus.js
2352
2493
  import { createBus } from "nanoeffects";
2353
2494
  import { scopePlugin } from "nanoeffects/scope";
2354
2495
 
2355
- // scopePlugin — makes the scope of a command work on this bus
2496
+ // scopePlugin — makes the scope of an action work on this bus
2356
2497
  export const bus = createBus().use(scopePlugin());
2357
2498
  ```
2358
2499
 
2359
2500
  ```js
2360
2501
  // user.js
2361
- import { createCommand } from "nanoeffects/actions";
2502
+ import { createAction } from "nanoeffects/actions";
2362
2503
 
2363
- // the third argument — fields of this command only
2364
- export const getUser = createCommand(
2504
+ // the third argument — fields of this action only
2505
+ export const getUser = createAction(
2365
2506
  "user/get",
2366
2507
  (id, { api }) => api.getUser(id),
2367
2508
  { api },
@@ -3059,7 +3200,7 @@ Source: docs/query-libraries.md
3059
3200
 
3060
3201
  # Use nanoeffects with React Query and SWR
3061
3202
 
3062
- 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.
3203
+ 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.
3063
3204
 
3064
3205
  You need a React project, `nanoeffects`, and one of the two libraries. `api` below is your own client: replace it with your requests.
3065
3206
 
@@ -3075,9 +3216,9 @@ The libraries and nanoeffects do different jobs, so each keeps its own.
3075
3216
 
3076
3217
  `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.
3077
3218
 
3078
- ## Make the command
3219
+ ## Make the action
3079
3220
 
3080
- 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.
3221
+ 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.
3081
3222
 
3082
3223
  ```js
3083
3224
  // bus.js
@@ -3093,11 +3234,11 @@ The handler tries the request up to three times and gives up after five seconds
3093
3234
 
3094
3235
  ```js
3095
3236
  // user.js
3096
- import { createCommand } from "nanoeffects/actions";
3237
+ import { createAction } from "nanoeffects/actions";
3097
3238
  import { retry, timeout } from "nanoeffects/effects";
3098
3239
 
3099
- // createCommand — a command with a handler: emit returns what the handler returns
3100
- export const getUser = createCommand("user/get", async (id, ctx) => {
3240
+ // createAction with a handler — emit returns what the handler returns
3241
+ export const getUser = createAction("user/get", async (id, ctx) => {
3101
3242
  // one try: the request, with the signal of timeout so it can stop it
3102
3243
  const load = ({ signal }) => api.getUser(id, { signal });
3103
3244
  // timeout — fail the try after 5 s
@@ -3132,13 +3273,13 @@ export function Profile({ id }) {
3132
3273
  }
3133
3274
  ```
3134
3275
 
3135
- 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.
3276
+ 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.
3136
3277
 
3137
3278
  ## Call it from SWR
3138
3279
 
3139
3280
  <!-- docs-check: continue -->
3140
3281
 
3141
- SWR does not pass a signal to the fetcher, so there is nothing to give to `emit`. The command still brings `retry` and `timeout`.
3282
+ SWR does not pass a signal to the fetcher, so there is nothing to give to `emit`. The action still brings `retry` and `timeout`.
3142
3283
 
3143
3284
  ```jsx
3144
3285
  // Profile.jsx
@@ -3162,7 +3303,7 @@ export function Profile({ id }) {
3162
3303
 
3163
3304
  <!-- docs-check: continue -->
3164
3305
 
3165
- 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:
3306
+ 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:
3166
3307
 
3167
3308
  ```js
3168
3309
  // user-events.js
@@ -3172,15 +3313,15 @@ import { createAction } from "nanoeffects/actions";
3172
3313
  export const userLoaded = createAction("user/loaded");
3173
3314
  ```
3174
3315
 
3175
- 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).
3316
+ 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).
3176
3317
 
3177
3318
  ## Know the limits
3178
3319
 
3179
- - **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.
3320
+ - **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.
3180
3321
  - **Only the call forms take it.** `bus.emit(getUser(id), { signal })` works; `bus.emit("user/get", id, { signal })` does not.
3181
- - **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.
3182
- - **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.
3183
- - **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.
3322
+ - **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.
3323
+ - **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.
3324
+ - **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.
3184
3325
 
3185
3326
  A whole small app, with a search, a filter, pages and a detail panel, is in the [Rick and Morty example](../examples/rickandmorty).
3186
3327
 
@@ -3205,9 +3346,9 @@ The fix is one `AbortController` for each unit of work that the client owns, and
3205
3346
  | A WebSocket | the socket connects | the socket closes |
3206
3347
  | A queued job | the job is accepted | the server shuts down |
3207
3348
 
3208
- ## Make the bus and the command
3349
+ ## Make the bus and the action
3209
3350
 
3210
- 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.
3351
+ 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.
3211
3352
 
3212
3353
  ```js
3213
3354
  // bus.js
@@ -3219,15 +3360,15 @@ import { signalPlugin } from "nanoeffects/signal";
3219
3360
  export const bus = createBus().use(signalPlugin());
3220
3361
  ```
3221
3362
 
3222
- 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.
3363
+ 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.
3223
3364
 
3224
3365
  ```js
3225
3366
  // user.js
3226
- import { createCommand } from "nanoeffects/actions";
3367
+ import { createAction } from "nanoeffects/actions";
3227
3368
  import { retry, timeout } from "nanoeffects/effects";
3228
3369
 
3229
- // createCommand — a command with a handler: emit returns what the handler returns
3230
- export const getUser = createCommand("user/get", async (id, ctx) => {
3370
+ // createAction — a command with a handler: emit returns what the handler returns
3371
+ export const getUser = createAction("user/get", async (id, ctx) => {
3231
3372
  // one try: the request, with the signal of timeout so it can stop it
3232
3373
  const load = ({ signal }) => api.getUser(id, { signal });
3233
3374
  // timeout — fail the try after 5 s
@@ -3280,7 +3421,7 @@ app.get("/users/:id", async (request, reply) => {
3280
3421
  });
3281
3422
  ```
3282
3423
 
3283
- 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.
3424
+ 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.
3284
3425
 
3285
3426
  ## Stop the server
3286
3427
 
@@ -3312,17 +3453,17 @@ The route above fills `inFlight` and empties it in the `close` listener. The [ap
3312
3453
 
3313
3454
  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.
3314
3455
 
3315
- First the command. It sends a price now and then every second, until its task is aborted.
3456
+ First the action. It sends a price now and then every second, until its task is aborted.
3316
3457
 
3317
3458
  ```js
3318
3459
  // prices.js
3319
- import { createCommand } from "nanoeffects/actions";
3460
+ import { createAction } from "nanoeffects/actions";
3320
3461
  import { poll } from "nanoeffects/effects";
3321
3462
  import { latest } from "nanoeffects/policies";
3322
3463
 
3323
- // createCommand — a command with a handler
3464
+ // createAction — a command with a handler
3324
3465
  // latest — a new subscription stops the old one; key — but only the old one of the same socket
3325
- export const watchPrices = createCommand(
3466
+ export const watchPrices = createAction(
3326
3467
  "prices/watch",
3327
3468
  latest(
3328
3469
  async (call, ctx) => {
@@ -3336,7 +3477,7 @@ export const watchPrices = createCommand(
3336
3477
  );
3337
3478
  ```
3338
3479
 
3339
- Without the `key`, one policy serves the whole command, and a new subscription on one socket would stop the subscription on another.
3480
+ Without the `key`, one policy serves the whole action, and a new subscription on one socket would stop the subscription on another.
3340
3481
 
3341
3482
  Then the route. Each socket gets an id and a controller.
3342
3483
 
@@ -3680,7 +3821,7 @@ Source: docs/chat-example.md
3680
3821
 
3681
3822
  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.
3682
3823
 
3683
- 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.
3824
+ 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.
3684
3825
 
3685
3826
  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:
3686
3827
 
@@ -3726,7 +3867,7 @@ The code is in layers, and a file imports only from the layers below it. The com
3726
3867
 
3727
3868
  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.
3728
3869
 
3729
- 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.
3870
+ 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.
3730
3871
 
3731
3872
  The layers come from [Feature-Sliced Design](https://feature-sliced.design/), reduced to the three that an app of this size needs.
3732
3873
 
@@ -3857,11 +3998,11 @@ export const runSearch = debounce(
3857
3998
  `take` waits for the next event of a name. Start it before the emit that causes the event, so nothing is missed:
3858
3999
 
3859
4000
  ```js
3860
- import { createCommand } from "nanoeffects/actions";
4001
+ import { createAction } from "nanoeffects/actions";
3861
4002
  import { take } from "nanoeffects/effects";
3862
4003
 
3863
- // createCommand — an order for the message entity, with the handler that does it
3864
- const markMessage = createCommand("messages/mark", (messageId) => console.log("mark", messageId));
4004
+ // createAction with a handler — an order for the message entity, with the handler that does it
4005
+ const markMessage = createAction("messages/mark", (messageId) => console.log("mark", messageId));
3865
4006
 
3866
4007
  // the room that is open, kept from the rooms/selected event
3867
4008
  let selectedRoom = "general";
@@ -3925,7 +4066,7 @@ Each entity and each feature that has state keeps it in a [Zustand](https://zust
3925
4066
 
3926
4067
  ## Where to go next
3927
4068
 
3928
- - The API of commands, jobs and modules: [Actions, commands, jobs and modules](actions.md).
4069
+ - The API of commands, jobs and modules: [Actions, jobs and modules](actions.md).
3929
4070
  - Policies such as `latest` and `queue`: [Policies](policies.md). Effects such as `allSettled`, `take` and `poll`: [Effects](effects.md).
3930
4071
  - 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.
3931
4072
 
@@ -4134,7 +4275,7 @@ bus.on("save", (draft, { store }) => {
4134
4275
  });
4135
4276
  ```
4136
4277
 
4137
- That is enough in JavaScript. The fields appear on handlers, `ctx.run`, `ctx.on`, commands, modules, and child tasks of effects and policies.
4278
+ That is enough in JavaScript. The fields appear on handlers, `ctx.run`, `ctx.on`, actions, modules, and child tasks of effects and policies.
4138
4279
 
4139
4280
  Library fields (`signal`, `emit`, `run`, …) win on name clashes. `context` may be a function `(task) => fields` when the value depends on the task.
4140
4281
 
@@ -4180,9 +4321,9 @@ const bus = createBus<Events>({ context: { store } });
4180
4321
  ---
4181
4322
  Source: docs/scope.md
4182
4323
 
4183
- # Pin context fields to a command, an action, a module or a call
4324
+ # Pin context fields to an action, a module or a call
4184
4325
 
4185
- 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.
4326
+ 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.
4186
4327
 
4187
4328
  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)`.
4188
4329
 
@@ -4201,21 +4342,21 @@ import { scopePlugin } from "nanoeffects/scope";
4201
4342
  export const bus = createBus().use(scopePlugin());
4202
4343
  ```
4203
4344
 
4204
- 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.
4345
+ 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.
4205
4346
 
4206
4347
  - Without `scopePlugin` a scope is **ignored**: no error and no warning. If a field is missing from `ctx`, check this first.
4207
4348
  - Use it in any order with the other plugins: a scope wins over `effectsPlugin`, `contextPlugin` and `createBus({ context })`.
4208
4349
 
4209
- ## Give a command its own fields
4350
+ ## Give an action its own fields
4210
4351
 
4211
4352
  <!-- docs-check: continue -->
4212
4353
 
4213
4354
  ```js
4214
4355
  // user.js
4215
- import { createCommand } from "nanoeffects/actions";
4356
+ import { createAction } from "nanoeffects/actions";
4216
4357
 
4217
- // createCommand — third argument is the scope of this command
4218
- export const getUser = createCommand(
4358
+ // createAction — third argument is the scope of this action
4359
+ export const getUser = createAction(
4219
4360
  "user/get",
4220
4361
  (id, { api, cache }) => {
4221
4362
  // api and cache are on ctx for this command only
@@ -4232,7 +4373,7 @@ import { getUser } from "./user";
4232
4373
  const user = await bus.emit(getUser("1")); // the handler has api and cache
4233
4374
  ```
4234
4375
 
4235
- The listeners of `"user/get"` get the same fields when the command is emitted, because the fields belong to the call.
4376
+ The listeners of `"user/get"` get the same fields when the action is emitted, because the fields belong to the call.
4236
4377
 
4237
4378
  ## Bind an effect to the task
4238
4379
 
@@ -4240,14 +4381,14 @@ An effect is a function with `{ signal }` as its first parameter. In `$effects`
4240
4381
 
4241
4382
  ```js
4242
4383
  // feed.js
4243
- import { createCommand } from "nanoeffects/actions";
4384
+ import { createAction } from "nanoeffects/actions";
4244
4385
  import { delay, race } from "nanoeffects/effects";
4245
4386
 
4246
4387
  // your own effect: first parameter is the task (only its signal is used)
4247
4388
  const fetchJson = (ctx, url) =>
4248
4389
  fetch(url, { signal: ctx.signal }).then((response) => response.json());
4249
4390
 
4250
- export const loadFeed = createCommand(
4391
+ export const loadFeed = createAction(
4251
4392
  "feed/load",
4252
4393
  (url, { fetchJson, race, delay }) =>
4253
4394
  // race, delay, fetchJson — bound to this task: no ctx argument
@@ -4293,9 +4434,9 @@ export const bus = createBus().use(scopePlugin());
4293
4434
 
4294
4435
  ```js
4295
4436
  // user.js
4296
- import { createCommand } from "nanoeffects/actions";
4437
+ import { createAction } from "nanoeffects/actions";
4297
4438
 
4298
- export const getUser = createCommand(
4439
+ export const getUser = createAction(
4299
4440
  "user/get",
4300
4441
  (id, { api }) => api.getUser(id),
4301
4442
  { api },
@@ -4316,7 +4457,7 @@ await bus.emit(getUser("1"), { api: fakeApi });
4316
4457
 
4317
4458
  Rules for the second argument:
4318
4459
 
4319
- - It works with an action or a command call (`emit(getUser("1"), scope)`, `ctx.emit(loaded(user), scope)`).
4460
+ - It works with the call of an action (`emit(getUser("1"), scope)`, `ctx.emit(loaded(user), scope)`).
4320
4461
  - It does not work with the string form: in `emit("user/get", payload)` the second argument is the payload.
4321
4462
  - It can replace `$effects` too: `{ $effects: { delay: instantDelay } }`.
4322
4463
  - Fields of the library (`signal`, `emit`, …) in it are ignored.
@@ -4381,13 +4522,13 @@ In `$effects` a function loses its first parameter, and TypeScript cannot keep a
4381
4522
 
4382
4523
  ```js
4383
4524
  // pick.js
4384
- import { createCommand } from "nanoeffects/actions";
4525
+ import { createAction } from "nanoeffects/actions";
4385
4526
  import { effect } from "nanoeffects/scope";
4386
4527
 
4387
4528
  // effect — marks the function, so a scope binds it to the task; the editor keeps its generic
4388
4529
  const first = effect((ctx, items) => items[0]);
4389
4530
 
4390
- export const pick = createCommand("pick", (items, { first }) => first(items), {
4531
+ export const pick = createAction("pick", (items, { first }) => first(items), {
4391
4532
  first, // a plain key of the scope, not an entry of `$effects`
4392
4533
  });
4393
4534
  ```
@@ -4397,7 +4538,7 @@ Call `effect` where you declare the function, as above, and not inline in the sc
4397
4538
  `Effect<F>` (TypeScript): declare how the effect looks **after** it is bound, and keep it in `$effects`.
4398
4539
 
4399
4540
  ```ts
4400
- import { createCommand } from "nanoeffects/actions";
4541
+ import { createAction } from "nanoeffects/actions";
4401
4542
  import type { Effect } from "nanoeffects/effects";
4402
4543
 
4403
4544
  // First — checker-only: how a handler calls the effect (no ctx)
@@ -4406,7 +4547,7 @@ type First = <T>(items: T[]) => T;
4406
4547
  // Effect<First> — the function takes the task first and is typed as First once bound
4407
4548
  const first: Effect<First> = (ctx, items) => items[0];
4408
4549
 
4409
- const pick = createCommand(
4550
+ const pick = createAction(
4410
4551
  "pick",
4411
4552
  (id: string, { first }) => first([id]), // string
4412
4553
  { $effects: { first } },
@@ -4417,14 +4558,14 @@ const pick = createCommand(
4417
4558
 
4418
4559
  ## Type the fields (optional)
4419
4560
 
4420
- 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`.
4561
+ 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`.
4421
4562
 
4422
4563
  ```ts
4423
- import { createCommand } from "nanoeffects/actions";
4564
+ import { createAction } from "nanoeffects/actions";
4424
4565
 
4425
4566
  type User = { id: string; name: string };
4426
4567
 
4427
- const getUser = createCommand(
4568
+ const getUser = createAction(
4428
4569
  "user/get",
4429
4570
  async (id: string, ctx) => {
4430
4571
  ctx.cache; // Map<string, User>, from the scope below
@@ -4442,10 +4583,10 @@ const getUser = createCommand(
4442
4583
  ## Limits
4443
4584
 
4444
4585
  - No plugin, no scope: see [Turn scopes on](#turn-scopes-on).
4445
- - 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.
4586
+ - 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.
4446
4587
  - The string form of `emit` takes no scope.
4447
4588
  - `effect(fn)` works only as a value of a scope, not as an entry of `$effects`.
4448
- - 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.
4589
+ - 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.
4449
4590
 
4450
4591
  ---
4451
4592
  Source: docs/oop.md
@@ -4713,6 +4854,8 @@ export const Cache = extendBus(bus, CacheBase);
4713
4854
 
4714
4855
  ## Subscribe a method with `@On` and `@Once`
4715
4856
 
4857
+ `On` here is a decorator. The main entry also exports a type named `On`, the type of `bus.on`; they are different things in different imports, and a file that needs both can rename one: `import { On as OnDecorator } from "nanoeffects/oop"`.
4858
+
4716
4859
  A decorator turns a method into a handler of an event for each instance, with `this` bound. You write no `this.on` in the constructor, and `dispose()` ends the subscription too.
4717
4860
 
4718
4861
  The bus and the events are the same as above, in TypeScript:
@@ -4866,21 +5009,21 @@ Source: docs/typescript.md
4866
5009
 
4867
5010
  # Use nanoeffects in a TypeScript app with many files
4868
5011
 
4869
- 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.
5012
+ 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.
4870
5013
 
4871
- ## Write a command with its handler
5014
+ ## Write an action with its handler
4872
5015
 
4873
- 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.
5016
+ 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.
4874
5017
 
4875
5018
  <!-- docs-check: skip - a file of examples/typescript, type-checked there -->
4876
5019
  ```ts
4877
5020
  // user.ts
4878
- import { createCommand, createModule } from "nanoeffects/actions";
5021
+ import { createAction, createModule } from "nanoeffects/actions";
4879
5022
 
4880
5023
  export type User = { id: string; name: string };
4881
5024
 
4882
5025
  // payload: string, from `id: string`; result: Promise<User>, from the body
4883
- export const getUser = createCommand(
5026
+ export const getUser = createAction(
4884
5027
  "user/get",
4885
5028
  async (id: string, { signal }) => {
4886
5029
  const response = await fetch(`/api/users/${id}`, { signal });
@@ -4922,11 +5065,11 @@ A wrong payload at the call (`getUser(1)`) is an error. Annotate the parameters,
4922
5065
 
4923
5066
  ## Give `ctx` the fields of your bus
4924
5067
 
4925
- 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`:
5068
+ 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`:
4926
5069
 
4927
5070
  <!-- docs-check: skip - shows the error on purpose -->
4928
5071
  ```ts
4929
- export const getUser = createCommand("user/get", (id: string, ctx) => {
5072
+ export const getUser = createAction("user/get", (id: string, ctx) => {
4930
5073
  ctx.store; // error: Property 'store' does not exist
4931
5074
  });
4932
5075
  ```
@@ -4959,7 +5102,7 @@ export type AppContext = Context<AppEvents>; // ctx of a handler of this bus: ha
4959
5102
  | Type | What it is | Why you name it |
4960
5103
  | ---- | ---------- | --------------- |
4961
5104
  | `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` |
4962
- | `Context<AppEvents>` | the `ctx` that a handler of the bus gets | annotate the `ctx` of a handler given to `createCommand` |
5105
+ | `Context<AppEvents>` | the `ctx` that a handler of the bus gets | annotate the `ctx` of a handler given to `createAction` |
4963
5106
 
4964
5107
  These types change nothing at runtime. They only tell the checker what the bus already does.
4965
5108
 
@@ -4968,10 +5111,10 @@ Annotate `ctx` with `AppContext` where the handler reads a field of the bus, and
4968
5111
  <!-- docs-check: skip - a file of examples/typescript-context, type-checked there -->
4969
5112
  ```ts
4970
5113
  // user.ts
4971
- import { createCommand } from "nanoeffects/actions";
5114
+ import { createAction } from "nanoeffects/actions";
4972
5115
  import { type AppContext, createModule } from "./bus";
4973
5116
 
4974
- export const getUser = createCommand(
5117
+ export const getUser = createAction(
4975
5118
  "user/get",
4976
5119
  (id: string, { store, signal }: AppContext) => store.fetchUser(id, { signal }),
4977
5120
  );
@@ -5011,14 +5154,14 @@ Pass `ctx` (or `{ signal }`) when you call it. The effects of `nanoeffects/effec
5011
5154
 
5012
5155
  ## Keep the contract apart from the code
5013
5156
 
5014
- 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:
5157
+ 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:
5015
5158
 
5016
5159
  <!-- docs-check: skip - fragment of the file above -->
5017
5160
  ```ts
5018
- export const getUser = createCommand<string, Promise<User>>("user/get"); // the contract
5161
+ export const getUser = createAction<Call<string, Promise<User>>>("user/get"); // the contract
5019
5162
 
5020
5163
  createModule("user", (m) => {
5021
- // payload and result come from the command, ctx from the app: no annotation
5164
+ // payload and result come from the Call, ctx from the app: no annotation
5022
5165
  m.handle(getUser, (id, { store, signal }) => store.fetchUser(id, { signal }));
5023
5166
  });
5024
5167
  ```
@@ -5027,11 +5170,11 @@ A wrong payload, a wrong field of `ctx` and a wrong result are errors.
5027
5170
 
5028
5171
  ## Check your setup
5029
5172
 
5030
- - **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.
5031
- - **`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())`).
5173
+ - **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.
5174
+ - **`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())`).
5032
5175
  - **`effectsPlugin` is not in the type.** `use` must come before you read `typeof bus`. Chain `createBus(...).use(effectsPlugin(...))` in one binding.
5033
5176
  - **A policy loses the types.** It does not: `latest(handler)` keeps the payload, the result and the context of `handler`.
5034
- - **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`.
5177
+ - **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`.
5035
5178
 
5036
5179
  ---
5037
5180
  Source: docs/migrating-from-saga.md
@@ -5918,7 +6061,7 @@ Give the flow a `traceGroupId` in the scope of the command, the job or the actio
5918
6061
 
5919
6062
  ```js
5920
6063
  import { createBus } from "nanoeffects";
5921
- import { createCommand } from "nanoeffects/actions";
6064
+ import { createAction } from "nanoeffects/actions";
5922
6065
  import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
5923
6066
 
5924
6067
  // createTracer: sends each trace event to the logger; consoleLogger: prints one line each
@@ -5929,7 +6072,7 @@ const bus = createBus({ trace })
5929
6072
  .use(tracePlugin());
5930
6073
 
5931
6074
  // the scope names the flow; traceGroupId is for the tracer, not a field of ctx
5932
- const pay = createCommand("checkout/pay", async () => {}, {
6075
+ const pay = createAction("checkout/pay", async () => {}, {
5933
6076
  traceGroupId: "checkout",
5934
6077
  });
5935
6078
  ```
@@ -6327,7 +6470,7 @@ When a file is edited, the bundler loads its new version and runs it again, whil
6327
6470
 
6328
6471
  ## Replace modules by name
6329
6472
 
6330
- 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.
6473
+ 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.
6331
6474
 
6332
6475
  - **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.
6333
6476
  - **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.