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.
- package/README.md +26 -24
- package/dist/{action-creator-D_c223C7.d.ts → action-creator-CcAEvUnw.d.ts} +39 -0
- package/dist/actions.d.ts +3 -3
- package/dist/actions.js +1 -1
- package/dist/bus-DlpEluzd.js +1 -0
- package/dist/{context-types-nGtzhjxD.d.ts → context-types-hCKgU8sC.d.ts} +1 -1
- package/dist/context.d.ts +2 -2
- package/dist/dev.d.ts +2 -2
- package/dist/dev.js +1 -1
- package/dist/effects.d.ts +2 -2
- package/dist/event-DVcW0mME.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/match.d.ts +1 -1
- package/dist/{module-WX0Gngnt.d.ts → module-C_oEXw1n.d.ts} +2 -2
- package/dist/notrace/{action-creator-Bux7Zzsl.d.ts → action-creator-DxOvVo8I.d.ts} +39 -0
- package/dist/notrace/actions.d.ts +2 -2
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/bus-DmKn4SDD.js +1 -0
- package/dist/notrace/effects.d.ts +2 -2
- package/dist/notrace/index.d.ts +1 -1
- package/dist/notrace/index.js +1 -1
- package/dist/notrace/{plugin-CATXOxtj.d.ts → plugin-lJjjd-SN.d.ts} +1 -1
- package/dist/notrace/policies.d.ts +1 -1
- package/dist/notrace/policies.js +1 -1
- package/dist/notrace/redux.d.ts +1 -1
- package/dist/observable.d.ts +1 -1
- package/dist/oop.d.ts +1 -1
- package/dist/oop.js +1 -1
- package/dist/{plugin-vvkmIIz2.d.ts → plugin-CuvIgjnX.d.ts} +2 -2
- package/dist/policies.d.ts +2 -2
- package/dist/react.d.ts +2 -2
- package/dist/redux.d.ts +1 -1
- package/dist/scope.d.ts +2 -2
- package/dist/signal.d.ts +1 -1
- package/dist/signal.js +1 -1
- package/dist/svelte.d.ts +1 -1
- package/dist/trace.d.ts +1 -1
- package/dist/{types-Bf-LX_tE.d.ts → types-FV8fuvI9.d.ts} +2 -2
- package/dist/vue.d.ts +1 -1
- package/dist/{with-policy-CPjx0sLm.d.ts → with-policy-3bhFLEXs.d.ts} +1 -1
- package/docs/README.md +1 -0
- package/docs/actions.md +1 -1
- package/docs/chat-example.md +4 -4
- package/docs/chat-widget.md +135 -0
- package/docs/guide.md +1 -1
- package/docs/oop.md +1 -1
- package/docs/package.md +1 -1
- package/docs/starter.md +3 -3
- package/docs/tracing.md +3 -3
- package/llms-full.txt +153 -14
- package/llms.txt +1 -0
- package/package.json +3 -3
- package/dist/bus-DQDHUN5e.js +0 -1
- package/dist/event-0RIBGYqS.js +0 -1
- package/dist/notrace/bus-CGi2lrd5.js +0 -1
- 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 `
|
|
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).
|
package/docs/chat-example.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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` |
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
|
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` |
|
|
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
|
|
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
|
|
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
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
|
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.
|
|
4
|
-
"description": "
|
|
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.
|
|
229
|
+
"limit": "3.31 kB"
|
|
230
230
|
},
|
|
231
231
|
{
|
|
232
232
|
"name": "match (plugin)",
|
package/dist/bus-DQDHUN5e.js
DELETED
|
@@ -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};
|
package/dist/event-0RIBGYqS.js
DELETED
|
@@ -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};
|