nanoeffects 0.28.0 → 0.30.1

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 (139) hide show
  1. package/README.md +6 -5
  2. package/dist/{action-creator-CcAEvUnw.d.ts → action-creator-6rlBTmH9.d.ts} +60 -33
  3. package/dist/actions.d.ts +18 -18
  4. package/dist/actions.js +1 -1
  5. package/dist/bus-DFlmNJ6h.js +1 -0
  6. package/dist/context-B5Z2RpJt.js +1 -0
  7. package/dist/context-types-DKNc5mkS.d.ts +31 -0
  8. package/dist/context.d.ts +8 -3
  9. package/dist/context.js +1 -1
  10. package/dist/{delay-18LJ8jSk.js → delay-5OhJZZQ0.js} +1 -1
  11. package/dist/dev.d.ts +44 -3
  12. package/dist/dev.js +2 -1
  13. package/dist/effects.d.ts +2 -2
  14. package/dist/effects.js +1 -1
  15. package/dist/emit-key-C6FL7Ewn.js +1 -0
  16. package/dist/guard.d.ts +59 -0
  17. package/dist/guard.js +1 -0
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.js +1 -1
  20. package/dist/match.d.ts +3 -3
  21. package/dist/match.js +1 -1
  22. package/dist/{module-C_oEXw1n.d.ts → module-G39MaVOt.d.ts} +38 -38
  23. package/dist/notrace/{action-creator-DxOvVo8I.d.ts → action-creator-BrMSO3RP.d.ts} +35 -32
  24. package/dist/notrace/actions.d.ts +52 -52
  25. package/dist/notrace/actions.js +1 -1
  26. package/dist/notrace/{bus-DmKn4SDD.js → bus-37nlVljc.js} +1 -1
  27. package/dist/notrace/effects.d.ts +2 -2
  28. package/dist/notrace/index.d.ts +2 -2
  29. package/dist/notrace/index.js +1 -1
  30. package/dist/notrace/{plugin-lJjjd-SN.d.ts → plugin-7GUL2EW5.d.ts} +1 -1
  31. package/dist/notrace/policies.d.ts +1 -1
  32. package/dist/notrace/redux.d.ts +1 -1
  33. package/dist/observable.d.ts +43 -1
  34. package/dist/observable.js +1 -1
  35. package/dist/onAbort-BgMfspK4.js +1 -0
  36. package/dist/oop.d.ts +3 -3
  37. package/dist/oop.js +1 -1
  38. package/dist/{plugin-CuvIgjnX.d.ts → plugin-BWNP23hr.d.ts} +2 -2
  39. package/dist/policies.d.ts +2 -2
  40. package/dist/policies.js +1 -1
  41. package/dist/production/dev.js +1 -1
  42. package/dist/react.d.ts +2 -2
  43. package/dist/react.js +1 -1
  44. package/dist/redux.d.ts +1 -1
  45. package/dist/scope.d.ts +9 -9
  46. package/dist/signal-field-key-CWT-3OXC.js +1 -0
  47. package/dist/signal.d.ts +34 -6
  48. package/dist/signal.js +1 -1
  49. package/dist/site-DLvUoXs3.js +1 -0
  50. package/dist/svelte.d.ts +1 -1
  51. package/dist/trace.d.ts +170 -6
  52. package/dist/trace.js +3 -1
  53. package/dist/{types-FV8fuvI9.d.ts → types-N_cdRr1r.d.ts} +2 -2
  54. package/dist/vue.d.ts +1 -1
  55. package/dist/{with-policy-D-SQRkPh.js → with-policy-CeM1rtES.js} +1 -1
  56. package/dist/{with-policy-3bhFLEXs.d.ts → with-policy-CthiBp_z.d.ts} +1 -1
  57. package/docs/README.md +65 -22
  58. package/docs/actions.md +56 -13
  59. package/docs/cancel-stale-requests.md +2 -0
  60. package/docs/chat-example.md +15 -1
  61. package/docs/chat-widget.md +9 -7
  62. package/docs/context.md +352 -8
  63. package/docs/debounce-and-abort.md +3 -1
  64. package/docs/debugging-loops.md +283 -0
  65. package/docs/effects.md +259 -131
  66. package/docs/errors.md +4 -2
  67. package/docs/examples.md +60 -0
  68. package/docs/guide.md +57 -22
  69. package/docs/hmr.md +35 -1
  70. package/docs/loop-guard.md +109 -0
  71. package/docs/migrating-from-effector.md +15 -2
  72. package/docs/migrating-from-mitt.md +6 -4
  73. package/docs/migrating-from-rxjs.md +2 -0
  74. package/docs/migrating-from-saga.md +2 -0
  75. package/docs/motivation.md +14 -21
  76. package/docs/observable.md +45 -0
  77. package/docs/oop.md +20 -1
  78. package/docs/package.md +25 -13
  79. package/docs/pass-the-signal.md +281 -0
  80. package/docs/plugins.md +90 -15
  81. package/docs/policies.md +73 -46
  82. package/docs/prevent-double-submit.md +3 -1
  83. package/docs/query-libraries.md +276 -6
  84. package/docs/react.md +50 -11
  85. package/docs/recipes.md +528 -0
  86. package/docs/ru/README.md +62 -0
  87. package/docs/ru/actions.md +412 -0
  88. package/docs/ru/cancel-stale-requests.md +106 -0
  89. package/docs/ru/chat-example.md +259 -0
  90. package/docs/ru/chat-widget.md +131 -0
  91. package/docs/ru/context.md +401 -0
  92. package/docs/ru/debounce-and-abort.md +105 -0
  93. package/docs/ru/debugging-loops.md +283 -0
  94. package/docs/ru/effects.md +538 -0
  95. package/docs/ru/errors.md +36 -0
  96. package/docs/ru/examples.md +60 -0
  97. package/docs/ru/guide.md +530 -0
  98. package/docs/ru/hmr.md +83 -0
  99. package/docs/ru/loop-guard.md +109 -0
  100. package/docs/ru/migrating-from-effector.md +339 -0
  101. package/docs/ru/migrating-from-mitt.md +99 -0
  102. package/docs/ru/migrating-from-rxjs.md +197 -0
  103. package/docs/ru/migrating-from-saga.md +97 -0
  104. package/docs/ru/motivation.md +47 -0
  105. package/docs/ru/observable.md +233 -0
  106. package/docs/ru/oop.md +431 -0
  107. package/docs/ru/package.md +42 -0
  108. package/docs/ru/pass-the-signal.md +281 -0
  109. package/docs/ru/plugins.md +398 -0
  110. package/docs/ru/policies.md +201 -0
  111. package/docs/ru/prevent-double-submit.md +122 -0
  112. package/docs/ru/query-libraries.md +392 -0
  113. package/docs/ru/react.md +309 -0
  114. package/docs/ru/recipes.md +528 -0
  115. package/docs/ru/semantics.md +19 -0
  116. package/docs/ru/server.md +204 -0
  117. package/docs/ru/starter.md +118 -0
  118. package/docs/ru/stores.md +673 -0
  119. package/docs/ru/svelte.md +78 -0
  120. package/docs/ru/tracing.md +466 -0
  121. package/docs/ru/typescript.md +170 -0
  122. package/docs/ru/vue.md +92 -0
  123. package/docs/semantics.md +12 -0
  124. package/docs/server.md +5 -3
  125. package/docs/starter.md +2 -0
  126. package/docs/stores.md +2 -0
  127. package/docs/svelte.md +3 -1
  128. package/docs/tracing.md +132 -7
  129. package/docs/typescript.md +3 -1
  130. package/docs/vue.md +3 -1
  131. package/llms-full.txt +2718 -545
  132. package/llms.txt +17 -14
  133. package/package.json +25 -8
  134. package/skills/nanoeffects/SKILL.md +2 -2
  135. package/dist/bus-DlpEluzd.js +0 -1
  136. package/dist/context-UcmrAIhP.js +0 -1
  137. package/dist/context-types-hCKgU8sC.d.ts +0 -14
  138. package/docs/scope.md +0 -266
  139. package/docs/stability.md +0 -90
@@ -0,0 +1,528 @@
1
+ # Common tasks
2
+
3
+ > **Levels A to E.** Read first: [README](../README.md).
4
+
5
+ Each task below starts with the problem, then a short solution. Where the page has more to say, the last line links to it. The tasks go from the simplest to the most involved.
6
+
7
+ ## On this page
8
+
9
+ - [Events and handlers](#events-and-handlers)
10
+ - [A handler needs an API client, a store or a logger](#a-handler-needs-an-api-client-a-store-or-a-logger)
11
+ - [One handler fails, the others must still run](#one-handler-fails-the-others-must-still-run)
12
+ - [A typo in an event name](#a-typo-in-an-event-name)
13
+ - [Races and repeated actions in the UI](#races-and-repeated-actions-in-the-ui)
14
+ - [An old response overwrites a new one](#an-old-response-overwrites-a-new-one)
15
+ - [A search box sends a request on every key](#a-search-box-sends-a-request-on-every-key)
16
+ - [A second click sends a second request](#a-second-click-sends-a-second-request)
17
+ - [Messages must go out in order](#messages-must-go-out-in-order)
18
+ - [A "Delete" button in a list blocks the other rows](#a-delete-button-in-a-list-blocks-the-other-rows)
19
+ - [An unreliable network and long flows](#an-unreliable-network-and-long-flows)
20
+ - [A request fails sometimes: retry and backoff](#a-request-fails-sometimes-retry-and-backoff)
21
+ - [A request hangs: timeout](#a-request-hangs-timeout)
22
+ - [Refresh data every N seconds: poll](#refresh-data-every-n-seconds-poll)
23
+ - [Wait for another event inside a handler: take](#wait-for-another-event-inside-a-handler-take)
24
+ - [Load several things at once: all](#load-several-things-at-once-all)
25
+ - [Take the fastest answer: race](#take-the-fastest-answer-race)
26
+ - [Features and domain logic](#features-and-domain-logic)
27
+ - [Get the result of emit](#get-the-result-of-emit)
28
+ - [Group a feature with its handlers: modules](#group-a-feature-with-its-handlers-modules)
29
+ - [Do not leave subscriptions after a widget closes](#do-not-leave-subscriptions-after-a-widget-closes)
30
+ - [Stop work on a server](#stop-work-on-a-server)
31
+ - [Debugging: what ran and why](#debugging-what-ran-and-why)
32
+ - [Other libraries in the project](#other-libraries-in-the-project)
33
+ - [Classes and TypeScript](#classes-and-typescript)
34
+ - [Use the bus in a class](#use-the-bus-in-a-class)
35
+ - [Keep the types of the bus in many files](#keep-the-types-of-the-bus-in-many-files)
36
+ - [Examples of whole apps](#examples-of-whole-apps)
37
+
38
+ ## Events and handlers
39
+
40
+ ### A handler needs an API client, a store or a logger
41
+
42
+ Many handlers use the same `api` client or logger. Importing it in every handler file ties the handlers to one client and makes tests harder. Put it on the bus once, and every task gets it on `ctx`.
43
+
44
+ ```js
45
+ import { createBus } from "nanoeffects";
46
+
47
+ // createBus({ context }) — every task gets these fields on ctx
48
+ const bus = createBus({ context: { api, logger } });
49
+
50
+ bus.on("save", async (draft, { api, logger }) => {
51
+ await api.save(draft);
52
+ logger.info("saved");
53
+ });
54
+ ```
55
+
56
+ More: [Context fields](context.md).
57
+
58
+ ### One handler fails, the others must still run
59
+
60
+ Two handlers listen to `order/placed`: one sends analytics, the other saves the order. By default the first error stops the loop, so a broken analytics call can keep the order from being saved. With `onError` every handler runs and the errors go to one place.
61
+
62
+ ```js
63
+ import { createBus } from "nanoeffects";
64
+
65
+ const bus = createBus({
66
+ // onError — isolates the handlers: a thrown error goes here and the next handler still runs
67
+ onError: (error, { type }) => report(error, type),
68
+ });
69
+
70
+ const sendAnalytics = (order) => {
71
+ throw new Error(`analytics is down for ${order.id}`);
72
+ };
73
+ const saveOrder = (order) => console.log("saved", order.id);
74
+
75
+ bus.on("order/placed", sendAnalytics); // throws
76
+ bus.on("order/placed", saveOrder); // still runs
77
+
78
+ bus.emit("order/placed", { id: 1 }); // saved 1
79
+ ```
80
+
81
+ More: [Errors and unheard events](errors.md#isolate-failing-handlers).
82
+
83
+ ### A typo in an event name
84
+
85
+ `emit("serch")` does not fail: no one listens to that name, so nothing happens, and you look for the bug in the handler. `onUnhandled` warns about an event that nobody heard. Turn it on in development.
86
+
87
+ ```js
88
+ import { createBus } from "nanoeffects";
89
+
90
+ const bus = createBus({
91
+ // onUnhandled — called when an event has no listeners
92
+ onUnhandled: ({ type }) => console.warn(`Nobody listens to "${type}"`),
93
+ });
94
+
95
+ bus.on("search", (query) => console.log(query));
96
+
97
+ bus.emit("serch", "nano"); // warning: Nobody listens to "serch"
98
+ ```
99
+
100
+ More: [Warn when nobody listens](errors.md#warn-when-nobody-listens).
101
+
102
+ ## Races and repeated actions in the UI
103
+
104
+ ### An old response overwrites a new one
105
+
106
+ A user types `re`, then `react`. Two requests are in flight. The server answers `react` fast and `re` slowly, so the answer for `re` arrives last and replaces the right list with the wrong one. This is what a plain React effect does:
107
+
108
+ ```jsx
109
+ import { useEffect, useState } from "react";
110
+
111
+ export function Search({ query }) {
112
+ const [items, setItems] = useState([]);
113
+
114
+ useEffect(() => {
115
+ // two requests are in flight, and the one that finishes last wins, even the older one
116
+ api.search(query).then(setItems);
117
+ }, [query]);
118
+
119
+ return (
120
+ <ul>
121
+ {items.map((item) => (
122
+ <li key={item.id}>{item.title}</li>
123
+ ))}
124
+ </ul>
125
+ );
126
+ }
127
+ ```
128
+
129
+ Move the request into a handler with `latest`: a new `search` aborts the previous request, and the aborted one delivers no result.
130
+
131
+ ```js
132
+ import { createBus } from "nanoeffects";
133
+ import { latest } from "nanoeffects/policies";
134
+
135
+ const bus = createBus();
136
+
137
+ // latest — a new "search" aborts the previous one and drops its result
138
+ bus.on(
139
+ "search",
140
+ latest(async (query, { signal }) => {
141
+ // signal — lets the browser cancel the request itself
142
+ const items = await api.search(query, { signal });
143
+ bus.emit("results", items);
144
+ }),
145
+ );
146
+
147
+ bus.on("results", (items) => render(items));
148
+ ```
149
+
150
+ More: [Cancel stale requests](cancel-stale-requests.md), [Policies](policies.md#latest-abort-the-previous-run), and the component side in [Build a sign-up form in React](react.md).
151
+
152
+ ### A search box sends a request on every key
153
+
154
+ Typing `react` sends five requests, and the list flickers. `debounce` waits for a pause in typing; `latest` aborts the request that is still in flight.
155
+
156
+ ```js
157
+ import { createBus } from "nanoeffects";
158
+ import { debounce, latest } from "nanoeffects/policies";
159
+
160
+ const bus = createBus();
161
+
162
+ // latest — a new search aborts the request in flight
163
+ const searchLatest = latest(async (query, { signal }) => {
164
+ const items = await api.search(query, { signal });
165
+ bus.emit("results", items);
166
+ });
167
+
168
+ // debounce — start after 300 ms without a new "typed"
169
+ bus.on("typed", debounce(300, searchLatest));
170
+ ```
171
+
172
+ More: [Pause between requests](debounce-and-abort.md).
173
+
174
+ ### A second click sends a second request
175
+
176
+ A user double-clicks "Sign up" on a slow network, and two accounts are created. `leading` drops every call that comes while the first one still runs.
177
+
178
+ ```js
179
+ import { createBus } from "nanoeffects";
180
+ import { leading } from "nanoeffects/policies";
181
+
182
+ const bus = createBus();
183
+
184
+ // leading — a call that comes while the previous one runs is dropped
185
+ bus.on(
186
+ "form/submitted",
187
+ leading(async (form, { signal }) => {
188
+ await api.signUp(form, { signal });
189
+ }),
190
+ );
191
+ ```
192
+
193
+ More: [Prevent a double submit](prevent-double-submit.md).
194
+
195
+ ### Messages must go out in order
196
+
197
+ In a chat, a user sends `Hello` and then `How are you?` within a second. Two parallel requests can reach the server in the wrong order. `queue` sends the next message only after the previous one is delivered.
198
+
199
+ ```js
200
+ import { createBus } from "nanoeffects";
201
+ import { queue } from "nanoeffects/policies";
202
+
203
+ const bus = createBus();
204
+
205
+ // queue — the next message waits until the previous one is delivered
206
+ bus.on(
207
+ "message/sent",
208
+ queue(async (message, { signal }) => {
209
+ await api.sendMessage(message, { signal });
210
+ }),
211
+ );
212
+ ```
213
+
214
+ More: [Policies, `queue`](policies.md#run-calls-one-after-another-with-queue).
215
+
216
+ ### A "Delete" button in a list blocks the other rows
217
+
218
+ You protect "Delete" with `leading`. While one row is deleted, clicks on the other rows are dropped too, because the whole handler has one state. `key` gives every row its own state.
219
+
220
+ ```js
221
+ import { createBus } from "nanoeffects";
222
+ import { leading } from "nanoeffects/policies";
223
+
224
+ const bus = createBus();
225
+
226
+ // leading with key — every row id has its own state, so other rows are not blocked
227
+ bus.on(
228
+ "row/deleted",
229
+ leading(
230
+ async (row, { signal }) => {
231
+ await api.deleteRow(row.id, { signal });
232
+ },
233
+ { key: (row) => row.id },
234
+ ),
235
+ );
236
+ ```
237
+
238
+ More: [Policies, `key`](policies.md#key-isolate-state-by-key).
239
+
240
+ ## An unreliable network and long flows
241
+
242
+ ### A request fails sometimes: retry and backoff
243
+
244
+ A request fails once in a while because of the network. `retry` repeats it. If the server is overloaded, repeating at once makes it worse, so use `backoff` to make the pause longer with each attempt.
245
+
246
+ ```js
247
+ import { createBus } from "nanoeffects";
248
+ import { backoff, retry } from "nanoeffects/effects";
249
+
250
+ const bus = createBus();
251
+
252
+ // retry — repeat the request after an error, up to 4 attempts
253
+ const loadWithRetry = retry((id, { signal }) => api.getUser(id, { signal }), {
254
+ times: 4,
255
+ // backoff — the pause grows (200, 400, 800 ms) with a random part, so clients do not retry at the same moment
256
+ delay: backoff({
257
+ base: 200,
258
+ max: 5000,
259
+ jitter: true,
260
+ }),
261
+ });
262
+
263
+ bus.on("user/opened", loadWithRetry);
264
+ ```
265
+
266
+ More: [`backoff`](effects.md#backoff).
267
+
268
+ ### A request hangs: timeout
269
+
270
+ A request does not fail and does not answer, and the spinner spins forever. `timeout` rejects the call after the time you set and aborts the request.
271
+
272
+ ```js
273
+ import { createBus } from "nanoeffects";
274
+ import { timeout } from "nanoeffects/effects";
275
+
276
+ const bus = createBus();
277
+
278
+ // timeout — reject with TimeoutError and abort the request after 5 s
279
+ const loadOnce = timeout(5000, (id, { signal }) => api.getUser(id, { signal }));
280
+
281
+ bus.on("user/opened", loadOnce);
282
+ ```
283
+
284
+ More: [`timeout`](effects.md#timeout).
285
+
286
+ ### Refresh data every N seconds: poll
287
+
288
+ A dashboard must refresh its numbers every ten seconds while it is open. `poll` is an effect: it calls your function on a timer and ends when the task is aborted, so a closed dashboard does not keep asking.
289
+
290
+ ```js
291
+ import { createBus } from "nanoeffects";
292
+ import { poll } from "nanoeffects/effects";
293
+
294
+ const bus = createBus();
295
+
296
+ bus.on("dashboard/opened", async (_, ctx) => {
297
+ // stop the polling when the user leaves
298
+ ctx.on("dashboard/closed", () => ctx.abort());
299
+
300
+ // poll — call refresh every 10 s until the task is aborted
301
+ await poll(ctx, () => refresh(), 10_000);
302
+ });
303
+ ```
304
+
305
+ More: [`poll`](effects.md#poll).
306
+
307
+ ### Wait for another event inside a handler: take
308
+
309
+ After a login the handler must wait for the user to log out before it cleans up. Instead of a flag and a second handler, `take` pauses the handler until the next event with that name.
310
+
311
+ ```js
312
+ import { createBus } from "nanoeffects";
313
+ import { take } from "nanoeffects/effects";
314
+
315
+ const bus = createBus();
316
+
317
+ bus.on("login", async (user, ctx) => {
318
+ // take — pause this handler until the next "logout"
319
+ const reason = await take(ctx, "logout");
320
+ cleanup(user, reason);
321
+ });
322
+ ```
323
+
324
+ More: [`take`](effects.md#take).
325
+
326
+ ### Load several things at once: all
327
+
328
+ A profile page needs the user and the posts. Loading them one after another is slow. `all` runs both requests together and aborts the second one if the first fails.
329
+
330
+ ```js
331
+ import { createBus } from "nanoeffects";
332
+ import { all } from "nanoeffects/effects";
333
+
334
+ const bus = createBus();
335
+
336
+ bus.on("profile/opened", async (id, ctx) => {
337
+ // all — run both requests together; if one fails, the other is aborted. child carries the signal of its own request
338
+ const { user, posts } = await all(ctx, {
339
+ user: (child) => fetchUser(id, child),
340
+ posts: (child) => fetchPosts(id, child),
341
+ });
342
+
343
+ ctx.emit("profile/loaded", { user, posts });
344
+ });
345
+ ```
346
+
347
+ ### Take the fastest answer: race
348
+
349
+ The same data lives on two servers, and you want the answer of the faster one. `race` returns the first answer and aborts the other request.
350
+
351
+ ```js
352
+ import { createBus } from "nanoeffects";
353
+ import { race } from "nanoeffects/effects";
354
+
355
+ const bus = createBus();
356
+
357
+ bus.on("user/opened", async (id, ctx) => {
358
+ // race — the first answer wins, the slower request is aborted
359
+ const result = await race(ctx, {
360
+ primary: (child) => api.getUser(id, child),
361
+ backup: (child) => api.getUserFromBackup(id, child),
362
+ });
363
+
364
+ const user = "primary" in result ? result.primary : result.backup;
365
+ ctx.emit("user/loaded", user);
366
+ });
367
+ ```
368
+
369
+ More: [`all`](effects.md#all), [`race`](effects.md#race).
370
+
371
+ ## Features and domain logic
372
+
373
+ ### Get the result of emit
374
+
375
+ `emit` of a plain event returns nothing, so to get an answer you need a second event. An action with a handler returns the result of its handler right from `emit`.
376
+
377
+ ```js
378
+ import { createBus } from "nanoeffects";
379
+ import { createAction } from "nanoeffects/actions";
380
+
381
+ const bus = createBus();
382
+
383
+ // createAction with a handler — emit runs it and returns what it returns
384
+ const double = createAction("math/double", (value) => value * 2);
385
+
386
+ const result = bus.emit(double(21)); // 42
387
+ ```
388
+
389
+ More: [Actions, jobs and modules](actions.md).
390
+
391
+ ### Group a feature with its handlers: modules
392
+
393
+ A feature has several subscriptions, and you must remember to remove each one. A module registers them together and removes them all with one `unmount`.
394
+
395
+ ```js
396
+ import { createBus } from "nanoeffects";
397
+ import { createModule, modulePlugin } from "nanoeffects/actions";
398
+
399
+ // createModule — one feature; unmount cancels everything it registered
400
+ const greeting = createModule("greeting", (m) => {
401
+ m.on("greet", (name) => console.log(`hello ${name}`));
402
+ });
403
+
404
+ const bus = createBus();
405
+ const app = bus.use(modulePlugin([greeting]));
406
+
407
+ app.unmount(greeting); // the subscription is gone
408
+ ```
409
+
410
+ More: [Modules](actions.md#modules) and [the guide](guide.md#9-mount-a-domain-as-a-module).
411
+
412
+ ### Do not leave subscriptions after a widget closes
413
+
414
+ A chat widget subscribes to the bus when it opens. If it closes without unsubscribing, the handlers stay: memory grows with every open, and they still update a widget that is gone. A module that you mount on open and unmount on close leaves nothing behind.
415
+
416
+ ```js
417
+ import { createBus } from "nanoeffects";
418
+ import { createModule, modulePlugin } from "nanoeffects/actions";
419
+
420
+ // the module owns the listeners of the widget
421
+ const chatModule = createModule("chat", (m) => {
422
+ m.on("message/received", (message) => render(message));
423
+ });
424
+
425
+ const bus = createBus();
426
+ const app = bus.use(modulePlugin());
427
+
428
+ const stop = app.mount(chatModule); // the widget is opened: the listeners are on the bus
429
+
430
+ stop(); // the widget is closed: no listeners are left
431
+ console.log(bus.get("message/received").length); // 0
432
+ ```
433
+
434
+ More: [Build a chat widget](chat-widget.md).
435
+
436
+ ## Stop work on a server
437
+
438
+ A client closes the tab, but the server goes on asking other services for it. Create an `AbortController` for the request, abort it when the connection closes, and pass its signal to `emit`: `timeout`, `retry` and the request itself stop.
439
+
440
+ ```js
441
+ import { createBus } from "nanoeffects";
442
+ import { createAction } from "nanoeffects/actions";
443
+ import { signalPlugin } from "nanoeffects/signal";
444
+
445
+ // signalPlugin — emit accepts { signal }
446
+ const bus = createBus().use(signalPlugin());
447
+
448
+ const getUser = createAction("user/get", (id, { signal }) => api.getUser(id, { signal }));
449
+
450
+ export async function handleRequest(request, reply) {
451
+ const controller = new AbortController();
452
+
453
+ // close — the client left: abort everything started for this request
454
+ reply.raw.on("close", () => controller.abort());
455
+
456
+ // the task, and the requests it made, stop with the controller
457
+ return bus.emit(getUser(request.params.id), { signal: controller.signal });
458
+ }
459
+ ```
460
+
461
+ More: [Stop work on a server](server.md), including WebSocket and queue workers.
462
+
463
+ ## Debugging: what ran and why
464
+
465
+ An effect fired and you cannot tell which event caused it. The tracer logs every task with its parent, so you see the chain of events.
466
+
467
+ ```js
468
+ import { createBus } from "nanoeffects";
469
+ import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
470
+
471
+ // createTracer — send every task to the console
472
+ const trace = createTracer(consoleLogger());
473
+
474
+ // tracePlugin — also report every emit
475
+ const bus = createBus({ trace }).use(tracePlugin());
476
+
477
+ bus.on("search", (query, ctx) => ctx.emit("results", [query]));
478
+ bus.emit("search", "nano"); // the log shows: search · emit, then results · emit with search as the cause
479
+ ```
480
+
481
+ More: [Trace tasks](tracing.md). To reload modules without a page refresh, see [Hot reload](hmr.md).
482
+
483
+ ## Other libraries in the project
484
+
485
+ - **Redux Toolkit, Zustand, MobX or Pinia.** [Connect a store](stores.md).
486
+ - **React Query, SWR or RTK Query.** [Query libraries](query-libraries.md).
487
+ - **RxJS.** [Use nanoeffects next to RxJS](migrating-from-rxjs.md) and [Observables](observable.md).
488
+ - **Moving from redux-saga.** [Coming from redux-saga](migrating-from-saga.md).
489
+ - **Moving from Effector.** [Coming from Effector](migrating-from-effector.md).
490
+ - **Moving from mitt or nanoevents.** [Coming from mitt](migrating-from-mitt.md).
491
+
492
+ ## Classes and TypeScript
493
+
494
+ ### Use the bus in a class
495
+
496
+ Your logic lives in classes, and each instance must remove its subscriptions when it is no longer needed. `Emitter` gives every instance its own bus and a `dispose` that removes all of its subscriptions.
497
+
498
+ ```js
499
+ import { Emitter } from "nanoeffects/oop";
500
+
501
+ // Emitter — every instance gets its own bus with on, once, off, emit and dispose
502
+ class VideoService extends Emitter {
503
+ constructor() {
504
+ super();
505
+ this.on("played", () => console.log("playing"));
506
+ }
507
+ }
508
+
509
+ const video = new VideoService();
510
+ video.emit("played");
511
+
512
+ video.dispose(); // removes every subscription of this instance
513
+ ```
514
+
515
+ More: [Use the bus from a class](oop.md).
516
+
517
+ ### Keep the types of the bus in many files
518
+
519
+ Handlers and modules are in different files, and TypeScript must know the payloads and the fields of `ctx`. See [TypeScript in many files](typescript.md).
520
+
521
+ ## Examples of whole apps
522
+
523
+ - **A form with a username check and a submit, in React.** [Build a sign-up form in React](react.md).
524
+ - **A whole app in layers: entities, features, modules, a server.** [Build a chat](chat-example.md).
525
+ - **A widget that listens only while it is open.** [Build a chat widget](chat-widget.md).
526
+ - **Vue.** [Use nanoeffects in a Vue app](vue.md).
527
+ - **Svelte.** [Use nanoeffects in a Svelte app](svelte.md).
528
+ - **A ready project.** [Start a project from a template](starter.md).
@@ -0,0 +1,62 @@
1
+ # Документация
2
+
3
+ nanoeffects выносит бизнес-логику и побочные эффекты из компонентов в обычные `async`-функции. Политики (`latest`, `debounce`, `queue`) и эффекты (`retry`, `timeout`, `poll`) решают гонки, повторы и отмену запросов.
4
+
5
+ ```js
6
+ import { createBus } from "nanoeffects";
7
+ import { latest } from "nanoeffects/policies";
8
+
9
+ const bus = createBus(); // шина событий
10
+
11
+ // Логика поиска живёт вне компонента; latest прерывает предыдущий запрос
12
+ bus.on(
13
+ "search",
14
+ latest(async (query, { signal }) => {
15
+ const items = await api.search(query, { signal });
16
+ bus.emit("found", items);
17
+ }),
18
+ );
19
+
20
+ // Компонент только слушает результат и отправляет события
21
+ bus.on("found", (items) => render(items));
22
+ bus.emit("search", "nano");
23
+ ```
24
+
25
+ Тот же пример с UI-фреймворком: [React](react.md), [Vue](vue.md), [Svelte](svelte.md). Готовые проекты: [Проект из шаблона](starter.md).
26
+
27
+ ## Установка
28
+
29
+ ```bash
30
+ npm install nanoeffects
31
+ ```
32
+
33
+ Новый проект из шаблона ([Проект из шаблона](starter.md)):
34
+
35
+ ```bash
36
+ npm create nanoeffects@latest my-app
37
+ ```
38
+
39
+ ## С чего начать
40
+
41
+ 1. Пройдите [Руководство](guide.md#1-создание-шины-событий): от первой шины до модулей.
42
+ 2. Типичные задачи async UI (устаревшие ответы, двойные клики, повторы запросов) разобраны в [Типовых задачах](recipes.md).
43
+
44
+ ## Виды проектов
45
+
46
+ | Уровень | Проект | Читать |
47
+ | ------- | ------ | ------ |
48
+ | **A** | Небольшое приложение, виджет или MVP | [Руководство](guide.md#1-создание-шины-событий) |
49
+ | **B** | SPA с async UI: поиск, отправка сообщений, размонтирование компонентов | [Политики](policies.md), [Руководство](guide.md#3-прерывание-предыдущих-запусков) |
50
+ | **C** | Сложная бизнес-логика | [Эффекты](effects.md), [Руководство](guide.md#5-retry-и-timeout) |
51
+ | **D** | Доменная логика вне UI | [Actions, jobs и модули](actions.md), [Руководство](guide.md#7-способы-отправки-событий) |
52
+ | **E** | Готовое приложение на Redux, Zustand, MobX или Pinia, либо библиотека данных | [State managers](stores.md), [Библиотеки запросов](query-libraries.md) |
53
+ | **F** | Платформа, SDK или долгоживущее приложение | [Трассировка](tracing.md), [Hot reload](hmr.md), [Семантика](semantics.md) |
54
+
55
+ ## Справочник
56
+
57
+ - [Точки входа пакета](package.md)
58
+ - [Плагины](plugins.md): что даёт каждый плагин шины и в каком порядке их подключать
59
+ - [Семантика](semantics.md): точные правила `emit`, отмены и порядка
60
+ - [Трассировка](tracing.md): логгеры, Redux DevTools, `notrace`
61
+ - [Observable](observable.md): события как Observable стандарта TC39
62
+ - [Hot reload и диагностика при разработке](hmr.md)