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
+ # Типовые задачи
2
+
3
+ > **[Уровни A–E](README.md#виды-проектов)**: от небольшого приложения до готового приложения на Redux, Zustand, MobX или Pinia. Сначала прочитайте: [Руководство](guide.md#1-создание-шины-событий).
4
+
5
+ У каждой задачи сначала описана проблема, затем идёт короткое решение. Если на странице есть подробности, последняя строка ведёт к ним. Задачи идут от простых к сложным.
6
+
7
+ ## На этой странице
8
+
9
+ - [События и обработчики](#события-и-обработчики)
10
+ - [Обработчику нужен API-клиент, store или логгер](#обработчику-нужен-api-клиент-store-или-логгер)
11
+ - [Один обработчик упал, остальные должны выполниться](#один-обработчик-упал-остальные-должны-выполниться)
12
+ - [Опечатка в имени события](#опечатка-в-имени-события)
13
+ - [Гонки и повторные действия в UI](#гонки-и-повторные-действия-в-ui)
14
+ - [Старый ответ затирает новый](#старый-ответ-затирает-новый)
15
+ - [Поле поиска отправляет запрос на каждую клавишу](#поле-поиска-отправляет-запрос-на-каждую-клавишу)
16
+ - [Второй клик отправляет второй запрос](#второй-клик-отправляет-второй-запрос)
17
+ - [Сообщения должны уходить по порядку](#сообщения-должны-уходить-по-порядку)
18
+ - [Кнопка «Удалить» в списке блокирует соседние строки](#кнопка-удалить-в-списке-блокирует-соседние-строки)
19
+ - [Ненадёжная сеть и длинные сценарии](#ненадёжная-сеть-и-длинные-сценарии)
20
+ - [Запрос иногда падает: retry и backoff](#запрос-иногда-падает-retry-и-backoff)
21
+ - [Запрос зависает: timeout](#запрос-зависает-timeout)
22
+ - [Обновлять данные каждые N секунд: poll](#обновлять-данные-каждые-n-секунд-poll)
23
+ - [Дождаться другого события внутри обработчика: take](#дождаться-другого-события-внутри-обработчика-take)
24
+ - [Загрузить несколько ресурсов сразу: all](#загрузить-несколько-ресурсов-сразу-all)
25
+ - [Взять самый быстрый ответ: race](#взять-самый-быстрый-ответ-race)
26
+ - [Фичи и доменная логика](#фичи-и-доменная-логика)
27
+ - [Получить результат от emit](#получить-результат-от-emit)
28
+ - [Сгруппировать фичу с её обработчиками: модули](#сгруппировать-фичу-с-её-обработчиками-модули)
29
+ - [Не оставлять подписки после закрытия виджета](#не-оставлять-подписки-после-закрытия-виджета)
30
+ - [Остановка работы на backend](#остановка-работы-на-backend)
31
+ - [Отладка: что выполнилось и почему](#отладка-что-выполнилось-и-почему)
32
+ - [Другие библиотеки в проекте](#другие-библиотеки-в-проекте)
33
+ - [ООП и TypeScript](#ооп-и-typescript)
34
+ - [Шина в классе](#шина-в-классе)
35
+ - [Типы шины в нескольких файлах](#типы-шины-в-нескольких-файлах)
36
+ - [Примеры приложений](#примеры-приложений)
37
+
38
+ ## События и обработчики
39
+
40
+ ### Обработчику нужен API-клиент, store или логгер
41
+
42
+ Многим обработчикам нужен один и тот же `api`-клиент или логгер. Если импортировать его в каждом файле, обработчики привязываются к одному клиенту, а тестировать их труднее. Положите клиент на шину один раз: каждая задача получит его в `ctx`.
43
+
44
+ ```js
45
+ import { createBus } from "nanoeffects";
46
+
47
+ // createBus({ context }) — каждая задача получает эти поля на 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
+ Подробнее: [Поля контекста](context.md).
57
+
58
+ ### Один обработчик упал, остальные должны выполниться
59
+
60
+ На `order/placed` подписаны два обработчика: один отправляет аналитику, другой сохраняет заказ. По умолчанию первая ошибка прекращает перебор, и упавшая аналитика может помешать сохранить заказ. С `onError` выполняется каждый обработчик, а ошибки попадают в одно место.
61
+
62
+ ```js
63
+ import { createBus } from "nanoeffects";
64
+
65
+ const bus = createBus({
66
+ // onError — изолирует обработчики: выброшенная ошибка попадает сюда, а следующий обработчик всё равно выполняется
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); // выбрасывает ошибку
76
+ bus.on("order/placed", saveOrder); // всё равно выполняется
77
+
78
+ bus.emit("order/placed", { id: 1 }); // saved 1
79
+ ```
80
+
81
+ Подробнее: [Ошибки и события без подписчиков](errors.md#изоляция-упавших-обработчиков).
82
+
83
+ ### Опечатка в имени события
84
+
85
+ `emit("serch")` не падает: на это имя никто не подписан, поэтому ничего не происходит, а ошибку ищут в обработчике. `onUnhandled` предупреждает о событии, которого никто не услышал. Включите его при разработке.
86
+
87
+ ```js
88
+ import { createBus } from "nanoeffects";
89
+
90
+ const bus = createBus({
91
+ // onUnhandled — вызывается, когда у события нет подписок
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"); // предупреждение: Nobody listens to "serch"
98
+ ```
99
+
100
+ Подробнее: [События без подписчиков](errors.md#события-без-подписчиков).
101
+
102
+ ## Гонки и повторные действия в UI
103
+
104
+ ### Старый ответ затирает новый
105
+
106
+ Пользователь печатает `re`, затем `react`. Идут два запроса. Сервер быстро отвечает на `react` и медленно на `re`, поэтому ответ на `re` приходит последним и подменяет правильный список неправильным. Так ведёт себя обычный эффект React:
107
+
108
+ ```jsx
109
+ import { useEffect, useState } from "react";
110
+
111
+ export function Search({ query }) {
112
+ const [items, setItems] = useState([]);
113
+
114
+ useEffect(() => {
115
+ // два запроса идут одновременно, и побеждает тот, что завершился последним, даже если он старый
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
+ Вынесите запрос в обработчик с `latest`: новый `search` прерывает предыдущий запрос, а прерванный результата не выдаёт.
130
+
131
+ ```js
132
+ import { createBus } from "nanoeffects";
133
+ import { latest } from "nanoeffects/policies";
134
+
135
+ const bus = createBus();
136
+
137
+ // latest — новый "search" прерывает предыдущий и отбрасывает его результат
138
+ bus.on(
139
+ "search",
140
+ latest(async (query, { signal }) => {
141
+ // signal — позволяет браузеру отменить сам запрос
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
+ Подробнее: [Отмена устаревших запросов](cancel-stale-requests.md), [Политики](policies.md#latest-прерывание-предыдущего-запуска) и сторона компонента в [Форме регистрации на React](react.md).
151
+
152
+ ### Поле поиска отправляет запрос на каждую клавишу
153
+
154
+ Набор слова `react` отправляет пять запросов, а список мигает. `debounce` ждёт паузы в наборе, `latest` прерывает запрос, который ещё идёт.
155
+
156
+ ```js
157
+ import { createBus } from "nanoeffects";
158
+ import { debounce, latest } from "nanoeffects/policies";
159
+
160
+ const bus = createBus();
161
+
162
+ // latest — новый поиск прерывает запрос в полёте
163
+ const searchLatest = latest(async (query, { signal }) => {
164
+ const items = await api.search(query, { signal });
165
+ bus.emit("results", items);
166
+ });
167
+
168
+ // debounce — запуск через 300 мс без нового "typed"
169
+ bus.on("typed", debounce(300, searchLatest));
170
+ ```
171
+
172
+ Подробнее: [Пауза между запросами](debounce-and-abort.md).
173
+
174
+ ### Второй клик отправляет второй запрос
175
+
176
+ Пользователь дважды кликает «Зарегистрироваться» на медленной сети, и создаются два аккаунта. `leading` отбрасывает все вызовы, пришедшие, пока идёт первый.
177
+
178
+ ```js
179
+ import { createBus } from "nanoeffects";
180
+ import { leading } from "nanoeffects/policies";
181
+
182
+ const bus = createBus();
183
+
184
+ // leading — вызов, пришедший во время предыдущего, отбрасывается
185
+ bus.on(
186
+ "form/submitted",
187
+ leading(async (form, { signal }) => {
188
+ await api.signUp(form, { signal });
189
+ }),
190
+ );
191
+ ```
192
+
193
+ Подробнее: [Защита от повторной отправки](prevent-double-submit.md).
194
+
195
+ ### Сообщения должны уходить по порядку
196
+
197
+ В чате пользователь отправляет `Привет` и через секунду `Как дела?`. Два параллельных запроса могут дойти до сервера в обратном порядке. `queue` отправляет следующее сообщение только после доставки предыдущего.
198
+
199
+ ```js
200
+ import { createBus } from "nanoeffects";
201
+ import { queue } from "nanoeffects/policies";
202
+
203
+ const bus = createBus();
204
+
205
+ // queue — следующее сообщение ждёт, пока доставится предыдущее
206
+ bus.on(
207
+ "message/sent",
208
+ queue(async (message, { signal }) => {
209
+ await api.sendMessage(message, { signal });
210
+ }),
211
+ );
212
+ ```
213
+
214
+ Подробнее: [Политики, очередь через `queue`](policies.md#очередь-событий-через-queue).
215
+
216
+ ### Кнопка «Удалить» в списке блокирует соседние строки
217
+
218
+ Вы защитили «Удалить» через `leading`. Пока удаляется одна строка, клики по остальным тоже отбрасываются: у всего обработчика одно state. `key` даёт каждой строке своё.
219
+
220
+ ```js
221
+ import { createBus } from "nanoeffects";
222
+ import { leading } from "nanoeffects/policies";
223
+
224
+ const bus = createBus();
225
+
226
+ // leading с key — у каждого id строки своё state, поэтому другие строки не блокируются
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
+ Подробнее: [Политики, `key`](policies.md#key-разделение-state-по-ключу).
239
+
240
+ ## Ненадёжная сеть и длинные сценарии
241
+
242
+ ### Запрос иногда падает: retry и backoff
243
+
244
+ Запрос иногда падает из-за сети. `retry` повторяет его. Если backend перегружен, мгновенные повторы только ухудшают дело, поэтому `backoff` увеличивает паузу с каждой попыткой.
245
+
246
+ ```js
247
+ import { createBus } from "nanoeffects";
248
+ import { backoff, retry } from "nanoeffects/effects";
249
+
250
+ const bus = createBus();
251
+
252
+ // retry — повторять запрос после ошибки, до 4 попыток
253
+ const loadWithRetry = retry((id, { signal }) => api.getUser(id, { signal }), {
254
+ times: 4,
255
+ // backoff — пауза растёт (200, 400, 800 мс) со случайной добавкой, чтобы клиенты не повторяли запрос в один момент
256
+ delay: backoff({
257
+ base: 200,
258
+ max: 5000,
259
+ jitter: true,
260
+ }),
261
+ });
262
+
263
+ bus.on("user/opened", loadWithRetry);
264
+ ```
265
+
266
+ Подробнее: [`backoff`](effects.md#backoff).
267
+
268
+ ### Запрос зависает: timeout
269
+
270
+ Запрос не падает и не отвечает, а спиннер крутится бесконечно. `timeout` отклоняет вызов через заданное время и прерывает запрос.
271
+
272
+ ```js
273
+ import { createBus } from "nanoeffects";
274
+ import { timeout } from "nanoeffects/effects";
275
+
276
+ const bus = createBus();
277
+
278
+ // timeout — отклонить с TimeoutError и прервать запрос через 5 с
279
+ const loadOnce = timeout(5000, (id, { signal }) => api.getUser(id, { signal }));
280
+
281
+ bus.on("user/opened", loadOnce);
282
+ ```
283
+
284
+ Подробнее: [`timeout`](effects.md#timeout).
285
+
286
+ ### Обновлять данные каждые N секунд: poll
287
+
288
+ Дашборд должен обновлять цифры каждые десять секунд, пока открыт. `poll` — эффект: он вызывает вашу функцию по таймеру и заканчивается, когда задача прервана, поэтому закрытый дашборд не продолжает опрашивать.
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
+ // остановить опрос, когда пользователь уходит
298
+ ctx.on("dashboard/closed", () => ctx.abort());
299
+
300
+ // poll — вызывать refresh каждые 10 с, пока задача не прервана
301
+ await poll(ctx, () => refresh(), 10_000);
302
+ });
303
+ ```
304
+
305
+ Подробнее: [`poll`](effects.md#poll).
306
+
307
+ ### Дождаться другого события внутри обработчика: take
308
+
309
+ После входа обработчик должен дождаться выхода пользователя и только потом очистить данные. Вместо флага и второго обработчика `take` приостанавливает обработчик до следующего события с таким именем.
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 — приостановить обработчик до следующего "logout"
319
+ const reason = await take(ctx, "logout");
320
+ cleanup(user, reason);
321
+ });
322
+ ```
323
+
324
+ Подробнее: [`take`](effects.md#take).
325
+
326
+ ### Загрузить несколько ресурсов сразу: all
327
+
328
+ Странице профиля нужны пользователь и его посты. Загружать их по очереди медленно. `all` запускает оба запроса вместе и прерывает второй, если первый упал.
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 — запустить оба запроса вместе; если один упал, второй прерывается. child несёт сигнал своего запроса
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
+ ### Взять самый быстрый ответ: race
348
+
349
+ Одни и те же данные есть на двух серверах, и нужен ответ того, что быстрее. `race` возвращает первый ответ и прерывает второй запрос.
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 — побеждает первый ответ, более медленный запрос прерывается
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
+ Подробнее: [`all`](effects.md#all), [`race`](effects.md#race).
370
+
371
+ ## Фичи и доменная логика
372
+
373
+ ### Получить результат от emit
374
+
375
+ `emit` обычного события ничего не возвращает, и для ответа нужно второе событие. Action с обработчиком возвращает результат своего обработчика прямо из `emit`.
376
+
377
+ ```js
378
+ import { createBus } from "nanoeffects";
379
+ import { createAction } from "nanoeffects/actions";
380
+
381
+ const bus = createBus();
382
+
383
+ // createAction с обработчиком — emit запускает его и возвращает результат
384
+ const double = createAction("math/double", (value) => value * 2);
385
+
386
+ const result = bus.emit(double(21)); // 42
387
+ ```
388
+
389
+ Подробнее: [Actions, jobs и модули](actions.md).
390
+
391
+ ### Сгруппировать фичу с её обработчиками: модули
392
+
393
+ У фичи несколько подписок, и каждую нужно не забыть снять. Модуль регистрирует их вместе и снимает все одним `unmount`.
394
+
395
+ ```js
396
+ import { createBus } from "nanoeffects";
397
+ import { createModule, modulePlugin } from "nanoeffects/actions";
398
+
399
+ // createModule — одна фича; unmount отменяет всё, что она зарегистрировала
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); // подписки нет
408
+ ```
409
+
410
+ Подробнее: [Модули](actions.md#модули) и [Руководство](guide.md#9-создание-модулей).
411
+
412
+ ### Не оставлять подписки после закрытия виджета
413
+
414
+ Виджет чата подписывается на шину при открытии. Если закрыть его без отписки, обработчики остаются: память растёт с каждым открытием, а они продолжают обновлять уже не существующий виджет. Модуль, который подключается при открытии и отключается при закрытии, ничего не оставляет.
415
+
416
+ ```js
417
+ import { createBus } from "nanoeffects";
418
+ import { createModule, modulePlugin } from "nanoeffects/actions";
419
+
420
+ // модуль владеет подписками виджета
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); // виджет открыт: подписки на шине
429
+
430
+ stop(); // виджет закрыт: подписок не осталось
431
+ console.log(bus.get("message/received").length); // 0
432
+ ```
433
+
434
+ Подробнее: [Виджет чата](chat-widget.md).
435
+
436
+ ## Остановка работы на backend
437
+
438
+ Клиент закрыл вкладку, а backend продолжает опрашивать для него другие сервисы. Создайте `AbortController` на запрос, прервите его при закрытии соединения и передайте сигнал в `emit`: `timeout`, `retry` и сам запрос остановятся.
439
+
440
+ ```js
441
+ import { createBus } from "nanoeffects";
442
+ import { createAction } from "nanoeffects/actions";
443
+ import { signalPlugin } from "nanoeffects/signal";
444
+
445
+ // signalPlugin — emit принимает { 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 — клиент ушёл: прервать всё, что запущено для этого запроса
454
+ reply.raw.on("close", () => controller.abort());
455
+
456
+ // задача и её запросы останавливаются вместе с контроллером
457
+ return bus.emit(getUser(request.params.id), { signal: controller.signal });
458
+ }
459
+ ```
460
+
461
+ Подробнее: [Остановка работы на backend](server.md), в том числе для WebSocket и воркеров очереди.
462
+
463
+ ## Отладка: что выполнилось и почему
464
+
465
+ Эффект сработал, а какое событие его вызвало, непонятно. Трассировщик пишет каждую задачу вместе с родителем, и вы видите цепочку событий.
466
+
467
+ ```js
468
+ import { createBus } from "nanoeffects";
469
+ import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
470
+
471
+ // createTracer — отправлять каждую задачу в консоль
472
+ const trace = createTracer(consoleLogger());
473
+
474
+ // tracePlugin — сообщать ещё и о каждом emit
475
+ const bus = createBus({ trace }).use(tracePlugin());
476
+
477
+ bus.on("search", (query, ctx) => ctx.emit("results", [query]));
478
+ bus.emit("search", "nano"); // в логе: search · emit, затем results · emit с search в качестве причины
479
+ ```
480
+
481
+ Подробнее: [Трассировка задач](tracing.md). Чтобы перезагружать модули без обновления страницы, см. [Hot reload](hmr.md).
482
+
483
+ ## Другие библиотеки в проекте
484
+
485
+ - **Redux Toolkit, Zustand, MobX или Pinia.** [Подключение store](stores.md).
486
+ - **React Query, SWR или RTK Query.** [Библиотеки запросов](query-libraries.md).
487
+ - **RxJS.** [nanoeffects рядом с RxJS](migrating-from-rxjs.md) и [Observable](observable.md).
488
+ - **Переход с redux-saga.** [Переход с redux-saga](migrating-from-saga.md).
489
+ - **Переход с Effector.** [Переход с Effector](migrating-from-effector.md).
490
+ - **Переход с mitt или nanoevents.** [Переход с mitt](migrating-from-mitt.md).
491
+
492
+ ## ООП и TypeScript
493
+
494
+ ### Шина в классе
495
+
496
+ Ваша логика живёт в классах, и каждый экземпляр должен снимать свои подписки, когда больше не нужен. `Emitter` даёт каждому экземпляру свою шину и `dispose`, который снимает все его подписки.
497
+
498
+ ```js
499
+ import { Emitter } from "nanoeffects/oop";
500
+
501
+ // Emitter — у каждого экземпляра своя шина с on, once, off, emit и 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(); // снимает все подписки этого экземпляра
513
+ ```
514
+
515
+ Подробнее: [Шина в классе](oop.md).
516
+
517
+ ### Типы шины в нескольких файлах
518
+
519
+ Обработчики и модули лежат в разных файлах, и TypeScript должен знать payload и поля `ctx`. См. [TypeScript в нескольких файлах](typescript.md).
520
+
521
+ ## Примеры приложений
522
+
523
+ - **Форма с проверкой имени и отправкой, на React.** [Форма регистрации на React](react.md).
524
+ - **Всё приложение по слоям: сущности, фичи, модули, backend.** [Чат с сущностями, фичами и модулями](chat-example.md).
525
+ - **Виджет, который слушает только пока открыт.** [Виджет чата](chat-widget.md).
526
+ - **Vue.** [nanoeffects в приложении на Vue](vue.md).
527
+ - **Svelte.** [nanoeffects в приложении на Svelte](svelte.md).
528
+ - **Готовый проект.** [Проект из шаблона](starter.md).
@@ -0,0 +1,19 @@
1
+ # Семантика
2
+
3
+ > **[Уровень F](README.md#виды-проектов)**: платформа, SDK или долгоживущее приложение. Сначала прочитайте: [Руководство](guide.md#1-создание-шины-событий).
4
+
5
+ ## Пять основных правил
6
+
7
+ 1. Обработчик — это `(payload, ctx)`. Используйте `ctx.signal`, `ctx.emit` и `ctx.on`; остальное в `ctx` необязательно.
8
+ 2. Передавайте `ctx.signal` в то, что вызывает обработчик (`fetch`, клиент): тогда прерывание остановит работу.
9
+ 3. Прерванная задача не выдаёт результата и ничего не отправляет.
10
+ 4. `emit` синхронный: первая ошибка обработчика выбрасывается, если не задан `onError`.
11
+ 5. Отклонения async-обработчиков без `onError` не обрабатываются.
12
+
13
+ Подробности и остальные правила:
14
+
15
+ - **Прерванная задача не выдаёт результата.** Если задачу прервали до завершения Promise, Promise отклоняется с причиной прерывания, даже если значение пришло в том же тике. По этому правилу работают `latest`, `debounce`, `all`, `race`, `take` и остальные.
16
+ - **`emit` — обычный синхронный цикл** (см. [Ошибки](errors.md)). `ctx.emit` внутри обработчика — тот же вызов. Без `onError` ошибка вложенного обработчика выбрасывается в обработчик, который отправил событие. С `onError` она уходит в `onError`, а `ctx.emit` ничего не выбрасывает.
17
+ - **Отклонения async-обработчиков** библиотека не обрабатывает. Без `onError` они остаются необработанными, и Node по умолчанию завершает из-за них процесс. Для async-обработчиков задайте `onError`.
18
+ - **`leading` снимает блокировку, когда задача прерывается**, а не только когда завершается `fn`. Если `fn` игнорирует сигнал прерывания, он может пересечься со следующим запуском. Передавайте `ctx.signal` в то, что делает `fn`.
19
+ - **Эффекты запускают свою функцию синхронно**, поэтому событие, отправленное сразу после вызова, доходит до `take` внутри неё.