nanoeffects 0.32.0 → 0.34.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/README.md +9 -1
- package/dist/actions.d.ts +2 -2
- package/dist/actions.js +1 -1
- package/dist/dev.d.ts +1 -1
- package/dist/{module-DPKjzfRk.d.ts → module-Fk4OtkZk.d.ts} +10 -27
- package/dist/notrace/actions.d.ts +10 -27
- package/dist/notrace/actions.js +1 -1
- package/dist/testing.js +1 -1
- package/dist/trace.d.ts +189 -42
- package/dist/trace.js +2 -2
- package/dist/tracer-v7m-Qah_.js +2 -0
- package/docs/actions.md +13 -7
- package/docs/{debugging-compared.md → debugging-cases.md} +3 -17
- package/docs/debugging-loops.md +79 -0
- package/docs/guide.md +6 -6
- package/docs/package.md +1 -1
- package/docs/ru/actions.md +13 -7
- package/docs/ru/{debugging-compared.md → debugging-cases.md} +3 -17
- package/docs/ru/debugging-loops.md +79 -0
- package/docs/ru/guide.md +6 -6
- package/docs/ru/package.md +1 -1
- package/docs/ru/tracing.md +106 -4
- package/docs/tracing.md +106 -4
- package/llms-full.txt +209 -36
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/dist/tracer-mMDm5yE7.js +0 -2
package/llms-full.txt
CHANGED
|
@@ -394,7 +394,7 @@ Use strings for the plain case. Use objects when other code already builds actio
|
|
|
394
394
|
|
|
395
395
|
**Problem.** The handler of an action is in the file of the feature, but a test needs a fake, or another layer owns the work. If the handler sits in `createAction`, the event file is tied to the work.
|
|
396
396
|
|
|
397
|
-
**What to do.** Create the action without a handler: it is an event. Bind the real handler with `
|
|
397
|
+
**What to do.** Create the action without a handler: it is an event. Bind the real handler with `actions.handle` when the bus is made. A later `actions.handle` binds a newer handler that wins until you remove it.
|
|
398
398
|
|
|
399
399
|
```js
|
|
400
400
|
// user.js
|
|
@@ -406,16 +406,16 @@ export const getUser = createAction("user/get"); // no handler yet: an event
|
|
|
406
406
|
```js
|
|
407
407
|
// bus.js
|
|
408
408
|
import { createBus } from "nanoeffects";
|
|
409
|
-
import { actionsPlugin
|
|
409
|
+
import { actionsPlugin } from "nanoeffects/actions";
|
|
410
410
|
import { getUser } from "./user";
|
|
411
411
|
|
|
412
412
|
export const bus = createBus();
|
|
413
413
|
|
|
414
|
-
// setHandler — binds the real handler to the action
|
|
415
|
-
const realUser = setHandler(getUser, (id) => api.user(id));
|
|
416
|
-
|
|
417
414
|
// actionsPlugin — the registry of bound handlers of this bus
|
|
418
|
-
export const actions = bus.use(actionsPlugin(
|
|
415
|
+
export const actions = bus.use(actionsPlugin());
|
|
416
|
+
|
|
417
|
+
// actions.handle — binds the real handler to the action
|
|
418
|
+
actions.handle(getUser, (id) => api.user(id));
|
|
419
419
|
```
|
|
420
420
|
|
|
421
421
|
```js
|
|
@@ -1321,7 +1321,7 @@ Each row is optional. The "when" column says what need brings you to it.
|
|
|
1321
1321
|
| `nanoeffects/svelte` | `sveltePlugin`: adds `eventStore` to a bus (needs Svelte 5 or newer) | Svelte components |
|
|
1322
1322
|
| `nanoeffects/vue` | `vuePlugin`: adds `watchEmitter` and `useEventRef` to a bus (needs Vue 3.3 or newer) | Vue components |
|
|
1323
1323
|
| `nanoeffects/dev` | `devPlugin` (no-op in production), `getLostSignalsCount` and `getLostSignals` for tests | you look for leaks and lost signals in development and in tests |
|
|
1324
|
-
| `nanoeffects/trace` | `createTracer`, the loggers (`graphLogger` finds event loops) and `tracePlugin` (names for the trace) | you want to see what ran and why |
|
|
1324
|
+
| `nanoeffects/trace` | `createTracer`, the loggers (`graphLogger` finds event loops; `mergeGraphs` and `findOrphans` find listeners no run reached) and `tracePlugin` (names for the trace) | you want to see what ran and why |
|
|
1325
1325
|
| `nanoeffects/testing` | `createMarbleBus`, `fakeTimersClock`: a bus for tests, with emits and phases as diagrams | you test `latest`, `debounce`, `queue`, `retry` or `timeout` with fake timers |
|
|
1326
1326
|
| `nanoeffects/guard` | `loopGuardPlugin`: refuses the emit that would pass a limit of a chain | a loop must not take the app down |
|
|
1327
1327
|
|
|
@@ -3049,15 +3049,21 @@ export const sendMail = createAction("mail/send");
|
|
|
3049
3049
|
```js
|
|
3050
3050
|
// bus.js
|
|
3051
3051
|
import { createBus } from "nanoeffects";
|
|
3052
|
-
import { actionsPlugin
|
|
3053
|
-
import { sendMail } from "./mail";
|
|
3052
|
+
import { actionsPlugin } from "nanoeffects/actions";
|
|
3054
3053
|
|
|
3055
3054
|
export const bus = createBus();
|
|
3056
3055
|
|
|
3057
|
-
// actionsPlugin — the registry of handlers of this bus
|
|
3058
|
-
export const actions = bus.use(
|
|
3059
|
-
|
|
3060
|
-
|
|
3056
|
+
// actionsPlugin — the registry of handlers of this bus
|
|
3057
|
+
export const actions = bus.use(actionsPlugin());
|
|
3058
|
+
```
|
|
3059
|
+
|
|
3060
|
+
```js
|
|
3061
|
+
// mail-handlers.js
|
|
3062
|
+
import { actions } from "./bus";
|
|
3063
|
+
import { sendMail } from "./mail";
|
|
3064
|
+
|
|
3065
|
+
// actions.handle — binds the work to the action on this bus
|
|
3066
|
+
actions.handle(sendMail, smtpSend);
|
|
3061
3067
|
```
|
|
3062
3068
|
|
|
3063
3069
|
```js
|
|
@@ -3071,7 +3077,7 @@ unbind();
|
|
|
3071
3077
|
|
|
3072
3078
|
A handler bound in the registry makes `bus.emit(sendMail(mail))` run it and return its result, the same as for an action that carries its handler. The listeners of the type are told first. With no handler bound, `sendMail` is an event again.
|
|
3073
3079
|
|
|
3074
|
-
The registry lives in `nanoeffects/actions`, so a bus without one carries none of that code. Without a registry, `emit` runs the handler that the action carries. `bus.use(actionsPlugin())` sets `bus.actions`. A second `actionsPlugin`
|
|
3080
|
+
The registry lives in `nanoeffects/actions`, so a bus without one carries none of that code. Without a registry, `emit` runs the handler that the action carries. `bus.use(actionsPlugin())` sets `bus.actions`. A second `actionsPlugin` returns that registry. There is no `actions` option in `createBus`; assign a custom runner with `bus.actions = { run }`, and add `bound(type)` to it to answer for actions that have no handler of their own.
|
|
3075
3081
|
|
|
3076
3082
|
### The result type of a bound handler
|
|
3077
3083
|
|
|
@@ -7852,10 +7858,13 @@ Tracing is optional. A bus works the same without it, and it costs almost nothin
|
|
|
7852
7858
|
- [Use built-in loggers](#use-built-in-loggers)
|
|
7853
7859
|
- [Cut volume in production](#cut-volume-in-production)
|
|
7854
7860
|
- [Keep the last events for an incident](#keep-the-last-events-for-an-incident)
|
|
7861
|
+
- [Measure time and find open tasks](#measure-time-and-find-open-tasks)
|
|
7862
|
+
- [Find the cause of an error](#find-the-cause-of-an-error)
|
|
7855
7863
|
- [Configure Redux DevTools](#configure-redux-devtools)
|
|
7856
7864
|
- [Find loops with `graphLogger`](#find-loops-with-graphlogger)
|
|
7857
7865
|
- [Send safe data to a tool](#send-safe-data-to-a-tool)
|
|
7858
7866
|
- [Write your own logger](#write-your-own-logger)
|
|
7867
|
+
- [Stop on an event](#stop-on-an-event)
|
|
7859
7868
|
- [Drop tracing from a build](#drop-tracing-from-a-build)
|
|
7860
7869
|
|
|
7861
7870
|
## Attach a tracer
|
|
@@ -8086,7 +8095,7 @@ console.log(describeBus(bus));
|
|
|
8086
8095
|
|
|
8087
8096
|
**When.** You debug in development and want to jump from a log line to the code.
|
|
8088
8097
|
|
|
8089
|
-
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.
|
|
8098
|
+
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.
|
|
8090
8099
|
|
|
8091
8100
|
```js
|
|
8092
8101
|
import { createBus } from "nanoeffects";
|
|
@@ -8114,7 +8123,7 @@ console.log(describeBus(bus, { source: true }));
|
|
|
8114
8123
|
|
|
8115
8124
|
```
|
|
8116
8125
|
[nanoeffects] saveName · run start (file:///app/src/user.js:13:5)
|
|
8117
|
-
[nanoeffects] name/changed · run start (file:///app/src/user.js:14:5)
|
|
8126
|
+
[nanoeffects] name/changed#1 · run start (file:///app/src/user.js:14:5)
|
|
8118
8127
|
{ "name/changed": ["saveName (file:///app/src/user.js:13:5)", "<anonymous> (file:///app/src/user.js:14:5)"] }
|
|
8119
8128
|
```
|
|
8120
8129
|
|
|
@@ -8167,7 +8176,7 @@ const bus = createBus({
|
|
|
8167
8176
|
| `consoleLogger()` | Prints one line per phase, indented by task depth |
|
|
8168
8177
|
| `performanceLogger()` | User Timing measures for the browser Performance panel |
|
|
8169
8178
|
| `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)) |
|
|
8170
|
-
| `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)) |
|
|
8179
|
+
| `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 |
|
|
8171
8180
|
|
|
8172
8181
|
### Cut volume in production
|
|
8173
8182
|
|
|
@@ -8227,12 +8236,91 @@ window.addEventListener("error", () => {
|
|
|
8227
8236
|
What to know before you send the events:
|
|
8228
8237
|
|
|
8229
8238
|
- **`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.
|
|
8230
|
-
- **There is no time on an event.** You get the order, not the duration.
|
|
8239
|
+
- **There is no time on an event.** You get the order, not the duration. [`timingLogger`](#measure-time-and-find-open-tasks) measures time.
|
|
8231
8240
|
- **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.
|
|
8232
8241
|
- **`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.
|
|
8233
8242
|
- **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).
|
|
8234
8243
|
- **A `capacity` that is not a positive integer** (`0`, a negative number, `NaN`) means no limit.
|
|
8235
8244
|
|
|
8245
|
+
### Measure time and find open tasks
|
|
8246
|
+
|
|
8247
|
+
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.
|
|
8248
|
+
|
|
8249
|
+
Pass it to the tracer next to the recorder: `createTracer` takes an array of loggers.
|
|
8250
|
+
|
|
8251
|
+
<!-- docs-check: new -->
|
|
8252
|
+
|
|
8253
|
+
```js
|
|
8254
|
+
// bus.js
|
|
8255
|
+
import { createBus } from "nanoeffects";
|
|
8256
|
+
import { createTraceRecorder, createTracer, timingLogger } from "nanoeffects/trace";
|
|
8257
|
+
|
|
8258
|
+
// createTraceRecorder: events in the order they came
|
|
8259
|
+
export const recorder = createTraceRecorder();
|
|
8260
|
+
|
|
8261
|
+
// timingLogger: the time of each event and the duration of tasks
|
|
8262
|
+
export const timing = timingLogger();
|
|
8263
|
+
|
|
8264
|
+
// createTracer: both loggers get every event
|
|
8265
|
+
const trace = createTracer([recorder, timing]);
|
|
8266
|
+
|
|
8267
|
+
export const bus = createBus({ trace });
|
|
8268
|
+
```
|
|
8269
|
+
|
|
8270
|
+
```js
|
|
8271
|
+
// check.js
|
|
8272
|
+
import { timing } from "./bus";
|
|
8273
|
+
|
|
8274
|
+
// durations: the tasks that ended, with the closing phase and the time in ms
|
|
8275
|
+
const slow = timing.durations.filter((task) => task.ms > 500);
|
|
8276
|
+
console.log(slow);
|
|
8277
|
+
|
|
8278
|
+
// pending: the tasks that started and have not ended
|
|
8279
|
+
const leftovers = timing.pending();
|
|
8280
|
+
if (leftovers.length > 0) {
|
|
8281
|
+
throw new Error(`${leftovers.length} tasks are still open`);
|
|
8282
|
+
}
|
|
8283
|
+
```
|
|
8284
|
+
|
|
8285
|
+
What to know:
|
|
8286
|
+
|
|
8287
|
+
- **The clock.** Time comes from `performance.now()`, or `Date.now()` where there is none. In a test pass your own clock: `timingLogger({ now })`.
|
|
8288
|
+
- **`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.
|
|
8289
|
+
- **`clear()`** empties both lists. Call it between tests.
|
|
8290
|
+
- **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.
|
|
8291
|
+
|
|
8292
|
+
### Find the cause of an error
|
|
8293
|
+
|
|
8294
|
+
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.
|
|
8295
|
+
|
|
8296
|
+
```js
|
|
8297
|
+
// why.js
|
|
8298
|
+
import { chainOf } from "nanoeffects/trace";
|
|
8299
|
+
import { recorder } from "./bus";
|
|
8300
|
+
|
|
8301
|
+
const failed = recorder.events.find((event) => event.phase === "error");
|
|
8302
|
+
|
|
8303
|
+
// chainOf: the path from the root to the failed task
|
|
8304
|
+
const path = chainOf(recorder, failed.id);
|
|
8305
|
+
|
|
8306
|
+
const text = path.map((step) => step.type).join(" → ");
|
|
8307
|
+
console.log(text); // queryChanged → searchLoaded
|
|
8308
|
+
```
|
|
8309
|
+
|
|
8310
|
+
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.
|
|
8311
|
+
|
|
8312
|
+
To see the path right in the console, turn on `chain` in `consoleLogger`:
|
|
8313
|
+
|
|
8314
|
+
```js
|
|
8315
|
+
import { consoleLogger, createTracer } from "nanoeffects/trace";
|
|
8316
|
+
|
|
8317
|
+
// consoleLogger: the line of an error ends with the path from the root
|
|
8318
|
+
const trace = createTracer(consoleLogger({ chain: true }));
|
|
8319
|
+
// [nanoeffects] render · run error (queryChanged → render)
|
|
8320
|
+
```
|
|
8321
|
+
|
|
8322
|
+
`chain` remembers the last 1000 tasks. It is off by default.
|
|
8323
|
+
|
|
8236
8324
|
### Configure Redux DevTools
|
|
8237
8325
|
|
|
8238
8326
|
<!-- docs-check: new -->
|
|
@@ -8364,6 +8452,26 @@ const errorsOnly = (): TraceLogger => (event) => {
|
|
|
8364
8452
|
};
|
|
8365
8453
|
```
|
|
8366
8454
|
|
|
8455
|
+
### Stop on an event
|
|
8456
|
+
|
|
8457
|
+
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.
|
|
8458
|
+
|
|
8459
|
+
```js
|
|
8460
|
+
import { createTracer } from "nanoeffects/trace";
|
|
8461
|
+
|
|
8462
|
+
// A logger that stops in the debugger at the start of a task with this type
|
|
8463
|
+
const stopOn = (type) => (event) => {
|
|
8464
|
+
if (event.type === type && event.phase === "start") {
|
|
8465
|
+
debugger; // the browser stops here when DevTools is open
|
|
8466
|
+
}
|
|
8467
|
+
};
|
|
8468
|
+
|
|
8469
|
+
// createTracer: pass the logger like any other
|
|
8470
|
+
const trace = createTracer(stopOn("search"));
|
|
8471
|
+
```
|
|
8472
|
+
|
|
8473
|
+
In the debugger read `event.payload` and walk up the stack: the synchronous part of the chain is all there.
|
|
8474
|
+
|
|
8367
8475
|
## Drop tracing from a build
|
|
8368
8476
|
|
|
8369
8477
|
Without a tracer the hooks cost almost nothing. `nanoeffects/trace` is only bundled when imported.
|
|
@@ -8382,35 +8490,21 @@ conditions: ["notrace"]
|
|
|
8382
8490
|
In a build from the sources, `define: { __NANOEFFECTS_TRACE__: "false" }` does the same. With the condition set, that build’s types have no `trace` option: `createBus({ trace })` is a type error. `nanoeffects/trace` has no `notrace` build. The saving is about 0.4 kB gzip of the whole library.
|
|
8383
8491
|
|
|
8384
8492
|
---
|
|
8385
|
-
Source: docs/debugging-
|
|
8493
|
+
Source: docs/debugging-cases.md
|
|
8386
8494
|
|
|
8387
|
-
# Debugging
|
|
8495
|
+
# Debugging cases
|
|
8388
8496
|
|
|
8389
8497
|
> **Level F.** Read first: [Trace tasks](tracing.md#attach-a-tracer).
|
|
8390
8498
|
|
|
8391
|
-
Read this page
|
|
8499
|
+
Read this page when something goes wrong and you want to know who listens to an event, what began a chain, or why a request stopped. Each case below is a short snippet that you can run. For loops, see [Find and cut event loops](debugging-loops.md).
|
|
8392
8500
|
|
|
8393
8501
|
## On this page
|
|
8394
8502
|
|
|
8395
|
-
- [Compare what you can see](#compare-what-you-can-see)
|
|
8396
8503
|
- [Find who listens to an event](#find-who-listens-to-an-event)
|
|
8397
8504
|
- [Find which task began a chain](#find-which-task-began-a-chain)
|
|
8398
8505
|
- [Find out why a request stopped](#find-out-why-a-request-stopped)
|
|
8399
8506
|
- [Know what is missing](#know-what-is-missing)
|
|
8400
8507
|
|
|
8401
|
-
## Compare what you can see
|
|
8402
|
-
|
|
8403
|
-
| Question | nanoeffects | RxJS | Effector | mitt / nanoevents |
|
|
8404
|
-
| -------- | ----------- | ---- | -------- | ----------------- |
|
|
8405
|
-
| Which task started which? | Yes: every task has `parentId` and `rootId` ([Trace tasks](tracing.md)) | You add `tap` per stream, or a tool such as [rxjs-spy](https://github.com/cartant/rxjs-spy) | Yes, through [`effector-logger`](https://github.com/effector/logger) and the [inspector](https://github.com/effector/inspector) | No |
|
|
8406
|
-
| Was a call cancelled or dropped? | Yes: the phases `cancel` and `drop` | An unsubscribe shows no event unless you add one | — | No |
|
|
8407
|
-
| A log of every emit | `tracePlugin` reports each emit, also with no listener | `tap` per stream | A logger over the units | A `"*"` listener |
|
|
8408
|
-
| A map of the app | `describeBus` before a run, `graphLogger` after ([Find and cut event loops](debugging-loops.md)) | Not built in | The graph of units, with names from the Babel/SWC plugin | `emitter.all` holds the handlers of each event (mitt) |
|
|
8409
|
-
| Find a loop | `graphLogger`, then `loopGuardPlugin` cuts it | — | — | No |
|
|
8410
|
-
| Test a flow | [`createMarbleBus`](testing.md): emits and phases as diagrams on fake timers; [`createTraceRecorder`](tracing.md#keep-the-last-events-for-an-incident) keeps the events | [Marble tests](https://rxjs.dev/guide/testing/marble-testing) | `fork` gives an isolated scope | You wire it yourself |
|
|
8411
|
-
|
|
8412
|
-
A dash means that this page does not compare it. Read the table as a map of where to look, not as a score. Effector and RxJS also hold state and streams, which nanoeffects does not, so their tools cover more ground. nanoeffects covers one thing in depth: why an async handler ran, and why it stopped.
|
|
8413
|
-
|
|
8414
8508
|
## Find who listens to an event
|
|
8415
8509
|
|
|
8416
8510
|
**Problem.** You read an emit and want to know what it starts.
|
|
@@ -8515,7 +8609,7 @@ Details: [Trace tasks](tracing.md) and [Cancel stale requests](cancel-stale-requ
|
|
|
8515
8609
|
|
|
8516
8610
|
## Know what is missing
|
|
8517
8611
|
|
|
8518
|
-
- **No browser extension or UI of its own.** Use [Redux DevTools](tracing.md#configure-redux-devtools), the console or the Performance panel.
|
|
8612
|
+
- **No browser extension or UI of its own.** Use [Redux DevTools](tracing.md#configure-redux-devtools), the console or the Performance panel.
|
|
8519
8613
|
- **No map from the source code.** `describeBus` lists the listeners registered so far, and `graphLogger` shows the events that happened. A listener that is not registered yet is not on the map.
|
|
8520
8614
|
- **No click-through from the log to the code.** With `tracePlugin({ source: true })` an event carries the `file:line:column` of its listener ([Find the line of a listener](tracing.md#find-the-line-of-a-listener)), but a link in a UI is yours to build.
|
|
8521
8615
|
- **No names from a compiler.** Function names give a name for free; a minifier can shorten them, so use `traceId` where a name must stay stable.
|
|
@@ -8542,6 +8636,7 @@ A loop is two handlers that emit each other's events, or one handler that emits
|
|
|
8542
8636
|
- [Cut a loop at run time](#cut-a-loop-at-run-time)
|
|
8543
8637
|
- [Fail a test on a loop](#fail-a-test-on-a-loop)
|
|
8544
8638
|
- [Export the graph and the payloads](#export-the-graph-and-the-payloads)
|
|
8639
|
+
- [Find listeners that never ran](#find-listeners-that-never-ran)
|
|
8545
8640
|
- [Know how a loop is found](#know-how-a-loop-is-found)
|
|
8546
8641
|
- [Know the limits](#know-the-limits)
|
|
8547
8642
|
|
|
@@ -8804,6 +8899,84 @@ console.log(JSON.stringify(data));
|
|
|
8804
8899
|
|
|
8805
8900
|
A payload can be a class instance, a `Map`, a circular object or an `Error`, which a plain `JSON.stringify` cannot take. The exports make them plain JSON: an `Error` keeps its name, message, stack and cause, a cycle becomes `"[Circular]"`, a `Map` becomes `{ "$map": [...] }`, and an instance of a class keeps its own fields and its name as `$class`. Getters are never called, and a value that is too deep or too big is cut.
|
|
8806
8901
|
|
|
8902
|
+
## Find listeners that never ran
|
|
8903
|
+
|
|
8904
|
+
A graph of one run shows what that run did. It cannot tell you which listeners the app has but no run ever reached: dead code, or a scenario nobody tests. To find them, join the graphs of several runs, then compare the result with the listeners the bus holds.
|
|
8905
|
+
|
|
8906
|
+
Prerequisites: the bus uses `tracePlugin` and `graphLogger` (see [Find a loop](#find-a-loop)), and each listener has a function name or a `traceId`. An anonymous listener has no node in the graph, so it is never reported.
|
|
8907
|
+
|
|
8908
|
+
1. Each run writes its graph to a file.
|
|
8909
|
+
2. A script reads the files and joins them with `mergeGraphs`.
|
|
8910
|
+
3. `findOrphans` compares the joined graph with `describeBus`.
|
|
8911
|
+
|
|
8912
|
+
```js
|
|
8913
|
+
// bus.js
|
|
8914
|
+
import { createBus } from "nanoeffects";
|
|
8915
|
+
import { createTracer, graphLogger, tracePlugin } from "nanoeffects/trace";
|
|
8916
|
+
|
|
8917
|
+
// graphLogger — the map of what ran
|
|
8918
|
+
export const graph = graphLogger({ name: "cart" });
|
|
8919
|
+
export const bus = createBus({ trace: createTracer(graph) }).use(tracePlugin());
|
|
8920
|
+
```
|
|
8921
|
+
|
|
8922
|
+
```js
|
|
8923
|
+
// handlers.js
|
|
8924
|
+
import { bus } from "./bus";
|
|
8925
|
+
|
|
8926
|
+
// the listener is named by its function; findOrphans matches this name
|
|
8927
|
+
bus.on("cart/added", function recalcTotal() {});
|
|
8928
|
+
bus.on("cart/cleared", function resetTotal() {});
|
|
8929
|
+
```
|
|
8930
|
+
|
|
8931
|
+
```js
|
|
8932
|
+
// graph-setup.js (the setup file of your test runner)
|
|
8933
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
8934
|
+
import { afterAll } from "vitest";
|
|
8935
|
+
import { graph } from "./bus";
|
|
8936
|
+
|
|
8937
|
+
afterAll(() => {
|
|
8938
|
+
mkdirSync("graphs", { recursive: true });
|
|
8939
|
+
|
|
8940
|
+
// toJSON — the graph of this run as plain data
|
|
8941
|
+
const data = graph.toJSON();
|
|
8942
|
+
writeFileSync(`graphs/${process.pid}.json`, JSON.stringify(data));
|
|
8943
|
+
});
|
|
8944
|
+
```
|
|
8945
|
+
|
|
8946
|
+
```js
|
|
8947
|
+
// orphans.js (run it after the tests)
|
|
8948
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
8949
|
+
import { describeBus, findOrphans, mergeGraphs } from "nanoeffects/trace";
|
|
8950
|
+
import { bus } from "./bus";
|
|
8951
|
+
import "./handlers";
|
|
8952
|
+
|
|
8953
|
+
const files = readdirSync("graphs");
|
|
8954
|
+
const runs = files.map((file) => JSON.parse(readFileSync(`graphs/${file}`, "utf8")));
|
|
8955
|
+
|
|
8956
|
+
// mergeGraphs — counts add up, so a listener that ran in any run is a node
|
|
8957
|
+
const all = mergeGraphs(...runs);
|
|
8958
|
+
|
|
8959
|
+
// describeBus — the listeners the bus holds, by event
|
|
8960
|
+
const described = describeBus(bus);
|
|
8961
|
+
|
|
8962
|
+
// findOrphans — the listeners the joined graph never saw run
|
|
8963
|
+
console.log(findOrphans(described, all));
|
|
8964
|
+
// [{ event: "cart/cleared", listener: "resetTotal", kind: "unemitted" }]
|
|
8965
|
+
```
|
|
8966
|
+
|
|
8967
|
+
Each item has a `kind`:
|
|
8968
|
+
|
|
8969
|
+
| `kind` | Meaning | What to check |
|
|
8970
|
+
| ------ | ------- | ------------- |
|
|
8971
|
+
| `unemitted` | Nothing emitted the event in any run. | Is the event still used? Is a scenario missing? |
|
|
8972
|
+
| `unrun` | The event was emitted, this listener did not run. | A condition or a [policy](policies.md) that never lets it run. |
|
|
8973
|
+
|
|
8974
|
+
Read the list as a hint, not as a failure. With few runs it holds many false items, and it only becomes a signal when the runs cover the main scenarios. Know the limits:
|
|
8975
|
+
|
|
8976
|
+
- Two listeners with one name are one node, so the second one is never reported. Give each a distinct name or `traceId`.
|
|
8977
|
+
- Merge runs of one build. A listener that was renamed shows up as an orphan.
|
|
8978
|
+
- `mergeGraphs` does not find a loop that only closes when the edges of two runs are joined. Use `graph.cycles()` in each run.
|
|
8979
|
+
|
|
8807
8980
|
## Know how a loop is found
|
|
8808
8981
|
|
|
8809
8982
|
`graphLogger` finds loops in two ways. They give two kinds of finding in `graph.cycles()`, told apart by `source`:
|
package/llms.txt
CHANGED
|
@@ -61,9 +61,9 @@ Rules that generated code often gets wrong:
|
|
|
61
61
|
- [Semantics](docs/semantics.md): exact rules of emit, abort and ordering
|
|
62
62
|
- [Keep the signal through a handler](docs/pass-the-signal.md): every way a policy handler loses `ctx.signal`, and the lint rules and `devPlugin` check that find it
|
|
63
63
|
- [Test time with diagrams](docs/testing.md): `createMarbleBus` and `fakeTimersClock`; emits and phases as diagrams on fake timers
|
|
64
|
-
- [Trace tasks](docs/tracing.md): loggers, Redux DevTools, `graphLogger`, the `notrace` build
|
|
65
|
-
- [Debugging
|
|
66
|
-
- [Find and cut event loops](docs/debugging-loops.md): symptoms, `graphLogger`, the graph as Mermaid, Graphviz or JSON, Redux DevTools
|
|
64
|
+
- [Trace tasks](docs/tracing.md): loggers, `timingLogger` for durations and open tasks, `chainOf` for the cause of an error, Redux DevTools, `graphLogger`, the `notrace` build
|
|
65
|
+
- [Debugging cases](docs/debugging-cases.md): who listens to an event, what began a chain, why a request stopped
|
|
66
|
+
- [Find and cut event loops](docs/debugging-loops.md): symptoms, `graphLogger`, the graph as Mermaid, Graphviz or JSON, Redux DevTools, `mergeGraphs` and `findOrphans` for listeners no run reached
|
|
67
67
|
- [Cut a loop with loopGuardPlugin](docs/loop-guard.md): limits on repeats, depth and emits from outside
|
|
68
68
|
- [Turn events into Observables](docs/observable.md): `createObserve`, `Subject`, `events` (an event as a `for await` loop)
|
|
69
69
|
- [Hot reload and development diagnostics](docs/hmr.md): `devPlugin`
|
package/package.json
CHANGED
package/dist/tracer-mMDm5yE7.js
DELETED
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{i as e,n as t,r as n}from"./caller-DULg_ZYV.js";import{n as r,t as i}from"./onAbort-BgMfspK4.js";import{a,c as o,n as s,o as c,r as l,s as u}from"./context-Dp_QRXzb.js";import{n as d}from"./promise-CSLpzwSR.js";import{t as f}from"./origin-key-BbWN3gmN.js";import{t as p}from"./scope-key-B7l7mUS5.js";const m=/* @__PURE__ */ new WeakMap,h=e=>{let t=m.get(e);if(t)return t;let n=/* @__PURE__ */ new Map;return m.set(e,n),n},g=e=>m.get(e),_=(e,t,n)=>{let r=e.get(t)??[];r.push(n),e.set(t,r)},v=(e,t,n)=>{let r=e.get(t),i=r?.indexOf(n)??-1;!r||i<0||(r.splice(i,1),r.length===0&&e.delete(t))},y=(e,t,n)=>(e.get(t)??[]).findLast(e=>e.handler===n);let b;const x=()=>b,S=(e,t)=>{let n=b;b=e;try{return t()}finally{b=n}},C=(e,t,n={})=>{let{trace:r}=e;if(!r){e.emit(t);return}let{traceId:i,traceGroupId:a}=n,{ctx:o,unlink:c}=s({event:{type:i??t.type,payload:t,...a===void 0?{}:{traceGroupId:a}},bus:{emit:e.emit,on:e.on,off:e.off,ctx:e.ctx,addContext:e.addContext},tracer:r});u(o,`run`,`start`);try{o.emit(t),u(o,`run`,`done`)}catch(e){throw u(o,`run`,`error`,e),e}finally{c()}},w=e=>/^(.+):(\d+):(\d+)$/.exec(e)?e:void 0,T=e=>{let t=e.slice(3).replace(/^async /,``),n=t.lastIndexOf(` (`),r=n<0?t:t.slice(n+2).replace(/\)$/,``);return w(r)},E=e=>w(e.slice(e.indexOf(`@`)+1)),D=e=>{if(typeof e!=`string`)return[];let t=[];for(let n of e.split(`
|
|
2
|
-
`)){let e=n.trim();e.startsWith(`at `)?t.push(T(e)):e.includes(`@`)&&t.push(E(e))}return t},O=(e,t)=>D(e)[t],k=e=>typeof e==`string`?e:Reflect.get(Object(e),`type`),A=(e,t)=>typeof e==`object`?Reflect.get(Object(e),`payload`):t[0],j=(r={})=>i=>{if(!(o&&i.trace))return i;let{trace:s}=i,u=(e,t)=>{let n=s.task({type:k(e),payload:A(e,t),leaf:!0},l());s.report(n,`emit`,`done`)},d=i.emit;i.emit=(e,...t)=>S(e,()=>(u(e,t),d(e,...t))),Reflect.set(i,f,(e,t)=>{C(i,e,t)});let p=h(i),m=(e,t,n)=>(r,i)=>(a(i,t,n),c(i,`run`,()=>e(r,i))),g=(e,t)=>{let n=Reflect.get(Object(t),`traceId`);return typeof n==`string`?n:e.name||void 0},b=r.source===!0;b&&n();let x=(e,t)=>e!==void 0||!b?e:typeof t==`string`?t:void 0,w=(e,t,n)=>{let r=x(e.name,t),i=typeof r==`string`?m(e.handler,r,e.source):e.handler;return n?(n,r)=>(v(p,t,e),i(n,r)):i},T=(n,r)=>{let i=(e,i,a)=>{let o=k(e),s=b?O(t(),2):void 0,c={handler:i,wrapper:i,name:g(i,a),source:s};c.wrapper=w(c,o,r),_(p,o,c);let l=n(e,c.wrapper,a);return()=>{v(p,o,c),l()}};return(t,n,r)=>e(()=>i(t,n,r))},E=i.off,D=(e,t)=>{if(e===void 0)p.clear();else if(t===void 0)p.delete(k(e));else{let n=y(p,k(e),t);n&&v(p,k(e),n),E(e,n?.wrapper??t);return}E(e,t)},j=i.on,M=i.once;return i.on=T(j,!1),i.once=T(M,!0),i.off=D,i},M=e=>{let t=typeof e==`function`?[e]:[...e??[]];return t.length===0?null:e=>{for(let n of t)try{n(e)}catch(e){r(`trace logger failed`,e)}}},N=()=>{},P=e=>{if(typeof e.traceGroupId==`string`)return e.traceGroupId;let t=x();if(typeof t!=`object`&&typeof t!=`function`||t===null)return;let n=Reflect.get(t,p),r=typeof n==`object`&&n?Reflect.get(n,`traceGroupId`):void 0;return typeof r==`string`?r:void 0},F=(e,t={})=>{let n=e,a=M(n),{filter:o,sample:s=1}=t,c=0,u=(Array.isArray(n)?n:[n]).flatMap(e=>e.task?[e.task]:[]),f=e=>{let t={ref:e,id:e.id,type:e.type};e.parentRef&&(t.parent=e.parentRef),e.event!==void 0&&(t.event=e.event),e.leaf&&(t.leaf=!0),t.emitted=e.emitted,e.group!==void 0&&(t.traceGroupId=e.group),e.source!==void 0&&(t.source=e.source);for(let e of u)try{e(t)}catch(e){r(`trace logger failed`,e)}},p=e=>typeof s==`function`?s({type:e.type,payload:e.payload}):Math.random()<s,m={task:(e,t)=>{let n={id:++c,root:t?.root??c,parent:t?.id,parentRef:u.length>0?t:void 0,leaf:e.leaf,emitted:l()===t,type:e.type,payload:e.payload,sampled:t?.sampled??p(e),group:P(e)??t?.group};return u.length>0&&n.sampled&&f(n),n},rename:(e,t,n)=>{e.event??=e.type,e.type=t,n!==void 0&&(e.source=n),u.length>0&&e.sampled&&f(e)},report:(e,t,n,r)=>{if(!e.sampled)return;let i={id:e.id,type:e.type,effect:t,phase:n,payload:e.payload};typeof e.parent==`number`&&(i.parentId=e.parent),e.root!==e.id&&(i.rootId=e.root),e.group!==void 0&&(i.traceGroupId=e.group),e.source!==void 0&&(i.source=e.source),n===`error`&&(i.error=r),(!o||o(i))&&a?.(i)},settler:(e,t)=>{if(!e.sampled)return N;let n=!1;return(r,i)=>{n||(n=!0,m.report(e,t,r,i))}},run:(e,t,n,r)=>{if(!e.sampled)return r();let a=m.settler(e,t);m.report(e,t,`start`),i({signal:n},()=>a(`cancel`));let o;try{o=r()}catch(e){throw a(`error`,e),e}return d(o)?Promise.resolve(o).then(e=>(a(`done`),e),e=>{throw a(`error`,e),e}):(a(`done`),o)}};return m};export{j as n,g as r,F as t};
|