nanoeffects 0.31.2 → 0.33.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.
- package/dist/{action-creator-6rlBTmH9.d.ts → action-creator-DprJV-pN.d.ts} +6 -2
- package/dist/actions.d.ts +3 -3
- package/dist/actions.js +1 -1
- package/dist/{bus-DFlmNJ6h.js → bus-Cmk4U1hQ.js} +1 -1
- package/dist/caller-DULg_ZYV.js +1 -0
- package/dist/context-Dp_QRXzb.js +1 -0
- package/dist/{context-types-DKNc5mkS.d.ts → context-types-BiTMp_lW.d.ts} +1 -1
- package/dist/context.d.ts +2 -2
- package/dist/dev.d.ts +2 -2
- package/dist/dev.js +2 -2
- package/dist/effects.d.ts +2 -2
- package/dist/effects.js +1 -1
- package/dist/guard.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/match.d.ts +1 -1
- package/dist/match.js +1 -1
- package/dist/{module-G39MaVOt.d.ts → module-DPKjzfRk.d.ts} +2 -2
- package/dist/notrace/{action-creator-BrMSO3RP.d.ts → action-creator-BWd9FWt6.d.ts} +2 -2
- package/dist/notrace/actions.d.ts +2 -2
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/effects.d.ts +2 -2
- package/dist/notrace/index.d.ts +1 -1
- package/dist/notrace/{plugin-7GUL2EW5.d.ts → plugin-DmAYT3Xk.d.ts} +1 -1
- package/dist/notrace/policies.d.ts +1 -1
- package/dist/notrace/redux.d.ts +1 -1
- package/dist/observable.d.ts +1 -1
- package/dist/oop.d.ts +1 -1
- package/dist/oop.js +1 -1
- package/dist/{plugin-BWNP23hr.d.ts → plugin-2d3uuW7o.d.ts} +2 -2
- package/dist/policies.d.ts +2 -2
- package/dist/policies.js +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/redux.d.ts +1 -1
- package/dist/scope.d.ts +2 -2
- package/dist/signal.d.ts +2 -2
- package/dist/signal.js +1 -1
- package/dist/svelte.d.ts +1 -1
- package/dist/testing.d.ts +80 -0
- package/dist/testing.js +1 -0
- package/dist/trace.d.ts +227 -44
- package/dist/trace.js +3 -3
- package/dist/tracer-v7m-Qah_.js +2 -0
- package/dist/{types-N_cdRr1r.d.ts → types-DxPZDzZG.d.ts} +2 -2
- package/dist/vue.d.ts +1 -1
- package/dist/{with-policy-CeM1rtES.js → with-policy-BmL0eRU3.js} +1 -1
- package/dist/{with-policy-CthiBp_z.d.ts → with-policy-Y12d-yx6.d.ts} +1 -1
- package/docs/debugging-cases.md +122 -0
- package/docs/debugging-loops.md +79 -0
- package/docs/migrating-from-rxjs.md +1 -1
- package/docs/package.md +2 -1
- package/docs/recipes.md +35 -0
- package/docs/ru/debugging-cases.md +122 -0
- package/docs/ru/debugging-loops.md +79 -0
- package/docs/ru/migrating-from-rxjs.md +1 -1
- package/docs/ru/package.md +2 -1
- package/docs/ru/recipes.md +35 -0
- package/docs/ru/testing.md +92 -0
- package/docs/ru/tracing.md +189 -3
- package/docs/testing.md +92 -0
- package/docs/tracing.md +189 -3
- package/llms-full.txt +528 -5
- package/llms.txt +4 -2
- package/package.json +12 -3
- package/dist/context-B5Z2RpJt.js +0 -1
package/docs/tracing.md
CHANGED
|
@@ -13,15 +13,20 @@ 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)
|
|
19
21
|
- [Cut volume in production](#cut-volume-in-production)
|
|
20
22
|
- [Keep the last events for an incident](#keep-the-last-events-for-an-incident)
|
|
23
|
+
- [Measure time and find open tasks](#measure-time-and-find-open-tasks)
|
|
24
|
+
- [Find the cause of an error](#find-the-cause-of-an-error)
|
|
21
25
|
- [Configure Redux DevTools](#configure-redux-devtools)
|
|
22
26
|
- [Find loops with `graphLogger`](#find-loops-with-graphlogger)
|
|
23
27
|
- [Send safe data to a tool](#send-safe-data-to-a-tool)
|
|
24
28
|
- [Write your own logger](#write-your-own-logger)
|
|
29
|
+
- [Stop on an event](#stop-on-an-event)
|
|
25
30
|
- [Drop tracing from a build](#drop-tracing-from-a-build)
|
|
26
31
|
|
|
27
32
|
## Attach a tracer
|
|
@@ -186,7 +191,7 @@ The emitters take it too: `useEmitter(cartChanged, cart, { traceId: "Cart", trac
|
|
|
186
191
|
|
|
187
192
|
**When.** An event has more than one listener and you read the trace to see which one did the work.
|
|
188
193
|
|
|
189
|
-
|
|
194
|
+
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
195
|
|
|
191
196
|
```js
|
|
192
197
|
import { createBus } from "nanoeffects";
|
|
@@ -209,6 +214,88 @@ bus.once("name/changed", (name) => console.log(name), { traceId: "auditLog" });
|
|
|
209
214
|
[nanoeffects] saveNotifier · run done
|
|
210
215
|
```
|
|
211
216
|
|
|
217
|
+
## List who listens
|
|
218
|
+
|
|
219
|
+
**Problem.** To know what an emit will start, you search the code for every `bus.on` of that event.
|
|
220
|
+
|
|
221
|
+
**When.** You read an app that you did not write, or a test must guard the wiring of the bus.
|
|
222
|
+
|
|
223
|
+
`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.
|
|
224
|
+
|
|
225
|
+
```js
|
|
226
|
+
import { createBus } from "nanoeffects";
|
|
227
|
+
import { consoleLogger, createTracer, describeBus, tracePlugin } from "nanoeffects/trace";
|
|
228
|
+
|
|
229
|
+
function saveName(name) {
|
|
230
|
+
console.log("save", name);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function audit(name) {
|
|
234
|
+
console.log("audit", name);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const trace = createTracer(consoleLogger());
|
|
238
|
+
|
|
239
|
+
const bus = createBus({ trace })
|
|
240
|
+
.use(tracePlugin());
|
|
241
|
+
|
|
242
|
+
bus.on("name/changed", saveName);
|
|
243
|
+
bus.on("name/changed", audit);
|
|
244
|
+
bus.on("saved", () => {}, { traceId: "refreshView" });
|
|
245
|
+
|
|
246
|
+
// describeBus: the map of events to listener names
|
|
247
|
+
console.log(describeBus(bus));
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
{ "name/changed": ["saveName", "audit"], saved: ["refreshView"] }
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Find the line of a listener
|
|
255
|
+
|
|
256
|
+
**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.
|
|
257
|
+
|
|
258
|
+
**When.** You debug in development and want to jump from a log line to the code.
|
|
259
|
+
|
|
260
|
+
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 and a number: `name/changed#1`, `name/changed#2`, in the order of registration. Two inline listeners on one event are two entries, and the place stays in `source`. Adding or reordering `bus.on` calls renumbers them, so give a `traceId` to a listener whose name must stay the same between builds.
|
|
261
|
+
|
|
262
|
+
```js
|
|
263
|
+
import { createBus } from "nanoeffects";
|
|
264
|
+
import { consoleLogger, createTracer, describeBus, tracePlugin } from "nanoeffects/trace";
|
|
265
|
+
|
|
266
|
+
// source: print where the listener was registered
|
|
267
|
+
const trace = createTracer(consoleLogger({ source: true }));
|
|
268
|
+
|
|
269
|
+
// source: read the place of each bus.on, once at registration
|
|
270
|
+
const bus = createBus({ trace })
|
|
271
|
+
.use(tracePlugin({ source: true }));
|
|
272
|
+
|
|
273
|
+
function saveName(name) {
|
|
274
|
+
console.log("save", name);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
bus.on("name/changed", saveName);
|
|
278
|
+
bus.on("name/changed", (name) => console.log("audit", name));
|
|
279
|
+
|
|
280
|
+
bus.emit("name/changed", "Eva");
|
|
281
|
+
|
|
282
|
+
// describeBus: the same places, without running anything
|
|
283
|
+
console.log(describeBus(bus, { source: true }));
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
```
|
|
287
|
+
[nanoeffects] saveName · run start (file:///app/src/user.js:13:5)
|
|
288
|
+
[nanoeffects] name/changed#1 · run start (file:///app/src/user.js:14:5)
|
|
289
|
+
{ "name/changed": ["saveName (file:///app/src/user.js:13:5)", "<anonymous> (file:///app/src/user.js:14:5)"] }
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Know the limits:
|
|
293
|
+
|
|
294
|
+
- **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.
|
|
295
|
+
- **`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.
|
|
296
|
+
- **No stack**, as with `Error.stackTraceLimit = 0`, gives no `source`. The listener runs as usual.
|
|
297
|
+
- **The default is off.** Reading a stack costs a little for each registration, so turn it on in development.
|
|
298
|
+
|
|
212
299
|
## Keep the chain: `ctx.emit`, not `bus.emit`
|
|
213
300
|
|
|
214
301
|
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.
|
|
@@ -251,7 +338,7 @@ const bus = createBus({
|
|
|
251
338
|
| `consoleLogger()` | Prints one line per phase, indented by task depth |
|
|
252
339
|
| `performanceLogger()` | User Timing measures for the browser Performance panel |
|
|
253
340
|
| `createTraceRecorder()` | Keeps events in `recorder.events`: all of them for tests, or with `{ capacity }` the last N ([see below](#keep-the-last-events-for-an-incident)) |
|
|
254
|
-
| `graphLogger()` | Builds the map of which event leads to which, finds loops while the app runs, and writes the map as Mermaid, Graphviz or JSON ([see below](#find-loops-with-graphlogger)) |
|
|
341
|
+
| `graphLogger()` | Builds the map of which event leads to which, finds loops while the app runs, and writes the map as Mermaid, Graphviz or JSON ([see below](#find-loops-with-graphlogger)); [`mergeGraphs` and `findOrphans`](debugging-loops.md#find-listeners-that-never-ran) join runs and find listeners that never ran |
|
|
255
342
|
|
|
256
343
|
### Cut volume in production
|
|
257
344
|
|
|
@@ -311,12 +398,91 @@ window.addEventListener("error", () => {
|
|
|
311
398
|
What to know before you send the events:
|
|
312
399
|
|
|
313
400
|
- **`payload` and `error` can hold private data**: what a user typed, tokens, personal text. The recorder keeps them as they are and does not redact. Choose what to send, as `withoutPayload` does.
|
|
314
|
-
- **There is no time on an event.** You get the order, not the duration.
|
|
401
|
+
- **There is no time on an event.** You get the order, not the duration. [`timingLogger`](#measure-time-and-find-open-tasks) measures time.
|
|
315
402
|
- **An old `parentId` can point at an event that was pushed out.** When you build a tree from the events, treat an unknown `parentId` as a root.
|
|
316
403
|
- **`recorder.events` is a snapshot with a `capacity`.** Each read builds a new array, so read it once at the moment you need it. Without a `capacity` it is one live array.
|
|
317
404
|
- **A low `sample` thins the buffer.** `createTracer(recorder, { sample: 0.05 })` records 5% of root tasks, so the buffer may hold little when the incident comes. See [Cut volume in production](#cut-volume-in-production).
|
|
318
405
|
- **A `capacity` that is not a positive integer** (`0`, a negative number, `NaN`) means no limit.
|
|
319
406
|
|
|
407
|
+
### Measure time and find open tasks
|
|
408
|
+
|
|
409
|
+
An event has no time, so the recorder cannot tell you which handler is slow, how long a request lived before `cancel`, or whether a test left something running. `timingLogger` stamps the time on each event when it gets it and pairs `start` with the first closing phase (`done`, `error`, `cancel` or `drop`) of the same task.
|
|
410
|
+
|
|
411
|
+
Pass it to the tracer next to the recorder: `createTracer` takes an array of loggers.
|
|
412
|
+
|
|
413
|
+
<!-- docs-check: new -->
|
|
414
|
+
|
|
415
|
+
```js
|
|
416
|
+
// bus.js
|
|
417
|
+
import { createBus } from "nanoeffects";
|
|
418
|
+
import { createTraceRecorder, createTracer, timingLogger } from "nanoeffects/trace";
|
|
419
|
+
|
|
420
|
+
// createTraceRecorder: events in the order they came
|
|
421
|
+
export const recorder = createTraceRecorder();
|
|
422
|
+
|
|
423
|
+
// timingLogger: the time of each event and the duration of tasks
|
|
424
|
+
export const timing = timingLogger();
|
|
425
|
+
|
|
426
|
+
// createTracer: both loggers get every event
|
|
427
|
+
const trace = createTracer([recorder, timing]);
|
|
428
|
+
|
|
429
|
+
export const bus = createBus({ trace });
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
```js
|
|
433
|
+
// check.js
|
|
434
|
+
import { timing } from "./bus";
|
|
435
|
+
|
|
436
|
+
// durations: the tasks that ended, with the closing phase and the time in ms
|
|
437
|
+
const slow = timing.durations.filter((task) => task.ms > 500);
|
|
438
|
+
console.log(slow);
|
|
439
|
+
|
|
440
|
+
// pending: the tasks that started and have not ended
|
|
441
|
+
const leftovers = timing.pending();
|
|
442
|
+
if (leftovers.length > 0) {
|
|
443
|
+
throw new Error(`${leftovers.length} tasks are still open`);
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
What to know:
|
|
448
|
+
|
|
449
|
+
- **The clock.** Time comes from `performance.now()`, or `Date.now()` where there is none. In a test pass your own clock: `timingLogger({ now })`.
|
|
450
|
+
- **`capacity` cuts only `durations`.** `timingLogger({ capacity: 200 })` keeps the last 200 ended tasks. `pending()` is never cut: a leak shows up as a growing list.
|
|
451
|
+
- **`clear()`** empties both lists. Call it between tests.
|
|
452
|
+
- **The logger sees what the tracer lets through.** A task whose `start` was filtered out or not sampled gets no pair, and its ending is ignored.
|
|
453
|
+
|
|
454
|
+
### Find the cause of an error
|
|
455
|
+
|
|
456
|
+
An error arrives as one event, and you cannot see what led to it. `chainOf` builds the path from the root to a task by `parentId`. Give it the recorder: then the listeners that send no event (a plain handler that calls `ctx.emit`) are in the path too.
|
|
457
|
+
|
|
458
|
+
```js
|
|
459
|
+
// why.js
|
|
460
|
+
import { chainOf } from "nanoeffects/trace";
|
|
461
|
+
import { recorder } from "./bus";
|
|
462
|
+
|
|
463
|
+
const failed = recorder.events.find((event) => event.phase === "error");
|
|
464
|
+
|
|
465
|
+
// chainOf: the path from the root to the failed task
|
|
466
|
+
const path = chainOf(recorder, failed.id);
|
|
467
|
+
|
|
468
|
+
const text = path.map((step) => step.type).join(" → ");
|
|
469
|
+
console.log(text); // queryChanged → searchLoaded
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
A step has `type`, and `effect` and `phase` (a listener with no events has neither), `source` and `traceGroupId` when known. If you pass a list of events (a snapshot, a file) instead of the recorder, the path holds only the tasks that have events.
|
|
473
|
+
|
|
474
|
+
To see the path right in the console, turn on `chain` in `consoleLogger`:
|
|
475
|
+
|
|
476
|
+
```js
|
|
477
|
+
import { consoleLogger, createTracer } from "nanoeffects/trace";
|
|
478
|
+
|
|
479
|
+
// consoleLogger: the line of an error ends with the path from the root
|
|
480
|
+
const trace = createTracer(consoleLogger({ chain: true }));
|
|
481
|
+
// [nanoeffects] render · run error (queryChanged → render)
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
`chain` remembers the last 1000 tasks. It is off by default.
|
|
485
|
+
|
|
320
486
|
### Configure Redux DevTools
|
|
321
487
|
|
|
322
488
|
<!-- docs-check: new -->
|
|
@@ -448,6 +614,26 @@ const errorsOnly = (): TraceLogger => (event) => {
|
|
|
448
614
|
};
|
|
449
615
|
```
|
|
450
616
|
|
|
617
|
+
### Stop on an event
|
|
618
|
+
|
|
619
|
+
You want the debugger to stop when a certain event comes. No API is needed for that: it is five lines in a logger of your own.
|
|
620
|
+
|
|
621
|
+
```js
|
|
622
|
+
import { createTracer } from "nanoeffects/trace";
|
|
623
|
+
|
|
624
|
+
// A logger that stops in the debugger at the start of a task with this type
|
|
625
|
+
const stopOn = (type) => (event) => {
|
|
626
|
+
if (event.type === type && event.phase === "start") {
|
|
627
|
+
debugger; // the browser stops here when DevTools is open
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
// createTracer: pass the logger like any other
|
|
632
|
+
const trace = createTracer(stopOn("search"));
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
In the debugger read `event.payload` and walk up the stack: the synchronous part of the chain is all there.
|
|
636
|
+
|
|
451
637
|
## Drop tracing from a build
|
|
452
638
|
|
|
453
639
|
Without a tracer the hooks cost almost nothing. `nanoeffects/trace` is only bundled when imported.
|