nanoeffects 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +53 -55
  2. package/dist/{action-Bdfp-Wvd.d.ts → action-creator-DOWcbXpy.d.ts} +79 -73
  3. package/dist/actions.d.ts +75 -6
  4. package/dist/actions.js +1 -1
  5. package/dist/bus-BAs9x7IQ.js +1 -0
  6. package/dist/{context-DlA80GOA.js → context-CK0ZomWt.js} +1 -1
  7. package/dist/context-plugin-B0sLWOxR.d.ts +34 -0
  8. package/dist/dev.d.ts +2 -2
  9. package/dist/effects.d.ts +153 -56
  10. package/dist/effects.js +1 -1
  11. package/dist/index.d.ts +3 -2
  12. package/dist/index.js +1 -1
  13. package/dist/{module-I1ixyJzx.d.ts → module-DCJl-qfi.d.ts} +24 -9
  14. package/dist/notrace/{action-Bdfp-Wvd.d.ts → action-creator-DOWcbXpy.d.ts} +79 -73
  15. package/dist/notrace/actions.d.ts +97 -13
  16. package/dist/notrace/actions.js +1 -1
  17. package/dist/notrace/bus-Bdk0YyFQ.js +1 -0
  18. package/dist/notrace/{context-yAWj0BFZ.js → context-CctVmHLC.js} +1 -1
  19. package/dist/notrace/context-plugin-B0sLWOxR.d.ts +34 -0
  20. package/dist/notrace/{delay-Cgvn16oY.js → delay-B_W1tnkS.js} +1 -1
  21. package/dist/notrace/effects.d.ts +153 -56
  22. package/dist/notrace/effects.js +1 -1
  23. package/dist/notrace/index.d.ts +3 -2
  24. package/dist/notrace/index.js +1 -1
  25. package/dist/notrace/policies.d.ts +1 -1
  26. package/dist/notrace/policies.js +1 -1
  27. package/dist/notrace/redux.d.ts +18 -20
  28. package/dist/notrace/redux.js +1 -1
  29. package/dist/observable.d.ts +1 -1
  30. package/dist/policies.d.ts +1 -1
  31. package/dist/policies.js +1 -1
  32. package/dist/react.d.ts +50 -0
  33. package/dist/react.js +1 -0
  34. package/dist/redux.d.ts +18 -20
  35. package/dist/redux.js +1 -1
  36. package/dist/trace.d.ts +1 -1
  37. package/docs/README.md +30 -9
  38. package/docs/actions.md +117 -41
  39. package/docs/context.md +48 -7
  40. package/docs/effects.md +229 -73
  41. package/docs/errors.md +23 -10
  42. package/docs/guide.md +238 -129
  43. package/docs/hmr.md +25 -13
  44. package/docs/migrating-from-effector.md +293 -0
  45. package/docs/migrating-from-rxjs.md +127 -0
  46. package/docs/migrating-from-saga.md +93 -0
  47. package/docs/observable.md +11 -9
  48. package/docs/package.md +24 -0
  49. package/docs/policies.md +93 -30
  50. package/docs/react.md +157 -0
  51. package/docs/semantics.md +5 -3
  52. package/docs/stores.md +488 -25
  53. package/docs/tracing.md +69 -54
  54. package/docs/typescript.md +153 -0
  55. package/llms-full.txt +2645 -0
  56. package/llms.txt +48 -0
  57. package/package.json +39 -9
  58. package/dist/bus-CRnlwmQ4.js +0 -1
  59. package/dist/notrace/bus-Dx0l9X6h.js +0 -1
  60. package/dist/notrace/event-0RIBGYqS.js +0 -1
package/docs/guide.md CHANGED
@@ -1,229 +1,338 @@
1
1
  # Guide
2
2
 
3
- From a bus with two calls to an app with modules, one step at a time. Every step builds on the one before, and the code is in [`examples/guide.ts`](../examples/guide.ts), where it is type-checked and run by a test.
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
- ## 1. A bus
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
- `on` subscribes, `emit` calls the subscribers, and `on` returns the function that unsubscribes, as in nanoevents.
12
+ Runnable code for every step: [`examples/guide.ts`](../examples/guide.ts).
8
13
 
9
- ```ts
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) => console.log(`hello ${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 now
29
+ bus.emit("greet", "Bob"); // nobody listens
19
30
  ```
20
31
 
21
- The handlers run at once, in the order they subscribed. That is all a bus is; the rest of the guide is what you can put on top.
32
+ Handlers run at once, in subscription order.
22
33
 
23
- ## 2. Types
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
- Describe the events once, and `on` and `emit` are checked. An event without data is `void`.
36
+ ```js
37
+ import { createBus } from "nanoeffects";
26
38
 
27
- ```ts
28
- type Events = {
29
- greet: string;
30
- moved: { x: number; y: number };
31
- reset: void;
32
- };
39
+ const bus = createBus();
33
40
 
34
- const bus = createBus<Events>();
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("moved", ({ x, y }) => console.log(x + y));
37
- bus.emit("moved", { x: 1, y: 2 });
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
- The map is optional: a bus without it takes any string and gives the payload as `unknown`.
50
+ ## 2. Use the handler context
44
51
 
45
- ## 3. The context of a handler
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
- The second argument is the context of this call. A handler can emit events through it.
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) ctx.emit("outOfBounds", { x, y });
60
+ if (x < 0) {
61
+ ctx.emit("outOfBounds", { x, y });
62
+ }
52
63
  });
53
64
  ```
54
65
 
55
- Each call of a handler is a **task**, and `ctx` is what the task knows about itself: its `signal`, a way to `abort` it, and `emit`, `on`, `run`. A task that is aborted stops emitting.
66
+ `ctx` has `signal`, `abort`, `emit`, `on`, and `run`. An aborted task stops emitting and delivers no result.
56
67
 
57
- ## 4. Three ways to name an event
68
+ ## 3. Abort the previous run with `latest`
58
69
 
59
- `emit` and `on` take the event as a string, as an object `{ type, payload }`, or as a function that has a `type` property.
60
-
61
- ```ts
62
- bus.emit("moved", { x: 1, y: 2 }); // a string, and the payload next to it
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
+ 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`.
70
71
 
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 double = createCommand("math/double", (value: number) => value * 2);
76
+ const bus = createBus();
77
77
 
78
- bus.emit(double, 21); // 42
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
- Strings are for the plain case, objects for actions that other code already makes, functions for requests that need a result.
87
+ Same `on`, same handler shape. More in [Policies](policies.md).
82
88
 
83
- ## 5. A command needs a handler
89
+ ## 4. Pick another policy when overlap differs
84
90
 
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.
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 |
86
98
 
87
- ```ts
88
- import { createCommand, commandsPlugin } from "nanoeffects/actions";
89
-
90
- const getUser = createCommand<string, Promise<User>>("user/get"); // no handler yet
91
-
92
- const bus = createBus<Events>();
93
- const commands = bus.use(commandsPlugin()); // the handlers of this bus
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
- ## 6. The first policy
103
+ const bus = createBus();
109
104
 
110
- Often a handler is called again before the last call has finished: a user types another letter, a button is clicked twice. A **policy** wraps the handler and decides what happens then. It is used in place of the handler:
105
+ // throttle — at most once per 100 ms
106
+ bus.on("scroll", throttle(100, (position) => {
107
+ updateScrollbar(position);
108
+ }));
111
109
 
112
- ```ts
113
- import { latest } from "nanoeffects/policies";
110
+ // debounce — run after 300 ms of quiet
111
+ bus.on("typed", debounce(300, (text) => {
112
+ validate(text);
113
+ }));
114
114
 
115
- // a newer search aborts the older one
115
+ // queue — one save after another, in order
116
116
  bus.on(
117
- "search",
118
- latest(async (query, { signal }) => {
119
- await api.search(query, { signal });
117
+ "save",
118
+ queue(async (draft, { signal }) => {
119
+ await api.save(draft, { signal });
120
120
  }),
121
121
  );
122
- ```
123
122
 
124
- The aborted call learns about it through `signal`: pass it to `fetch` and the request is cancelled. Nothing else changes: the handler is the same function with the same arguments.
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
- ## 7. More policies
130
+ Full options: [Policies](policies.md).
127
131
 
128
- | Policy | When calls overlap |
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
- Even a plain function works, it is just called less often. And policies nest:
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
- ```ts
139
- import { debounce, queue, throttle } from "nanoeffects/policies";
136
+ ```js
137
+ import { createBus } from "nanoeffects";
138
+ import { retry, timeout } from "nanoeffects/effects";
139
+ import { latest } from "nanoeffects/policies";
140
140
 
141
- bus.on("scroll", throttle(100, (position) => updateScrollbar(position)));
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
- // wait for quiet, then keep only the latest
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
- More in [policies](policies.md).
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
- ## 8. The first effects
150
+ // latest — a newer typed query aborts the whole retry chain
151
+ bus.on("typed", latest(searchWithRetry));
152
+ ```
152
153
 
153
- An **effect** adds what an async handler usually needs. `retry` and `timeout` wrap a handler the way a policy does, so they go into `on` with the policies and need no `ctx`:
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
- ```ts
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 search = (query: string, { signal }) => api.search(query, { signal });
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 items = await ctx.run(retry(timeout(5000, search), { times: 3 }), query);
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 handler, the effect takes `ctx` first: `await retry(ctx, () => api.search(q, { signal: ctx.signal }))`. More in [effects](effects.md).
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
- ## 9. Several things at once, and waiting
176
+ ## 6. Run work in parallel or wait for an event
179
177
 
180
- ```ts
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"); // resolves with the next scroll
195
+ const position = await take(ctx, "scroll");
196
+ ctx.emit("scrolled", { query, position });
192
197
  });
193
198
  ```
194
199
 
195
- `all` runs the starters together and fails, and cancels the rest, when one fails. `take` waits for the next event of a name. `ctx.on` subscribes as the task and ends with it. More in [effects](effects.md).
200
+ More in [Effects](effects.md).
196
201
 
197
- ## 10. Modules
202
+ ## 7. Name events three ways
198
203
 
199
- A module is what one part of the app does, in one place: its handlers, its commands, its background tasks. Unmounting it undoes all of that.
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
- ```ts
202
- import { createModule, modulePlugin } from "nanoeffects/actions";
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"));
245
+
246
+ unregister();
247
+ ```
203
248
 
204
- const greeting = createModule<Events>("greeting", (m) => {
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
- m.handle(getUser, (id) => api.user(id)); // a contract gets its handler here
207
- m.use(double); // a command that carries a handler: the module brings it
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
- app.unmount(greeting); // the subscriptions and the handler are gone
282
+ bus.emit("greet", "Ada"); // hello Ada
283
+ app.unmount(greeting); // greet listener and getUser binding are gone
212
284
  ```
213
285
 
214
- Modules are also what makes [hot reload](hmr.md) work. More in [actions](actions.md).
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.
215
289
 
216
- ## 11. Errors and typos
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)
293
+
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
- const bus = createBus<Events>({
220
- onError: (error) => report(error), // a failing handler does not stop the others
221
- onUnhandled: ({ type }) => console.warn(`nobody listens to ${type}`), // a typo in a name
297
+ import { createBus } from "nanoeffects";
298
+
299
+ // Events — event name → payload for this bus (checker only)
300
+ type Events = {
301
+ greet: string;
302
+ moved: { x: number; y: number };
303
+ reset: void;
304
+ };
305
+
306
+ const bus = createBus<Events>();
307
+
308
+ bus.on("moved", ({ x, y }) => console.log(x + y));
309
+ bus.emit("moved", { x: 1, y: 2 });
310
+ bus.emit("reset");
311
+ // bus.emit("moved", { x: "1" }); // error
312
+ // bus.emit("jumped", 1); // error
313
+ ```
314
+
315
+ Without a map, any string event is allowed and the payload is `unknown`.
316
+
317
+ ## 11. Isolate errors and warn on typos
318
+
319
+ ```js
320
+ import { createBus } from "nanoeffects";
321
+
322
+ const bus = createBus({
323
+ onError: (error) => report(error), // isolate failing handlers
324
+ onUnhandled: ({ type }) => console.warn(`nobody listens to ${type}`),
222
325
  });
223
326
  ```
224
327
 
225
- More in [errors](errors.md).
328
+ More in [Errors](errors.md).
226
329
 
227
330
  ## Where next
228
331
 
229
- [Context fields](context.md) (a store or a client in every task) · [Tracing](tracing.md) · [Redux, Zustand, MobX](stores.md) · [Semantics](semantics.md)
332
+ | Need | Page |
333
+ | ---- | ---- |
334
+ | Store or client on every task | [Context fields](context.md) |
335
+ | Redux / Zustand / MobX | [Stores](stores.md) |
336
+ | Observe tasks | [Tracing](tracing.md) |
337
+ | Guarantees | [Semantics](semantics.md) |
338
+ | 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
- When a file is edited in development, 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: a file that calls `bus.on(...)` subscribes a second time, a file that makes the bus makes a new one, and tasks of the old code keep running.
3
+ Use this page in development with modules and hot reload. Skip until you mount domains with `modulePlugin`.
4
4
 
5
- ## Modules are replaced by name
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
- Mounting a module whose name is taken replaces the mounted one. The old module is unmounted first, and with it its subscriptions, its bound commands, its sub-modules and its background tasks, whose signals abort. So the new version of a module file only has to mount again, and nothing piles up.
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
- - **Names must be unique.** The name says "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.
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 module replaces it with all its children.
12
- - 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 the other files do not talk to.
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
- ## `nanoeffects/dev`
22
+ ## Report leaks with `devPlugin`
17
23
 
18
24
  `bus.use(devPlugin({ app }))` reports what is easy to miss while developing:
19
25
 
20
- ```ts
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: a file that is reloaded and subscribes again, or an unsubscribe that is never called. The count follows the function that `on` returns, and `bus.off(event, fn)` is not seen.
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, `maxListeners` sets the limit, `log` receives the messages instead of the console.
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` and `app.unmount` and adds messages.
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
- ### In production
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
- `nanoeffects/dev` is a no-op in a production build. `package.json` maps it to an empty build under the `production` export condition, which Vite and webpack set in production mode, so `bus.use(devPlugin({ app }))` can stay without a guard: it touches nothing and costs a few bytes. 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.
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.