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,78 @@
1
+ # nanoeffects в приложении на Svelte
2
+
3
+ > **[Уровень B](README.md#виды-проектов)**: SPA с async UI. Сначала прочитайте: [Руководство](guide.md#1-создание-шины-событий).
4
+
5
+ Задача: компонент на Svelte 5 должен показать результат события (например, найденные элементы). Для этого `sveltePlugin` даёт `eventStore`. Работающий пример — шаблон `svelte-ts` из [Проекта из шаблона](starter.md#начало-со-svelte).
6
+
7
+ Нужны Svelte 5 или новее и установленная `nanoeffects`. Не знакомы с шиной? Сначала прочитайте [Руководство](guide.md).
8
+
9
+ ## Плагин на шине
10
+
11
+ **Проблема.** Компонент Svelte читает значение через `$store`, а событие шины — не store. Без помощи компонент пишет свой `readable` и `bus.on` и должен не забыть отписаться. Эти строки копируются в каждый компонент.
12
+
13
+ **Когда.** Компонент показывает payload события, например результат поиска.
14
+
15
+ `sveltePlugin()` добавляет на шину `eventStore`. Достаньте его из шины один раз в файле, который её создаёт, и импортируйте где нужно:
16
+
17
+ ```js
18
+ // bus.js
19
+ import { createBus } from "nanoeffects";
20
+ import { sveltePlugin } from "nanoeffects/svelte";
21
+
22
+ // createBus — шина событий, создаётся один раз вне Svelte
23
+ // sveltePlugin — добавляет на шину eventStore
24
+ export const bus = createBus().use(sveltePlugin());
25
+ export const { eventStore } = bus; // свободная функция: она не читает `this`
26
+ ```
27
+
28
+ ## Показ результата события
29
+
30
+ <!-- docs-check: continue -->
31
+
32
+ `eventStore(event, initial)` возвращает store Svelte. Сначала его значение — `initial`, затем — payload каждого `event`. В компоненте `$found` читает store, а Svelte сам подписывается и отписывается. Если payload — функция, store сохраняет её, а не вызывает.
33
+
34
+ ```js
35
+ // search.js
36
+ import { createAction } from "nanoeffects/actions";
37
+
38
+ // createAction — событие с именем; вызов строит { type, payload }
39
+ export const searchLoaded = createAction("search/loaded");
40
+ ```
41
+
42
+ <!-- docs-check: skip - файл .svelte -->
43
+ ```svelte
44
+ <script>
45
+ import { eventStore } from "./bus";
46
+ import { searchLoaded } from "./search";
47
+
48
+ // eventStore — как store из [], но значение устанавливает searchLoaded
49
+ const found = eventStore(searchLoaded, []);
50
+ </script>
51
+
52
+ <ul>
53
+ {#each $found as title (title)}
54
+ <li>{title}</li>
55
+ {/each}
56
+ </ul>
57
+ ```
58
+
59
+ В TypeScript тип store берётся из события: для `createAction<string[]>("search/loaded")` это `Readable<string[]>`, а для события без типа payload он берётся из `initial`.
60
+
61
+ Чтобы отправить значение в шину, вызовите `bus.emit` из обработчика события, например `oninput`. При монтировании компонента ничего не отправляется, поэтому для этого направления функция плагина не нужна.
62
+
63
+ ### Это не store приложения
64
+
65
+ Store слушает шину, только пока его кто-то читает: первый `$found` запускает прослушивание, последний останавливает. Шина значений не хранит, поэтому событие, пришедшее без читателей, теряется, а компонент, смонтированный позже, начинает с `initial`. Чтобы сохранить значение для будущих компонентов, держите его в собственном store и записывайте туда из обработчика события:
66
+
67
+ ```js
68
+ // found-store.js
69
+ import { writable } from "svelte/store";
70
+ import { bus } from "./bus";
71
+ import { searchLoaded } from "./search";
72
+
73
+ // writable — store Svelte, который вы держите на всё приложение
74
+ export const foundTitles = writable([]);
75
+
76
+ // bus.on — копировать каждый searchLoaded в store, открыт компонент или нет
77
+ bus.on(searchLoaded, (titles) => foundTitles.set(titles));
78
+ ```
@@ -0,0 +1,466 @@
1
+ # Трассировка задач
2
+
3
+ > **[Уровень F](README.md#виды-проектов)**: платформа, SDK или долгоживущее приложение. Сначала прочитайте: [Руководство](guide.md#1-создание-шины-событий).
4
+
5
+ Трассировка показывает, что делало приложение и почему: какие события отправлялись, какие вызовы политика отменила или отбросила, какой запрос упал. Её видно в консоли браузера, в Redux DevTools и на панели Performance.
6
+
7
+ Трассировка необязательна. Шина работает так же и без неё, а пока трассировка выключена, она почти ничего не стоит. Чтобы просто видеть, что происходит при разработке, откройте [Каждый emit в трассировке](#каждый-emit-в-трассировке) и [Настройка Redux DevTools](#настройка-redux-devtools). Остальное — для приложений, которым нужно следить за потоком или убирать трассировку из сборки.
8
+
9
+ ## На этой странице
10
+
11
+ - [Подключение трассировщика](#подключение-трассировщика)
12
+ - [Каждый emit в трассировке](#каждый-emit-в-трассировке)
13
+ - [Какая подписка начала цепочку](#какая-подписка-начала-цепочку)
14
+ - [Один поток через traceGroupId](#один-поток-через-tracegroupid)
15
+ - [Имя обработчика](#имя-обработчика)
16
+ - [Сохранение цепочки: `ctx.emit` вместо `bus.emit`](#сохранение-цепочки-ctxemit-вместо-busemit)
17
+ - [Отключение имён](#отключение-имён)
18
+ - [Встроенные логгеры](#встроенные-логгеры)
19
+ - [Сокращение объёма в production](#сокращение-объёма-в-production)
20
+ - [Последние события на случай инцидента](#последние-события-на-случай-инцидента)
21
+ - [Настройка Redux DevTools](#настройка-redux-devtools)
22
+ - [Поиск циклов через `graphLogger`](#поиск-циклов-через-graphlogger)
23
+ - [Безопасные данные для инструмента](#безопасные-данные-для-инструмента)
24
+ - [Собственный логгер](#собственный-логгер)
25
+ - [Удаление трассировки из сборки](#удаление-трассировки-из-сборки)
26
+
27
+ ## Подключение трассировщика
28
+
29
+ Передайте `trace` в `createBus`. Создайте его через `createTracer` из `nanoeffects/trace`, который принимает логгер (функцию) или массив логгеров. Код трассировки находится в этой точке входа, поэтому шина без `trace` его не содержит.
30
+
31
+ ```js
32
+ import { createBus } from "nanoeffects";
33
+ import { createTracer } from "nanoeffects/trace";
34
+
35
+ const bus = createBus({
36
+ // createTracer — наблюдать каждую фазу задачи
37
+ trace: createTracer((event) => console.log(event)),
38
+ });
39
+ ```
40
+
41
+ У каждого `TraceEvent` есть:
42
+
43
+ | Поле | Значение |
44
+ | ---- | -------- |
45
+ | `id` | Уникальный идентификатор задачи в пределах шины |
46
+ | `parentId` | Идентификатор задачи, которая запустила эту (у задач обработчиков отсутствует) |
47
+ | `rootId` | Идентификатор корневой задачи цепочки; у самого корня отсутствует и одинаков через обработчик, который ничего не сообщает |
48
+ | `type` | Тип события, над которым работает задача |
49
+ | `effect` | `latest`, `debounce`, `throttle`, `leading`, `run`, `all`, `race`, …; `emit` для события emit от `tracePlugin` |
50
+ | `phase` | `start`, `done`, `error`, `cancel` или `drop` |
51
+ | `payload` | Payload события |
52
+ | `error` | Выброшенное значение, только для `error` |
53
+ | `traceGroupId` | Поток задачи, если он есть у неё или у задачи выше |
54
+
55
+ Фазы:
56
+
57
+ - `start` — работа началась.
58
+ - `done` / `error` — она завершилась или выбросила ошибку.
59
+ - `cancel` — прервана после старта (вытеснена `latest`, родитель прерван, проиграла `race`).
60
+ - `drop` — так и не стартовала (`leading`, пока занято, вызов `throttle`, заменённый более новым, вызов `debounce`, заменённый до конца задержки).
61
+
62
+ `all` и `race` трассируются, когда вы передаёте им `ctx` обработчика: группа — одна задача; каждая функция группы — дочерняя задача `run`.
63
+
64
+ ```js
65
+ import { createBus } from "nanoeffects";
66
+ import { all } from "nanoeffects/effects";
67
+
68
+ const bus = createBus();
69
+
70
+ bus.on("load", async (id, ctx) => {
71
+ await all(ctx, {
72
+ user: () => fetchUser(id),
73
+ posts: () => fetchPosts(id),
74
+ });
75
+ });
76
+ ```
77
+
78
+ ## Каждый emit в трассировке
79
+
80
+ **Когда.** Лог должен показывать всё, что отправлено, в том числе когда никто не слушает или обработчик — обычная функция. Без этого в трассировке только задачи, которые что-то сообщают: политика (`latest`, ...) или `ctx.run`.
81
+
82
+ `tracePlugin` сообщает одно событие на каждый emit, с `effect: "emit"` и `phase: "done"`, до запуска обработчиков. `type` — имя события, а `payload` — то, что было отправлено. У `ctx.emit` в качестве `parentId` стоит задача, которая отправляет.
83
+
84
+ ```js
85
+ // bus.js
86
+ import { createBus } from "nanoeffects";
87
+ import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
88
+
89
+ // createTracer: отправляет каждое событие трассировки логгеру; consoleLogger: печатает по одной строке на каждое
90
+ const trace = createTracer(consoleLogger());
91
+
92
+ export const bus = createBus({ trace })
93
+ .use(tracePlugin()); // tracePlugin: сообщает каждый emit как событие
94
+ ```
95
+
96
+ ```js
97
+ import { bus } from "./bus";
98
+
99
+ bus.emit("search", "nano"); // в логе: search · emit done
100
+ ```
101
+
102
+ Больше это событие ничего не меняет: остальные задачи сохраняют свои `parentId`, `rootId` и `traceGroupId`. Без плагина в трассировке нет событий `emit`.
103
+
104
+ - **Что считается emit.** Каждый `bus.emit`, в том числе те, что за вас делает библиотека: job отправляет свои `pending` и `success` (или `fail`, `cancel`), так что один вызов job — несколько событий `emit`.
105
+ - **Цена.** Одна небольшая задача на каждый emit, около 60 нс на шине, у которой уже есть трассировщик (шина с трассировщиком, но без плагина: примерно на 250 нс больше, чем шина без него). `sample` и `filter` в `createTracer` сокращают это в логе, но не в работе.
106
+
107
+ ## Какая подписка начала цепочку
108
+
109
+ **Проблема.** `useEmitter` и `bus.emitter` кладут событие на шину извне любой задачи, например из рендера или callback store. В трассировке у такого события нет следа его происхождения. При множестве таких подписок трудно сказать, какая из них запустила цепочку actions и повторов.
110
+
111
+ **Когда.** У шины есть трассировщик, а в проекте несколько вызовов `useEmitter` или `bus.emitter`. Имена приходят от `tracePlugin`: подключите его на шине, иначе `traceId` игнорируется.
112
+
113
+ Дайте каждому `traceId`. Если у шины есть трассировщик, emit выполняется из корневой задачи: её `type` — это `traceId`, а effect — `run`. Всё, что запускает событие, становится дочерним для этого корня. Без трассировщика ничего не меняется.
114
+
115
+ ```js
116
+ import { createBus } from "nanoeffects";
117
+ import { createAction, emitterPlugin } from "nanoeffects/actions";
118
+ import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
119
+
120
+ // createTracer: сообщает каждую фазу задачи в консоль
121
+ const trace = createTracer(consoleLogger());
122
+
123
+ const bus = createBus({ trace })
124
+ .use(tracePlugin()) // tracePlugin: создаёт корневую задачу, которую называет traceId
125
+ .use(emitterPlugin());
126
+
127
+ const cartChanged = createAction("cart/changed");
128
+
129
+ // emitter: callback для store; traceId называет его в трассировке
130
+ const emitCartChanged = bus.emitter(cartChanged, { traceId: "cartStore" });
131
+ ```
132
+
133
+ В компоненте тот же параметр передаётся в параметры хука: `useEmitter(nameChanged, name, { traceId: "Profile" })`. В консоли:
134
+
135
+ ```
136
+ [nanoeffects] Profile · run start
137
+ [nanoeffects] user/save · action start
138
+ [nanoeffects] user/save · action done
139
+ [nanoeffects] Profile · run done
140
+ ```
141
+
142
+ `traceId` — строка по вашему выбору, например имя компонента или store; держите её стабильной. Без `traceId` у корня тип события.
143
+
144
+ Две вещи, которые нужно знать:
145
+
146
+ - **`sample` видит `traceId`.** Корень эмиттера — такая же задача, как любая другая, поэтому функция `sample` получает `{ type, payload }`, где `type` — это `traceId`, а `payload` — отправленный action. Функция, которая сопоставляет тип события, например `event.type === "search"`, не выберет корень с другим `traceId`; сопоставляйте `traceId` или `payload.type`.
147
+ - **Внутри обработчика отправляйте через `ctx.emit`.** `ctx.emit` сохраняет цепочку: запущенные им задачи становятся дочерними для обработчика. Обычный `bus.emit` внутри обработчика запускает новый корень без родителя и без `rootId`, так что трассировка теряет связь.
148
+
149
+ У каждого события задачи, лежащей ниже другой, есть `rootId` — идентификатор корня цепочки, в том числе когда между ними стоит обработчик, который ничего не сообщает. `parentId` — задача непосредственно выше, и это может быть такой обработчик.
150
+
151
+ ## Один поток через traceGroupId
152
+
153
+ **Проблема.** У потока вроде оформления заказа несколько корней (клик, таймер, store) и много ветвей. Идентификаторы родителей дают дерево, но вам нужно фильтровать лог по потоку.
154
+
155
+ **Когда.** Один бизнес-поток проходит через несколько actions, и вы читаете лог по потокам.
156
+
157
+ Дайте потоку `traceGroupId` в scope action или job, который его запускает. Каждая задача, которую он запускает через `ctx.emit` или `ctx.run`, получает тот же `traceGroupId`, как и каждое событие трассировки этих задач. Задача, которая объявляет собственный `traceGroupId`, использует его для себя и для того, что запускает.
158
+
159
+ ```js
160
+ import { createBus } from "nanoeffects";
161
+ import { createAction } from "nanoeffects/actions";
162
+ import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
163
+
164
+ // createTracer: отправляет каждое событие трассировки логгеру; consoleLogger: печатает по одной строке на каждое
165
+ const trace = createTracer(consoleLogger());
166
+
167
+ // tracePlugin: читает traceGroupId из scope вызова
168
+ const bus = createBus({ trace })
169
+ .use(tracePlugin());
170
+
171
+ // scope называет поток; traceGroupId предназначен трассировщику, а не полю ctx
172
+ const pay = createAction("checkout/pay", async () => {}, {
173
+ traceGroupId: "checkout",
174
+ });
175
+ ```
176
+
177
+ ```
178
+ [nanoeffects] [checkout] checkout/pay · action start
179
+ ```
180
+
181
+ Эмиттеры его тоже принимают: `useEmitter(cartChanged, cart, { traceId: "Cart", traceGroupId: "checkout" })`. `traceGroupId` работает без `scopePlugin` и требует `tracePlugin`.
182
+
183
+ ## Имя обработчика
184
+
185
+ **Проблема.** Лог показывает, что запустило цепочку и что она запустила, но обработчик события ничего не сообщает, поэтому нельзя сказать, какой из нескольких обработчиков выполнился.
186
+
187
+ **Когда.** У события больше одного обработчика, и вы читаете трассировку, чтобы увидеть, кто сделал работу.
188
+
189
+ Дайте обработчику `traceId` при регистрации. Плагин делает каждый его вызов задачей с этим именем, а то, что он отправляет через `ctx.emit`, становится дочерним. `off` работает как раньше. Это работает и в модуле (`m.on(event, handler, { traceId })`), и на экземпляре, созданном через `extendBus`.
190
+
191
+ ```js
192
+ import { createBus } from "nanoeffects";
193
+ import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
194
+
195
+ // createTracer: отправляет каждое событие трассировки логгеру; consoleLogger: печатает по одной строке на каждое
196
+ const trace = createTracer(consoleLogger());
197
+
198
+ const bus = createBus({ trace })
199
+ .use(tracePlugin());
200
+
201
+ // traceId: имя этого обработчика в трассировке
202
+ bus.on("name/changed", saveName, { traceId: "saveNotifier" });
203
+ bus.once("name/changed", (name) => console.log(name), { traceId: "auditLog" });
204
+ ```
205
+
206
+ ```
207
+ [nanoeffects] saveNotifier · run start
208
+ [nanoeffects] user/save · action start
209
+ [nanoeffects] saveNotifier · run done
210
+ ```
211
+
212
+ ## Сохранение цепочки: `ctx.emit` вместо `bus.emit`
213
+
214
+ Внутри обработчика отправляйте через `ctx.emit`. Он сохраняет цепочку: запущенные им задачи становятся дочерними для обработчика, а у его событий есть `rootId` цепочки. Обычный `bus.emit` внутри обработчика запускает новый корень без `parentId` и без `rootId`, так что трассировка теряет связь. Шина не может об этом предупредить, потому что не знает, какой обработчик сейчас выполняется.
215
+
216
+ Имена работают везде, где создаётся событие или обработчик:
217
+
218
+ | Где | Как назвать |
219
+ | --- | ----------- |
220
+ | `useEmitter` и `bus.emitter` | `{ traceId, traceGroupId }` в параметрах |
221
+ | Redux dispatch | `createEffectsMiddleware(bus, { traceId, traceGroupId })` |
222
+ | Обработчик, `bus.on` и `bus.once` | `{ traceId }` третьим аргументом |
223
+ | Обработчик модуля или экземпляра `extendBus` | тот же третий аргумент его `on` и `once` |
224
+ | Метод с `@On` или `@Once` | `{ traceId }` в декораторе |
225
+ | Action или job | `traceGroupId` в его scope |
226
+
227
+ ## Отключение имён
228
+
229
+ `tracePlugin` необязателен, и ничто из него не меняет работу вашего кода:
230
+
231
+ - **Без плагина** или на шине без трассировщика `traceId` и `traceGroupId` игнорируются. Обработчики выполняются как есть, с теми же результатами, ошибками и отклонениями, а `onError` видит те же ошибки.
232
+ - **Типы одинаковы** с плагином и без него, так что его можно использовать только при разработке: `if (isDev) bus.use(tracePlugin())` компилируется в обеих сборках.
233
+ - **`traceGroupId` в scope** никогда не попадает на `ctx`.
234
+
235
+ ## Встроенные логгеры
236
+
237
+ Логгер — это `(event) => void`. `createTracer` принимает один логгер или массив. Логгер, который выбросил ошибку, сообщается в `console.error` и никогда не ломает задачу или другие логгеры.
238
+
239
+ ```js
240
+ import { createBus } from "nanoeffects";
241
+ import { consoleLogger, createTracer, reduxDevtools } from "nanoeffects/trace";
242
+
243
+ const bus = createBus({
244
+ trace: createTracer([reduxDevtools({ name: "app" }), consoleLogger()]),
245
+ });
246
+ ```
247
+
248
+ | Логгер | Что делает |
249
+ | ------ | ---------- |
250
+ | `reduxDevtools()` | Отправляет emit в Redux DevTools, а с `policies` — стадии обработчиков как `type/effect/phase`; state — `{ running }` |
251
+ | `consoleLogger()` | Печатает по одной строке на фазу, с отступом по глубине задачи |
252
+ | `performanceLogger()` | Измерения User Timing для панели Performance браузера |
253
+ | `graphLogger()` | Строит карту того, какое событие к какому ведёт, находит циклы во время работы приложения и записывает карту как Mermaid, Graphviz или JSON ([см. ниже](#поиск-циклов-через-graphlogger)) |
254
+ | `createTraceRecorder()` | Хранит события в `recorder.events`: все — для тестов, либо с `{ capacity }` последние N ([см. ниже](#последние-события-на-случай-инцидента)) |
255
+
256
+ ### Сокращение объёма в production
257
+
258
+ ```js
259
+ import { createTracer } from "nanoeffects/trace";
260
+
261
+ createTracer(logger, {
262
+ sample: 0.05, // трассировать 5% вызовов обработчиков (доля или функция от { type, payload })
263
+ filter: (event) => event.phase === "error", // оставить только сбои
264
+ });
265
+ ```
266
+
267
+ - `sample` решается один раз на корневую задачу; каждая задача ниже следует за ней, так что трассировка никогда не обрывается посередине. Задачи вне выборки почти ничего не стоят.
268
+ - `filter` выполняется на каждом событии до того, как его увидит любой логгер.
269
+
270
+ ### Последние события на случай инцидента
271
+
272
+ `createTraceRecorder()` без параметров хранит каждое событие, поэтому длинная сессия заполняет память. В тесте это нормально, а в работающем приложении — нет. Задайте `capacity`, и рекордер будет хранить только последние N событий (кольцевой буфер): когда что-то пойдёт не так, у вас останется то, что к этому привело.
273
+
274
+ <!-- docs-check: new -->
275
+
276
+ ```js
277
+ // bus.js
278
+ import { createBus } from "nanoeffects";
279
+ import { createTraceRecorder, createTracer } from "nanoeffects/trace";
280
+
281
+ // Логгер, который хранит последние 500 событий и отбрасывает более старые.
282
+ export const recorder = createTraceRecorder({ capacity: 500 });
283
+
284
+ // Подайте рекордер из трассировщика.
285
+ const trace = createTracer(recorder);
286
+
287
+ export const bus = createBus({ trace });
288
+ ```
289
+
290
+ ```js
291
+ // incident.js
292
+ import { recorder } from "./bus";
293
+
294
+ // Оставьте то, что безопасно отправлять. У события есть ещё `payload` и `error`.
295
+ const withoutPayload = ({ id, parentId, type, effect, phase }) => ({
296
+ id,
297
+ parentId,
298
+ type,
299
+ effect,
300
+ phase,
301
+ });
302
+
303
+ window.addEventListener("error", () => {
304
+ const events = recorder.events; // снимок, самое старое событие первым
305
+ const safe = events.map(withoutPayload);
306
+ const body = JSON.stringify(safe);
307
+ navigator.sendBeacon("/api/incident", body);
308
+ });
309
+ ```
310
+
311
+ Что нужно знать, прежде чем отправлять события:
312
+
313
+ - **`payload` и `error` могут содержать приватные данные**: то, что ввёл пользователь, токены, личный текст. Рекордер хранит их как есть и не вычищает. Выбирайте, что отправлять, как это делает `withoutPayload`.
314
+ - **У события нет времени.** Вы получаете порядок, а не длительность. Чтобы добавить время, оберните рекордер в логгер, который проставляет `performance.now()` на копии события.
315
+ - **Старый `parentId` может указывать на событие, которое вытеснено.** Когда вы строите дерево из событий, считайте неизвестный `parentId` корнем.
316
+ - **`recorder.events` с `capacity` — снимок.** Каждое чтение строит новый массив, поэтому читайте его один раз в нужный момент. Без `capacity` это один живой массив.
317
+ - **Низкий `sample` разрежает буфер.** `createTracer(recorder, { sample: 0.05 })` записывает 5% корневых задач, поэтому к моменту инцидента в буфере может быть мало. См. [Сокращение объёма в production](#сокращение-объёма-в-production).
318
+ - **`capacity`, не являющийся положительным целым** (`0`, отрицательное число, `NaN`), означает «без ограничения».
319
+
320
+ ### Настройка Redux DevTools
321
+
322
+ <!-- docs-check: new -->
323
+
324
+ Логгер только читает (шина не владеет state, поэтому путешествия по времени нет). Если расширение не установлено, он ничего не делает. В списке экземпляров DevTools у него своя запись, отдельная от вашего store.
325
+
326
+ По умолчанию логгер выводит мало: сначала видно, что случилось, а подробности включаются отдельно:
327
+
328
+ - **Один action на emit**, названный по событию (`search`), с его payload. Для этого нужен `bus.use(tracePlugin())` (см. [Каждый emit в трассировке](#каждый-emit-в-трассировке)); без него лог по умолчанию пуст, а логгер печатает один `console.warn`, когда заканчивается первая задача обработчика без emit перед ней.
329
+ - **Упавший обработчик** отправляется как `search/latest/error`.
330
+ - Больше ничего: ни start, ни `done`, ни стадий политик, ни эффектов.
331
+
332
+ Чтобы увидеть больше, включайте:
333
+
334
+ - `policies` — стадии задач обработчиков, как `event/policy/phase`. `true` показывает каждую фазу, список показывает эти фазы, функция получает `{ effect, phase }`.
335
+ - `starts: true` — отправлять ещё и старт каждой показываемой задачи. Выполняющиеся задачи хранятся в state `{ running }`.
336
+ - `depth: 1` — ещё и эффекты, которые запускает обработчик (`retry`, `timeout`); `depth: Infinity` — всё ниже.
337
+ - `payload` — у каких actions в событии есть payload:
338
+ - `"essential"` (по умолчанию) — actions из emit, а также `cancel`, `drop` и `error` задачи, где payload — единственный способ понять, какой это был вызов. `start` и `done` повторяют emit и payload не имеют, так что большая payload отправляется один раз.
339
+ - `"all"` — каждый action, как в 0.12.
340
+ - `"none"` — ни один action. Подходит для большой payload и для данных, которые не должны попасть в расширение браузера (токены, личные данные). Тогда нельзя будет сказать, какой вызов остановила политика.
341
+ - `maxAge: 200` — сколько actions хранит DevTools (по умолчанию 50).
342
+
343
+ ```js
344
+ // bus.js
345
+ import { createBus } from "nanoeffects";
346
+ import { createTracer, reduxDevtools, tracePlugin } from "nanoeffects/trace";
347
+
348
+ // reduxDevtools: отправляет трассировку в расширение; только emit и сбои
349
+ const logger = reduxDevtools({ name: "app" });
350
+
351
+ export const bus = createBus({ trace: createTracer(logger) })
352
+ .use(tracePlugin()); // tracePlugin: создаёт события emit
353
+ ```
354
+
355
+ ```js
356
+ import { reduxDevtools } from "nanoeffects/trace";
357
+
358
+ // отладка гонки: какие вызовы политика отменила или отбросила?
359
+ reduxDevtools({
360
+ name: "app",
361
+ policies: ["cancel", "drop"],
362
+ });
363
+
364
+ // каждая стадия каждой политики, одна функция для выбора
365
+ reduxDevtools({
366
+ name: "app",
367
+ policies: ({ effect, phase }) => effect === "latest" && phase !== "start",
368
+ });
369
+
370
+ // старый лог: каждая стадия и эффекты под обработчиками
371
+ reduxDevtools({
372
+ name: "app",
373
+ policies: true,
374
+ depth: 1,
375
+ payload: "all",
376
+ });
377
+
378
+ // не пускать payload в расширение
379
+ reduxDevtools({
380
+ name: "app",
381
+ payload: "none",
382
+ });
383
+ ```
384
+
385
+ При двух быстрых emit `search` и обработчике `latest` список по умолчанию — `search`, `search`. С `policies: true` к нему добавляются стадии `latest` каждого вызова, например `search/latest/cancel` для первого.
386
+
387
+ ### Поиск циклов через `graphLogger`
388
+
389
+ Два обработчика, которые отправляют события друг друга, не остановятся сами. `graphLogger` — логгер, который находит такой цикл во время работы приложения, один раз пишет о нём отчёт и хранит карту событий, чтобы её можно было нарисовать.
390
+
391
+ ```js
392
+ // bus.js
393
+ import { createBus } from "nanoeffects";
394
+ import { createTracer, graphLogger, tracePlugin } from "nanoeffects/trace";
395
+
396
+ // graphLogger — карта событий и поиск циклов в одном логгере
397
+ export const graph = graphLogger({ name: "app" });
398
+
399
+ // createTracer — передаёт логгеру каждую задачу шины
400
+ const trace = createTracer(graph);
401
+
402
+ // tracePlugin — тоже сообщает о каждом emit; логгер работает и без него, но карта грубее
403
+ export const bus = createBus({ trace }).use(tracePlugin());
404
+
405
+ // graph.cycles() перечисляет найденные циклы; graph.toMermaid() рисует карту
406
+ ```
407
+
408
+ Он сообщает об узле, который стоит в одной цепочке три раза, о шторме emit вне обработчиков и о цепочке, которая запускает слишком много задач. Отчёт идёт в `console.warn`, в `onCycle` или, с `devtools: true`, в Redux DevTools. Ничего не останавливается: чтобы оборвать цикл, используйте [`loopGuardPlugin`](loop-guard.md). Полный разбор со сценариями, которые можно запустить, — [Поиск и обрыв циклов событий](debugging-loops.md).
409
+
410
+ ### Безопасные данные для инструмента
411
+
412
+ Payload или ошибка могут быть чем угодно: экземпляром класса, `Map`, объектом с циклической ссылкой, функцией, `Error` с `cause`. Инструмент, который получает такое значение как есть, может упасть или ничего не показать. Встроенные логгеры следят за тем, чтобы из вашего процесса уходили обычные данные:
413
+
414
+ | Логгер | Что делает с payload и ошибкой |
415
+ | ------ | ------------------------------ |
416
+ | `reduxDevtools()` | Превращает их в обычные данные через `safeSerialize`, прежде чем расширение их получит. `Error` отправляется как сообщение. Изменить это можно через `serialize`. |
417
+ | `performanceLogger()` | Отправляет id и фазу; ошибка — текст. Payload не отправляется. |
418
+ | `consoleLogger()` | Передаёт значение в `console`, который печатает любое значение и оставляет его раскрываемым. |
419
+ | `createTraceRecorder()` | Хранит события как есть. `recorder.toJSON()` и `JSON.stringify(recorder)` дают обычные данные. |
420
+ | `graphLogger()` | Не экспортирует payload, пока вы не попросите, а тогда делает его обычным. |
421
+
422
+ `safeSerialize(value)` экспортируется для вашего логгера. Он не бросает ошибок и не зацикливается: у `Error` остаются имя, сообщение, стек и причина, цикл становится `"[Circular]"`, `Map` — `{ $map }`, `Set` — `{ $set }`, экземпляр класса сохраняет свои поля и своё имя в `$class`, а геттер не вызывается. Слишком глубокое или слишком большое значение обрезается.
423
+
424
+ ```js
425
+ import { createTracer, reduxDevtools, safeSerialize } from "nanoeffects/trace";
426
+
427
+ const trace = createTracer(
428
+ reduxDevtools({
429
+ name: "app",
430
+ // serialize — вызывается для каждого payload и каждой ошибки перед отправкой в расширение;
431
+ // сначала скройте секрет, потом сделайте остальное обычными данными
432
+ serialize: (value) => safeSerialize(value, { maxDepth: 4 }),
433
+ }),
434
+ );
435
+ ```
436
+
437
+ `serialize: false` отправляет значения как есть: это быстрее, но на циклическом значении может упасть.
438
+
439
+ ### Собственный логгер
440
+
441
+ ```ts
442
+ import type { TraceLogger } from "nanoeffects/trace";
443
+
444
+ const errorsOnly = (): TraceLogger => (event) => {
445
+ if (event.phase === "error") {
446
+ report(event.error);
447
+ }
448
+ };
449
+ ```
450
+
451
+ ## Удаление трассировки из сборки
452
+
453
+ Без трассировщика хуки почти ничего не стоят. `nanoeffects/trace` попадает в бандл, только если его импортируют.
454
+
455
+ Чтобы убрать код трассировки и из ядра, включите условие экспорта `notrace`. В пакете есть вторая сборка `index`, `effects`, `policies` и `actions` с `__NANOEFFECTS_TRACE__`, равным `false`.
456
+
457
+ ```js
458
+ // vite / rolldown
459
+ resolve: { conditions: ["notrace"] }
460
+ // webpack
461
+ resolve: { conditionNames: ["notrace", "import", "..."] }
462
+ // esbuild
463
+ conditions: ["notrace"]
464
+ ```
465
+
466
+ В сборке из исходников `define: { __NANOEFFECTS_TRACE__: "false" }` делает то же самое. С установленным условием у типов этой сборки нет параметра `trace`: `createBus({ trace })` — ошибка типов. У `nanoeffects/trace` нет сборки `notrace`. Экономия — около 0,4 кБ gzip от всей библиотеки.