nanoeffects 0.31.1 → 0.32.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 (64) hide show
  1. package/dist/{action-creator-6rlBTmH9.d.ts → action-creator-DprJV-pN.d.ts} +6 -2
  2. package/dist/actions.d.ts +3 -3
  3. package/dist/actions.js +1 -1
  4. package/dist/{bus-DFlmNJ6h.js → bus-Cmk4U1hQ.js} +1 -1
  5. package/dist/caller-DULg_ZYV.js +1 -0
  6. package/dist/context-Dp_QRXzb.js +1 -0
  7. package/dist/{context-types-DKNc5mkS.d.ts → context-types-BiTMp_lW.d.ts} +1 -1
  8. package/dist/context.d.ts +2 -2
  9. package/dist/dev.d.ts +2 -2
  10. package/dist/dev.js +2 -2
  11. package/dist/effects.d.ts +2 -2
  12. package/dist/effects.js +1 -1
  13. package/dist/guard.d.ts +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/match.d.ts +1 -1
  17. package/dist/match.js +1 -1
  18. package/dist/{module-G39MaVOt.d.ts → module-DPKjzfRk.d.ts} +2 -2
  19. package/dist/notrace/{action-creator-BrMSO3RP.d.ts → action-creator-BWd9FWt6.d.ts} +2 -2
  20. package/dist/notrace/actions.d.ts +2 -2
  21. package/dist/notrace/actions.js +1 -1
  22. package/dist/notrace/effects.d.ts +2 -2
  23. package/dist/notrace/index.d.ts +1 -1
  24. package/dist/notrace/{plugin-7GUL2EW5.d.ts → plugin-DmAYT3Xk.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-BWNP23hr.d.ts → plugin-2d3uuW7o.d.ts} +2 -2
  31. package/dist/policies.d.ts +2 -2
  32. package/dist/policies.js +1 -1
  33. package/dist/react.d.ts +2 -2
  34. package/dist/react.js +1 -1
  35. package/dist/redux.d.ts +1 -1
  36. package/dist/scope.d.ts +2 -2
  37. package/dist/signal.d.ts +2 -2
  38. package/dist/signal.js +1 -1
  39. package/dist/svelte.d.ts +1 -1
  40. package/dist/testing.d.ts +80 -0
  41. package/dist/testing.js +1 -0
  42. package/dist/trace.d.ts +39 -3
  43. package/dist/trace.js +3 -3
  44. package/dist/tracer-mMDm5yE7.js +2 -0
  45. package/dist/{types-N_cdRr1r.d.ts → types-DxPZDzZG.d.ts} +2 -2
  46. package/dist/vue.d.ts +1 -1
  47. package/dist/{with-policy-CeM1rtES.js → with-policy-BmL0eRU3.js} +1 -1
  48. package/dist/{with-policy-CthiBp_z.d.ts → with-policy-Y12d-yx6.d.ts} +1 -1
  49. package/docs/debugging-compared.md +136 -0
  50. package/docs/migrating-from-rxjs.md +1 -1
  51. package/docs/package.md +1 -0
  52. package/docs/recipes.md +35 -0
  53. package/docs/ru/debugging-compared.md +136 -0
  54. package/docs/ru/migrating-from-rxjs.md +1 -1
  55. package/docs/ru/package.md +1 -0
  56. package/docs/ru/recipes.md +35 -0
  57. package/docs/ru/testing.md +92 -0
  58. package/docs/ru/tracing.md +85 -1
  59. package/docs/testing.md +92 -0
  60. package/docs/tracing.md +85 -1
  61. package/llms-full.txt +358 -2
  62. package/llms.txt +2 -0
  63. package/package.json +12 -3
  64. package/dist/context-B5Z2RpJt.js +0 -1
@@ -194,4 +194,4 @@ bus.on(
194
194
  - **Прерывание заменяет отписку для работы в полёте.** Остановленная задача останавливается через её `signal`. Передайте его в `fetch` или в клиент.
195
195
  - **Ошибки не часть потока.** Упавшая задача отклоняет собственный Promise. Без `onError` на шине об ошибке никто больше не узнает ([Ошибки](errors.md)).
196
196
  - **Нет завершения.** Шина событий не заканчивается. Задача заканчивается, когда кончается её работа или прерывается её сигнал.
197
- - **Нет marble-тестов.** Тестируйте настоящий обработчик с поддельными таймерами.
197
+ - **Marble-тесты — инструмент поменьше.** [`createMarbleBus`](testing.md) записывает отправки и фазы задач диаграммами на поддельных таймерах. В нём нет горячих и холодных источников и нет `|`.
@@ -27,6 +27,7 @@
27
27
  | `nanoeffects/vue` | `vuePlugin`: добавляет `watchEmitter` и `useEventRef` на шину (нужен Vue 3.3 или новее) | компоненты Vue |
28
28
  | `nanoeffects/dev` | `devPlugin` (в production ничего не делает), `getLostSignalsCount` и `getLostSignals` для тестов | нужно находить утечки подписок и потерянные сигналы при разработке и в тестах |
29
29
  | `nanoeffects/trace` | `createTracer`, логгеры (`graphLogger` находит циклы событий) и `tracePlugin` (имена для трассировки) | нужно видеть, что выполнилось и почему |
30
+ | `nanoeffects/testing` | `createMarbleBus`, `fakeTimersClock`: шина для тестов, где отправки и фазы записаны диаграммами | вы тестируете `latest`, `debounce`, `queue`, `retry` или `timeout` на поддельных таймерах |
30
31
  | `nanoeffects/guard` | `loopGuardPlugin`: отклоняет emit, который превысил бы предел цепочки | цикл не должен ронять приложение |
31
32
 
32
33
  Плагины с именами `...Plugin` подключаются через `bus.use`. Страница [Плагины](plugins.md) перечисляет их все, говорит, что каждый возвращает и в каком порядке их подключать.
@@ -29,6 +29,7 @@
29
29
  - [Не оставлять подписки после закрытия виджета](#не-оставлять-подписки-после-закрытия-виджета)
30
30
  - [Остановка работы на backend](#остановка-работы-на-backend)
31
31
  - [Отладка: что выполнилось и почему](#отладка-что-выполнилось-и-почему)
32
+ - [Тест политики на поддельных таймерах](#тест-политики-на-поддельных-таймерах)
32
33
  - [Другие библиотеки в проекте](#другие-библиотеки-в-проекте)
33
34
  - [ООП и TypeScript](#ооп-и-typescript)
34
35
  - [Шина в классе](#шина-в-классе)
@@ -480,6 +481,40 @@ bus.emit("search", "nano"); // в логе: search · emit, затем results
480
481
 
481
482
  Подробнее: [Трассировка задач](tracing.md). Чтобы перезагружать модули без обновления страницы, см. [Hot reload](hmr.md).
482
483
 
484
+ ## Тест политики на поддельных таймерах
485
+
486
+ Тест `latest` или `debounce` полон чисел `advanceTimersByTime`, и по ним никто не поймёт, что тест доказывает. Запишите отправки и фазы диаграммами.
487
+
488
+ ```js
489
+ import { it, vi } from "vitest";
490
+ import { latest } from "nanoeffects/policies";
491
+ import { createMarbleBus, fakeTimersClock } from "nanoeffects/testing";
492
+
493
+ // поиск, который занимает 50 мс
494
+ function search(query) {
495
+ return new Promise((resolve) => setTimeout(resolve, 50));
496
+ }
497
+
498
+ it("latest cancels the search that is running", async () => {
499
+ vi.useFakeTimers();
500
+
501
+ // createMarbleBus: шина, которая записывает трассировку; кадр — 10 мс
502
+ const marbles = createMarbleBus({ clock: fakeTimersClock(vi) });
503
+ marbles.bus.on("search/changed", latest(search), { traceId: "search" });
504
+
505
+ // "a-b": две отправки, в кадре 0 и в кадре 2
506
+ const result = await marbles.run({
507
+ emits: { "search/changed": { marble: "a-b" } },
508
+ frames: 12,
509
+ });
510
+
511
+ // s старт, c отмена, d готово
512
+ result.expect({ "search/changed:latest": "s-(cs)----d----" });
513
+ });
514
+ ```
515
+
516
+ Подробнее: [Тесты времени диаграммами](testing.md).
517
+
483
518
  ## Другие библиотеки в проекте
484
519
 
485
520
  - **Redux Toolkit, Zustand, MobX или Pinia.** [Подключение store](stores.md).
@@ -0,0 +1,92 @@
1
+ # Тесты времени диаграммами
2
+
3
+ > **[Уровень B](README.md#виды-проектов)**: SPA с async UI. Сначала прочитайте: [Политики](policies.md).
4
+
5
+ Тест `latest`, `debounce`, `queue`, `retry` или `timeout` — это тест времени. С поддельными таймерами время прячется в числах (`advanceTimersByTime(30)`), а список вызовов вы проверяете вручную. Эта страница показывает, как записать расписание и ожидание двумя короткими диаграммами, как это делают marble-тесты RxJS.
6
+
7
+ Нужен тест-раннер с поддельными таймерами (Vitest или Jest). Сам хелпер его не импортирует.
8
+
9
+ ## Тест политики диаграммой
10
+
11
+ **Проблема.** Нужен тест, который говорит: второй поиск отменяет первый, и завершается только второй.
12
+
13
+ **Когда.** Политика или эффект решает, что и когда выполняется, и ответ зависит от времени между событиями.
14
+
15
+ 1. Создайте шину через `createMarbleBus`. Она записывает трассировку своих задач и даёт `run`.
16
+ 2. Зарегистрируйте обработчик на `marbles.bus`, как в приложении.
17
+ 3. Вызовите `run` с диаграммой отправок. Один символ — один кадр (по умолчанию 10 мс).
18
+ 4. Вызовите `expect` с диаграммой фаз, которую ждёте.
19
+
20
+ ```js
21
+ // search.test.js
22
+ import { it, vi } from "vitest";
23
+ import { latest } from "nanoeffects/policies";
24
+ import { createMarbleBus, fakeTimersClock } from "nanoeffects/testing";
25
+
26
+ // поиск, который занимает 50 мс
27
+ function search(query) {
28
+ return new Promise((resolve) => setTimeout(resolve, 50));
29
+ }
30
+
31
+ it("latest cancels the search that is running", async () => {
32
+ vi.useFakeTimers();
33
+
34
+ // createMarbleBus: шина, которая записывает трассировку, и run, который двигает время по кадрам
35
+ const marbles = createMarbleBus({ clock: fakeTimersClock(vi) });
36
+
37
+ // latest: новый поиск отменяет выполняющийся
38
+ marbles.bus.on("search/changed", latest(search), { traceId: "search" });
39
+
40
+ // "a-b": отправить "x" в кадре 0 и "xy" в кадре 2
41
+ const result = await marbles.run({
42
+ emits: { "search/changed": { marble: "a-b", values: { a: "x", b: "xy" } } },
43
+ frames: 12,
44
+ });
45
+
46
+ // s старт, c отмена, d готово: в кадре 2 второй поиск отменяет первый, и завершается в кадре 7
47
+ result.expect({ "search/changed:latest": "s-(cs)----d----" });
48
+ });
49
+ ```
50
+
51
+ Когда строка не совпала, `expect` бросает ошибку с двумя диаграммами одна под другой:
52
+
53
+ ```
54
+ marble row "search/changed:latest" differs
55
+ expected s-(cs)----d----
56
+ received s-(cs)---d-----
57
+ ```
58
+
59
+ Такой же тест для `queue` и `debounce` лежит в [`examples/testing`](../../examples/testing). Запустите его командой `pnpm vitest run examples/testing`.
60
+
61
+ ## Чтение диаграмм
62
+
63
+ В диаграмме один символ или одна группа в скобках на кадр.
64
+
65
+ | Символ | В `emits` | В результате |
66
+ | ------ | --------- | ------------ |
67
+ | `-` | ничего не происходит | ничего не происходит |
68
+ | буква или цифра | отправляет событие со значением `values[символ]` (без `values` — сам символ) | `s` старт, `d` готово, `e` ошибка, `c` отмена, `x` отброшено |
69
+ | `(ab)` | две отправки в одном кадре, в таком порядке | две фазы в одном кадре, в порядке, в котором они произошли |
70
+
71
+ Другие символы (`|`, пробелы) вызывают ошибку с названием диаграммы и позицией.
72
+
73
+ У задачи в результате есть строка `type:effect`: `search/changed:latest` — задача политики `latest`, `search:run` — обработчик с именем `search`. Вызовите `result.rows()`, чтобы напечатать все строки, и скопируйте нужные в `expect`. В `expect` можно написать один эффект (`latest`), если он есть в одной строке. Если он есть в двух, ошибка попросит полный ключ.
74
+
75
+ ## Кадр и число кадров
76
+
77
+ Кадр — 10 мс. Чтобы изменить его, передайте `frame` в `createMarbleBus`. Кадр события — его время, делённое на длину кадра, с округлением вниз: таймер на 100 мс, начатый в кадре 0, заканчивается в кадре 10, поэтому для него нужно `frames` не меньше 11.
78
+
79
+ `frames` — число кадров для запуска. Без него хелпер выполняет столько, сколько в самой длинной диаграмме `emits`. Работа, которая дольше диаграммы, обрезается, поэтому задавайте `frames`, когда обработчик занимает время.
80
+
81
+ ## Другие часы
82
+
83
+ `fakeTimersClock(vi)` двигает поддельные таймеры и читает `Date.now()`. Тест включает поддельные таймеры сам (`vi.useFakeTimers()`) и оставляет `Date` поддельным, как по умолчанию. Jest работает так же: `fakeTimersClock(jest)`.
84
+
85
+ Часами служит любой объект с `advance(ms)` и `now()`. `advance` может вернуть промис.
86
+
87
+ ## Чего нет
88
+
89
+ - **Нужен трассировщик.** Хелпер читает трассировку шины, поэтому у `nanoeffects/testing` нет сборки `notrace`.
90
+ - **Одна строка на все вызовы.** Два вызова одного обработчика делят строку, поэтому `(sd)` показывает порядок фаз, а не то, к какому вызову они относятся.
91
+ - **Нет данных в диаграммах.** Диаграмма показывает, когда, а не что. Значения проверяйте тест-раннером как обычно.
92
+ - **Не алфавит RxJS.** Нет горячих и холодных источников и нет `|` для конца потока.
@@ -13,6 +13,8 @@
13
13
  - [Какая подписка начала цепочку](#какая-подписка-начала-цепочку)
14
14
  - [Один поток через traceGroupId](#один-поток-через-tracegroupid)
15
15
  - [Имя обработчика](#имя-обработчика)
16
+ - [Список подписчиков события](#список-подписчиков-события)
17
+ - [Строка кода обработчика](#строка-кода-обработчика)
16
18
  - [Сохранение цепочки: `ctx.emit` вместо `bus.emit`](#сохранение-цепочки-ctxemit-вместо-busemit)
17
19
  - [Отключение имён](#отключение-имён)
18
20
  - [Встроенные логгеры](#встроенные-логгеры)
@@ -186,7 +188,7 @@ const pay = createAction("checkout/pay", async () => {}, {
186
188
 
187
189
  **Когда.** У события больше одного обработчика, и вы читаете трассировку, чтобы увидеть, кто сделал работу.
188
190
 
189
- Дайте обработчику `traceId` при регистрации. Плагин делает каждый его вызов задачей с этим именем, а то, что он отправляет через `ctx.emit`, становится дочерним. `off` работает как раньше. Это работает и в модуле (`m.on(event, handler, { traceId })`), и на экземпляре, созданном через `extendBus`.
191
+ Обработчик получает имя своей функции, если регистрируете именованную функцию (`function saveName() {}`), поэтому лог приложения, которое никто не размечал, уже читается. Стрелка без имени остаётся как есть. Чтобы выбрать имя или назвать стрелку, дайте обработчику `traceId` при регистрации: он главнее имени функции. Плагин делает каждый вызов именованного обработчика задачей с этим именем, а то, что он отправляет через `ctx.emit`, становится дочерним. Минификатор может сократить имена функций в production-сборке: используйте `traceId`, где имя должно быть стабильным, и для обработчика, который возвращает политика (`latest(fn)`): у него нет собственного имени. `off` работает как раньше. Это работает и в модуле (`m.on(event, handler, { traceId })`), и на экземпляре, созданном через `extendBus`.
190
192
 
191
193
  ```js
192
194
  import { createBus } from "nanoeffects";
@@ -209,6 +211,88 @@ bus.once("name/changed", (name) => console.log(name), { traceId: "auditLog" });
209
211
  [nanoeffects] saveNotifier · run done
210
212
  ```
211
213
 
214
+ ## Список подписчиков события
215
+
216
+ **Проблема.** Чтобы узнать, что запустит emit, приходится искать в коде каждый `bus.on` этого события.
217
+
218
+ **Когда.** Вы читаете чужое приложение или тест должен охранять подключения шины.
219
+
220
+ `describeBus(bus)` возвращает обработчики каждого события на данный момент: по именам и в порядке добавления. Имя — это `traceId`, иначе имя функции, иначе `"<anonymous>"`. Функция читает то, что хранит `tracePlugin`, поэтому на шине без плагина или без трассировщика, а также в сборке `notrace` она возвращает `{}`. Она ничего не запускает и не меняет шину.
221
+
222
+ ```js
223
+ import { createBus } from "nanoeffects";
224
+ import { consoleLogger, createTracer, describeBus, tracePlugin } from "nanoeffects/trace";
225
+
226
+ function saveName(name) {
227
+ console.log("save", name);
228
+ }
229
+
230
+ function audit(name) {
231
+ console.log("audit", name);
232
+ }
233
+
234
+ const trace = createTracer(consoleLogger());
235
+
236
+ const bus = createBus({ trace })
237
+ .use(tracePlugin());
238
+
239
+ bus.on("name/changed", saveName);
240
+ bus.on("name/changed", audit);
241
+ bus.on("saved", () => {}, { traceId: "refreshView" });
242
+
243
+ // describeBus: карта событий и имён обработчиков
244
+ console.log(describeBus(bus));
245
+ ```
246
+
247
+ ```
248
+ { "name/changed": ["saveName", "audit"], saved: ["refreshView"] }
249
+ ```
250
+
251
+ ## Строка кода обработчика
252
+
253
+ **Проблема.** Лог говорит, что выполнился `saveNotifier`, и приходится искать в коде `bus.on`, который его зарегистрировал. У стрелки без имени нет имени вообще, поэтому её нет и в логе.
254
+
255
+ **Когда.** Вы отлаживаете в разработке и хотите перейти от строки лога к коду.
256
+
257
+ Передайте `source: true` в `tracePlugin`. Он читает место каждого вызова `bus.on` и `bus.once` один раз, при регистрации, и никогда при `emit`. События обработчика получают `source` (`file:line:column`), `consoleLogger({ source: true })` печатает его, а `describeBus(bus, { source: true })` его перечисляет. Обработчик без имени тоже попадает в трассировку, под типом своего события.
258
+
259
+ ```js
260
+ import { createBus } from "nanoeffects";
261
+ import { consoleLogger, createTracer, describeBus, tracePlugin } from "nanoeffects/trace";
262
+
263
+ // source: печатать, где зарегистрирован обработчик
264
+ const trace = createTracer(consoleLogger({ source: true }));
265
+
266
+ // source: читать место каждого bus.on один раз, при регистрации
267
+ const bus = createBus({ trace })
268
+ .use(tracePlugin({ source: true }));
269
+
270
+ function saveName(name) {
271
+ console.log("save", name);
272
+ }
273
+
274
+ bus.on("name/changed", saveName);
275
+ bus.on("name/changed", (name) => console.log("audit", name));
276
+
277
+ bus.emit("name/changed", "Eva");
278
+
279
+ // describeBus: те же места, ничего не запуская
280
+ console.log(describeBus(bus, { source: true }));
281
+ ```
282
+
283
+ ```
284
+ [nanoeffects] saveName · run start (file:///app/src/user.js:13:5)
285
+ [nanoeffects] name/changed · run start (file:///app/src/user.js:14:5)
286
+ { "name/changed": ["saveName (file:///app/src/user.js:13:5)", "<anonymous> (file:///app/src/user.js:14:5)"] }
287
+ ```
288
+
289
+ Ограничения:
290
+
291
+ - **Минифицированный или собранный код** даёт строку сборки. Включите source maps (`node --enable-source-maps` или source maps браузера), чтобы увидеть исходную строку.
292
+ - **`ctx.on`, вызванный вручную** внутри обработчика, показывает строку в библиотеке. `on` модуля и экземпляра, созданного через `extendBus`, а также `on` у `matchPlugin`, `signalPlugin` и `devPlugin` показывают вашу строку при любом порядке плагинов и в бандле.
293
+ - **Нет стека**, например при `Error.stackTraceLimit = 0`: `source` не ставится, обработчик работает как обычно.
294
+ - **По умолчанию выключено.** Чтение стека стоит немного на каждую регистрацию, поэтому включайте его в разработке.
295
+
212
296
  ## Сохранение цепочки: `ctx.emit` вместо `bus.emit`
213
297
 
214
298
  Внутри обработчика отправляйте через `ctx.emit`. Он сохраняет цепочку: запущенные им задачи становятся дочерними для обработчика, а у его событий есть `rootId` цепочки. Обычный `bus.emit` внутри обработчика запускает новый корень без `parentId` и без `rootId`, так что трассировка теряет связь. Шина не может об этом предупредить, потому что не знает, какой обработчик сейчас выполняется.
@@ -0,0 +1,92 @@
1
+ # Test time with diagrams
2
+
3
+ > **Level B.** Read first: [Policies](policies.md).
4
+
5
+ A test of `latest`, `debounce`, `queue`, `retry` or `timeout` is a test of time. With fake timers the time hides in numbers (`advanceTimersByTime(30)`), and you check a list of calls by hand. This page shows how to write the schedule and the expectation as two short diagrams, as RxJS marble tests do.
6
+
7
+ You need a test runner with fake timers (Vitest or Jest). The helper does not import one.
8
+
9
+ ## Test a policy with a diagram
10
+
11
+ **Problem.** You want a test that says: a second search cancels the first one, and only the second finishes.
12
+
13
+ **When.** A policy or an effect decides what runs when, and the answer depends on the time between events.
14
+
15
+ 1. Create a bus with `createMarbleBus`. It records the trace of its tasks and gives you `run`.
16
+ 2. Register the handler on `marbles.bus`, as you do in your app.
17
+ 3. Call `run` with a diagram of the emits. One character is one frame (10 ms by default).
18
+ 4. Call `expect` with a diagram of the phases that you expect.
19
+
20
+ ```js
21
+ // search.test.js
22
+ import { it, vi } from "vitest";
23
+ import { latest } from "nanoeffects/policies";
24
+ import { createMarbleBus, fakeTimersClock } from "nanoeffects/testing";
25
+
26
+ // a search that takes 50 ms
27
+ function search(query) {
28
+ return new Promise((resolve) => setTimeout(resolve, 50));
29
+ }
30
+
31
+ it("latest cancels the search that is running", async () => {
32
+ vi.useFakeTimers();
33
+
34
+ // createMarbleBus: a bus that records its trace, and a run that moves time frame by frame
35
+ const marbles = createMarbleBus({ clock: fakeTimersClock(vi) });
36
+
37
+ // latest: a new search cancels the one that is running
38
+ marbles.bus.on("search/changed", latest(search), { traceId: "search" });
39
+
40
+ // "a-b": emit "x" at frame 0 and "xy" at frame 2
41
+ const result = await marbles.run({
42
+ emits: { "search/changed": { marble: "a-b", values: { a: "x", b: "xy" } } },
43
+ frames: 12,
44
+ });
45
+
46
+ // s start, c cancel, d done: at frame 2 the second search cancels the first, and it is done at frame 7
47
+ result.expect({ "search/changed:latest": "s-(cs)----d----" });
48
+ });
49
+ ```
50
+
51
+ When a row differs, `expect` throws an error with both diagrams one under the other:
52
+
53
+ ```
54
+ marble row "search/changed:latest" differs
55
+ expected s-(cs)----d----
56
+ received s-(cs)---d-----
57
+ ```
58
+
59
+ The same test for `queue` and `debounce` is in [`examples/testing`](../examples/testing). Run it with `pnpm vitest run examples/testing`.
60
+
61
+ ## Read the diagrams
62
+
63
+ A diagram has one character, or one group in parentheses, for each frame.
64
+
65
+ | Character | In `emits` | In the result |
66
+ | --------- | ---------- | ------------- |
67
+ | `-` | nothing happens | nothing happens |
68
+ | a letter or digit | emits the event with `values[character]` (the character itself without `values`) | `s` start, `d` done, `e` error, `c` cancel, `x` drop |
69
+ | `(ab)` | two emits in one frame, in this order | two phases in one frame, in the order they happened |
70
+
71
+ Other characters (`|`, spaces) throw an error that names the diagram and the position.
72
+
73
+ A task has a row in the result named `type:effect`: `search/changed:latest` is the task of the `latest` policy, `search:run` is the listener named `search`. Call `result.rows()` to print every row, then copy the ones you want into `expect`. In `expect` you can write the effect alone (`latest`) when one row has it. If two rows have it, the error asks for the full key.
74
+
75
+ ## Set the frame and the number of frames
76
+
77
+ A frame is 10 ms. Pass `frame` to `createMarbleBus` to change it. The frame of an event is its time divided by the frame, rounded down: a timer of 100 ms that starts at frame 0 ends in frame 10, so `frames` must be 11 or more to show it.
78
+
79
+ `frames` is the number of frames to run. Without it the helper runs as many as the longest `emits` diagram has. Work that outlives the diagram is cut, so set `frames` when a handler takes time.
80
+
81
+ ## Use another clock
82
+
83
+ `fakeTimersClock(vi)` moves fake timers and reads `Date.now()`. A test turns fake timers on itself (`vi.useFakeTimers()`) and leaves `Date` faked, which is the default. Jest works the same way: `fakeTimersClock(jest)`.
84
+
85
+ Any object with `advance(ms)` and `now()` is a clock. `advance` may return a promise.
86
+
87
+ ## Know what is not there
88
+
89
+ - **It needs a tracer.** The helper reads the trace of the bus, so there is no `notrace` build of `nanoeffects/testing`.
90
+ - **One row for all calls.** Two calls of one listener share a row, so `(sd)` shows the order in which phases happened, not which call they belong to.
91
+ - **No payloads in diagrams.** The diagram shows when, not what. Check the values with your test runner as usual.
92
+ - **Not the RxJS alphabet.** There are no hot or cold sources and no `|` for the end of a stream.
package/docs/tracing.md CHANGED
@@ -13,6 +13,8 @@ Tracing is optional. A bus works the same without it, and it costs almost nothin
13
13
  - [Show which subscription began a chain](#show-which-subscription-began-a-chain)
14
14
  - [Follow one flow with a traceGroupId](#follow-one-flow-with-a-tracegroupid)
15
15
  - [Name a listener](#name-a-listener)
16
+ - [List who listens](#list-who-listens)
17
+ - [Find the line of a listener](#find-the-line-of-a-listener)
16
18
  - [Keep the chain: `ctx.emit`, not `bus.emit`](#keep-the-chain-ctxemit-not-busemit)
17
19
  - [Turn the names off](#turn-the-names-off)
18
20
  - [Use built-in loggers](#use-built-in-loggers)
@@ -186,7 +188,7 @@ The emitters take it too: `useEmitter(cartChanged, cart, { traceId: "Cart", trac
186
188
 
187
189
  **When.** An event has more than one listener and you read the trace to see which one did the work.
188
190
 
189
- Give the listener a `traceId` when you register it. The plugin makes each call of it a task with that name, and what it emits with `ctx.emit` is below it. `off` works as before. It also works in a module (`m.on(event, handler, { traceId })`) and on an instance made by `extendBus`.
191
+ A listener is named by its function when you register one with a name (`function saveName() {}`), so a log of an app that nobody annotated is already readable. An inline arrow has no name and stays as it is. To choose the name, or to name an arrow, give the listener a `traceId` when you register it; it wins over the function name. The plugin makes each call of a named listener a task with that name, and what it emits with `ctx.emit` is below it. A minifier can shorten function names in a production build: use `traceId` where a name must stay stable, and for a handler that a policy returns (`latest(fn)`), which has no name of its own. `off` works as before. It also works in a module (`m.on(event, handler, { traceId })`) and on an instance made by `extendBus`.
190
192
 
191
193
  ```js
192
194
  import { createBus } from "nanoeffects";
@@ -209,6 +211,88 @@ bus.once("name/changed", (name) => console.log(name), { traceId: "auditLog" });
209
211
  [nanoeffects] saveNotifier · run done
210
212
  ```
211
213
 
214
+ ## List who listens
215
+
216
+ **Problem.** To know what an emit will start, you search the code for every `bus.on` of that event.
217
+
218
+ **When.** You read an app that you did not write, or a test must guard the wiring of the bus.
219
+
220
+ `describeBus(bus)` returns the listeners that each event has right now, by name and in the order they were added. A name is the `traceId`, else the function name, else `"<anonymous>"`. It reads what `tracePlugin` keeps, so it returns `{}` on a bus without the plugin or a tracer, and in the `notrace` build. It does not run anything and does not change the bus.
221
+
222
+ ```js
223
+ import { createBus } from "nanoeffects";
224
+ import { consoleLogger, createTracer, describeBus, tracePlugin } from "nanoeffects/trace";
225
+
226
+ function saveName(name) {
227
+ console.log("save", name);
228
+ }
229
+
230
+ function audit(name) {
231
+ console.log("audit", name);
232
+ }
233
+
234
+ const trace = createTracer(consoleLogger());
235
+
236
+ const bus = createBus({ trace })
237
+ .use(tracePlugin());
238
+
239
+ bus.on("name/changed", saveName);
240
+ bus.on("name/changed", audit);
241
+ bus.on("saved", () => {}, { traceId: "refreshView" });
242
+
243
+ // describeBus: the map of events to listener names
244
+ console.log(describeBus(bus));
245
+ ```
246
+
247
+ ```
248
+ { "name/changed": ["saveName", "audit"], saved: ["refreshView"] }
249
+ ```
250
+
251
+ ## Find the line of a listener
252
+
253
+ **Problem.** The log says that `saveNotifier` ran, and you must search the code for the `bus.on` that registered it. An inline arrow has no name at all, so it is not in the log.
254
+
255
+ **When.** You debug in development and want to jump from a log line to the code.
256
+
257
+ Pass `source: true` to `tracePlugin`. It reads the place of each `bus.on` and `bus.once` call once, when the listener is registered, and never on `emit`. The events of the listener get a `source` (`file:line:column`), `consoleLogger({ source: true })` prints it, and `describeBus(bus, { source: true })` lists it. An anonymous listener is reported too, under the type of its event.
258
+
259
+ ```js
260
+ import { createBus } from "nanoeffects";
261
+ import { consoleLogger, createTracer, describeBus, tracePlugin } from "nanoeffects/trace";
262
+
263
+ // source: print where the listener was registered
264
+ const trace = createTracer(consoleLogger({ source: true }));
265
+
266
+ // source: read the place of each bus.on, once at registration
267
+ const bus = createBus({ trace })
268
+ .use(tracePlugin({ source: true }));
269
+
270
+ function saveName(name) {
271
+ console.log("save", name);
272
+ }
273
+
274
+ bus.on("name/changed", saveName);
275
+ bus.on("name/changed", (name) => console.log("audit", name));
276
+
277
+ bus.emit("name/changed", "Eva");
278
+
279
+ // describeBus: the same places, without running anything
280
+ console.log(describeBus(bus, { source: true }));
281
+ ```
282
+
283
+ ```
284
+ [nanoeffects] saveName · run start (file:///app/src/user.js:13:5)
285
+ [nanoeffects] name/changed · run start (file:///app/src/user.js:14:5)
286
+ { "name/changed": ["saveName (file:///app/src/user.js:13:5)", "<anonymous> (file:///app/src/user.js:14:5)"] }
287
+ ```
288
+
289
+ Know the limits:
290
+
291
+ - **Minified or bundled code** gives the line of the bundle. Turn on source maps (`node --enable-source-maps`, or the browser's) to see the original line.
292
+ - **`ctx.on` called by hand** inside a handler shows the line in the library as its place. The `on` of a module and of an instance made by `extendBus`, and the `on` of `matchPlugin`, `signalPlugin` and `devPlugin`, show your line, in any order of plugins and in a bundle.
293
+ - **No stack**, as with `Error.stackTraceLimit = 0`, gives no `source`. The listener runs as usual.
294
+ - **The default is off.** Reading a stack costs a little for each registration, so turn it on in development.
295
+
212
296
  ## Keep the chain: `ctx.emit`, not `bus.emit`
213
297
 
214
298
  Inside a handler, emit with `ctx.emit`. It keeps the chain: what it starts is below the handler, and its events have the `rootId` of the chain. A plain `bus.emit` inside a handler starts a new root with no `parentId` and no `rootId`, so the trace loses the link. The bus cannot warn about it, because it does not know which handler is running.