nanoeffects 0.29.0 → 0.31.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 (127) hide show
  1. package/README.md +6 -5
  2. package/dist/{action-creator-jeEqIKgK.d.ts → action-creator-6rlBTmH9.d.ts} +27 -2
  3. package/dist/actions.d.ts +3 -3
  4. package/dist/actions.js +1 -1
  5. package/dist/{bus-0uYn_kRj.js → bus-DFlmNJ6h.js} +1 -1
  6. package/dist/context-B5Z2RpJt.js +1 -0
  7. package/dist/{context-types-Bme1mzkn.d.ts → context-types-DKNc5mkS.d.ts} +1 -1
  8. package/dist/context.d.ts +2 -2
  9. package/dist/dev.d.ts +49 -3
  10. package/dist/dev.js +2 -1
  11. package/dist/effects.d.ts +2 -2
  12. package/dist/effects.js +1 -1
  13. package/dist/emit-key-C6FL7Ewn.js +1 -0
  14. package/dist/guard.d.ts +59 -0
  15. package/dist/guard.js +1 -0
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/match.d.ts +1 -1
  19. package/dist/{module-BWMW83qk.d.ts → module-G39MaVOt.d.ts} +2 -2
  20. package/dist/notrace/{action-creator-xpQ_I2h8.d.ts → action-creator-BrMSO3RP.d.ts} +1 -0
  21. package/dist/notrace/actions.d.ts +2 -2
  22. package/dist/notrace/effects.d.ts +2 -2
  23. package/dist/notrace/index.d.ts +1 -1
  24. package/dist/notrace/{plugin-6LhNx0th.d.ts → plugin-7GUL2EW5.d.ts} +1 -1
  25. package/dist/notrace/policies.d.ts +1 -1
  26. package/dist/notrace/redux.d.ts +1 -1
  27. package/dist/observable.d.ts +1 -1
  28. package/dist/oop.d.ts +1 -1
  29. package/dist/oop.js +1 -1
  30. package/dist/{plugin-BaErevLy.d.ts → plugin-BWNP23hr.d.ts} +2 -2
  31. package/dist/policies.d.ts +2 -2
  32. package/dist/policies.js +1 -1
  33. package/dist/production/dev.js +1 -1
  34. package/dist/react.d.ts +2 -2
  35. package/dist/react.js +1 -1
  36. package/dist/redux.d.ts +1 -1
  37. package/dist/scope.d.ts +2 -2
  38. package/dist/signal.d.ts +22 -2
  39. package/dist/signal.js +1 -1
  40. package/dist/site-DLvUoXs3.js +1 -0
  41. package/dist/svelte.d.ts +1 -1
  42. package/dist/trace.d.ts +151 -2
  43. package/dist/trace.js +3 -1
  44. package/dist/{types-D5xsR37j.d.ts → types-N_cdRr1r.d.ts} +2 -2
  45. package/dist/vue.d.ts +1 -1
  46. package/dist/{with-policy-C6BIiPIi.js → with-policy-CeM1rtES.js} +1 -1
  47. package/dist/{with-policy-BZVRBUOm.d.ts → with-policy-CthiBp_z.d.ts} +1 -1
  48. package/docs/README.md +62 -31
  49. package/docs/actions.md +44 -27
  50. package/docs/cancel-stale-requests.md +2 -0
  51. package/docs/chat-example.md +3 -1
  52. package/docs/chat-widget.md +2 -0
  53. package/docs/context.md +322 -9
  54. package/docs/debounce-and-abort.md +3 -1
  55. package/docs/debugging-loops.md +318 -0
  56. package/docs/effects.md +255 -138
  57. package/docs/errors.md +4 -2
  58. package/docs/examples.md +60 -0
  59. package/docs/guide.md +39 -22
  60. package/docs/hmr.md +35 -1
  61. package/docs/loop-guard.md +109 -0
  62. package/docs/migrating-from-effector.md +4 -2
  63. package/docs/migrating-from-mitt.md +6 -4
  64. package/docs/migrating-from-rxjs.md +2 -0
  65. package/docs/migrating-from-saga.md +2 -0
  66. package/docs/motivation.md +14 -21
  67. package/docs/observable.md +2 -0
  68. package/docs/oop.md +3 -1
  69. package/docs/package.md +4 -1
  70. package/docs/pass-the-signal.md +346 -0
  71. package/docs/plugins.md +22 -5
  72. package/docs/policies.md +73 -46
  73. package/docs/prevent-double-submit.md +3 -1
  74. package/docs/query-libraries.md +3 -1
  75. package/docs/react.md +42 -13
  76. package/docs/recipes.md +528 -0
  77. package/docs/ru/README.md +62 -0
  78. package/docs/ru/actions.md +412 -0
  79. package/docs/ru/cancel-stale-requests.md +106 -0
  80. package/docs/ru/chat-example.md +259 -0
  81. package/docs/ru/chat-widget.md +131 -0
  82. package/docs/ru/context.md +402 -0
  83. package/docs/ru/debounce-and-abort.md +105 -0
  84. package/docs/ru/debugging-loops.md +318 -0
  85. package/docs/ru/effects.md +538 -0
  86. package/docs/ru/errors.md +36 -0
  87. package/docs/ru/examples.md +60 -0
  88. package/docs/ru/guide.md +530 -0
  89. package/docs/ru/hmr.md +83 -0
  90. package/docs/ru/loop-guard.md +109 -0
  91. package/docs/ru/migrating-from-effector.md +339 -0
  92. package/docs/ru/migrating-from-mitt.md +99 -0
  93. package/docs/ru/migrating-from-rxjs.md +197 -0
  94. package/docs/ru/migrating-from-saga.md +97 -0
  95. package/docs/ru/motivation.md +47 -0
  96. package/docs/ru/observable.md +233 -0
  97. package/docs/ru/oop.md +431 -0
  98. package/docs/ru/package.md +42 -0
  99. package/docs/ru/pass-the-signal.md +346 -0
  100. package/docs/ru/plugins.md +398 -0
  101. package/docs/ru/policies.md +201 -0
  102. package/docs/ru/prevent-double-submit.md +122 -0
  103. package/docs/ru/query-libraries.md +392 -0
  104. package/docs/ru/react.md +309 -0
  105. package/docs/ru/recipes.md +528 -0
  106. package/docs/ru/semantics.md +19 -0
  107. package/docs/ru/server.md +204 -0
  108. package/docs/ru/starter.md +118 -0
  109. package/docs/ru/stores.md +673 -0
  110. package/docs/ru/svelte.md +78 -0
  111. package/docs/ru/tracing.md +466 -0
  112. package/docs/ru/typescript.md +170 -0
  113. package/docs/ru/vue.md +92 -0
  114. package/docs/semantics.md +2 -0
  115. package/docs/server.md +2 -0
  116. package/docs/starter.md +2 -0
  117. package/docs/stores.md +2 -0
  118. package/docs/svelte.md +3 -1
  119. package/docs/tracing.md +58 -1
  120. package/docs/typescript.md +2 -0
  121. package/docs/vue.md +3 -1
  122. package/llms-full.txt +2087 -418
  123. package/llms.txt +11 -8
  124. package/package.json +23 -6
  125. package/dist/context-DtUnzC-1.js +0 -1
  126. package/docs/scope.md +0 -278
  127. package/docs/stability.md +0 -90
@@ -0,0 +1,318 @@
1
+ # Поиск и обрыв циклов событий
2
+
3
+ > **[Уровень F](README.md#виды-проектов)**: платформа, SDK или долгоживущее приложение. Сначала прочитайте: [Трассировка задач](tracing.md#подключение-трассировщика). Чтобы только оборвать цикл, перейдите к [`loopGuardPlugin`](loop-guard.md).
4
+
5
+ Цикл — это два обработчика, которые отправляют события друг друга, или один, который отправляет своё же. На этой странице цикл находят, показывают и останавливают. Каждый сценарий ниже — запускаемый файл в [`examples/debug`](https://github.com/nanoeffects/nanoeffects/tree/main/examples/debug), а страница с кнопками и Redux DevTools лежит в [`examples/debug-playground`](https://github.com/nanoeffects/nanoeffects/tree/main/examples/debug-playground).
6
+
7
+ ## На этой странице
8
+
9
+ - [Симптомы](#симптомы)
10
+ - [Поиск цикла](#поиск-цикла)
11
+ - [Чтение отчёта](#чтение-отчёта)
12
+ - [Построение графа](#построение-графа)
13
+ - [Цикл в Redux DevTools](#цикл-в-redux-devtools)
14
+ - [Цикл без цепочки](#цикл-без-цепочки)
15
+ - [Разрыв цикла](#разрыв-цикла)
16
+ - [Обрыв цикла во время работы](#обрыв-цикла-во-время-работы)
17
+ - [Падение теста при цикле](#падение-теста-при-цикле)
18
+ - [Экспорт графа и payload](#экспорт-графа-и-payload)
19
+ - [Ограничения](#ограничения)
20
+
21
+ ## Симптомы
22
+
23
+ | Что вы видите | Вероятная причина | Куда перейти |
24
+ | ------------- | ----------------- | ------------ |
25
+ | Страница зависает | Цикл из микрозадач или синхронный цикл | [Поиск цикла](#поиск-цикла), затем [Обрыв цикла во время работы](#обрыв-цикла-во-время-работы) |
26
+ | Запрос повторяется сам | Обработчик отправляет событие, которое его запускает | [Чтение отчёта](#чтение-отчёта) |
27
+ | `RangeError: Maximum call stack size exceeded` | Синхронный цикл через `emit` | [Поиск цикла](#поиск-цикла) |
28
+ | Компонент рендерится снова и снова | Значение отправляется на шину на каждом рендере | [Цикл без цепочки](#цикл-без-цепочки) |
29
+ | Непонятно, какое событие к какому ведёт | Нет карты событий | [Построение графа](#построение-графа) |
30
+
31
+ ## Поиск цикла
32
+
33
+ `graphLogger` — логгер трассировки. Передайте его в `createTracer`, как любой другой. Он строит карту того, какое событие к какому ведёт, и один раз пишет отчёт, когда цепочка проходит через одно и то же событие три раза. Добавьте `tracePlugin`, чтобы он видел и каждый emit, и имена ваших подписок. Без плагина он тоже работает, но карта грубее.
34
+
35
+ ```js
36
+ // bus.js
37
+ import { createBus } from "nanoeffects";
38
+ import { createTracer, graphLogger, tracePlugin } from "nanoeffects/trace";
39
+
40
+ // graphLogger — карта событий и поиск циклов в одном логгере
41
+ export const graph = graphLogger({ name: "cart" });
42
+
43
+ // createTracer — передаёт логгеру каждую задачу шины
44
+ const trace = createTracer(graph);
45
+
46
+ // tracePlugin — сообщает о каждом emit, поэтому в графе есть и события, которые никто не слушает
47
+ export const bus = createBus({ trace }).use(tracePlugin());
48
+ ```
49
+
50
+ Обработчики — это часть вашего приложения, где есть цикл. Здесь корзина и итог обновляют друг друга:
51
+
52
+ ```js
53
+ // cart-handlers.js
54
+ import { bus } from "./bus";
55
+
56
+ // traceId — имя подписки в графе и в отчёте
57
+ bus.on(
58
+ "cart/changed",
59
+ (cart, ctx) => {
60
+ ctx.emit("total/changed", cart);
61
+ },
62
+ { traceId: "recalcTotal" },
63
+ );
64
+
65
+ bus.on(
66
+ "total/changed",
67
+ (total, ctx) => {
68
+ ctx.emit("cart/changed", total);
69
+ },
70
+ { traceId: "syncCart" },
71
+ );
72
+ ```
73
+
74
+ `graphLogger` стоит одного поиска на каждую задачу. Включайте его при разработке или в production вместе с `createTracer(graph, { sample })`.
75
+
76
+ ## Чтение отчёта
77
+
78
+ <!-- docs-check: continue -->
79
+
80
+ Отчёт — одна строка `console.warn` на каждый цикл:
81
+
82
+ ```text
83
+ [nanoeffects cart] cycle: recalcTotal → total/changed → syncCart → cart/changed → recalcTotal (repeated 3 times, started at cart/changed)
84
+ recalcTotal at src/cart-handlers.js:6; syncCart at src/cart-handlers.js:14 (only with devPlugin)
85
+ handlers feed each other: break the loop with a condition, or emit a different event for the result
86
+ ```
87
+
88
+ - **Путь** идёт от первого витка цикла обратно к нему. У именованной подписки перед ней показано её событие.
89
+ - **`started at`** — первое событие цепочки.
90
+ - **`at файл:строка`** появляется, если вы используете и [`devPlugin`](hmr.md#сообщения-об-утечках-через-devplugin).
91
+ - **Последняя строка** — совет для такой формы цикла.
92
+
93
+ Чтобы получить находку в коде, передайте `onCycle`. Он заменяет отчёт в консоли:
94
+
95
+ ```js
96
+ // report.js
97
+ import { graphLogger } from "nanoeffects/trace";
98
+
99
+ export const graph = graphLogger({
100
+ // onCycle — вызывается один раз на каждый цикл; в находке есть путь, повторы и корень
101
+ onCycle: (finding) => {
102
+ console.error(finding.kind, finding.path.join(" → "));
103
+ },
104
+ });
105
+ ```
106
+
107
+ `graph.cycles()` перечисляет найденные циклы и `turns`: сколько раз цикл прошёл после обнаружения. В списке есть и циклы, которых не показала ни одна цепочка, когда петлю замыкают рёбра разных запусков (`source: "graph"`).
108
+
109
+ ## Построение графа
110
+
111
+ <!-- docs-check: continue -->
112
+
113
+ В графе у каждого ребра есть счётчик, а рёбра цикла отмечены. Запишите его в формате, который читает ваш инструмент:
114
+
115
+ ```js
116
+ // draw.js
117
+ import { graph } from "./bus";
118
+
119
+ // Mermaid: вставьте в README, в pull request или на https://mermaid.live
120
+ const mermaid = graph.toMermaid({ group: "prefix" }); // события группируются по части имени до "/"
121
+
122
+ // Graphviz: dot -Tsvg graph.dot > graph.svg
123
+ const dot = graph.toDot();
124
+
125
+ // обычные данные для ваших инструментов
126
+ const data = graph.toJSON();
127
+
128
+ console.log(mermaid, dot, data);
129
+ ```
130
+
131
+ ```mermaid
132
+ flowchart LR
133
+ n0["cart/changed"]
134
+ n1["recalcTotal"]
135
+ n2["total/changed"]
136
+ n3["syncCart"]
137
+ n0 -->|x21| n1
138
+ n1 -->|x20| n2
139
+ n2 -->|x20| n3
140
+ n3 -->|x20| n0
141
+ linkStyle 0,1,2,3 stroke:red,stroke-width:2px
142
+ ```
143
+
144
+ Счётчики (`x21`) — сколько раз видели каждое ребро; они зависят от того, как долго шёл цикл.
145
+
146
+ ## Цикл в Redux DevTools
147
+
148
+ `devtools: true` отправляет каждую находку в расширение как действие `cycle/detected`. Добавьте рядом `reduxDevtools`, чтобы видеть и emit:
149
+
150
+ ```js
151
+ // devtools-bus.js
152
+ import { createBus } from "nanoeffects";
153
+ import { createTracer, graphLogger, reduxDevtools, tracePlugin } from "nanoeffects/trace";
154
+
155
+ // devtools: true — одно действие cycle/detected на каждый цикл, с путём
156
+ const graph = graphLogger({ name: "app", devtools: true });
157
+
158
+ // reduxDevtools — emit шины в виде действий
159
+ const trace = createTracer([reduxDevtools({ name: "app" }), graph]);
160
+
161
+ export const bus = createBus({ trace }).use(tracePlugin());
162
+ ```
163
+
164
+ Redux DevTools — расширение браузера, поэтому откройте страницу в отдельной вкладке и выберите экземпляр в его списке. Без расширения ничего не отправляется и ничего не падает. Попробовать можно на кнопках [`examples/debug-playground`](https://github.com/nanoeffects/nanoeffects/tree/main/examples/debug-playground).
165
+
166
+ ## Цикл без цепочки
167
+
168
+ У некоторых циклов нет цепочки emit:
169
+
170
+ - **Компонент, который отправляет событие на каждом рендере.** `useEmitter` и `bus.emit` в эффекте работают вне обработчиков, поэтому каждый emit — корень. `graphLogger` считает такие emit для каждого события и сообщает о **шторме**, когда одно событие приходит 100 раз за секунду. Предел меняется через `storm: { count, perMs }`, а `storm: false` отключает проверку.
171
+ - **`bus.emit` внутри обработчика.** Он начинает новую цепочку, поэтому обработчик, который вызывает его снова, не похож на цикл. Внутри обработчиков используйте `ctx.emit`, чтобы цепочка сохранялась.
172
+ - **Цепочка из сгенерированных имён** (`step/${n}`) не повторяет имя. `graphLogger` сообщает о **runaway**-цепочке, когда один корень запускает больше `maxSteps` задач (по умолчанию 5000).
173
+
174
+ ```js
175
+ // storm.js
176
+ import { graphLogger } from "nanoeffects/trace";
177
+
178
+ export const graph = graphLogger({
179
+ storm: { count: 50, perMs: 1000 }, // 50 emit одного события за секунду извне
180
+ maxSteps: 2000, // 2000 задач, запущенных одним корнем
181
+ ignore: ["tree/visit"], // намеренная рекурсия не считается
182
+ });
183
+ ```
184
+
185
+ ## Разрыв цикла
186
+
187
+ | Форма | Исправление |
188
+ | ----- | ----------- |
189
+ | Обработчик отправляет своё же событие | Отправляйте для результата другое событие (`user/loaded`, а не `user/load`). |
190
+ | Два обработчика кормят друг друга | Добавьте условие, которое прекращает цикл, или отправьте результат в третье событие, которое в цикле никто не слушает. |
191
+ | Job, у которого исход имеет тип триггера | Дайте исходам собственные типы. |
192
+ | Компонент отправляет событие на каждом рендере | Передайте `equals` в `useEmitter`, чтобы то же значение не было emit, либо добавьте `debounce` обработчику. |
193
+ | Сбой повторно отправляет событие, которое упало | Ограничивайте число попыток через `retry`, а не событием. |
194
+
195
+ ## Обрыв цикла во время работы
196
+
197
+ Отчёт цикл не останавливает. Если цикл не должен ронять страницу, добавьте [`loopGuardPlugin`](loop-guard.md). Он отклоняет emit, который превысил бы 50 повторов или цепочку из 1000 emit, и сообщает об этом:
198
+
199
+ ```js
200
+ // guarded-bus.js
201
+ import { createBus } from "nanoeffects";
202
+ import { loopGuardPlugin } from "nanoeffects/guard";
203
+ import { createTracer, graphLogger, tracePlugin } from "nanoeffects/trace";
204
+
205
+ const graph = graphLogger({ name: "app" });
206
+ const trace = createTracer(graph);
207
+
208
+ // loopGuardPlugin — обрывает цикл, о котором сообщает граф
209
+ export const bus = createBus({ trace })
210
+ .use(tracePlugin())
211
+ .use(loopGuardPlugin({ maxRepeats: 50 }));
212
+ ```
213
+
214
+ Предохранитель не зависит от логгера: используйте любой из них отдельно.
215
+
216
+ ## Падение теста при цикле
217
+
218
+ Запустите сценарий приложения и проверьте, что в графе нет цикла. Цикл, который кто-то добавит позже, уронит сборку.
219
+
220
+ ```js
221
+ // bus.js
222
+ import { createBus } from "nanoeffects";
223
+ import { createTracer, graphLogger, tracePlugin } from "nanoeffects/trace";
224
+
225
+ // bus.js из раздела «Поиск цикла»: логгер графа на трассировщике шины
226
+ export const graph = graphLogger({ name: "cart" });
227
+ export const bus = createBus({ trace: createTracer(graph) }).use(tracePlugin());
228
+ ```
229
+
230
+ ```js
231
+ // cart-handlers.js
232
+ import { bus } from "./bus";
233
+
234
+ bus.on("checkout/started", (_order, ctx) => {
235
+ ctx.emit("checkout/paid");
236
+ });
237
+ ```
238
+
239
+ ```js
240
+ // cycles.test.js
241
+ import { expect, test } from "vitest";
242
+ import { bus, graph } from "./bus";
243
+ import "./cart-handlers";
244
+
245
+ test("the checkout flow has no event cycles", () => {
246
+ // запустите сценарий так, как это сделал бы пользователь
247
+ bus.emit("checkout/started");
248
+
249
+ // cycles() — пуст, если в графе нет цикла
250
+ expect(graph.cycles()).toEqual([]);
251
+ });
252
+ ```
253
+
254
+ ## Экспорт графа и payload
255
+
256
+ Экспорт не включает payload, потому что в нём могут быть личные данные. Запросите их, когда они нужны:
257
+
258
+ ```js
259
+ // bus.js
260
+ import { createBus } from "nanoeffects";
261
+ import { createTracer, graphLogger, tracePlugin } from "nanoeffects/trace";
262
+
263
+ // bus.js из раздела «Поиск цикла»: логгер графа на трассировщике шины
264
+ export const graph = graphLogger({ name: "cart" });
265
+ export const bus = createBus({ trace: createTracer(graph) }).use(tracePlugin());
266
+ ```
267
+
268
+ ```js
269
+ // payloads.js
270
+ import { graph } from "./bus";
271
+
272
+ // payload: true — добавляет последний payload и ошибку каждого узла в безопасном виде
273
+ const data = graph.toJSON({ payload: true });
274
+
275
+ console.log(JSON.stringify(data));
276
+ ```
277
+
278
+ Payload может быть экземпляром класса, `Map`, объектом с циклической ссылкой или `Error`, которые обычный `JSON.stringify` не примет. Экспорт превращает их в обычный JSON: у `Error` остаются имя, сообщение, стек и причина, цикл становится `"[Circular]"`, `Map` — `{ "$map": [...] }`, а экземпляр класса сохраняет собственные поля и имя класса в `$class`. Геттеры не вызываются, а слишком глубокое или слишком большое значение обрезается.
279
+
280
+ ## Ограничения
281
+
282
+ ### Намеренный повтор выглядит как цикл
283
+
284
+ `graphLogger` считает циклом событие, которое встретилось в одной цепочке три раза. Некоторый код делает так намеренно. Обработчик, который обходит дерево, отправляет `tree/visit` для каждого потомка, поэтому одно и то же событие стоит в цепочке по разу на каждый уровень. Отличить это от ошибки логгер не может, поэтому сообщает о цикле.
285
+
286
+ Скажите ему об этом через `ignore`. Событие остаётся в графе и в экспорте, но не считается ни повтором, ни шагом, ни штормом:
287
+
288
+ ```js
289
+ // graph.js
290
+ import { graphLogger } from "nanoeffects/trace";
291
+
292
+ export const graph = graphLogger({
293
+ // tree/visit повторяется на каждом уровне дерева намеренно
294
+ ignore: ["tree/visit"],
295
+ });
296
+ ```
297
+
298
+ Не добавляйте событие в список только ради тишины при настоящем цикле: отчёт нужен именно для него. `ignore` принимает и функцию от имени: `ignore: (name) => name.startsWith("tree/")`.
299
+
300
+ ### О цикле сообщается один раз, но он продолжает считаться
301
+
302
+ Цикл на 500 оборотов пишет одно предупреждение, а не 500. После предупреждения логгер молча считает обороты. Чтобы узнать, идёт ли цикл до сих пор, прочитайте счётчик из `graph.cycles()`:
303
+
304
+ ```js
305
+ // turns.js
306
+ import { graph } from "./graph";
307
+
308
+ const [loop] = graph.cycles();
309
+
310
+ // turns — сколько раз цикл прошёл с момента обнаружения, первый раз включительно
311
+ console.log(loop.path.join(" → "), loop.turns);
312
+ ```
313
+
314
+ Прочитайте его дважды с паузой. Если число выросло, цикл ещё идёт. Если осталось тем же, он остановился. Чтобы отреагировать на первое сообщение в коде, используйте `onCycle`: он тоже вызывается один раз.
315
+
316
+ ### Без tracePlugin граф грубее
317
+
318
+ Без `tracePlugin` логгер видит обработчики, но не видит emit, которые никто не слушает, и не может назвать подписку. Цикл всё равно находится, по типу события.