nanoeffects 0.3.0 → 0.4.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 +50 -55
- package/dist/{action-Bdfp-Wvd.d.ts → action-creator-D48fp13T.d.ts} +64 -72
- package/dist/actions.d.ts +40 -6
- package/dist/actions.js +1 -1
- package/dist/bus-Cho463Sq.js +1 -0
- package/dist/dev.d.ts +2 -2
- package/dist/effects.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/{module-I1ixyJzx.d.ts → module-DVX8aYlq.d.ts} +9 -8
- package/dist/notrace/{action-Bdfp-Wvd.d.ts → action-creator-D48fp13T.d.ts} +64 -72
- package/dist/notrace/actions.d.ts +47 -12
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/bus-B12SvxLv.js +1 -0
- package/dist/notrace/effects.d.ts +1 -1
- package/dist/notrace/index.d.ts +2 -2
- package/dist/notrace/index.js +1 -1
- package/dist/notrace/policies.d.ts +1 -1
- package/dist/notrace/redux.d.ts +1 -1
- package/dist/notrace/redux.js +1 -1
- package/dist/observable.d.ts +1 -1
- package/dist/policies.d.ts +1 -1
- package/dist/redux.d.ts +1 -1
- package/dist/redux.js +1 -1
- package/dist/trace.d.ts +1 -1
- package/docs/README.md +24 -9
- package/docs/actions.md +117 -41
- package/docs/context.md +27 -8
- package/docs/effects.md +152 -73
- package/docs/errors.md +23 -10
- package/docs/guide.md +236 -129
- package/docs/hmr.md +25 -13
- package/docs/observable.md +10 -9
- package/docs/package.md +14 -0
- package/docs/policies.md +92 -30
- package/docs/semantics.md +5 -3
- package/docs/stores.md +96 -22
- package/docs/tracing.md +69 -54
- package/package.json +5 -4
- package/dist/bus-CRnlwmQ4.js +0 -1
- package/dist/notrace/bus-Dx0l9X6h.js +0 -1
- package/dist/notrace/event-0RIBGYqS.js +0 -1
package/docs/guide.md
CHANGED
|
@@ -1,229 +1,336 @@
|
|
|
1
1
|
# Guide
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Climb one step at a time: small bus → policies → effects → commands and modules. Stop when your project has what it needs.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
| Your project | Stop after |
|
|
6
|
+
| ------------ | ---------- |
|
|
7
|
+
| Widget / prototype | §1–2 |
|
|
8
|
+
| SPA with async UI | §3–4 |
|
|
9
|
+
| Unreliable network / long flows | §5–6 |
|
|
10
|
+
| Domain logic off the UI | §7–9 |
|
|
6
11
|
|
|
7
|
-
|
|
12
|
+
Runnable code for every step: [`examples/guide.ts`](../examples/guide.ts).
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
## 1. Create a bus
|
|
15
|
+
|
|
16
|
+
`on` subscribes. `emit` calls the subscribers. `on` returns the unsubscribe function.
|
|
17
|
+
|
|
18
|
+
```js
|
|
10
19
|
import { createBus } from "nanoeffects";
|
|
11
20
|
|
|
12
|
-
const bus = createBus();
|
|
21
|
+
const bus = createBus(); // event bus; each handler call is a task
|
|
13
22
|
|
|
14
|
-
const unbind = bus.on("greet", (name) =>
|
|
23
|
+
const unbind = bus.on("greet", (name) => {
|
|
24
|
+
console.log(`hello ${name}`);
|
|
25
|
+
});
|
|
15
26
|
|
|
16
27
|
bus.emit("greet", "Ada"); // hello Ada
|
|
17
28
|
unbind();
|
|
18
|
-
bus.emit("greet", "Bob"); // nobody listens
|
|
29
|
+
bus.emit("greet", "Bob"); // nobody listens
|
|
19
30
|
```
|
|
20
31
|
|
|
21
|
-
|
|
32
|
+
Handlers run at once, in subscription order.
|
|
22
33
|
|
|
23
|
-
|
|
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.
|
|
24
35
|
|
|
25
|
-
|
|
36
|
+
```js
|
|
37
|
+
import { createBus } from "nanoeffects";
|
|
26
38
|
|
|
27
|
-
|
|
28
|
-
type Events = {
|
|
29
|
-
greet: string;
|
|
30
|
-
moved: { x: number; y: number };
|
|
31
|
-
reset: void;
|
|
32
|
-
};
|
|
39
|
+
const bus = createBus();
|
|
33
40
|
|
|
34
|
-
|
|
41
|
+
// once — runs at most once, then unsubscribes
|
|
42
|
+
bus.once("ready", () => console.log("ready"));
|
|
43
|
+
bus.emit("ready"); // ready
|
|
44
|
+
bus.emit("ready"); // nobody listens
|
|
35
45
|
|
|
36
|
-
bus.on("
|
|
37
|
-
bus.
|
|
38
|
-
bus.emit("reset");
|
|
39
|
-
bus.emit("moved", { x: "1" }); // error: x is a number
|
|
40
|
-
bus.emit("jumped", 1); // error: there is no such event
|
|
46
|
+
bus.on("tick", () => {});
|
|
47
|
+
bus.off(); // clear every subscription
|
|
41
48
|
```
|
|
42
49
|
|
|
43
|
-
|
|
50
|
+
## 2. Use the handler context
|
|
44
51
|
|
|
45
|
-
|
|
52
|
+
The second argument is the **task** context for this call. Emit further events through it. Pass `ctx.signal` to `fetch` when you cancel work.
|
|
46
53
|
|
|
47
|
-
|
|
54
|
+
```js
|
|
55
|
+
import { createBus } from "nanoeffects";
|
|
56
|
+
|
|
57
|
+
const bus = createBus();
|
|
48
58
|
|
|
49
|
-
```ts
|
|
50
59
|
bus.on("moved", ({ x, y }, ctx) => {
|
|
51
|
-
if (x < 0)
|
|
60
|
+
if (x < 0) {
|
|
61
|
+
ctx.emit("outOfBounds", { x, y });
|
|
62
|
+
}
|
|
52
63
|
});
|
|
53
64
|
```
|
|
54
65
|
|
|
55
|
-
|
|
66
|
+
`ctx` has `signal`, `abort`, `emit`, `on`, and `run`. An aborted task stops emitting and delivers no result.
|
|
56
67
|
|
|
57
|
-
##
|
|
68
|
+
## 3. Abort the previous run with `latest`
|
|
58
69
|
|
|
59
|
-
|
|
70
|
+
A handler is often called again before the last call finished (typing, double-clicks). A **policy** wraps the handler and decides what happens then. Start with `latest`: a newer call aborts the older one through `signal`.
|
|
60
71
|
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
bus.emit({ type: "moved", payload: { x: 2, y: 2 } }); // an object, the shape of a Redux action
|
|
65
|
-
|
|
66
|
-
const moved = createAction("moved");
|
|
67
|
-
bus.emit(moved({ x: 3, y: 2 })); // what an action creator makes is such an object
|
|
68
|
-
bus.on(moved, ({ x }) => console.log(x)); // and it names the event in `on` too
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
The third way is a function. A function with a `type` is a **command**: `emit` tells the subscribers of its type, then runs it, and **returns what it returns**.
|
|
72
|
-
|
|
73
|
-
```ts
|
|
74
|
-
import { createAction, createCommand } from "nanoeffects/actions";
|
|
72
|
+
```js
|
|
73
|
+
import { createBus } from "nanoeffects";
|
|
74
|
+
import { latest } from "nanoeffects/policies";
|
|
75
75
|
|
|
76
|
-
const
|
|
76
|
+
const bus = createBus();
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
// latest — a newer run aborts the older one
|
|
79
|
+
bus.on(
|
|
80
|
+
"search",
|
|
81
|
+
latest(async (query, { signal }) => {
|
|
82
|
+
await api.search(query, { signal });
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
79
85
|
```
|
|
80
86
|
|
|
81
|
-
|
|
87
|
+
Same `on`, same handler shape. More in [Policies](policies.md).
|
|
82
88
|
|
|
83
|
-
##
|
|
84
|
-
|
|
85
|
-
A command made with a function carries its handler, as above. More often it is only a contract, a name with the types of the request and the result, and the code that does the work is registered separately: in another file, or a fake one in a test.
|
|
86
|
-
|
|
87
|
-
```ts
|
|
88
|
-
import { createCommand, commandsPlugin } from "nanoeffects/actions";
|
|
89
|
+
## 4. Pick another policy when overlap differs
|
|
89
90
|
|
|
90
|
-
|
|
91
|
+
| Policy | When calls overlap |
|
|
92
|
+
| ------ | ------------------ |
|
|
93
|
+
| `latest` | previous call is aborted |
|
|
94
|
+
| `leading` | new calls drop while one runs |
|
|
95
|
+
| `debounce` | runs after a pause |
|
|
96
|
+
| `throttle` | at most once per window |
|
|
97
|
+
| `queue` | one after another |
|
|
91
98
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
bus.emit(getUser, "1"); // throws: No handler for command "user/get"
|
|
96
|
-
|
|
97
|
-
const unregister = commands.handle(getUser, (id) => api.user(id));
|
|
98
|
-
const user = await bus.emit(getUser, "1"); // what the handler returned
|
|
99
|
-
|
|
100
|
-
unregister(); // the command has no handler again
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
- `handle` returns the function that removes the handler, like `on`.
|
|
104
|
-
- If a command has several handlers, the one registered last runs. A handler registered here wins over the one the command was made with.
|
|
105
|
-
- Each bus has its own registry, so every test can bring its own handlers.
|
|
106
|
-
- A [module](#10-modules) registers handlers for itself and removes them when it is unmounted.
|
|
99
|
+
```js
|
|
100
|
+
import { createBus } from "nanoeffects";
|
|
101
|
+
import { debounce, latest, queue, throttle } from "nanoeffects/policies";
|
|
107
102
|
|
|
108
|
-
|
|
103
|
+
const bus = createBus();
|
|
109
104
|
|
|
110
|
-
|
|
105
|
+
// throttle — at most once per 100 ms
|
|
106
|
+
bus.on("scroll", throttle(100, (position) => {
|
|
107
|
+
updateScrollbar(position);
|
|
108
|
+
}));
|
|
111
109
|
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
// debounce — run after 300 ms of quiet
|
|
111
|
+
bus.on("typed", debounce(300, (text) => {
|
|
112
|
+
validate(text);
|
|
113
|
+
}));
|
|
114
114
|
|
|
115
|
-
//
|
|
115
|
+
// queue — one save after another, in order
|
|
116
116
|
bus.on(
|
|
117
|
-
"
|
|
118
|
-
|
|
119
|
-
await api.
|
|
117
|
+
"save",
|
|
118
|
+
queue(async (draft, { signal }) => {
|
|
119
|
+
await api.save(draft, { signal });
|
|
120
120
|
}),
|
|
121
121
|
);
|
|
122
|
-
```
|
|
123
122
|
|
|
124
|
-
|
|
123
|
+
// nest: wait for quiet, then keep only the latest search
|
|
124
|
+
const searchLatest = latest(async (query, { signal }) => {
|
|
125
|
+
await api.search(query, { signal });
|
|
126
|
+
});
|
|
127
|
+
bus.on("search", debounce(300, searchLatest));
|
|
128
|
+
```
|
|
125
129
|
|
|
126
|
-
|
|
130
|
+
Full options: [Policies](policies.md).
|
|
127
131
|
|
|
128
|
-
|
|
129
|
-
| ---------- | ------------------------------------------- |
|
|
130
|
-
| `latest` | the previous call is aborted |
|
|
131
|
-
| `leading` | new calls are dropped while one runs |
|
|
132
|
-
| `debounce` | runs after a pause in the calls |
|
|
133
|
-
| `throttle` | runs at most once in a time window |
|
|
134
|
-
| `queue` | one after another, in order |
|
|
132
|
+
## 5. Add retry and timeout
|
|
135
133
|
|
|
136
|
-
|
|
134
|
+
An **effect** adds what an async handler usually needs. `retry` and `timeout` wrap a handler like a policy, so they go into `on` with no `ctx` in their arguments.
|
|
137
135
|
|
|
138
|
-
```
|
|
139
|
-
import {
|
|
136
|
+
```js
|
|
137
|
+
import { createBus } from "nanoeffects";
|
|
138
|
+
import { retry, timeout } from "nanoeffects/effects";
|
|
139
|
+
import { latest } from "nanoeffects/policies";
|
|
140
140
|
|
|
141
|
-
bus
|
|
142
|
-
bus.on("typed", debounce(300, (text) => validate(text)));
|
|
143
|
-
bus.on("save", queue(async (draft, { signal }) => api.save(draft, { signal })));
|
|
141
|
+
const bus = createBus();
|
|
144
142
|
|
|
145
|
-
|
|
146
|
-
bus.on("search", debounce(300, latest(async (query, { signal }) => api.search(query, { signal }))));
|
|
147
|
-
```
|
|
143
|
+
const search = (query, { signal }) => api.search(query, { signal });
|
|
148
144
|
|
|
149
|
-
|
|
145
|
+
// timeout — fail one try after 5 s
|
|
146
|
+
const searchOnce = timeout(5000, search);
|
|
147
|
+
// retry — up to 3 tries
|
|
148
|
+
const searchWithRetry = retry(searchOnce, { times: 3 });
|
|
150
149
|
|
|
151
|
-
|
|
150
|
+
// latest — a newer typed query aborts the whole retry chain
|
|
151
|
+
bus.on("typed", latest(searchWithRetry));
|
|
152
|
+
```
|
|
152
153
|
|
|
153
|
-
|
|
154
|
+
When the handler itself needs the result, `ctx.run` runs the wrapped function as a child task (aborted with the parent):
|
|
154
155
|
|
|
155
|
-
```
|
|
156
|
+
```js
|
|
157
|
+
import { createBus } from "nanoeffects";
|
|
156
158
|
import { retry, timeout } from "nanoeffects/effects";
|
|
159
|
+
import { latest } from "nanoeffects/policies";
|
|
157
160
|
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
// 5 seconds for each try, up to 3 tries, and a newer query supersedes the whole thing
|
|
161
|
-
bus.on("typed", latest(retry(timeout(5000, search), { times: 3 })));
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
When the handler needs the result, `ctx.run` runs the wrapped function as a child task: it is aborted with the handler, and it is the one place where `ctx` is written.
|
|
161
|
+
const bus = createBus();
|
|
162
|
+
const search = (query, { signal }) => api.search(query, { signal });
|
|
165
163
|
|
|
166
|
-
```ts
|
|
167
164
|
bus.on(
|
|
168
165
|
"search",
|
|
169
166
|
latest(async (query, ctx) => {
|
|
170
|
-
const
|
|
167
|
+
const load = retry(timeout(5000, search), { times: 3 });
|
|
168
|
+
const items = await ctx.run(load, query); // child task; aborted with this one
|
|
171
169
|
ctx.emit("results", items);
|
|
172
170
|
}),
|
|
173
171
|
);
|
|
174
172
|
```
|
|
175
173
|
|
|
176
|
-
For one call in the middle of a
|
|
174
|
+
For one call in the middle of a body: `await retry(ctx, () => api.search(q, { signal: ctx.signal }))`. More in [Effects](effects.md).
|
|
177
175
|
|
|
178
|
-
##
|
|
176
|
+
## 6. Run work in parallel or wait for an event
|
|
179
177
|
|
|
180
|
-
```
|
|
178
|
+
```js
|
|
179
|
+
import { createBus } from "nanoeffects";
|
|
181
180
|
import { all, take } from "nanoeffects/effects";
|
|
182
181
|
|
|
182
|
+
const bus = createBus();
|
|
183
|
+
|
|
184
|
+
// all — run together; one failure cancels the rest
|
|
183
185
|
bus.on("search", async (id, ctx) => {
|
|
184
186
|
const { user, posts } = await all(ctx, {
|
|
185
187
|
user: ({ signal }) => api.user(id, { signal }),
|
|
186
188
|
posts: ({ signal }) => api.posts(id, { signal }),
|
|
187
189
|
});
|
|
190
|
+
ctx.emit("loaded", { user, posts });
|
|
188
191
|
});
|
|
189
192
|
|
|
193
|
+
// take — wait for the next event of that name
|
|
190
194
|
bus.on("typed", async (query, ctx) => {
|
|
191
|
-
const position = await take(ctx, "scroll");
|
|
195
|
+
const position = await take(ctx, "scroll");
|
|
196
|
+
ctx.emit("scrolled", { query, position });
|
|
192
197
|
});
|
|
193
198
|
```
|
|
194
199
|
|
|
195
|
-
|
|
200
|
+
More in [Effects](effects.md).
|
|
196
201
|
|
|
197
|
-
##
|
|
202
|
+
## 7. Name events three ways
|
|
198
203
|
|
|
199
|
-
|
|
204
|
+
`emit` and `on` take a string, an object `{ type, payload }` (a Redux action fits), or a function with a `type` property (action creators and commands).
|
|
200
205
|
|
|
201
|
-
```
|
|
202
|
-
import {
|
|
206
|
+
```js
|
|
207
|
+
import { createBus } from "nanoeffects";
|
|
208
|
+
import { createAction, createCommand } from "nanoeffects/actions";
|
|
209
|
+
|
|
210
|
+
const bus = createBus();
|
|
211
|
+
|
|
212
|
+
bus.emit("moved", { x: 1, y: 2 });
|
|
213
|
+
bus.emit({ type: "moved", payload: { x: 2, y: 2 } });
|
|
214
|
+
|
|
215
|
+
// createAction — names an event; calling it builds { type, payload }
|
|
216
|
+
const moved = createAction("moved");
|
|
217
|
+
bus.emit(moved({ x: 3, y: 2 }));
|
|
218
|
+
bus.on(moved, ({ x }) => console.log(x));
|
|
219
|
+
|
|
220
|
+
// createCommand — like an action, but emit runs a handler and returns its result
|
|
221
|
+
const double = createCommand("math/double", (value) => value * 2);
|
|
222
|
+
bus.emit(double(21)); // 42
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Use strings for the plain case. Use objects when other code already builds actions. Use commands when you need a result from `emit`.
|
|
226
|
+
|
|
227
|
+
## 8. Register a command handler
|
|
228
|
+
|
|
229
|
+
A command may carry its handler, or only be a **contract** (name + types) with the work registered elsewhere — another file, or a fake in a test.
|
|
230
|
+
|
|
231
|
+
```js
|
|
232
|
+
import { createBus } from "nanoeffects";
|
|
233
|
+
import { createCommand, commandsPlugin } from "nanoeffects/actions";
|
|
234
|
+
|
|
235
|
+
const getUser = createCommand("user/get"); // contract — no handler yet
|
|
236
|
+
|
|
237
|
+
const bus = createBus();
|
|
238
|
+
// commandsPlugin — registry of handlers for this bus
|
|
239
|
+
const commands = bus.use(commandsPlugin());
|
|
240
|
+
|
|
241
|
+
bus.emit(getUser("1")); // throws: No handler for command "user/get"
|
|
242
|
+
|
|
243
|
+
const unregister = commands.handle(getUser, (id) => api.user(id));
|
|
244
|
+
const user = await bus.emit(getUser("1"));
|
|
203
245
|
|
|
204
|
-
|
|
246
|
+
unregister();
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Each bus has its own registry. A [module](#9-mount-a-domain-as-a-module) can register handlers and remove them on unmount. More in [Actions](actions.md).
|
|
250
|
+
|
|
251
|
+
## 9. Mount a domain as a module
|
|
252
|
+
|
|
253
|
+
**Problem.** A feature owns several things on the bus: who answers `getUser`, who listens to `greet`, maybe a command that already carries its handler. With plain `bus.on` / `commands.handle` you must remember every unsubscribe when the feature goes away. Miss one and listeners pile up.
|
|
254
|
+
|
|
255
|
+
**When.** Domain logic lives on the bus, off the UI, and you need one mount / one unmount for that feature. Skip modules while a few top-level `bus.on` calls are enough.
|
|
256
|
+
|
|
257
|
+
**What you do.** `createModule(name, setup)` describes the feature. `bus.use(modulePlugin([…]))` mounts it. `app.unmount(module)` undoes everything the setup registered.
|
|
258
|
+
|
|
259
|
+
```js
|
|
260
|
+
import { createBus } from "nanoeffects";
|
|
261
|
+
import { createCommand, createModule, modulePlugin } from "nanoeffects/actions";
|
|
262
|
+
|
|
263
|
+
const getUser = createCommand("user/get"); // contract — no handler yet
|
|
264
|
+
const double = createCommand("math/double", (value) => value * 2); // carries its handler
|
|
265
|
+
|
|
266
|
+
const bus = createBus();
|
|
267
|
+
|
|
268
|
+
// createModule — one feature; name is stable for hot reload replace
|
|
269
|
+
const greeting = createModule("greeting", (m) => {
|
|
270
|
+
// m.on — like bus.on, removed automatically on unmount
|
|
205
271
|
m.on("greet", (name) => console.log(`hello ${name}`));
|
|
206
|
-
|
|
207
|
-
m.
|
|
272
|
+
|
|
273
|
+
// m.handle — this module answers getUser while mounted
|
|
274
|
+
m.handle(getUser, (id) => api.user(id));
|
|
275
|
+
|
|
276
|
+
// m.use — register a command that already has a handler, for this module’s life
|
|
277
|
+
m.use(double);
|
|
208
278
|
});
|
|
209
279
|
|
|
280
|
+
// modulePlugin — mount modules on the bus
|
|
210
281
|
const app = bus.use(modulePlugin([greeting]));
|
|
211
|
-
|
|
282
|
+
bus.emit("greet", "Ada"); // hello Ada
|
|
283
|
+
app.unmount(greeting); // greet listener and getUser binding are gone
|
|
212
284
|
```
|
|
213
285
|
|
|
214
|
-
|
|
286
|
+
- **`m.on`** — same job as `bus.on`, scoped to the module’s lifetime.
|
|
287
|
+
- **`m.handle`** — bind a handler to a contract command; gone on unmount.
|
|
288
|
+
- **`m.use`** — bring a command that already carries a handler (or a sub-module). Prefer `handle` when the handler must die with the feature.
|
|
289
|
+
|
|
290
|
+
Modules also make [hot reload](hmr.md) work. Full API in [Actions](actions.md); a longer recipe in [`examples/modules.ts`](../examples/modules.ts).
|
|
291
|
+
|
|
292
|
+
## 10. Add types when you want checking (optional)
|
|
215
293
|
|
|
216
|
-
|
|
294
|
+
Describe events once when you want `on` / `emit` checked. An event without data is `void`. Skip this while you are learning the bus.
|
|
217
295
|
|
|
218
296
|
```ts
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
297
|
+
// Events — event name → payload for this bus (checker only)
|
|
298
|
+
type Events = {
|
|
299
|
+
greet: string;
|
|
300
|
+
moved: { x: number; y: number };
|
|
301
|
+
reset: void;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const bus = createBus<Events>();
|
|
305
|
+
|
|
306
|
+
bus.on("moved", ({ x, y }) => console.log(x + y));
|
|
307
|
+
bus.emit("moved", { x: 1, y: 2 });
|
|
308
|
+
bus.emit("reset");
|
|
309
|
+
// bus.emit("moved", { x: "1" }); // error
|
|
310
|
+
// bus.emit("jumped", 1); // error
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Without a map, any string event is allowed and the payload is `unknown`.
|
|
314
|
+
|
|
315
|
+
## 11. Isolate errors and warn on typos
|
|
316
|
+
|
|
317
|
+
```js
|
|
318
|
+
import { createBus } from "nanoeffects";
|
|
319
|
+
|
|
320
|
+
const bus = createBus({
|
|
321
|
+
onError: (error) => report(error), // isolate failing handlers
|
|
322
|
+
onUnhandled: ({ type }) => console.warn(`nobody listens to ${type}`),
|
|
222
323
|
});
|
|
223
324
|
```
|
|
224
325
|
|
|
225
|
-
More in [
|
|
326
|
+
More in [Errors](errors.md).
|
|
226
327
|
|
|
227
328
|
## Where next
|
|
228
329
|
|
|
229
|
-
|
|
330
|
+
| Need | Page |
|
|
331
|
+
| ---- | ---- |
|
|
332
|
+
| Store or client on every task | [Context fields](context.md) |
|
|
333
|
+
| Redux / Zustand / MobX | [Stores](stores.md) |
|
|
334
|
+
| Observe tasks | [Tracing](tracing.md) |
|
|
335
|
+
| Guarantees | [Semantics](semantics.md) |
|
|
336
|
+
| Which import | [Package entries](package.md) |
|
package/docs/hmr.md
CHANGED
|
@@ -1,36 +1,48 @@
|
|
|
1
1
|
# Hot reload and development diagnostics
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use this page in development with modules and hot reload. Skip until you mount domains with `modulePlugin`.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
When a file is edited, the bundler loads its new version and runs it again, while the page and the rest of the code stay alive. For a bus that can go wrong in three ways:
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
1. A file that calls `bus.on(...)` subscribes a second time.
|
|
8
|
+
2. A file that makes the bus makes a new one.
|
|
9
|
+
3. Tasks of the old code keep running.
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
## Replace modules by name
|
|
12
|
+
|
|
13
|
+
Mounting a module whose name is taken replaces the mounted one. The old module is unmounted first — and with it its subscriptions, bound commands, sub-modules, and background tasks (their signals abort). The new version of a module file only has to mount again; nothing piles up.
|
|
14
|
+
|
|
15
|
+
- **Names must be unique.** The name means “this is the new version of that module”, so two different modules with one name replace each other. The name is also the type of the module’s tasks in the trace.
|
|
10
16
|
- **Pass `signal` to what the module does.** An unmount aborts the tasks, but a `fetch` or a loop that ignores `ctx.signal` runs on with the old code.
|
|
11
|
-
- **Sub-modules** (`m.use(...)`) are replaced by name inside their parent. A reload of the parent
|
|
12
|
-
- Keep the bus and the app in a file that is not reloaded
|
|
17
|
+
- **Sub-modules** (`m.use(...)`) are replaced by name inside their parent. A reload of the parent replaces it with all its children.
|
|
18
|
+
- Keep the bus and the app in a file that is not reloaded (or in what your bundler keeps across reloads), so a reload does not make a new bus that other files do not talk to.
|
|
13
19
|
|
|
14
20
|
A `bus.on` outside a module returns the function that unsubscribes: hand it to the dispose hook of your bundler, or put the subscription in a module.
|
|
15
21
|
|
|
16
|
-
## `
|
|
22
|
+
## Report leaks with `devPlugin`
|
|
17
23
|
|
|
18
24
|
`bus.use(devPlugin({ app }))` reports what is easy to miss while developing:
|
|
19
25
|
|
|
20
|
-
```
|
|
26
|
+
```js
|
|
27
|
+
import { createBus } from "nanoeffects";
|
|
28
|
+
import { modulePlugin } from "nanoeffects/actions";
|
|
21
29
|
import { devPlugin } from "nanoeffects/dev";
|
|
22
30
|
|
|
31
|
+
const bus = createBus();
|
|
23
32
|
const app = bus.use(modulePlugin([userModule]));
|
|
33
|
+
// devPlugin — warn on piled-up listeners; log module mount/replace/unmount
|
|
24
34
|
bus.use(devPlugin({ app }));
|
|
25
35
|
```
|
|
26
36
|
|
|
27
|
-
- **Listeners that pile up.** When more than `maxListeners` (default 10) listeners of one event were added and not unsubscribed, one warning names the event and the count
|
|
37
|
+
- **Listeners that pile up.** When more than `maxListeners` (default `10`) listeners of one event were added and not unsubscribed, one warning names the event and the count. The count follows the function that `on` returns; `bus.off(event, fn)` is not seen.
|
|
28
38
|
- **Modules.** With `app` it logs `module "user" mounted`, `replaced` when a module of the same name was mounted, and `unmounted`. Only what goes through `app.mount` and `app.unmount` is reported, not sub-modules.
|
|
29
|
-
- **Options.** `leaks: false` and `modules: false` switch a diagnostic off
|
|
39
|
+
- **Options.** `leaks: false` and `modules: false` switch a diagnostic off; `maxListeners` sets the limit; `log` receives the messages instead of the console.
|
|
30
40
|
- **Disconnect.** `bus.use(devPlugin(...))` returns the function that stops the diagnostics and puts `bus.on` and `app.mount` back.
|
|
31
41
|
|
|
32
|
-
It changes no behavior: it wraps `bus.on`, `app.mount
|
|
42
|
+
It changes no behavior: it wraps `bus.on`, `app.mount`, and `app.unmount` and adds messages.
|
|
43
|
+
|
|
44
|
+
### Keep the import in production
|
|
33
45
|
|
|
34
|
-
|
|
46
|
+
`nanoeffects/dev` is a no-op in a production build. `package.json` maps it to an empty build under the `production` export condition (Vite and webpack set that in production mode), so `bus.use(devPlugin({ app }))` can stay without a guard: it touches nothing and costs a few bytes.
|
|
35
47
|
|
|
36
|
-
|
|
48
|
+
If you want even the call gone, guard it (`if (import.meta.env.DEV)` in Vite). An app that does not import the entry does not carry it.
|
package/docs/observable.md
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Turn events into Observables
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Use this page only when you already work in Observables / RxJS. Prefer `bus.on` with policies for cancellation and tracing.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`createObserve(bus)` from `nanoeffects/observable` turns a bus event into an Observable that follows the TC39 proposal (`subscribe`, `unsubscribe`, `Symbol.observable`). RxJS and other stream libraries take it as it is. Nothing here depends on them.
|
|
6
|
+
|
|
7
|
+
```js
|
|
6
8
|
import { from } from "rxjs";
|
|
7
9
|
import { debounceTime, distinctUntilChanged, switchMap } from "rxjs/operators";
|
|
8
10
|
import { createObserve } from "nanoeffects/observable";
|
|
9
11
|
|
|
10
|
-
const observe = createObserve(bus);
|
|
12
|
+
const observe = createObserve(bus); // Observable per event name
|
|
11
13
|
|
|
12
|
-
// events of the bus as a stream
|
|
13
14
|
from(observe("typed"))
|
|
14
15
|
.pipe(debounceTime(300), distinctUntilChanged(), switchMap(searchRequest))
|
|
15
16
|
.subscribe((items) => bus.emit("results", items));
|
|
16
17
|
```
|
|
17
18
|
|
|
18
|
-
- `observe(event)` returns an Observable of the payloads of that event. Each `subscribe` is a `bus.on` of its own
|
|
19
|
+
- `observe(event)` returns an Observable of the payloads of that event. Each `subscribe` is a `bus.on` of its own; `unsubscribe()` removes it.
|
|
19
20
|
- `subscribe` takes a function or an object with `next`.
|
|
20
|
-
- The stream never completes and never fails: events are the only thing that arrives. Errors of the handler
|
|
21
|
-
- A subscription made this way is not tied to a task
|
|
22
|
-
- For cancellation, policies and tracing keep to `bus.on` with `latest`, `debounce`, `queue
|
|
21
|
+
- The stream never completes and never fails: events are the only thing that arrives. Errors of the subscriber’s handler belong to the subscriber.
|
|
22
|
+
- A subscription made this way is not tied to a task. End it yourself: `onAbort(ctx, subscription.unsubscribe)` inside a task, or with the signal of a module.
|
|
23
|
+
- For cancellation, policies, and tracing, keep to `bus.on` with `latest`, `debounce`, `queue` — they know about tasks. The Observable is for places where a stream is already how you work.
|
package/docs/package.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Choose a package entry
|
|
2
|
+
|
|
3
|
+
Imports are split so you only load what you use. `createBus` stays small; the rest is another entry.
|
|
4
|
+
|
|
5
|
+
| Import | What is in it |
|
|
6
|
+
| ------ | ------------- |
|
|
7
|
+
| `nanoeffects` | `createBus`, the types |
|
|
8
|
+
| `nanoeffects/policies` | `latest`, `leading`, `debounce`, `throttle`, `queue` |
|
|
9
|
+
| `nanoeffects/effects` | `retry`, `timeout`, `poll`, `delay`, `take`, `all`, `allSettled`, `race`, … |
|
|
10
|
+
| `nanoeffects/actions` | `createAction`, `createCommand`, `createJob`, `createModule`, `modulePlugin` |
|
|
11
|
+
| `nanoeffects/redux` | `createEffectsMiddleware` |
|
|
12
|
+
| `nanoeffects/observable` | `createObserve` |
|
|
13
|
+
| `nanoeffects/dev` | `devPlugin` (no-op in production) |
|
|
14
|
+
| `nanoeffects/trace` | `createTracer` and the loggers |
|