nanoeffects 0.27.0 → 0.28.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 (57) hide show
  1. package/README.md +26 -24
  2. package/dist/{action-creator-D_c223C7.d.ts → action-creator-CcAEvUnw.d.ts} +39 -0
  3. package/dist/actions.d.ts +3 -3
  4. package/dist/actions.js +1 -1
  5. package/dist/bus-DlpEluzd.js +1 -0
  6. package/dist/{context-types-nGtzhjxD.d.ts → context-types-hCKgU8sC.d.ts} +1 -1
  7. package/dist/context.d.ts +2 -2
  8. package/dist/dev.d.ts +2 -2
  9. package/dist/dev.js +1 -1
  10. package/dist/effects.d.ts +2 -2
  11. package/dist/event-DVcW0mME.js +1 -0
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.js +1 -1
  14. package/dist/match.d.ts +1 -1
  15. package/dist/{module-WX0Gngnt.d.ts → module-C_oEXw1n.d.ts} +2 -2
  16. package/dist/notrace/{action-creator-Bux7Zzsl.d.ts → action-creator-DxOvVo8I.d.ts} +39 -0
  17. package/dist/notrace/actions.d.ts +2 -2
  18. package/dist/notrace/actions.js +1 -1
  19. package/dist/notrace/bus-DmKn4SDD.js +1 -0
  20. package/dist/notrace/effects.d.ts +2 -2
  21. package/dist/notrace/index.d.ts +1 -1
  22. package/dist/notrace/index.js +1 -1
  23. package/dist/notrace/{plugin-CATXOxtj.d.ts → plugin-lJjjd-SN.d.ts} +1 -1
  24. package/dist/notrace/policies.d.ts +1 -1
  25. package/dist/notrace/policies.js +1 -1
  26. package/dist/notrace/redux.d.ts +1 -1
  27. package/dist/observable.d.ts +1 -1
  28. package/dist/oop.d.ts +1 -1
  29. package/dist/oop.js +1 -1
  30. package/dist/{plugin-vvkmIIz2.d.ts → plugin-CuvIgjnX.d.ts} +2 -2
  31. package/dist/policies.d.ts +2 -2
  32. package/dist/react.d.ts +2 -2
  33. package/dist/redux.d.ts +1 -1
  34. package/dist/scope.d.ts +2 -2
  35. package/dist/signal.d.ts +1 -1
  36. package/dist/signal.js +1 -1
  37. package/dist/svelte.d.ts +1 -1
  38. package/dist/trace.d.ts +1 -1
  39. package/dist/{types-Bf-LX_tE.d.ts → types-FV8fuvI9.d.ts} +2 -2
  40. package/dist/vue.d.ts +1 -1
  41. package/dist/{with-policy-CPjx0sLm.d.ts → with-policy-3bhFLEXs.d.ts} +1 -1
  42. package/docs/README.md +1 -0
  43. package/docs/actions.md +1 -1
  44. package/docs/chat-example.md +4 -4
  45. package/docs/chat-widget.md +135 -0
  46. package/docs/guide.md +1 -1
  47. package/docs/oop.md +1 -1
  48. package/docs/package.md +1 -1
  49. package/docs/starter.md +3 -3
  50. package/docs/tracing.md +3 -3
  51. package/llms-full.txt +153 -14
  52. package/llms.txt +1 -0
  53. package/package.json +3 -3
  54. package/dist/bus-DQDHUN5e.js +0 -1
  55. package/dist/event-0RIBGYqS.js +0 -1
  56. package/dist/notrace/bus-CGi2lrd5.js +0 -1
  57. package/dist/notrace/promise-CSLpzwSR.js +0 -1
package/docs/actions.md CHANGED
@@ -326,7 +326,7 @@ If the `setup` of one module throws, the modules of that call that were already
326
326
  - **A module is known by its name.** Mounting a module whose name is taken replaces the mounted one (see [Hot reload](hmr.md)).
327
327
  - **Registry.** If `bus.use(actionsPlugin())` ran first, that registry is used; otherwise the module plugin attaches its own. Either way it is `bus.actions` and `app.actions`.
328
328
  - A disposed app cannot mount modules.
329
- - **The given modules are mounted at the end of the block that made the app**, or at the first use of the app or the bus if that comes first (the first `bus.emit`, or a call of `mount`, `unmount`, `isMounted`, `dispose`, or the read of `commands`). Nothing needs to use the app: `bus.use(modulePlugin([a, b]))` is enough to start them. The plugins that you use after `modulePlugin` in the same block still reach them, because the modules wait until that block has finished. A plugin used after an `await` can come too late: use every plugin before the first `await`. The `bus.on` calls made in between are held and made right after, so the listeners of a module stay ahead of them. See [using plugins in any order](plugins.md#use-them-in-any-order).
329
+ - **The given modules are mounted at the end of the block that made the app**, or at the first use of the app or the bus if that comes first (the first `bus.emit`, or a call of `mount`, `unmount`, `isMounted`, `dispose`, or the read of `actions`). 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).
330
330
  - **To mount by hand instead**, pass no modules: `modulePlugin()` starts nothing, and `app.mount(...)` starts the modules when you want.
331
331
  - Module context = task context plus `handle`, `use`, and `spawn`:
332
332
  - `handle` — give an action a handler (one that has none, or an override).
@@ -48,7 +48,7 @@ The code is in layers, and a file imports only from the layers below it. The com
48
48
 
49
49
  That rule is why `show-history` exists. Selecting a room must load its history, but `room` does not know `message` and `message` does not know `room`. The feature `show-history` is the one place that knows both: it listens to `rooms/selected` and starts the job `messages/loadHistory`. The same goes for a message that arrives: `receive-messages` emits `incoming/received`, an event that the `message` entity owns and listens to.
50
50
 
51
- A folder has its events (`room.ts`, which imports no bus), its work (`room-handlers.ts`), the state it keeps (`room-store.ts`) and a module (`room-module.ts`) that wires them. Another layer reaches an entity through its events and commands, never through its handlers or its store. Events and commands both live in the events file. An event (`createAction` 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.
51
+ A folder has its events (`room.ts`, which imports no bus), its work (`room-handlers.ts`), the state it keeps (`room-store.ts`) and a module (`room-module.ts`) that wires them. Another layer reaches an entity through its actions, never through its handlers or its store. All actions live in the events file. An action with no handler says that something happened, and any feature may listen: `rooms/selected` is heard by two. An action 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 action 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 an action with a handler that also sends its outcomes, but its handler is bound in the module (`module.handle`): a job usually runs under a policy, and the type of its outcomes is exact only when the handler is not part of the type.
52
52
 
53
53
  The layers come from [Feature-Sliced Design](https://feature-sliced.design/), reduced to the three that an app of this size needs.
54
54
 
@@ -64,7 +64,7 @@ A job sends `pending` when its handler starts and exactly one of `success`, `fai
64
64
  // message.js
65
65
  import { createJob } from "nanoeffects/actions";
66
66
 
67
- // createJob — a command that also sends pending, success, fail and cancel
67
+ // createJob — an action with a handler that also sends pending, success, fail and cancel
68
68
  export const loadHistory = createJob("messages/loadHistory");
69
69
  ```
70
70
 
@@ -195,7 +195,7 @@ export const goToResult = async (hit, ctx) => {
195
195
  ctx.emit("rooms/selected", hit.roomId);
196
196
  await loaded;
197
197
  }
198
- // ctx.emit with a command — the message entity marks the message
198
+ // ctx.emit with an action that has a handler — the message entity marks the message
199
199
  ctx.emit(markMessage(hit.id));
200
200
  };
201
201
  ```
@@ -247,6 +247,6 @@ Each entity and each feature that has state keeps it in a [Zustand](https://zust
247
247
 
248
248
  ## Where to go next
249
249
 
250
- - The API of commands, jobs and modules: [Actions, jobs and modules](actions.md).
250
+ - The API of actions, jobs and modules: [Actions, jobs and modules](actions.md).
251
251
  - Policies such as `latest` and `queue`: [Policies](policies.md). Effects such as `allSettled`, `take` and `poll`: [Effects](effects.md).
252
252
  - A smaller project with two domains: [Start with domains](starter.md#start-with-domains). Its two domains do not talk to each other, so it needs no layers.
@@ -0,0 +1,135 @@
1
+ # Build a chat widget that listens only while it is open
2
+
3
+ Use this page when a site has a chat with a bot behind a launcher button, and the chat must cost nothing while it is closed. Closed means no subscriptions on the bus, no polling, no keyboard listener, and no request still running. Opening turns all of it on, and closing turns all of it off in one call.
4
+
5
+ The code is in [`examples/chat-widget`](../examples/chat-widget): plain DOM and TypeScript, a fake bot in the browser, no server. Run it on [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/chat-widget), or on your machine:
6
+
7
+ ```bash
8
+ cd examples/chat-widget
9
+ npm install
10
+ npm run dev
11
+ ```
12
+
13
+ It is a level D example: the logic lives in one module, and the page only mounts and unmounts it. For an app with several features see [Build a chat with entities, features, modules and a small server](chat-example.md).
14
+
15
+ ## Know what happens on open and on close
16
+
17
+ | Moment | What happens |
18
+ | ------ | ------------ |
19
+ | Open | The module is mounted: the bot handler is subscribed, the operator messages are polled, the `Escape` listener is added. The panel is drawn and listens to the bus. |
20
+ | The user sends a text | The bot is asked, one text after another, with a time limit and a retry. |
21
+ | `Escape` | The module emits `chat/closeRequested`; the launcher closes the chat. |
22
+ | Close | The panel is removed and unsubscribed, the module is unmounted. A request in flight is aborted, the polling stops, the `Escape` listener goes away. |
23
+
24
+ ## Find the parts in the files
25
+
26
+ | File | Job |
27
+ | ---- | --- |
28
+ | `src/bus.ts` | The bus, and `modulePlugin()` with no module: the launcher mounts the module itself. |
29
+ | `src/chat.ts` | The events only: `messageSent`, `messageAdded`, `typingChanged`, `closeRequested`. |
30
+ | `src/chat-handlers.ts` | `askBot` (`timeout` and `retry` inside) and `watchOperator` (`poll`): plain `async` functions that know nothing about the page. |
31
+ | `src/chat-module.ts` | The list of what is alive while the chat is open. |
32
+ | `src/panel.ts` | The view: shows the lines, sends the texts, returns one function that removes it. |
33
+ | `src/main.ts` | The launcher: the only file that knows both the module and the panel. |
34
+
35
+ ## Bind everything in one module
36
+
37
+ A module is the list of what is alive while the chat is open. When it unmounts, the bus undoes every line of it.
38
+
39
+ <!-- docs-check: skip - the other files of the example are in examples/chat-widget -->
40
+ ```js
41
+ // chat-module.js
42
+ import { createModule } from "nanoeffects/actions";
43
+ import { queue } from "nanoeffects/policies";
44
+ import { closeRequested, messageSent } from "./chat";
45
+ import { askBot, watchOperator } from "./chat-handlers";
46
+
47
+ // a task that stays pending until the module unmounts; its signal then aborts
48
+ const untilAborted = (ctx) =>
49
+ new Promise((resolve) => {
50
+ ctx.signal.addEventListener("abort", resolve, { once: true });
51
+ });
52
+
53
+ // a DOM listener that lives as long as the module: `signal` removes it when it aborts
54
+ const listenToEscape = async (_, ctx) => {
55
+ const onKey = (event) => {
56
+ if (event.key === "Escape") {
57
+ ctx.emit(closeRequested());
58
+ }
59
+ };
60
+ document.addEventListener("keydown", onKey, { signal: ctx.signal });
61
+ await untilAborted(ctx);
62
+ };
63
+
64
+ export const chatModule = createModule("chat", (module) => {
65
+ // module.run — runs askBot as a child task of the module, so an unmount aborts it
66
+ const answer = (text) => module.run(askBot, text);
67
+ // on + queue — answer the texts one after another, in the order they were sent
68
+ module.on(messageSent, queue(answer));
69
+ // spawn — a background task that lives as long as the module
70
+ module.spawn(watchOperator);
71
+ module.spawn(listenToEscape);
72
+ });
73
+ ```
74
+
75
+ Three details that make the cleanup complete:
76
+
77
+ - **`module.run`, not `askBot` directly.** A handler given to `module.on` runs as a child of whoever emitted the event (here the panel), so unmounting stops listening but does not abort a request that is already running. Running it with `module.run` makes it a child of the module.
78
+ - **`queue`, not `latest`.** `queue` keeps the order when the user sends two texts fast. [`latest`](policies.md) would abort the first answer, which is wrong for a conversation.
79
+ - **A DOM listener with `signal`.** `addEventListener` takes a `signal` option and removes the listener when it aborts. The task stays pending until then, because a task that has finished is not aborted again.
80
+
81
+ ## Open and close from the launcher
82
+
83
+ `app.mount` returns the function that unmounts the module. The launcher puts it together with the one that removes the panel.
84
+
85
+ <!-- docs-check: skip - the other files of the example are in examples/chat-widget -->
86
+ ```js
87
+ // main.js
88
+ import { app, bus } from "./bus";
89
+ import { closeRequested } from "./chat";
90
+ import { chatModule } from "./chat-module";
91
+ import { renderPanel } from "./panel";
92
+
93
+ const button = document.querySelector("#chat-launcher");
94
+ let close = null; // set while the chat is open
95
+
96
+ const open = () => {
97
+ const stopModule = app.mount(chatModule); // subscriptions, polling and Escape start
98
+ const stopPanel = renderPanel(document.body);
99
+ close = () => {
100
+ stopPanel();
101
+ stopModule(); // everything the module started ends, a running request included
102
+ close = null;
103
+ };
104
+ };
105
+
106
+ button.addEventListener("click", () => {
107
+ if (close) {
108
+ close();
109
+ } else {
110
+ open();
111
+ }
112
+ });
113
+
114
+ // the launcher stays subscribed: it must hear Escape to close the chat
115
+ bus.on(closeRequested, () => close?.());
116
+ ```
117
+
118
+ The launcher's own `bus.on` is the only subscription that lives while the chat is closed, and it does nothing then: `close` is `null`.
119
+
120
+ ## Check that closing cleans up
121
+
122
+ 1. Open the chat and send a text. The bot answers after about a second. Every third question fails, and the retry asks again.
123
+ 2. Send a text and close the chat before the answer arrives. No line appears, also when you open the chat again.
124
+ 3. Look at the box "Listeners on the bus": it shows `bus.get(event).length` for four events and updates when the chat opens or closes. Closed: only `chat/closeRequested` has a listener (the launcher). Open: `chat/sent`, `chat/messageAdded` and `chat/typing` have one each as well. That is the proof that nothing is left subscribed.
125
+ 4. Wait 10 s with the chat closed. The operator is not polled: no line from the operator appears.
126
+ 5. Press `Escape` with the chat closed. Nothing happens. Open the chat and press it again: it closes.
127
+ 6. Open the chat again. The old lines are back, and the polling runs.
128
+
129
+ `src/tests/chat-widget-example.test.ts` checks the same points without a browser.
130
+
131
+ ## Where to go next
132
+
133
+ - Show an unread count on the launcher while the chat is closed: a second module that stays mounted and listens only to `chat/messageAdded`.
134
+ - Replace the polling by a WebSocket in a spawned task: the abort closes it the same way (see [`examples/trades`](../examples/trades) for a socket under `retry` and `latest`).
135
+ - Watch the whole thing in the [trace](tracing.md): the module, the bot task, the retries and the polling are tasks of one tree.
package/docs/guide.md CHANGED
@@ -31,7 +31,7 @@ bus.emit("greet", "Bob"); // nobody listens
31
31
 
32
32
  Handlers run at once, in subscription order.
33
33
 
34
- `bus.once` is like `on`, but the handler runs at most once (it is removed before it is invoked). `bus.off("greet")` clears every listener of that event; `bus.off()` with no arguments clears **every** listener on the bus (including `"*"`). `ctx.off` is the same `off` as the bus’s — so `ctx.off()` also clears the whole bus.
34
+ `bus.once` is like `on`, but the handler runs at most once (it is removed before it is invoked). `bus.off("greet")` clears every listener of that event; `bus.off()` with no arguments clears **every** listener on the bus (including `"*"`). `ctx.off` is the same `off` as the bus’s — so `ctx.off()` also clears the whole bus. `bus.get("greet")` gives the listeners that event has right now; `bus.get("greet").length === 0` tells that nobody listens any more (`"*"` gives the any-event listeners, which a named event does not include; `bus.get()` with no name gives every listener of every event). It takes the event name as a string: for an action creator use `creator.type`.
35
35
 
36
36
  ```js
37
37
  import { createBus } from "nanoeffects";
package/docs/oop.md CHANGED
@@ -310,7 +310,7 @@ export const SearchService = extendBus(bus, SearchServiceBase);
310
310
 
311
311
  Things to know:
312
312
 
313
- - The event is a name, an action creator or a command, as for `bus.on`. For a creator or a command the payload of the method is checked against it.
313
+ - The event is a name or an action creator, as for `bus.on`. For a creator the payload of the method is checked against it.
314
314
  - The second argument, `wrap`, takes the bound handler and returns the one to subscribe: `latest`, or `(handler) => debounce(300, handler)`. A policy needs it: `latest(this.search)` wraps the function without `this`, and the bound handler is what `wrap` gets.
315
315
  - The subscriptions are made right after the constructor of `Base`. An event that is emitted inside that constructor does not reach them.
316
316
  - A subclass inherits them. A method that a subclass overrides is subscribed only if the override is decorated.
package/docs/package.md CHANGED
@@ -12,7 +12,7 @@ Imports are split so you only load what you use. `createBus` stays small; the re
12
12
  | `nanoeffects/context` | `contextPlugin`: your own fields (a store, a client) on `ctx` of every task, typed from the value |
13
13
  | `nanoeffects/signal` | `signalPlugin`: `emit(call, { signal })` ties the tasks of an emit to an outside `AbortSignal` |
14
14
  | `nanoeffects/oop` | `Emitter`, `extendBus`, `WithBus`, `On`, `Once`, `disposeBus`: the methods of a bus on a class, with the subscriptions of each instance ended by `dispose()` |
15
- | `nanoeffects/scope` | `scopePlugin`, `effect`: fields for one command, action, job, module or `emit` (the types `Scope`, `Effect`) |
15
+ | `nanoeffects/scope` | `scopePlugin`, `effect`: fields for one action, job, module or `emit` (the types `Scope`, `Effect`) |
16
16
  | `nanoeffects/redux` | `createEffectsMiddleware` |
17
17
  | `nanoeffects/observable` | `createObserve`, `Subject` |
18
18
  | `nanoeffects/react` | `reactPlugin`: adds the `useEmitter` hook to a bus (needs React 18 or newer) |
package/docs/starter.md CHANGED
@@ -46,7 +46,7 @@ Pass `--template` to skip the question. With npm, put `--` before the option.
46
46
  | `react` | The same handler with `useEmitter` and `useEventState`, and `usePolicy` for a debounced save in a component | You build a React app |
47
47
  | `vanilla-ts` | The `vanilla` app in TypeScript | You want the compiler to check events and handlers |
48
48
  | `react-ts` | The `react` app in TypeScript | You build a React app in TypeScript |
49
- | `domains-ts` | Commands, a job and modules, with the logic split into domain folders and React as a thin layer | You build an app with more than one feature and want the logic off the UI |
49
+ | `domains-ts` | Actions with handlers, a job and modules, with the logic split into domain folders and React as a thin layer | You build an app with more than one feature and want the logic off the UI |
50
50
  | `vue-ts` | The `react-ts` app in Vue 3, with `watchEmitter` and `useEventRef` from `nanoeffects/vue` | You build a Vue 3 app |
51
51
  | `svelte-ts` | The `react-ts` app in Svelte 5, with `eventStore` from `nanoeffects/svelte` | You build a Svelte app |
52
52
 
@@ -94,13 +94,13 @@ npm create nanoeffects@latest my-app -- --template domains-ts
94
94
  | ---- | --- |
95
95
  | `src/bus.ts` | Creates the bus. It imports no events and no domain. |
96
96
  | `src/app.ts` | Mounts the domains with `modulePlugin`. |
97
- | `src/domains/catalog/` | Searching products: `catalog.ts` (events and a command), `catalog-handlers.ts` (`debounce` and `latest`), `catalog-module.ts`. |
97
+ | `src/domains/catalog/` | Searching products: `catalog.ts` (events and an action with a handler), `catalog-handlers.ts` (`debounce` and `latest`), `catalog-module.ts`. |
98
98
  | `src/domains/checkout/` | Placing an order: `checkout.ts` (a job), `checkout-handlers.ts` (`leading` and `retry`), `checkout-store.ts` (state written from the job's outcomes), `checkout-module.ts`. |
99
99
  | `src/ui/` | The components. None of them calls the fake server in `src/api.ts`. |
100
100
 
101
101
  For a bigger example, a chat in which parts react to each other, see [Build a chat with entities, features, modules and a small server](chat-example.md). It has more layers than `domains-ts`: when domains must react to each other, it keeps them from importing each other by splitting them into entities and features.
102
102
 
103
- The commands, jobs and modules themselves are explained in [Actions, jobs and modules](actions.md). The template keeps found products in a Context provider with one `useEventState`, and the order state in a store of its own: `useEventState` is [not a store](react.md#show-the-last-value-of-an-event-as-state).
103
+ The actions, jobs and modules themselves are explained in [Actions, jobs and modules](actions.md). The template keeps found products in a Context provider with one `useEventState`, and the order state in a store of its own: `useEventState` is [not a store](react.md#show-the-last-value-of-an-event-as-state).
104
104
 
105
105
  ## Read the files
106
106
 
package/docs/tracing.md CHANGED
@@ -132,9 +132,9 @@ Every event of a task below another one has `rootId`, the id of the root of the
132
132
 
133
133
  **Problem.** A flow such as a checkout has several roots (a click, a timer, a store) and many branches. Parent ids give a tree, but you want to filter the log by the flow.
134
134
 
135
- **When.** One business flow runs through several commands, and you read the log by flow.
135
+ **When.** One business flow runs through several actions, and you read the log by flow.
136
136
 
137
- Give the flow a `traceGroupId` in the scope of the command, the job or the action that starts it. Every task that it starts through `ctx.emit` or `ctx.run` has the same `traceGroupId`, and so does every trace event of those tasks. A task that declares its own `traceGroupId` uses it for itself and for what it starts.
137
+ Give the flow a `traceGroupId` in the scope of the action or the job that starts it. Every task that it starts through `ctx.emit` or `ctx.run` has the same `traceGroupId`, and so does every trace event of those tasks. A task that declares its own `traceGroupId` uses it for itself and for what it starts.
138
138
 
139
139
  ```js
140
140
  import { createBus } from "nanoeffects";
@@ -202,7 +202,7 @@ The names work wherever an event or a listener is made:
202
202
  | A listener, `bus.on` and `bus.once` | `{ traceId }` as the third argument |
203
203
  | A listener of a module or of an instance of `extendBus` | the same third argument of its `on` and `once` |
204
204
  | A method with `@On` or `@Once` | `{ traceId }` in the decorator |
205
- | A command, a job or an action | `traceGroupId` in its scope |
205
+ | An action or a job | `traceGroupId` in its scope |
206
206
 
207
207
  ## Turn the names off
208
208
 
package/llms-full.txt CHANGED
@@ -48,7 +48,7 @@ bus.emit("greet", "Bob"); // nobody listens
48
48
 
49
49
  Handlers run at once, in subscription order.
50
50
 
51
- `bus.once` is like `on`, but the handler runs at most once (it is removed before it is invoked). `bus.off("greet")` clears every listener of that event; `bus.off()` with no arguments clears **every** listener on the bus (including `"*"`). `ctx.off` is the same `off` as the bus’s — so `ctx.off()` also clears the whole bus.
51
+ `bus.once` is like `on`, but the handler runs at most once (it is removed before it is invoked). `bus.off("greet")` clears every listener of that event; `bus.off()` with no arguments clears **every** listener on the bus (including `"*"`). `ctx.off` is the same `off` as the bus’s — so `ctx.off()` also clears the whole bus. `bus.get("greet")` gives the listeners that event has right now; `bus.get("greet").length === 0` tells that nobody listens any more (`"*"` gives the any-event listeners, which a named event does not include; `bus.get()` with no name gives every listener of every event). It takes the event name as a string: for an action creator use `creator.type`.
52
52
 
53
53
  ```js
54
54
  import { createBus } from "nanoeffects";
@@ -563,7 +563,7 @@ Pass `--template` to skip the question. With npm, put `--` before the option.
563
563
  | `react` | The same handler with `useEmitter` and `useEventState`, and `usePolicy` for a debounced save in a component | You build a React app |
564
564
  | `vanilla-ts` | The `vanilla` app in TypeScript | You want the compiler to check events and handlers |
565
565
  | `react-ts` | The `react` app in TypeScript | You build a React app in TypeScript |
566
- | `domains-ts` | Commands, a job and modules, with the logic split into domain folders and React as a thin layer | You build an app with more than one feature and want the logic off the UI |
566
+ | `domains-ts` | Actions with handlers, a job and modules, with the logic split into domain folders and React as a thin layer | You build an app with more than one feature and want the logic off the UI |
567
567
  | `vue-ts` | The `react-ts` app in Vue 3, with `watchEmitter` and `useEventRef` from `nanoeffects/vue` | You build a Vue 3 app |
568
568
  | `svelte-ts` | The `react-ts` app in Svelte 5, with `eventStore` from `nanoeffects/svelte` | You build a Svelte app |
569
569
 
@@ -611,13 +611,13 @@ npm create nanoeffects@latest my-app -- --template domains-ts
611
611
  | ---- | --- |
612
612
  | `src/bus.ts` | Creates the bus. It imports no events and no domain. |
613
613
  | `src/app.ts` | Mounts the domains with `modulePlugin`. |
614
- | `src/domains/catalog/` | Searching products: `catalog.ts` (events and a command), `catalog-handlers.ts` (`debounce` and `latest`), `catalog-module.ts`. |
614
+ | `src/domains/catalog/` | Searching products: `catalog.ts` (events and an action with a handler), `catalog-handlers.ts` (`debounce` and `latest`), `catalog-module.ts`. |
615
615
  | `src/domains/checkout/` | Placing an order: `checkout.ts` (a job), `checkout-handlers.ts` (`leading` and `retry`), `checkout-store.ts` (state written from the job's outcomes), `checkout-module.ts`. |
616
616
  | `src/ui/` | The components. None of them calls the fake server in `src/api.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, 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 actions, 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
 
@@ -706,7 +706,7 @@ Imports are split so you only load what you use. `createBus` stays small; the re
706
706
  | `nanoeffects/context` | `contextPlugin`: your own fields (a store, a client) on `ctx` of every task, typed from the value |
707
707
  | `nanoeffects/signal` | `signalPlugin`: `emit(call, { signal })` ties the tasks of an emit to an outside `AbortSignal` |
708
708
  | `nanoeffects/oop` | `Emitter`, `extendBus`, `WithBus`, `On`, `Once`, `disposeBus`: the methods of a bus on a class, with the subscriptions of each instance ended by `dispose()` |
709
- | `nanoeffects/scope` | `scopePlugin`, `effect`: fields for one command, action, job, module or `emit` (the types `Scope`, `Effect`) |
709
+ | `nanoeffects/scope` | `scopePlugin`, `effect`: fields for one action, job, module or `emit` (the types `Scope`, `Effect`) |
710
710
  | `nanoeffects/redux` | `createEffectsMiddleware` |
711
711
  | `nanoeffects/observable` | `createObserve`, `Subject` |
712
712
  | `nanoeffects/react` | `reactPlugin`: adds the `useEmitter` hook to a bus (needs React 18 or newer) |
@@ -2476,7 +2476,7 @@ If the `setup` of one module throws, the modules of that call that were already
2476
2476
  - **A module is known by its name.** Mounting a module whose name is taken replaces the mounted one (see [Hot reload](hmr.md)).
2477
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`.
2478
2478
  - A disposed app cannot mount modules.
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).
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 `actions`). 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).
2480
2480
  - **To mount by hand instead**, pass no modules: `modulePlugin()` starts nothing, and `app.mount(...)` starts the modules when you want.
2481
2481
  - Module context = task context plus `handle`, `use`, and `spawn`:
2482
2482
  - `handle` — give an action a handler (one that has none, or an override).
@@ -3867,7 +3867,7 @@ The code is in layers, and a file imports only from the layers below it. The com
3867
3867
 
3868
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.
3869
3869
 
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.
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 actions, never through its handlers or its store. All actions live in the events file. An action with no handler says that something happened, and any feature may listen: `rooms/selected` is heard by two. An action 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 action 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 an action with a handler 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.
3871
3871
 
3872
3872
  The layers come from [Feature-Sliced Design](https://feature-sliced.design/), reduced to the three that an app of this size needs.
3873
3873
 
@@ -3883,7 +3883,7 @@ A job sends `pending` when its handler starts and exactly one of `success`, `fai
3883
3883
  // message.js
3884
3884
  import { createJob } from "nanoeffects/actions";
3885
3885
 
3886
- // createJob — a command that also sends pending, success, fail and cancel
3886
+ // createJob — an action with a handler that also sends pending, success, fail and cancel
3887
3887
  export const loadHistory = createJob("messages/loadHistory");
3888
3888
  ```
3889
3889
 
@@ -4014,7 +4014,7 @@ export const goToResult = async (hit, ctx) => {
4014
4014
  ctx.emit("rooms/selected", hit.roomId);
4015
4015
  await loaded;
4016
4016
  }
4017
- // ctx.emit with a command — the message entity marks the message
4017
+ // ctx.emit with an action that has a handler — the message entity marks the message
4018
4018
  ctx.emit(markMessage(hit.id));
4019
4019
  };
4020
4020
  ```
@@ -4066,10 +4066,149 @@ Each entity and each feature that has state keeps it in a [Zustand](https://zust
4066
4066
 
4067
4067
  ## Where to go next
4068
4068
 
4069
- - The API of commands, jobs and modules: [Actions, jobs and modules](actions.md).
4069
+ - The API of actions, jobs and modules: [Actions, jobs and modules](actions.md).
4070
4070
  - Policies such as `latest` and `queue`: [Policies](policies.md). Effects such as `allSettled`, `take` and `poll`: [Effects](effects.md).
4071
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.
4072
4072
 
4073
+ ---
4074
+ Source: docs/chat-widget.md
4075
+
4076
+ # Build a chat widget that listens only while it is open
4077
+
4078
+ Use this page when a site has a chat with a bot behind a launcher button, and the chat must cost nothing while it is closed. Closed means no subscriptions on the bus, no polling, no keyboard listener, and no request still running. Opening turns all of it on, and closing turns all of it off in one call.
4079
+
4080
+ The code is in [`examples/chat-widget`](../examples/chat-widget): plain DOM and TypeScript, a fake bot in the browser, no server. Run it on [StackBlitz](https://stackblitz.com/github/nanoeffects/nanoeffects/tree/main/examples/chat-widget), or on your machine:
4081
+
4082
+ ```bash
4083
+ cd examples/chat-widget
4084
+ npm install
4085
+ npm run dev
4086
+ ```
4087
+
4088
+ It is a level D example: the logic lives in one module, and the page only mounts and unmounts it. For an app with several features see [Build a chat with entities, features, modules and a small server](chat-example.md).
4089
+
4090
+ ## Know what happens on open and on close
4091
+
4092
+ | Moment | What happens |
4093
+ | ------ | ------------ |
4094
+ | Open | The module is mounted: the bot handler is subscribed, the operator messages are polled, the `Escape` listener is added. The panel is drawn and listens to the bus. |
4095
+ | The user sends a text | The bot is asked, one text after another, with a time limit and a retry. |
4096
+ | `Escape` | The module emits `chat/closeRequested`; the launcher closes the chat. |
4097
+ | Close | The panel is removed and unsubscribed, the module is unmounted. A request in flight is aborted, the polling stops, the `Escape` listener goes away. |
4098
+
4099
+ ## Find the parts in the files
4100
+
4101
+ | File | Job |
4102
+ | ---- | --- |
4103
+ | `src/bus.ts` | The bus, and `modulePlugin()` with no module: the launcher mounts the module itself. |
4104
+ | `src/chat.ts` | The events only: `messageSent`, `messageAdded`, `typingChanged`, `closeRequested`. |
4105
+ | `src/chat-handlers.ts` | `askBot` (`timeout` and `retry` inside) and `watchOperator` (`poll`): plain `async` functions that know nothing about the page. |
4106
+ | `src/chat-module.ts` | The list of what is alive while the chat is open. |
4107
+ | `src/panel.ts` | The view: shows the lines, sends the texts, returns one function that removes it. |
4108
+ | `src/main.ts` | The launcher: the only file that knows both the module and the panel. |
4109
+
4110
+ ## Bind everything in one module
4111
+
4112
+ A module is the list of what is alive while the chat is open. When it unmounts, the bus undoes every line of it.
4113
+
4114
+ <!-- docs-check: skip - the other files of the example are in examples/chat-widget -->
4115
+ ```js
4116
+ // chat-module.js
4117
+ import { createModule } from "nanoeffects/actions";
4118
+ import { queue } from "nanoeffects/policies";
4119
+ import { closeRequested, messageSent } from "./chat";
4120
+ import { askBot, watchOperator } from "./chat-handlers";
4121
+
4122
+ // a task that stays pending until the module unmounts; its signal then aborts
4123
+ const untilAborted = (ctx) =>
4124
+ new Promise((resolve) => {
4125
+ ctx.signal.addEventListener("abort", resolve, { once: true });
4126
+ });
4127
+
4128
+ // a DOM listener that lives as long as the module: `signal` removes it when it aborts
4129
+ const listenToEscape = async (_, ctx) => {
4130
+ const onKey = (event) => {
4131
+ if (event.key === "Escape") {
4132
+ ctx.emit(closeRequested());
4133
+ }
4134
+ };
4135
+ document.addEventListener("keydown", onKey, { signal: ctx.signal });
4136
+ await untilAborted(ctx);
4137
+ };
4138
+
4139
+ export const chatModule = createModule("chat", (module) => {
4140
+ // module.run — runs askBot as a child task of the module, so an unmount aborts it
4141
+ const answer = (text) => module.run(askBot, text);
4142
+ // on + queue — answer the texts one after another, in the order they were sent
4143
+ module.on(messageSent, queue(answer));
4144
+ // spawn — a background task that lives as long as the module
4145
+ module.spawn(watchOperator);
4146
+ module.spawn(listenToEscape);
4147
+ });
4148
+ ```
4149
+
4150
+ Three details that make the cleanup complete:
4151
+
4152
+ - **`module.run`, not `askBot` directly.** A handler given to `module.on` runs as a child of whoever emitted the event (here the panel), so unmounting stops listening but does not abort a request that is already running. Running it with `module.run` makes it a child of the module.
4153
+ - **`queue`, not `latest`.** `queue` keeps the order when the user sends two texts fast. [`latest`](policies.md) would abort the first answer, which is wrong for a conversation.
4154
+ - **A DOM listener with `signal`.** `addEventListener` takes a `signal` option and removes the listener when it aborts. The task stays pending until then, because a task that has finished is not aborted again.
4155
+
4156
+ ## Open and close from the launcher
4157
+
4158
+ `app.mount` returns the function that unmounts the module. The launcher puts it together with the one that removes the panel.
4159
+
4160
+ <!-- docs-check: skip - the other files of the example are in examples/chat-widget -->
4161
+ ```js
4162
+ // main.js
4163
+ import { app, bus } from "./bus";
4164
+ import { closeRequested } from "./chat";
4165
+ import { chatModule } from "./chat-module";
4166
+ import { renderPanel } from "./panel";
4167
+
4168
+ const button = document.querySelector("#chat-launcher");
4169
+ let close = null; // set while the chat is open
4170
+
4171
+ const open = () => {
4172
+ const stopModule = app.mount(chatModule); // subscriptions, polling and Escape start
4173
+ const stopPanel = renderPanel(document.body);
4174
+ close = () => {
4175
+ stopPanel();
4176
+ stopModule(); // everything the module started ends, a running request included
4177
+ close = null;
4178
+ };
4179
+ };
4180
+
4181
+ button.addEventListener("click", () => {
4182
+ if (close) {
4183
+ close();
4184
+ } else {
4185
+ open();
4186
+ }
4187
+ });
4188
+
4189
+ // the launcher stays subscribed: it must hear Escape to close the chat
4190
+ bus.on(closeRequested, () => close?.());
4191
+ ```
4192
+
4193
+ The launcher's own `bus.on` is the only subscription that lives while the chat is closed, and it does nothing then: `close` is `null`.
4194
+
4195
+ ## Check that closing cleans up
4196
+
4197
+ 1. Open the chat and send a text. The bot answers after about a second. Every third question fails, and the retry asks again.
4198
+ 2. Send a text and close the chat before the answer arrives. No line appears, also when you open the chat again.
4199
+ 3. Look at the box "Listeners on the bus": it shows `bus.get(event).length` for four events and updates when the chat opens or closes. Closed: only `chat/closeRequested` has a listener (the launcher). Open: `chat/sent`, `chat/messageAdded` and `chat/typing` have one each as well. That is the proof that nothing is left subscribed.
4200
+ 4. Wait 10 s with the chat closed. The operator is not polled: no line from the operator appears.
4201
+ 5. Press `Escape` with the chat closed. Nothing happens. Open the chat and press it again: it closes.
4202
+ 6. Open the chat again. The old lines are back, and the polling runs.
4203
+
4204
+ `src/tests/chat-widget-example.test.ts` checks the same points without a browser.
4205
+
4206
+ ## Where to go next
4207
+
4208
+ - Show an unread count on the launcher while the chat is closed: a second module that stays mounted and listens only to `chat/messageAdded`.
4209
+ - Replace the polling by a WebSocket in a spawned task: the abort closes it the same way (see [`examples/trades`](../examples/trades) for a socket under `retry` and `latest`).
4210
+ - Watch the whole thing in the [trace](tracing.md): the module, the bot task, the retries and the polling are tasks of one tree.
4211
+
4073
4212
  ---
4074
4213
  Source: docs/vue.md
4075
4214
 
@@ -4903,7 +5042,7 @@ export const SearchService = extendBus(bus, SearchServiceBase);
4903
5042
 
4904
5043
  Things to know:
4905
5044
 
4906
- - The event is a name, an action creator or a command, as for `bus.on`. For a creator or a command the payload of the method is checked against it.
5045
+ - The event is a name or an action creator, as for `bus.on`. For a creator the payload of the method is checked against it.
4907
5046
  - The second argument, `wrap`, takes the bound handler and returns the one to subscribe: `latest`, or `(handler) => debounce(300, handler)`. A policy needs it: `latest(this.search)` wraps the function without `this`, and the bound handler is what `wrap` gets.
4908
5047
  - The subscriptions are made right after the constructor of `Base`. An event that is emitted inside that constructor does not reach them.
4909
5048
  - A subclass inherits them. A method that a subclass overrides is subscribed only if the override is decorated.
@@ -6055,9 +6194,9 @@ Every event of a task below another one has `rootId`, the id of the root of the
6055
6194
 
6056
6195
  **Problem.** A flow such as a checkout has several roots (a click, a timer, a store) and many branches. Parent ids give a tree, but you want to filter the log by the flow.
6057
6196
 
6058
- **When.** One business flow runs through several commands, and you read the log by flow.
6197
+ **When.** One business flow runs through several actions, and you read the log by flow.
6059
6198
 
6060
- Give the flow a `traceGroupId` in the scope of the command, the job or the action that starts it. Every task that it starts through `ctx.emit` or `ctx.run` has the same `traceGroupId`, and so does every trace event of those tasks. A task that declares its own `traceGroupId` uses it for itself and for what it starts.
6199
+ Give the flow a `traceGroupId` in the scope of the action or the job that starts it. Every task that it starts through `ctx.emit` or `ctx.run` has the same `traceGroupId`, and so does every trace event of those tasks. A task that declares its own `traceGroupId` uses it for itself and for what it starts.
6061
6200
 
6062
6201
  ```js
6063
6202
  import { createBus } from "nanoeffects";
@@ -6125,7 +6264,7 @@ The names work wherever an event or a listener is made:
6125
6264
  | A listener, `bus.on` and `bus.once` | `{ traceId }` as the third argument |
6126
6265
  | A listener of a module or of an instance of `extendBus` | the same third argument of its `on` and `once` |
6127
6266
  | A method with `@On` or `@Once` | `{ traceId }` in the decorator |
6128
- | A command, a job or an action | `traceGroupId` in its scope |
6267
+ | An action or a job | `traceGroupId` in its scope |
6129
6268
 
6130
6269
  ## Turn the names off
6131
6270
 
package/llms.txt CHANGED
@@ -38,6 +38,7 @@ Rules that generated code often gets wrong:
38
38
  - [Stop work on a server when the client leaves](docs/server.md): one `AbortController` for each request or socket, `signalPlugin`, a clean shutdown, and a queue worker; Fastify examples
39
39
  - [Build a sign-up form in React](docs/react.md): a form with a username check, a submit that cannot fire twice, and retry; `useEventState` shows an event as state, `usePolicy` keeps a debounced handler between renders
40
40
  - [Build a chat with entities, features, modules and a small server](docs/chat-example.md): rooms, a history under `latest`, a send under `queue` and `retry`, a search with `allSettled` and `timeout`, new messages polled with `poll`
41
+ - [Build a chat widget that listens only while it is open](docs/chat-widget.md): a bot widget for a site; opening mounts a module (handlers, polling, Escape), closing unmounts it and aborts running requests
41
42
  - [Use nanoeffects in a Vue app](docs/vue.md): `vuePlugin` adds `watchEmitter` (a value of a component to the bus) and `useEventRef` (an event as a ref)
42
43
  - [Use nanoeffects in a Svelte app](docs/svelte.md): `sveltePlugin` adds `eventStore` (an event as a Svelte store)
43
44
  - [Context fields](docs/context.md): put a store or client on every task
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nanoeffects",
3
- "version": "0.27.0",
4
- "description": "Cancel stale async work and fix race conditions in event handlers: a typed event bus where every handler is a task with an AbortSignal, plus latest, debounce, throttle, queue, retry, timeout and poll. About 1.4 kB gzip.",
3
+ "version": "0.28.0",
4
+ "description": "Business logic and side effects outside UI components, as plain async functions. Tiny, no dependencies.",
5
5
  "keywords": [
6
6
  "event-bus",
7
7
  "event-emitter",
@@ -226,7 +226,7 @@
226
226
  {
227
227
  "name": "actions (all)",
228
228
  "path": "dist/actions.js",
229
- "limit": "3.3 kB"
229
+ "limit": "3.31 kB"
230
230
  },
231
231
  {
232
232
  "name": "match (plugin)",
@@ -1 +0,0 @@
1
- import{d as e,n as t,o as n,r}from"./context-UcmrAIhP.js";import{n as i}from"./promise-CSLpzwSR.js";import{n as a,t as o}from"./event-0RIBGYqS.js";const s=()=>{let e={},t={},n=n=>(t[n]=void 0,e[n]??=/* @__PURE__ */ new Map,e[n]),r=(e,t)=>{let r=n(e),i=r.get(t)??0;i>1?r.set(t,i-1):t?r.delete(t):r.clear()};return{get:n=>(t[n]??=[...e[n]??[]].flatMap(([e,t])=>Array(t).fill(e)),t[n]),add:(e,t)=>{let i=n(e);return i.set(t,(i.get(t)??0)+1),()=>{r(e,t)}},remove:r,clear:()=>{e={},t={}}}},c=Symbol(),l=(e,t,n)=>{let r={type:t,payload:n};return Object.defineProperty(r,c,{value:e,enumerable:!1}),r},u=e=>{if(typeof e==`function`)return e;if(typeof e==`object`&&e&&c in e)return e[c]},d=(e,t,n)=>Reflect.apply(e,null,[t,n]),f=(c={})=>{let l=s(),f=(e,t)=>l.add(o(e),t),p=(e,t)=>{let n=f(e,(e,r)=>(n(),t(e,r)));return n},m=(e,t)=>{e===void 0?l.clear():l.remove(o(e),t)},h=(n,a)=>{let{onError:o}=c,{ctx:s}=t({event:a,bus:g,tracer:c.trace,parent:{ref:r()}});if(!o){n(a.payload,s);return}let l=t=>{try{o(t,a)}catch(t){e(`onError failed`,t)}};try{let e=n(a.payload,s);i(e)&&Promise.resolve(e).catch(l)}catch(e){l(e)}},g={on:f,once:p,off:m,emit:(e,i)=>{let s=o(e),f=a(e,i),p={type:s,payload:f},m=u(e);m||=g.actions?.bound?.(s);let _=[...l.get(s),...l.get(`*`)];_.length===0&&!m&&c.onUnhandled?.(p);for(let e of _)h(e,p);if(m){let{ctx:e}=t({event:p,bus:g,tracer:c.trace,parent:{ref:r()}}),i=g.actions?.run??d;return n(e,`command`,()=>i(m,f,e))}},use:e=>e(g),ctx:c.context,trace:c.trace};return g};export{f as n,l as r,u as t};
@@ -1 +0,0 @@
1
- const e=e=>typeof e==`string`?e:e.type,t=(e,t)=>typeof e==`object`?e.payload:t;export{t as n,e as t};
@@ -1 +0,0 @@
1
- import{a as e,l as t,n,r}from"./context-_HWcdjZf.js";import{n as i}from"./promise-CSLpzwSR.js";const a=e=>typeof e==`string`?e:e.type,o=(e,t)=>typeof e==`object`?e.payload:t,s=()=>{let e={},t={},n=n=>(t[n]=void 0,e[n]??=/* @__PURE__ */ new Map,e[n]),r=(e,t)=>{let r=n(e),i=r.get(t)??0;i>1?r.set(t,i-1):t?r.delete(t):r.clear()};return{get:n=>(t[n]??=[...e[n]??[]].flatMap(([e,t])=>Array(t).fill(e)),t[n]),add:(e,t)=>{let i=n(e);return i.set(t,(i.get(t)??0)+1),()=>{r(e,t)}},remove:r,clear:()=>{e={},t={}}}},c=Symbol(),l=(e,t,n)=>{let r={type:t,payload:n};return Object.defineProperty(r,c,{value:e,enumerable:!1}),r},u=e=>{if(typeof e==`function`)return e;if(typeof e==`object`&&e&&c in e)return e[c]},d=(e,t,n)=>Reflect.apply(e,null,[t,n]),f=(c={})=>{let l=s(),f=(e,t)=>l.add(a(e),t),p=(e,t)=>{let n=f(e,(e,r)=>(n(),t(e,r)));return n},m=(e,t)=>{e===void 0?l.clear():l.remove(a(e),t)},h=(e,a)=>{let{onError:o}=c,{ctx:s}=n({event:a,bus:g,tracer:c.trace,parent:{ref:r()}});if(!o){e(a.payload,s);return}let l=e=>{try{o(e,a)}catch(e){t(`onError failed`,e)}};try{let t=e(a.payload,s);i(t)&&Promise.resolve(t).catch(l)}catch(e){l(e)}},g={on:f,once:p,off:m,emit:(t,i)=>{let s=a(t),f=o(t,i),p={type:s,payload:f},m=u(t);m||=g.actions?.bound?.(s);let _=[...l.get(s),...l.get(`*`)];_.length===0&&!m&&c.onUnhandled?.(p);for(let e of _)h(e,p);if(m){let{ctx:t}=n({event:p,bus:g,tracer:c.trace,parent:{ref:r()}}),i=g.actions?.run??d;return e(t,`command`,()=>i(m,f,t))}},use:e=>e(g),ctx:c.context,trace:c.trace};return g};export{l as n,a as r,f as t};
@@ -1 +0,0 @@
1
- const e=e=>typeof e!=`object`||!e?!1:`then`in e&&typeof e.then==`function`,t=(t,n)=>e(t)?Promise.resolve(t).finally(n):(n(),t);export{e as n,t};