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/llms-full.txt
CHANGED
|
@@ -581,6 +581,7 @@ Each task below starts with the problem, then a short solution. Where the page h
|
|
|
581
581
|
- [Do not leave subscriptions after a widget closes](#do-not-leave-subscriptions-after-a-widget-closes)
|
|
582
582
|
- [Stop work on a server](#stop-work-on-a-server)
|
|
583
583
|
- [Debugging: what ran and why](#debugging-what-ran-and-why)
|
|
584
|
+
- [Test a policy with fake timers](#test-a-policy-with-fake-timers)
|
|
584
585
|
- [Other libraries in the project](#other-libraries-in-the-project)
|
|
585
586
|
- [Classes and TypeScript](#classes-and-typescript)
|
|
586
587
|
- [Use the bus in a class](#use-the-bus-in-a-class)
|
|
@@ -1032,6 +1033,40 @@ bus.emit("search", "nano"); // the log shows: search · emit, then results · em
|
|
|
1032
1033
|
|
|
1033
1034
|
More: [Trace tasks](tracing.md). To reload modules without a page refresh, see [Hot reload](hmr.md).
|
|
1034
1035
|
|
|
1036
|
+
## Test a policy with fake timers
|
|
1037
|
+
|
|
1038
|
+
A test of `latest` or `debounce` is full of `advanceTimersByTime` numbers, and nobody can tell from them what the test proves. Write the emits and the phases as diagrams instead.
|
|
1039
|
+
|
|
1040
|
+
```js
|
|
1041
|
+
import { it, vi } from "vitest";
|
|
1042
|
+
import { latest } from "nanoeffects/policies";
|
|
1043
|
+
import { createMarbleBus, fakeTimersClock } from "nanoeffects/testing";
|
|
1044
|
+
|
|
1045
|
+
// a search that takes 50 ms
|
|
1046
|
+
function search(query) {
|
|
1047
|
+
return new Promise((resolve) => setTimeout(resolve, 50));
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
it("latest cancels the search that is running", async () => {
|
|
1051
|
+
vi.useFakeTimers();
|
|
1052
|
+
|
|
1053
|
+
// createMarbleBus: a bus that records its trace; a frame is 10 ms
|
|
1054
|
+
const marbles = createMarbleBus({ clock: fakeTimersClock(vi) });
|
|
1055
|
+
marbles.bus.on("search/changed", latest(search), { traceId: "search" });
|
|
1056
|
+
|
|
1057
|
+
// "a-b": two emits, at frame 0 and frame 2
|
|
1058
|
+
const result = await marbles.run({
|
|
1059
|
+
emits: { "search/changed": { marble: "a-b" } },
|
|
1060
|
+
frames: 12,
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
// s start, c cancel, d done
|
|
1064
|
+
result.expect({ "search/changed:latest": "s-(cs)----d----" });
|
|
1065
|
+
});
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
More: [Test time with diagrams](testing.md).
|
|
1069
|
+
|
|
1035
1070
|
## Other libraries in the project
|
|
1036
1071
|
|
|
1037
1072
|
- **Redux Toolkit, Zustand, MobX or Pinia.** [Connect a store](stores.md).
|
|
@@ -1286,7 +1321,8 @@ Each row is optional. The "when" column says what need brings you to it.
|
|
|
1286
1321
|
| `nanoeffects/svelte` | `sveltePlugin`: adds `eventStore` to a bus (needs Svelte 5 or newer) | Svelte components |
|
|
1287
1322
|
| `nanoeffects/vue` | `vuePlugin`: adds `watchEmitter` and `useEventRef` to a bus (needs Vue 3.3 or newer) | Vue components |
|
|
1288
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 |
|
|
1289
|
-
| `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
|
+
| `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 |
|
|
1290
1326
|
| `nanoeffects/guard` | `loopGuardPlugin`: refuses the emit that would pass a limit of a chain | a loop must not take the app down |
|
|
1291
1327
|
|
|
1292
1328
|
The `...Plugin` names above are used with `bus.use`. [Plugins](plugins.md) lists all of them, what each returns, and the order to use them in.
|
|
@@ -7317,7 +7353,7 @@ The subscription is not tied to a task. Call `subscription.unsubscribe()` when t
|
|
|
7317
7353
|
- **Abort replaces unsubscribe for work in flight.** A stopped task is stopped through its `signal`. Pass it to `fetch` or to the client.
|
|
7318
7354
|
- **Errors are not part of the stream.** A failed task rejects its own promise. Without `onError` on the bus nobody else hears it ([Errors](errors.md)).
|
|
7319
7355
|
- **No completion.** An event bus does not end. A task ends when its work ends or its signal aborts.
|
|
7320
|
-
- **
|
|
7356
|
+
- **Marble tests are a smaller tool.** [`createMarbleBus`](testing.md) writes the emits and the phases of tasks as diagrams on fake timers. It has no hot or cold sources and no `|`.
|
|
7321
7357
|
|
|
7322
7358
|
---
|
|
7323
7359
|
Source: docs/semantics.md
|
|
@@ -7695,6 +7731,102 @@ test("a newer search stops the older one at every step", async () => {
|
|
|
7695
7731
|
- **A handler that never emits.** `devPlugin` sees a lost signal by an emit after the cancel; a handler that only writes elsewhere is seen by the linter alone.
|
|
7696
7732
|
- **Shadowed names.** The rule uses no scope analysis, so an inner variable named `signal` can hide a lost signal.
|
|
7697
7733
|
|
|
7734
|
+
---
|
|
7735
|
+
Source: docs/testing.md
|
|
7736
|
+
|
|
7737
|
+
# Test time with diagrams
|
|
7738
|
+
|
|
7739
|
+
> **Level B.** Read first: [Policies](policies.md).
|
|
7740
|
+
|
|
7741
|
+
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.
|
|
7742
|
+
|
|
7743
|
+
You need a test runner with fake timers (Vitest or Jest). The helper does not import one.
|
|
7744
|
+
|
|
7745
|
+
## Test a policy with a diagram
|
|
7746
|
+
|
|
7747
|
+
**Problem.** You want a test that says: a second search cancels the first one, and only the second finishes.
|
|
7748
|
+
|
|
7749
|
+
**When.** A policy or an effect decides what runs when, and the answer depends on the time between events.
|
|
7750
|
+
|
|
7751
|
+
1. Create a bus with `createMarbleBus`. It records the trace of its tasks and gives you `run`.
|
|
7752
|
+
2. Register the handler on `marbles.bus`, as you do in your app.
|
|
7753
|
+
3. Call `run` with a diagram of the emits. One character is one frame (10 ms by default).
|
|
7754
|
+
4. Call `expect` with a diagram of the phases that you expect.
|
|
7755
|
+
|
|
7756
|
+
```js
|
|
7757
|
+
// search.test.js
|
|
7758
|
+
import { it, vi } from "vitest";
|
|
7759
|
+
import { latest } from "nanoeffects/policies";
|
|
7760
|
+
import { createMarbleBus, fakeTimersClock } from "nanoeffects/testing";
|
|
7761
|
+
|
|
7762
|
+
// a search that takes 50 ms
|
|
7763
|
+
function search(query) {
|
|
7764
|
+
return new Promise((resolve) => setTimeout(resolve, 50));
|
|
7765
|
+
}
|
|
7766
|
+
|
|
7767
|
+
it("latest cancels the search that is running", async () => {
|
|
7768
|
+
vi.useFakeTimers();
|
|
7769
|
+
|
|
7770
|
+
// createMarbleBus: a bus that records its trace, and a run that moves time frame by frame
|
|
7771
|
+
const marbles = createMarbleBus({ clock: fakeTimersClock(vi) });
|
|
7772
|
+
|
|
7773
|
+
// latest: a new search cancels the one that is running
|
|
7774
|
+
marbles.bus.on("search/changed", latest(search), { traceId: "search" });
|
|
7775
|
+
|
|
7776
|
+
// "a-b": emit "x" at frame 0 and "xy" at frame 2
|
|
7777
|
+
const result = await marbles.run({
|
|
7778
|
+
emits: { "search/changed": { marble: "a-b", values: { a: "x", b: "xy" } } },
|
|
7779
|
+
frames: 12,
|
|
7780
|
+
});
|
|
7781
|
+
|
|
7782
|
+
// s start, c cancel, d done: at frame 2 the second search cancels the first, and it is done at frame 7
|
|
7783
|
+
result.expect({ "search/changed:latest": "s-(cs)----d----" });
|
|
7784
|
+
});
|
|
7785
|
+
```
|
|
7786
|
+
|
|
7787
|
+
When a row differs, `expect` throws an error with both diagrams one under the other:
|
|
7788
|
+
|
|
7789
|
+
```
|
|
7790
|
+
marble row "search/changed:latest" differs
|
|
7791
|
+
expected s-(cs)----d----
|
|
7792
|
+
received s-(cs)---d-----
|
|
7793
|
+
```
|
|
7794
|
+
|
|
7795
|
+
The same test for `queue` and `debounce` is in [`examples/testing`](../examples/testing). Run it with `pnpm vitest run examples/testing`.
|
|
7796
|
+
|
|
7797
|
+
## Read the diagrams
|
|
7798
|
+
|
|
7799
|
+
A diagram has one character, or one group in parentheses, for each frame.
|
|
7800
|
+
|
|
7801
|
+
| Character | In `emits` | In the result |
|
|
7802
|
+
| --------- | ---------- | ------------- |
|
|
7803
|
+
| `-` | nothing happens | nothing happens |
|
|
7804
|
+
| 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 |
|
|
7805
|
+
| `(ab)` | two emits in one frame, in this order | two phases in one frame, in the order they happened |
|
|
7806
|
+
|
|
7807
|
+
Other characters (`|`, spaces) throw an error that names the diagram and the position.
|
|
7808
|
+
|
|
7809
|
+
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.
|
|
7810
|
+
|
|
7811
|
+
## Set the frame and the number of frames
|
|
7812
|
+
|
|
7813
|
+
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.
|
|
7814
|
+
|
|
7815
|
+
`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.
|
|
7816
|
+
|
|
7817
|
+
## Use another clock
|
|
7818
|
+
|
|
7819
|
+
`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)`.
|
|
7820
|
+
|
|
7821
|
+
Any object with `advance(ms)` and `now()` is a clock. `advance` may return a promise.
|
|
7822
|
+
|
|
7823
|
+
## Know what is not there
|
|
7824
|
+
|
|
7825
|
+
- **It needs a tracer.** The helper reads the trace of the bus, so there is no `notrace` build of `nanoeffects/testing`.
|
|
7826
|
+
- **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.
|
|
7827
|
+
- **No payloads in diagrams.** The diagram shows when, not what. Check the values with your test runner as usual.
|
|
7828
|
+
- **Not the RxJS alphabet.** There are no hot or cold sources and no `|` for the end of a stream.
|
|
7829
|
+
|
|
7698
7830
|
---
|
|
7699
7831
|
Source: docs/tracing.md
|
|
7700
7832
|
|
|
@@ -7713,15 +7845,20 @@ Tracing is optional. A bus works the same without it, and it costs almost nothin
|
|
|
7713
7845
|
- [Show which subscription began a chain](#show-which-subscription-began-a-chain)
|
|
7714
7846
|
- [Follow one flow with a traceGroupId](#follow-one-flow-with-a-tracegroupid)
|
|
7715
7847
|
- [Name a listener](#name-a-listener)
|
|
7848
|
+
- [List who listens](#list-who-listens)
|
|
7849
|
+
- [Find the line of a listener](#find-the-line-of-a-listener)
|
|
7716
7850
|
- [Keep the chain: `ctx.emit`, not `bus.emit`](#keep-the-chain-ctxemit-not-busemit)
|
|
7717
7851
|
- [Turn the names off](#turn-the-names-off)
|
|
7718
7852
|
- [Use built-in loggers](#use-built-in-loggers)
|
|
7719
7853
|
- [Cut volume in production](#cut-volume-in-production)
|
|
7720
7854
|
- [Keep the last events for an incident](#keep-the-last-events-for-an-incident)
|
|
7855
|
+
- [Measure time and find open tasks](#measure-time-and-find-open-tasks)
|
|
7856
|
+
- [Find the cause of an error](#find-the-cause-of-an-error)
|
|
7721
7857
|
- [Configure Redux DevTools](#configure-redux-devtools)
|
|
7722
7858
|
- [Find loops with `graphLogger`](#find-loops-with-graphlogger)
|
|
7723
7859
|
- [Send safe data to a tool](#send-safe-data-to-a-tool)
|
|
7724
7860
|
- [Write your own logger](#write-your-own-logger)
|
|
7861
|
+
- [Stop on an event](#stop-on-an-event)
|
|
7725
7862
|
- [Drop tracing from a build](#drop-tracing-from-a-build)
|
|
7726
7863
|
|
|
7727
7864
|
## Attach a tracer
|
|
@@ -7886,7 +8023,7 @@ The emitters take it too: `useEmitter(cartChanged, cart, { traceId: "Cart", trac
|
|
|
7886
8023
|
|
|
7887
8024
|
**When.** An event has more than one listener and you read the trace to see which one did the work.
|
|
7888
8025
|
|
|
7889
|
-
|
|
8026
|
+
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`.
|
|
7890
8027
|
|
|
7891
8028
|
```js
|
|
7892
8029
|
import { createBus } from "nanoeffects";
|
|
@@ -7909,6 +8046,88 @@ bus.once("name/changed", (name) => console.log(name), { traceId: "auditLog" });
|
|
|
7909
8046
|
[nanoeffects] saveNotifier · run done
|
|
7910
8047
|
```
|
|
7911
8048
|
|
|
8049
|
+
## List who listens
|
|
8050
|
+
|
|
8051
|
+
**Problem.** To know what an emit will start, you search the code for every `bus.on` of that event.
|
|
8052
|
+
|
|
8053
|
+
**When.** You read an app that you did not write, or a test must guard the wiring of the bus.
|
|
8054
|
+
|
|
8055
|
+
`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.
|
|
8056
|
+
|
|
8057
|
+
```js
|
|
8058
|
+
import { createBus } from "nanoeffects";
|
|
8059
|
+
import { consoleLogger, createTracer, describeBus, tracePlugin } from "nanoeffects/trace";
|
|
8060
|
+
|
|
8061
|
+
function saveName(name) {
|
|
8062
|
+
console.log("save", name);
|
|
8063
|
+
}
|
|
8064
|
+
|
|
8065
|
+
function audit(name) {
|
|
8066
|
+
console.log("audit", name);
|
|
8067
|
+
}
|
|
8068
|
+
|
|
8069
|
+
const trace = createTracer(consoleLogger());
|
|
8070
|
+
|
|
8071
|
+
const bus = createBus({ trace })
|
|
8072
|
+
.use(tracePlugin());
|
|
8073
|
+
|
|
8074
|
+
bus.on("name/changed", saveName);
|
|
8075
|
+
bus.on("name/changed", audit);
|
|
8076
|
+
bus.on("saved", () => {}, { traceId: "refreshView" });
|
|
8077
|
+
|
|
8078
|
+
// describeBus: the map of events to listener names
|
|
8079
|
+
console.log(describeBus(bus));
|
|
8080
|
+
```
|
|
8081
|
+
|
|
8082
|
+
```
|
|
8083
|
+
{ "name/changed": ["saveName", "audit"], saved: ["refreshView"] }
|
|
8084
|
+
```
|
|
8085
|
+
|
|
8086
|
+
## Find the line of a listener
|
|
8087
|
+
|
|
8088
|
+
**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.
|
|
8089
|
+
|
|
8090
|
+
**When.** You debug in development and want to jump from a log line to the code.
|
|
8091
|
+
|
|
8092
|
+
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.
|
|
8093
|
+
|
|
8094
|
+
```js
|
|
8095
|
+
import { createBus } from "nanoeffects";
|
|
8096
|
+
import { consoleLogger, createTracer, describeBus, tracePlugin } from "nanoeffects/trace";
|
|
8097
|
+
|
|
8098
|
+
// source: print where the listener was registered
|
|
8099
|
+
const trace = createTracer(consoleLogger({ source: true }));
|
|
8100
|
+
|
|
8101
|
+
// source: read the place of each bus.on, once at registration
|
|
8102
|
+
const bus = createBus({ trace })
|
|
8103
|
+
.use(tracePlugin({ source: true }));
|
|
8104
|
+
|
|
8105
|
+
function saveName(name) {
|
|
8106
|
+
console.log("save", name);
|
|
8107
|
+
}
|
|
8108
|
+
|
|
8109
|
+
bus.on("name/changed", saveName);
|
|
8110
|
+
bus.on("name/changed", (name) => console.log("audit", name));
|
|
8111
|
+
|
|
8112
|
+
bus.emit("name/changed", "Eva");
|
|
8113
|
+
|
|
8114
|
+
// describeBus: the same places, without running anything
|
|
8115
|
+
console.log(describeBus(bus, { source: true }));
|
|
8116
|
+
```
|
|
8117
|
+
|
|
8118
|
+
```
|
|
8119
|
+
[nanoeffects] saveName · run start (file:///app/src/user.js:13:5)
|
|
8120
|
+
[nanoeffects] name/changed#1 · run start (file:///app/src/user.js:14:5)
|
|
8121
|
+
{ "name/changed": ["saveName (file:///app/src/user.js:13:5)", "<anonymous> (file:///app/src/user.js:14:5)"] }
|
|
8122
|
+
```
|
|
8123
|
+
|
|
8124
|
+
Know the limits:
|
|
8125
|
+
|
|
8126
|
+
- **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.
|
|
8127
|
+
- **`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.
|
|
8128
|
+
- **No stack**, as with `Error.stackTraceLimit = 0`, gives no `source`. The listener runs as usual.
|
|
8129
|
+
- **The default is off.** Reading a stack costs a little for each registration, so turn it on in development.
|
|
8130
|
+
|
|
7912
8131
|
## Keep the chain: `ctx.emit`, not `bus.emit`
|
|
7913
8132
|
|
|
7914
8133
|
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.
|
|
@@ -7951,7 +8170,7 @@ const bus = createBus({
|
|
|
7951
8170
|
| `consoleLogger()` | Prints one line per phase, indented by task depth |
|
|
7952
8171
|
| `performanceLogger()` | User Timing measures for the browser Performance panel |
|
|
7953
8172
|
| `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)) |
|
|
7954
|
-
| `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)) |
|
|
8173
|
+
| `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 |
|
|
7955
8174
|
|
|
7956
8175
|
### Cut volume in production
|
|
7957
8176
|
|
|
@@ -8011,12 +8230,91 @@ window.addEventListener("error", () => {
|
|
|
8011
8230
|
What to know before you send the events:
|
|
8012
8231
|
|
|
8013
8232
|
- **`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.
|
|
8014
|
-
- **There is no time on an event.** You get the order, not the duration.
|
|
8233
|
+
- **There is no time on an event.** You get the order, not the duration. [`timingLogger`](#measure-time-and-find-open-tasks) measures time.
|
|
8015
8234
|
- **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.
|
|
8016
8235
|
- **`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.
|
|
8017
8236
|
- **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).
|
|
8018
8237
|
- **A `capacity` that is not a positive integer** (`0`, a negative number, `NaN`) means no limit.
|
|
8019
8238
|
|
|
8239
|
+
### Measure time and find open tasks
|
|
8240
|
+
|
|
8241
|
+
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.
|
|
8242
|
+
|
|
8243
|
+
Pass it to the tracer next to the recorder: `createTracer` takes an array of loggers.
|
|
8244
|
+
|
|
8245
|
+
<!-- docs-check: new -->
|
|
8246
|
+
|
|
8247
|
+
```js
|
|
8248
|
+
// bus.js
|
|
8249
|
+
import { createBus } from "nanoeffects";
|
|
8250
|
+
import { createTraceRecorder, createTracer, timingLogger } from "nanoeffects/trace";
|
|
8251
|
+
|
|
8252
|
+
// createTraceRecorder: events in the order they came
|
|
8253
|
+
export const recorder = createTraceRecorder();
|
|
8254
|
+
|
|
8255
|
+
// timingLogger: the time of each event and the duration of tasks
|
|
8256
|
+
export const timing = timingLogger();
|
|
8257
|
+
|
|
8258
|
+
// createTracer: both loggers get every event
|
|
8259
|
+
const trace = createTracer([recorder, timing]);
|
|
8260
|
+
|
|
8261
|
+
export const bus = createBus({ trace });
|
|
8262
|
+
```
|
|
8263
|
+
|
|
8264
|
+
```js
|
|
8265
|
+
// check.js
|
|
8266
|
+
import { timing } from "./bus";
|
|
8267
|
+
|
|
8268
|
+
// durations: the tasks that ended, with the closing phase and the time in ms
|
|
8269
|
+
const slow = timing.durations.filter((task) => task.ms > 500);
|
|
8270
|
+
console.log(slow);
|
|
8271
|
+
|
|
8272
|
+
// pending: the tasks that started and have not ended
|
|
8273
|
+
const leftovers = timing.pending();
|
|
8274
|
+
if (leftovers.length > 0) {
|
|
8275
|
+
throw new Error(`${leftovers.length} tasks are still open`);
|
|
8276
|
+
}
|
|
8277
|
+
```
|
|
8278
|
+
|
|
8279
|
+
What to know:
|
|
8280
|
+
|
|
8281
|
+
- **The clock.** Time comes from `performance.now()`, or `Date.now()` where there is none. In a test pass your own clock: `timingLogger({ now })`.
|
|
8282
|
+
- **`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.
|
|
8283
|
+
- **`clear()`** empties both lists. Call it between tests.
|
|
8284
|
+
- **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.
|
|
8285
|
+
|
|
8286
|
+
### Find the cause of an error
|
|
8287
|
+
|
|
8288
|
+
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.
|
|
8289
|
+
|
|
8290
|
+
```js
|
|
8291
|
+
// why.js
|
|
8292
|
+
import { chainOf } from "nanoeffects/trace";
|
|
8293
|
+
import { recorder } from "./bus";
|
|
8294
|
+
|
|
8295
|
+
const failed = recorder.events.find((event) => event.phase === "error");
|
|
8296
|
+
|
|
8297
|
+
// chainOf: the path from the root to the failed task
|
|
8298
|
+
const path = chainOf(recorder, failed.id);
|
|
8299
|
+
|
|
8300
|
+
const text = path.map((step) => step.type).join(" → ");
|
|
8301
|
+
console.log(text); // queryChanged → searchLoaded
|
|
8302
|
+
```
|
|
8303
|
+
|
|
8304
|
+
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.
|
|
8305
|
+
|
|
8306
|
+
To see the path right in the console, turn on `chain` in `consoleLogger`:
|
|
8307
|
+
|
|
8308
|
+
```js
|
|
8309
|
+
import { consoleLogger, createTracer } from "nanoeffects/trace";
|
|
8310
|
+
|
|
8311
|
+
// consoleLogger: the line of an error ends with the path from the root
|
|
8312
|
+
const trace = createTracer(consoleLogger({ chain: true }));
|
|
8313
|
+
// [nanoeffects] render · run error (queryChanged → render)
|
|
8314
|
+
```
|
|
8315
|
+
|
|
8316
|
+
`chain` remembers the last 1000 tasks. It is off by default.
|
|
8317
|
+
|
|
8020
8318
|
### Configure Redux DevTools
|
|
8021
8319
|
|
|
8022
8320
|
<!-- docs-check: new -->
|
|
@@ -8148,6 +8446,26 @@ const errorsOnly = (): TraceLogger => (event) => {
|
|
|
8148
8446
|
};
|
|
8149
8447
|
```
|
|
8150
8448
|
|
|
8449
|
+
### Stop on an event
|
|
8450
|
+
|
|
8451
|
+
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.
|
|
8452
|
+
|
|
8453
|
+
```js
|
|
8454
|
+
import { createTracer } from "nanoeffects/trace";
|
|
8455
|
+
|
|
8456
|
+
// A logger that stops in the debugger at the start of a task with this type
|
|
8457
|
+
const stopOn = (type) => (event) => {
|
|
8458
|
+
if (event.type === type && event.phase === "start") {
|
|
8459
|
+
debugger; // the browser stops here when DevTools is open
|
|
8460
|
+
}
|
|
8461
|
+
};
|
|
8462
|
+
|
|
8463
|
+
// createTracer: pass the logger like any other
|
|
8464
|
+
const trace = createTracer(stopOn("search"));
|
|
8465
|
+
```
|
|
8466
|
+
|
|
8467
|
+
In the debugger read `event.payload` and walk up the stack: the synchronous part of the chain is all there.
|
|
8468
|
+
|
|
8151
8469
|
## Drop tracing from a build
|
|
8152
8470
|
|
|
8153
8471
|
Without a tracer the hooks cost almost nothing. `nanoeffects/trace` is only bundled when imported.
|
|
@@ -8165,6 +8483,132 @@ conditions: ["notrace"]
|
|
|
8165
8483
|
|
|
8166
8484
|
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.
|
|
8167
8485
|
|
|
8486
|
+
---
|
|
8487
|
+
Source: docs/debugging-cases.md
|
|
8488
|
+
|
|
8489
|
+
# Debugging cases
|
|
8490
|
+
|
|
8491
|
+
> **Level F.** Read first: [Trace tasks](tracing.md#attach-a-tracer).
|
|
8492
|
+
|
|
8493
|
+
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).
|
|
8494
|
+
|
|
8495
|
+
## On this page
|
|
8496
|
+
|
|
8497
|
+
- [Find who listens to an event](#find-who-listens-to-an-event)
|
|
8498
|
+
- [Find which task began a chain](#find-which-task-began-a-chain)
|
|
8499
|
+
- [Find out why a request stopped](#find-out-why-a-request-stopped)
|
|
8500
|
+
- [Know what is missing](#know-what-is-missing)
|
|
8501
|
+
|
|
8502
|
+
## Find who listens to an event
|
|
8503
|
+
|
|
8504
|
+
**Problem.** You read an emit and want to know what it starts.
|
|
8505
|
+
|
|
8506
|
+
Name the listeners with functions or a `traceId`, use `tracePlugin`, and call `describeBus`:
|
|
8507
|
+
|
|
8508
|
+
```js
|
|
8509
|
+
import { createBus } from "nanoeffects";
|
|
8510
|
+
import { consoleLogger, createTracer, describeBus, tracePlugin } from "nanoeffects/trace";
|
|
8511
|
+
|
|
8512
|
+
function saveName(name) {
|
|
8513
|
+
console.log("save", name);
|
|
8514
|
+
}
|
|
8515
|
+
|
|
8516
|
+
function audit(name) {
|
|
8517
|
+
console.log("audit", name);
|
|
8518
|
+
}
|
|
8519
|
+
|
|
8520
|
+
const trace = createTracer(consoleLogger());
|
|
8521
|
+
|
|
8522
|
+
const bus = createBus({ trace })
|
|
8523
|
+
.use(tracePlugin());
|
|
8524
|
+
|
|
8525
|
+
bus.on("name/changed", saveName);
|
|
8526
|
+
bus.on("name/changed", audit);
|
|
8527
|
+
|
|
8528
|
+
// describeBus: the listeners of each event, in the order they run
|
|
8529
|
+
console.log(describeBus(bus));
|
|
8530
|
+
```
|
|
8531
|
+
|
|
8532
|
+
```
|
|
8533
|
+
{ "name/changed": ["saveName", "audit"] }
|
|
8534
|
+
```
|
|
8535
|
+
|
|
8536
|
+
Details: [List who listens](tracing.md#list-who-listens).
|
|
8537
|
+
|
|
8538
|
+
## Find which task began a chain
|
|
8539
|
+
|
|
8540
|
+
**Problem.** A request ran and you do not know what asked for it.
|
|
8541
|
+
|
|
8542
|
+
Every task of a trace has a `rootId` and a `parentId`. The recorder keeps them, so you can walk up from the request to the emit that began it:
|
|
8543
|
+
|
|
8544
|
+
```js
|
|
8545
|
+
import { createBus } from "nanoeffects";
|
|
8546
|
+
import { createTraceRecorder, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
8547
|
+
|
|
8548
|
+
// createTraceRecorder: keeps the last events in memory
|
|
8549
|
+
const recorder = createTraceRecorder();
|
|
8550
|
+
const trace = createTracer(recorder);
|
|
8551
|
+
|
|
8552
|
+
const bus = createBus({ trace })
|
|
8553
|
+
.use(tracePlugin());
|
|
8554
|
+
|
|
8555
|
+
function loadUser(id, ctx) {
|
|
8556
|
+
ctx.emit("user/loading", id);
|
|
8557
|
+
}
|
|
8558
|
+
|
|
8559
|
+
bus.on("user/open", loadUser);
|
|
8560
|
+
bus.emit("user/open", 7);
|
|
8561
|
+
|
|
8562
|
+
// each event says which task is its parent
|
|
8563
|
+
for (const event of recorder.events) {
|
|
8564
|
+
console.log(event.id, event.parentId, event.type, event.phase);
|
|
8565
|
+
}
|
|
8566
|
+
```
|
|
8567
|
+
|
|
8568
|
+
Details: [Show which subscription began a chain](tracing.md#show-which-subscription-began-a-chain) and [Keep the chain: `ctx.emit`, not `bus.emit`](tracing.md#keep-the-chain-ctxemit-not-busemit).
|
|
8569
|
+
|
|
8570
|
+
## Find out why a request stopped
|
|
8571
|
+
|
|
8572
|
+
**Problem.** A search request never finished and no error appeared.
|
|
8573
|
+
|
|
8574
|
+
A policy that stops a call reports `cancel` or `drop` in the trace. Pick the call and read its last phase:
|
|
8575
|
+
|
|
8576
|
+
```js
|
|
8577
|
+
import { createBus } from "nanoeffects";
|
|
8578
|
+
import { latest } from "nanoeffects/policies";
|
|
8579
|
+
import { createTraceRecorder, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
8580
|
+
|
|
8581
|
+
const recorder = createTraceRecorder();
|
|
8582
|
+
const trace = createTracer(recorder);
|
|
8583
|
+
|
|
8584
|
+
const bus = createBus({ trace })
|
|
8585
|
+
.use(tracePlugin());
|
|
8586
|
+
|
|
8587
|
+
// latest: a new search stops the one that is still running
|
|
8588
|
+
const search = latest(async (query, ctx) => {
|
|
8589
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
8590
|
+
ctx.emit("search/loaded", query);
|
|
8591
|
+
});
|
|
8592
|
+
|
|
8593
|
+
bus.on("search/changed", search, { traceId: "search" });
|
|
8594
|
+
bus.emit("search/changed", "a");
|
|
8595
|
+
bus.emit("search/changed", "ab");
|
|
8596
|
+
|
|
8597
|
+
// a "cancel" phase on the first call says that the second one stopped it
|
|
8598
|
+
const phases = recorder.events.map((event) => `${event.type}:${event.phase}`);
|
|
8599
|
+
console.log(phases);
|
|
8600
|
+
```
|
|
8601
|
+
|
|
8602
|
+
Details: [Trace tasks](tracing.md) and [Cancel stale requests](cancel-stale-requests.md).
|
|
8603
|
+
|
|
8604
|
+
## Know what is missing
|
|
8605
|
+
|
|
8606
|
+
- **No browser extension or UI of its own.** Use [Redux DevTools](tracing.md#configure-redux-devtools), the console or the Performance panel.
|
|
8607
|
+
- **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.
|
|
8608
|
+
- **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.
|
|
8609
|
+
- **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.
|
|
8610
|
+
- **No replay and no time travel.** The recorder keeps what happened and cannot run it again.
|
|
8611
|
+
|
|
8168
8612
|
---
|
|
8169
8613
|
Source: docs/debugging-loops.md
|
|
8170
8614
|
|
|
@@ -8186,6 +8630,7 @@ A loop is two handlers that emit each other's events, or one handler that emits
|
|
|
8186
8630
|
- [Cut a loop at run time](#cut-a-loop-at-run-time)
|
|
8187
8631
|
- [Fail a test on a loop](#fail-a-test-on-a-loop)
|
|
8188
8632
|
- [Export the graph and the payloads](#export-the-graph-and-the-payloads)
|
|
8633
|
+
- [Find listeners that never ran](#find-listeners-that-never-ran)
|
|
8189
8634
|
- [Know how a loop is found](#know-how-a-loop-is-found)
|
|
8190
8635
|
- [Know the limits](#know-the-limits)
|
|
8191
8636
|
|
|
@@ -8448,6 +8893,84 @@ console.log(JSON.stringify(data));
|
|
|
8448
8893
|
|
|
8449
8894
|
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.
|
|
8450
8895
|
|
|
8896
|
+
## Find listeners that never ran
|
|
8897
|
+
|
|
8898
|
+
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.
|
|
8899
|
+
|
|
8900
|
+
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.
|
|
8901
|
+
|
|
8902
|
+
1. Each run writes its graph to a file.
|
|
8903
|
+
2. A script reads the files and joins them with `mergeGraphs`.
|
|
8904
|
+
3. `findOrphans` compares the joined graph with `describeBus`.
|
|
8905
|
+
|
|
8906
|
+
```js
|
|
8907
|
+
// bus.js
|
|
8908
|
+
import { createBus } from "nanoeffects";
|
|
8909
|
+
import { createTracer, graphLogger, tracePlugin } from "nanoeffects/trace";
|
|
8910
|
+
|
|
8911
|
+
// graphLogger — the map of what ran
|
|
8912
|
+
export const graph = graphLogger({ name: "cart" });
|
|
8913
|
+
export const bus = createBus({ trace: createTracer(graph) }).use(tracePlugin());
|
|
8914
|
+
```
|
|
8915
|
+
|
|
8916
|
+
```js
|
|
8917
|
+
// handlers.js
|
|
8918
|
+
import { bus } from "./bus";
|
|
8919
|
+
|
|
8920
|
+
// the listener is named by its function; findOrphans matches this name
|
|
8921
|
+
bus.on("cart/added", function recalcTotal() {});
|
|
8922
|
+
bus.on("cart/cleared", function resetTotal() {});
|
|
8923
|
+
```
|
|
8924
|
+
|
|
8925
|
+
```js
|
|
8926
|
+
// graph-setup.js (the setup file of your test runner)
|
|
8927
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
8928
|
+
import { afterAll } from "vitest";
|
|
8929
|
+
import { graph } from "./bus";
|
|
8930
|
+
|
|
8931
|
+
afterAll(() => {
|
|
8932
|
+
mkdirSync("graphs", { recursive: true });
|
|
8933
|
+
|
|
8934
|
+
// toJSON — the graph of this run as plain data
|
|
8935
|
+
const data = graph.toJSON();
|
|
8936
|
+
writeFileSync(`graphs/${process.pid}.json`, JSON.stringify(data));
|
|
8937
|
+
});
|
|
8938
|
+
```
|
|
8939
|
+
|
|
8940
|
+
```js
|
|
8941
|
+
// orphans.js (run it after the tests)
|
|
8942
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
8943
|
+
import { describeBus, findOrphans, mergeGraphs } from "nanoeffects/trace";
|
|
8944
|
+
import { bus } from "./bus";
|
|
8945
|
+
import "./handlers";
|
|
8946
|
+
|
|
8947
|
+
const files = readdirSync("graphs");
|
|
8948
|
+
const runs = files.map((file) => JSON.parse(readFileSync(`graphs/${file}`, "utf8")));
|
|
8949
|
+
|
|
8950
|
+
// mergeGraphs — counts add up, so a listener that ran in any run is a node
|
|
8951
|
+
const all = mergeGraphs(...runs);
|
|
8952
|
+
|
|
8953
|
+
// describeBus — the listeners the bus holds, by event
|
|
8954
|
+
const described = describeBus(bus);
|
|
8955
|
+
|
|
8956
|
+
// findOrphans — the listeners the joined graph never saw run
|
|
8957
|
+
console.log(findOrphans(described, all));
|
|
8958
|
+
// [{ event: "cart/cleared", listener: "resetTotal", kind: "unemitted" }]
|
|
8959
|
+
```
|
|
8960
|
+
|
|
8961
|
+
Each item has a `kind`:
|
|
8962
|
+
|
|
8963
|
+
| `kind` | Meaning | What to check |
|
|
8964
|
+
| ------ | ------- | ------------- |
|
|
8965
|
+
| `unemitted` | Nothing emitted the event in any run. | Is the event still used? Is a scenario missing? |
|
|
8966
|
+
| `unrun` | The event was emitted, this listener did not run. | A condition or a [policy](policies.md) that never lets it run. |
|
|
8967
|
+
|
|
8968
|
+
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:
|
|
8969
|
+
|
|
8970
|
+
- Two listeners with one name are one node, so the second one is never reported. Give each a distinct name or `traceId`.
|
|
8971
|
+
- Merge runs of one build. A listener that was renamed shows up as an orphan.
|
|
8972
|
+
- `mergeGraphs` does not find a loop that only closes when the edges of two runs are joined. Use `graph.cycles()` in each run.
|
|
8973
|
+
|
|
8451
8974
|
## Know how a loop is found
|
|
8452
8975
|
|
|
8453
8976
|
`graphLogger` finds loops in two ways. They give two kinds of finding in `graph.cycles()`, told apart by `source`:
|
package/llms.txt
CHANGED
|
@@ -60,8 +60,10 @@ Rules that generated code often gets wrong:
|
|
|
60
60
|
|
|
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
|
-
- [
|
|
64
|
-
- [
|
|
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, `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
|
|
65
67
|
- [Cut a loop with loopGuardPlugin](docs/loop-guard.md): limits on repeats, depth and emits from outside
|
|
66
68
|
- [Turn events into Observables](docs/observable.md): `createObserve`, `Subject`, `events` (an event as a `for await` loop)
|
|
67
69
|
- [Hot reload and development diagnostics](docs/hmr.md): `devPlugin`
|