nanoeffects 0.32.0 → 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/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/{debugging-compared.md → debugging-cases.md} +3 -17
- package/docs/debugging-loops.md +79 -0
- package/docs/package.md +1 -1
- package/docs/ru/{debugging-compared.md → debugging-cases.md} +3 -17
- package/docs/ru/debugging-loops.md +79 -0
- package/docs/ru/package.md +1 -1
- package/docs/ru/tracing.md +106 -4
- package/docs/tracing.md +106 -4
- package/llms-full.txt +190 -23
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/dist/tracer-mMDm5yE7.js +0 -2
package/dist/testing.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as e}from"./bus-Cmk4U1hQ.js";import{n as t,t as n}from"./tracer-
|
|
1
|
+
import{t as e}from"./bus-Cmk4U1hQ.js";import{n as t,t as n}from"./tracer-v7m-Qah_.js";const r=e=>({advance:t=>e.advanceTimersByTimeAsync(t).then(()=>{}),now:()=>Date.now()}),i={start:`s`,done:`d`,error:`e`,cancel:`c`,drop:`x`},a=new Set(Object.values(i)),o=e=>i[e],s=(e,t,n)=>{throw Error(`marble "${e}": ${n} at position ${t}`)},c=(e,t,n)=>{for(let r=0;r<n.length;r+=1){let i=n.charAt(r);(/\s/.test(i)||i===`|`)&&s(e,t+r,`"${i.trim()||`space`}" is not part of a diagram`)}},l=(e,t)=>{let n=e.indexOf(`)`,t);n<0&&s(e,t,`a group that is not closed with ")"`);let r=e.slice(t+1,n);r===``&&s(e,t,`an empty group`);let i=r.indexOf(`(`);i>=0&&s(e,t+1+i,`a group inside a group`);let a=r.indexOf(`-`);return a>=0&&s(e,t+1+a,`"-" inside a group`),c(e,t+1,r),{frame:[...r],next:n+1}},u=(e,t)=>{let n=e.charAt(t);return n===`(`?l(e,t):(n===`)`&&s(e,t,`")" without "("`),c(e,t,n),{frame:n===`-`?[]:[n],next:t+1})},d=e=>{let t=[],n=0;for(;n<e.length;){let{frame:r,next:i}=u(e,n);t.push(r),n=i}return t},f=e=>{let t=d(e),n=0;for(let r of t){for(let t of r)a.has(t)||s(e,n,`"${t}" is not a phase (s d e c x)`);n+=r.length||1}return t},p=(e,t)=>{let n=``;for(let r=0;r<t;r+=1){let t=e[r]??[];t.length===0?n+=`-`:t.length===1?n+=t[0]:n+=`(${t.join(``)})`}return n},m=(e,t)=>e&&Object.hasOwn(e,t)?e[t]:t,h=(e,t,n,r,i)=>{let a=Array.from({length:i},()=>[]);for(let o of e){let e=Math.floor((o.at-n)/r);o.key===t&&e>=0&&e<i&&a[e]?.push(o.letter)}return a},g=(e,t)=>t.includes(`:`)?e.includes(t)?[t]:[]:e.filter(e=>e.endsWith(`:${t}`)),_=(e,t,n)=>`marble row "${e}" differs\n expected ${t}\n received ${n}`,v=(e,t,n,r)=>{let i=[...new Set(e.map(e=>e.key))],a=()=>Object.fromEntries(i.map(i=>[i,p(h(e,i,t,n,r),r)])),o={frames:r,rows:a,expect:e=>{let t=a();for(let[n,a]of Object.entries(e)){let e=g(i,n),[o]=e;if(o===void 0)throw Error(`marble row "${n}" matches no task; rows: ${i.join(`, `)||`none`}`);if(e.length>1)throw Error(`marble row "${n}" matches more than one task (${e.join(`, `)}): give the full key`);let s=p(f(a),r);if(s!==t[o])throw Error(_(o,s,t[o]??``))}return o}};return o},y=r=>{let{clock:i,frame:a=10,...s}=r,c=[],l=e=>{e.effect!==`emit`&&c.push({key:`${e.type}:${e.effect}`,letter:o(e.phase),at:i.now()})},u=e({...s,trace:n(l)});return u.use(t()),{bus:u,run:async({emits:e={},frames:t}={})=>{let n=Object.entries(e).map(([e,t])=>({event:e,frames:d(t.marble),values:t.values})),r=t??Math.max(0,...n.map(e=>e.frames.length));c.length=0;let o=i.now();for(let e=0;e<r;e+=1){for(let t of n)for(let n of t.frames[e]??[])Reflect.apply(u.emit,u,[t.event,m(t.values,n)]);await i.advance(a)}return v(c,o,a,r)}}};export{y as createMarbleBus,r as fakeTimersClock};
|
package/dist/trace.d.ts
CHANGED
|
@@ -136,6 +136,141 @@ type GraphLogger = TraceLogger & {
|
|
|
136
136
|
*/
|
|
137
137
|
export declare const graphLogger: (options?: GraphLoggerOptions) => GraphLogger;
|
|
138
138
|
//#endregion
|
|
139
|
+
//#region src/graph/merge.d.ts
|
|
140
|
+
/**
|
|
141
|
+
* Joins the graphs of several runs (tests, e2e, a manual session) into one, so a
|
|
142
|
+
* listener or an edge that no single run reached can still be seen by another.
|
|
143
|
+
* Counts add up, nodes and edges are the union, and a cycle found twice is one
|
|
144
|
+
* cycle. The snapshots you pass are not changed.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```ts
|
|
148
|
+
* import { mergeGraphs } from "nanoeffects/trace";
|
|
149
|
+
*
|
|
150
|
+
* // each run wrote `graph.toJSON()` to a file
|
|
151
|
+
* const all = mergeGraphs(unitRun, e2eRun);
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
export declare const mergeGraphs: (...snapshots: GraphSnapshot[]) => GraphSnapshot;
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/graph/orphans.d.ts
|
|
157
|
+
/**
|
|
158
|
+
* A listener that the graph never saw run. `unemitted`: the graph has no node for
|
|
159
|
+
* the event, so nobody emitted it. `unrun`: the event was emitted, this listener
|
|
160
|
+
* did not run.
|
|
161
|
+
*/
|
|
162
|
+
type Orphan = {
|
|
163
|
+
event: string;
|
|
164
|
+
listener: string;
|
|
165
|
+
kind: "unemitted" | "unrun";
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Compares the listeners a bus has with the graph of what ran, and lists the
|
|
169
|
+
* listeners the graph never saw. Pass `describeBus(bus)` (with or without `source`) and
|
|
170
|
+
* `graph.toJSON()`, or the result of `mergeGraphs` for wider coverage. A listener
|
|
171
|
+
* is a node of the graph once it has run, by its `traceId` or function name;
|
|
172
|
+
* anonymous listeners have no node and are left out. One run shows many false
|
|
173
|
+
* orphans: read the list as a hint, not as a failure.
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```ts
|
|
177
|
+
* import { describeBus, findOrphans } from "nanoeffects/trace";
|
|
178
|
+
*
|
|
179
|
+
* const described = describeBus(bus);
|
|
180
|
+
* const orphans = findOrphans(described, graph.toJSON());
|
|
181
|
+
* // [{ event: "cart/cleared", listener: "reset", kind: "unemitted" }]
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
export declare const findOrphans: (described: Readonly<Record<string, readonly string[]>>, snapshot: GraphSnapshot) => Orphan[];
|
|
185
|
+
//#endregion
|
|
186
|
+
//#region src/trace/task-index.d.ts
|
|
187
|
+
/** What the `task` hook told about one task, also a task that reports no event. */
|
|
188
|
+
type KnownTask = {
|
|
189
|
+
type: string;
|
|
190
|
+
parentId?: number;
|
|
191
|
+
source?: string;
|
|
192
|
+
traceGroupId?: string;
|
|
193
|
+
};
|
|
194
|
+
//#endregion
|
|
195
|
+
//#region src/trace/recorder.d.ts
|
|
196
|
+
type Recorder<E = Record<string, unknown>> = TraceLogger<E> & {
|
|
197
|
+
/**
|
|
198
|
+
* The events received so far, oldest first. Without a `capacity` it is one
|
|
199
|
+
* live array; with one, each read is a new array that is a snapshot.
|
|
200
|
+
*/
|
|
201
|
+
readonly events: TraceEvent<E>[];
|
|
202
|
+
/**
|
|
203
|
+
* The tasks the tracer told about, by `id`, also the ones that report no event (a plain
|
|
204
|
+
* listener). `chainOf` reads them to keep a path whole. Cut by `capacity` like `events`.
|
|
205
|
+
*/
|
|
206
|
+
readonly tasks: ReadonlyMap<number, KnownTask>;
|
|
207
|
+
clear: () => void;
|
|
208
|
+
/**
|
|
209
|
+
* The events as plain data that `JSON.stringify` takes: the payload and the error are made safe, so a
|
|
210
|
+
* circular object, an `Error`, a `Map` or a class instance cannot break it. `JSON.stringify(recorder)` uses it.
|
|
211
|
+
*/
|
|
212
|
+
toJSON: () => JsonValue[];
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* Trace logger that keeps events in memory. With no option it keeps all of
|
|
216
|
+
* them, for tests. With `capacity` it keeps the last events, to send when
|
|
217
|
+
* something goes wrong.
|
|
218
|
+
*
|
|
219
|
+
* `payload` and `error` are kept as they are, and can hold private data: cut
|
|
220
|
+
* what you send. An event has no time, and a `parentId` can name an event that
|
|
221
|
+
* was pushed out: read it as a root.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```ts
|
|
225
|
+
* const recorder = createTraceRecorder<Events>();
|
|
226
|
+
* const bus = createBus<Events>({ trace: createTracer(recorder) });
|
|
227
|
+
*
|
|
228
|
+
* bus.emit("search", "re");
|
|
229
|
+
* bus.emit("search", "redux");
|
|
230
|
+
* recorder.events.map((e) => e.phase); // ["start", "cancel", "start", "done"]
|
|
231
|
+
*
|
|
232
|
+
* const lastEvents = createTraceRecorder<Events>({ capacity: 500 });
|
|
233
|
+
* ```
|
|
234
|
+
*/
|
|
235
|
+
export declare const createTraceRecorder: <E = Record<string, unknown>>(
|
|
236
|
+
/** `capacity`: keep the last N events (a ring buffer); not a positive integer, or left out: keep all. */
|
|
237
|
+
options?: {
|
|
238
|
+
capacity?: number;
|
|
239
|
+
}) => Recorder<E>;
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region src/trace/chain.d.ts
|
|
242
|
+
/** One task on the way from the root to the task you asked about. */
|
|
243
|
+
type ChainStep = {
|
|
244
|
+
id: number;
|
|
245
|
+
type: string;
|
|
246
|
+
/** Absent for a task that reported no event (a plain listener). */
|
|
247
|
+
effect?: EffectName;
|
|
248
|
+
/** The latest phase of the task; absent when it reported no event. */
|
|
249
|
+
phase?: Phase;
|
|
250
|
+
source?: string;
|
|
251
|
+
traceGroupId?: string;
|
|
252
|
+
};
|
|
253
|
+
type ChainSource<E> = readonly TraceEvent<E>[] | Pick<Recorder<E>, "events" | "tasks">;
|
|
254
|
+
/**
|
|
255
|
+
* The causal path of a task: the tasks that led to it, from the root, ending
|
|
256
|
+
* with the task itself. Use it to answer "what started the task that failed".
|
|
257
|
+
*
|
|
258
|
+
* Give it a recorder to keep the listeners that report no event in the path;
|
|
259
|
+
* a plain list of events (a snapshot, a file read back) shows only the tasks
|
|
260
|
+
* that have events.
|
|
261
|
+
*
|
|
262
|
+
* A parent that is not known (pushed out by a `capacity`) ends the path:
|
|
263
|
+
* it starts at the oldest task that is known. An unknown `id` gives `[]`.
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* ```ts
|
|
267
|
+
* const failed = recorder.events.find((event) => event.phase === "error");
|
|
268
|
+
* const path = chainOf(recorder, failed.id);
|
|
269
|
+
* path.map((step) => step.type).join(" → "); // "queryChanged → searchLoaded"
|
|
270
|
+
* ```
|
|
271
|
+
*/
|
|
272
|
+
export declare const chainOf: <E = Record<string, unknown>>(source: ChainSource<E>, id: number) => ChainStep[];
|
|
273
|
+
//#endregion
|
|
139
274
|
//#region src/trace/console.d.ts
|
|
140
275
|
type ConsoleLoggerOptions = {
|
|
141
276
|
/** Defaults to `globalThis.console`. */
|
|
@@ -144,6 +279,8 @@ type ConsoleLoggerOptions = {
|
|
|
144
279
|
prefix?: string;
|
|
145
280
|
/** Append where the listener was registered, to the lines of events that have it. Default `false`. */
|
|
146
281
|
source?: boolean;
|
|
282
|
+
/** Append the path from the root, with the listeners that report nothing, to the line of an `error`. Default `false`. */
|
|
283
|
+
chain?: boolean;
|
|
147
284
|
};
|
|
148
285
|
/**
|
|
149
286
|
* Trace logger that prints one line per phase, indented by task depth.
|
|
@@ -251,47 +388,6 @@ type TracePluginOptions = {
|
|
|
251
388
|
*/
|
|
252
389
|
export declare const tracePlugin: (options?: TracePluginOptions) => <E extends EventMap>(bus: Bus<E>) => Bus<E>;
|
|
253
390
|
//#endregion
|
|
254
|
-
//#region src/trace/recorder.d.ts
|
|
255
|
-
type Recorder<E = Record<string, unknown>> = TraceLogger<E> & {
|
|
256
|
-
/**
|
|
257
|
-
* The events received so far, oldest first. Without a `capacity` it is one
|
|
258
|
-
* live array; with one, each read is a new array that is a snapshot.
|
|
259
|
-
*/
|
|
260
|
-
readonly events: TraceEvent<E>[];
|
|
261
|
-
clear: () => void;
|
|
262
|
-
/**
|
|
263
|
-
* The events as plain data that `JSON.stringify` takes: the payload and the error are made safe, so a
|
|
264
|
-
* circular object, an `Error`, a `Map` or a class instance cannot break it. `JSON.stringify(recorder)` uses it.
|
|
265
|
-
*/
|
|
266
|
-
toJSON: () => JsonValue[];
|
|
267
|
-
};
|
|
268
|
-
/**
|
|
269
|
-
* Trace logger that keeps events in memory. With no option it keeps all of
|
|
270
|
-
* them, for tests. With `capacity` it keeps the last events, to send when
|
|
271
|
-
* something goes wrong.
|
|
272
|
-
*
|
|
273
|
-
* `payload` and `error` are kept as they are, and can hold private data: cut
|
|
274
|
-
* what you send. An event has no time, and a `parentId` can name an event that
|
|
275
|
-
* was pushed out: read it as a root.
|
|
276
|
-
*
|
|
277
|
-
* @example
|
|
278
|
-
* ```ts
|
|
279
|
-
* const recorder = createTraceRecorder<Events>();
|
|
280
|
-
* const bus = createBus<Events>({ trace: createTracer(recorder) });
|
|
281
|
-
*
|
|
282
|
-
* bus.emit("search", "re");
|
|
283
|
-
* bus.emit("search", "redux");
|
|
284
|
-
* recorder.events.map((e) => e.phase); // ["start", "cancel", "start", "done"]
|
|
285
|
-
*
|
|
286
|
-
* const lastEvents = createTraceRecorder<Events>({ capacity: 500 });
|
|
287
|
-
* ```
|
|
288
|
-
*/
|
|
289
|
-
export declare const createTraceRecorder: <E = Record<string, unknown>>(
|
|
290
|
-
/** `capacity`: keep the last N events (a ring buffer); not a positive integer, or left out: keep all. */
|
|
291
|
-
options?: {
|
|
292
|
-
capacity?: number;
|
|
293
|
-
}) => Recorder<E>;
|
|
294
|
-
//#endregion
|
|
295
391
|
//#region src/trace/redux.d.ts
|
|
296
392
|
type ReduxConnection = {
|
|
297
393
|
init: (state: unknown) => void;
|
|
@@ -389,6 +485,57 @@ type ReduxDevtoolsState = {
|
|
|
389
485
|
*/
|
|
390
486
|
export declare const reduxDevtools: (options?: ReduxDevtoolsOptions) => TraceLogger;
|
|
391
487
|
//#endregion
|
|
488
|
+
//#region src/trace/timing.d.ts
|
|
489
|
+
/** One task of the trace with the time it took, or the time since it started. */
|
|
490
|
+
type TimedTask = {
|
|
491
|
+
id: number;
|
|
492
|
+
type: string;
|
|
493
|
+
effect: EffectName;
|
|
494
|
+
parentId?: number;
|
|
495
|
+
rootId?: number;
|
|
496
|
+
source?: string;
|
|
497
|
+
/** Milliseconds from `start` to the closing phase, or to now for an open task. */
|
|
498
|
+
ms: number;
|
|
499
|
+
};
|
|
500
|
+
/** A task that closed: how it ended and how long it took. */
|
|
501
|
+
type ClosedTask = TimedTask & {
|
|
502
|
+
phase: Exclude<Phase, "start">;
|
|
503
|
+
};
|
|
504
|
+
type TimingLoggerOptions = {
|
|
505
|
+
/** The clock, in ms. Default `performance.now`, or `Date.now` where there is none. */
|
|
506
|
+
now?: () => number;
|
|
507
|
+
/** Keep only the last N closed tasks in `durations`. `pending()` is never cut. Not a positive integer, or left out: keep all. */
|
|
508
|
+
capacity?: number;
|
|
509
|
+
};
|
|
510
|
+
type TimingLogger = TraceLogger & {
|
|
511
|
+
/** The tasks that closed, oldest closed first. One live array. */
|
|
512
|
+
readonly durations: ClosedTask[];
|
|
513
|
+
/** The tasks that started and have not closed, oldest started first, with the time since `start`. */
|
|
514
|
+
pending: () => TimedTask[];
|
|
515
|
+
clear: () => void;
|
|
516
|
+
};
|
|
517
|
+
/**
|
|
518
|
+
* Trace logger that measures time. It stamps each event when it gets it,
|
|
519
|
+
* pairs `start` with the first `done`, `error`, `cancel` or `drop` of the same
|
|
520
|
+
* task and effect, and keeps the result. Use it to find a slow handler, to see
|
|
521
|
+
* how long a request lived before `cancel`, or to check that a test left
|
|
522
|
+
* no task open.
|
|
523
|
+
*
|
|
524
|
+
* The logger sees what the tracer lets through: a task whose `start` was
|
|
525
|
+
* filtered or not sampled never pairs, and its closing event is ignored.
|
|
526
|
+
*
|
|
527
|
+
* @example
|
|
528
|
+
* ```ts
|
|
529
|
+
* const timing = timingLogger();
|
|
530
|
+
* const bus = createBus<Events>({ trace: createTracer(timing) });
|
|
531
|
+
*
|
|
532
|
+
* bus.emit("search", "redux");
|
|
533
|
+
* timing.pending(); // [{ id: 1, type: "search", effect: "latest", ms: 0 }]
|
|
534
|
+
* timing.durations; // after it ends: [{ ..., phase: "done", ms: 120 }]
|
|
535
|
+
* ```
|
|
536
|
+
*/
|
|
537
|
+
export declare const timingLogger: (options?: TimingLoggerOptions) => TimingLogger;
|
|
538
|
+
//#endregion
|
|
392
539
|
//#region src/trace/tracer.d.ts
|
|
393
540
|
type TracerOptions = {
|
|
394
541
|
/** Drops the events it returns `false` for, before any logger sees them. */
|
|
@@ -424,4 +571,4 @@ type TracerOptions = {
|
|
|
424
571
|
*/
|
|
425
572
|
export declare const createTracer: <E = Record<string, unknown>>(trace: Trace<E>, options?: TracerOptions) => Tracer;
|
|
426
573
|
//#endregion
|
|
427
|
-
export type { ConsoleLoggerOptions, DescribeBusOptions, EffectName, ExportOptions, Finding, GraphLogger, GraphLoggerOptions, GraphSnapshot, JsonOptions, JsonValue, PayloadMode, PerformanceLoggerOptions, Phase, PolicyStage, PolicyStages, Recorder, ReduxDevtoolsOptions, ReduxDevtoolsState, ReduxExtension, SerializeOptions, TaskInfo, Trace, TraceEvent, TraceLogger, TracePluginOptions, Tracer, TracerOptions };
|
|
574
|
+
export type { ChainStep, ClosedTask, ConsoleLoggerOptions, DescribeBusOptions, EffectName, ExportOptions, Finding, GraphLogger, GraphLoggerOptions, GraphSnapshot, JsonOptions, JsonValue, Orphan, PayloadMode, PerformanceLoggerOptions, Phase, PolicyStage, PolicyStages, Recorder, ReduxDevtoolsOptions, ReduxDevtoolsState, ReduxExtension, SerializeOptions, TaskInfo, TimedTask, TimingLogger, TimingLoggerOptions, Trace, TraceEvent, TraceLogger, TracePluginOptions, Tracer, TracerOptions };
|
package/dist/trace.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{n as e}from"./site-DLvUoXs3.js";import{n as t,r as n,t as r}from"./tracer-
|
|
1
|
+
import{n as e}from"./site-DLvUoXs3.js";import{n as t,r as n,t as r}from"./tracer-v7m-Qah_.js";const i=e=>{let t=/* @__PURE__ */ new Map;for(let{from:n,to:r}of e){let e=t.get(n)??[];e.push(r),t.set(n,e),t.has(r)||t.set(r,[])}let n=/* @__PURE__ */ new Map,r=/* @__PURE__ */ new Map,i=/* @__PURE__ */ new Set,a=[],o=[],s=0,c=(e,t)=>{n.set(e,s),r.set(e,s),s+=1,a.push(e),i.add(e),t.push({name:e,next:0})},l=e=>{let n=[],r;do{if(r=a.pop(),r===void 0)break;i.delete(r),n.push(r)}while(r!==e);let[s]=n,c=n.length===1&&s!==void 0&&t.get(s)?.includes(s);(n.length>1||c)&&o.push(n.reverse())},u=(e,t)=>{r.set(e,Math.min(r.get(e)??t,t))},d=(e,r)=>{let a=t.get(r.name)?.[r.next];return a!==void 0&&(r.next+=1,n.has(a)?i.has(a)&&u(r.name,n.get(a)??0):c(a,e),!0)},f=(e,t)=>{e.pop();let i=e.at(-1);i&&u(i.name,r.get(t.name)??0),r.get(t.name)===n.get(t.name)&&l(t.name)},p=e=>{let t=[];c(e,t);for(let e=t.at(-1);e;e=t.at(-1))d(t,e)||f(t,e)};for(let e of t.keys())n.has(e)||p(e);return o},a=(e,t)=>{if(t===`prefix`){let t=e.indexOf(`/`);return t>0?e.slice(0,t):void 0}return t?.(e)},o=e=>e.replaceAll(`"`,`#quot;`).replaceAll(`<`,`#lt;`).replaceAll(`>`,`#gt;`).replaceAll(/\r?\n/g,` `),s=e=>e.replaceAll(`\\`,`\\\\`).replaceAll(`"`,`\\"`).replaceAll(/\r?\n/g,`\\n`),c=e=>new Map(e.nodes.map((e,t)=>[e.name,`n${t}`])),l=(e,t)=>{let n=/* @__PURE__ */ new Map;for(let{name:r}of e.nodes){let e=a(r,t);e!==void 0&&n.set(e,[...n.get(e)??[],r])}return n},u=(e,t={})=>{let n=c(e),r=[`flowchart LR`],i=/* @__PURE__ */ new Set;for(let[a,s]of l(e,t.group)){r.push(` subgraph g_${r.length}["${o(a)}"]`);for(let e of s)i.add(e),r.push(` ${n.get(e)}["${o(e)}"]`);r.push(` end`)}for(let{name:t}of e.nodes)i.has(t)||r.push(` ${n.get(t)}["${o(t)}"]`);let a=[];for(let[t,i]of e.edges.entries())r.push(` ${n.get(i.from)} -->|x${i.count}| ${n.get(i.to)}`),i.inCycle&&a.push(t);return a.length>0&&r.push(` linkStyle ${a.join(`,`)} stroke:red,stroke-width:2px`),r.join(`
|
|
2
2
|
`)},d=(e,t={})=>{let n=c(e),r=[`digraph events {`];e.name!==void 0&&r.push(` label="${s(e.name)}";`);let i=/* @__PURE__ */ new Set;for(let[a,o]of l(e,t.group)){r.push(` subgraph "cluster_${s(a)}" {`,` label="${s(a)}";`);for(let e of o)i.add(e),r.push(` ${n.get(e)} [label="${s(e)}"];`);r.push(` }`)}for(let{name:t}of e.nodes)i.has(t)||r.push(` ${n.get(t)} [label="${s(t)}"];`);for(let t of e.edges){let e=t.inCycle?`, color=red, penwidth=2`:``;r.push(` ${n.get(t.from)} -> ${n.get(t.to)} [label="x${t.count}"${e}];`)}return r.push(`}`),r.join(`
|
|
3
|
-
`)},f=e=>e.kind===`storm`?"an emit from a component or a timer repeats: stop it with `equals` of useEmitter, debounce, or a guard":e.kind===`runaway`?`one chain started a huge number of tasks: look for event names that are made from a counter or an id`:e.path.length<=2?`a handler emits its own event: emit a different event for the result`:`handlers feed each other: break the loop with a condition, or emit a different event for the result`,p=(t,n)=>{let r=n===void 0?`[nanoeffects]`:`[nanoeffects ${n}]`,i=t.traceGroupId===void 0?``:` (flow ${t.traceGroupId})`,a=t.kind===`cycle`?`cycle: ${t.path.join(` → `)} (repeated ${t.repeats} times, started at ${t.root})`:`${t.kind}: ${t.path.join(` → `)} (${t.repeats} in a row, started at ${t.root})`,o=[...new Set(t.path)].flatMap(t=>{let n=e(t);return n===void 0?[]:[`${t} at ${n}`]});return`${r} ${a}${i}${o.length>0?`\n ${o.join(`; `)}`:``}\n ${f(t)}`},m=(e,t)=>{if(!e)return;let n=e.connect({name:t});n.init({findings:0});let r=0;return e=>{r+=1,n.send({type:`cycle/detected`,kind:e.kind,path:e.path,repeats:e.repeats,root:e.root},{findings:r})}},h=2e3,g=e=>{let t=Object.getPrototypeOf(e),n=t===null?void 0:Reflect.get(t,`constructor`),r=typeof n==`function`?n.name:``;return typeof r==`string`&&r!==``?r:`Object`},_=e=>{let t=Object.getPrototypeOf(e);return t===null||t===Object.prototype},v=e=>typeof Reflect.get(e,`nodeType`)==`number`&&typeof Reflect.get(e,`nodeName`)==`string`,y=e=>e.length>h?`${e.slice(0,h)}…`:e,b=e=>{switch(typeof e){case`string`:return y(e);case`boolean`:return e;case`number`:return Number.isFinite(e)?e:String(e);case`bigint`:return`${e}n`;case`symbol`:return String(e);case`function`:return`[Function ${e.name||`anonymous`}]`;default:return e===null?null:void 0}},x=e=>{if(e instanceof WeakMap)return`[WeakMap]`;if(e instanceof WeakSet)return`[WeakSet]`;if(e instanceof Promise)return`[Promise]`;if(v(e))return`[DOM ${String(Reflect.get(e,`nodeName`))}]`;if(e instanceof Date)return Number.isNaN(e.getTime())?`[Invalid Date]`:e.toISOString()},S=(e,t,n)=>{if(t.budget<=0)return`[Cut]`;--t.budget;let r=b(e);return r===void 0?typeof e!=`object`||!e?`[undefined]`:C(e,t,n):r},C=(e,t,n)=>{try{let r=x(e);return r===void 0?t.path.has(e)?`[Circular]`:n>=t.maxDepth?Array.isArray(e)?`[Array]`:`[${g(e)}]`:w(e,t,n):r}catch{return`[Unserializable]`}},w=(e,t,n)=>{t.path.add(e);try{return T(e,t,n)}finally{t.path.delete(e)}},T=(e,t,n)=>e instanceof Error?E(e,t,n):Array.isArray(e)?D(e,t,n):e instanceof Map?{$map:D([...e.entries()],t,n)}:e instanceof Set?{$set:D([...e.values()],t,n)}:ArrayBuffer.isView(e)?{$class:g(e),length:Number(Reflect.get(e,`length`))}:O(e,t,n,_(e)?void 0:g(e)),E=(e,t,n)=>{let r={name:y(e.name),message:y(e.message)};return typeof e.stack==`string`&&(r.stack=y(e.stack)),`cause`in e&&(r.cause=S(e.cause,t,n+1)),r},D=(e,t,n)=>{let r=[];for(let[i,a]of e.entries()){if(t.budget<=0){r.push(`[${e.length-i} more]`);break}r.push(S(a,t,n+1))}return r},O=(e,t,n,r)=>{let i={};r!==void 0&&(i.$class=r);for(let r of Object.keys(e)){let a=Object.getOwnPropertyDescriptor(e,r);a&&(i[r]=`value`in a?S(a.value,t,n+1):`[Getter]`)}return i},k=(e,t={})=>{let n={maxDepth:t.maxDepth??6,budget:t.maxSize??1e3,path:/* @__PURE__ */ new Set};try{return S(e,n,0)}catch{return`[Unserializable]`}},A=(e,t)=>`${e}\u0000${t}`,j=e=>{if(typeof e==`function`)return e;let t=new Set(e);return e=>t.has(e)},M=e=>e.event===void 0?[e.node]:[e.event,e.node],N=e=>{let t=e.slice(0,-1),n=t.reduce((e,n,r)=>n<(t[e]??n)?r:e,0);return[...t.slice(n),...t.slice(0,n)].join(`\0`)},P=(e,t)=>{let{node:n}=t,r=[],i=e;for(;i&&i.node!==n;)r.push(i),i=i.parent;let a=r.reverse();return{path:[n,...a.flatMap(M),...M(t)],names:[n,...a.map(e=>e.node),n]}},F=(t={})=>{let n=t.repeats??3,r=t.maxSteps??5e3,a=t.storm===!1?void 0:t.storm??{count:100,perMs:1e3},o=j(t.ignore),s=t.now??(()=>globalThis.performance.now()),c=t.console??globalThis.console,l=/* @__PURE__ */ new Map,f=/* @__PURE__ */ new Map,h=/* @__PURE__ */ new Map,g=/* @__PURE__ */ new WeakMap,_=/* @__PURE__ */ new Map,v=/* @__PURE__ */ new Set,y=!1,b=!1,x,S,C=!1,w=!1,T=e=>{let t=l.get(e)??{count:0};t.count+=1,l.set(e,t)},E=(e,t)=>{let n=A(e,t),r=f.get(n)??{from:e,to:t,count:0,inCycle:!1};r.count+=1,f.set(n,r)},D=e=>{if(t.devtools&&!C){C=!0;let e=t.extension??globalThis.__REDUX_DEVTOOLS_EXTENSION__;S=m(e,t.name??`nanoeffects graph`)}S?.(e),t.onCycle?t.onCycle(e):c?.warn(p(e,t.name))},O=(e,t)=>{let n=h.get(e);if(n){n.turns+=1;return}let r=t();if(h.set(e,r),!w){w=!0;try{D(r)}catch(e){globalThis.console?.error(`[nanoeffects] graph logger report failed`,e)}finally{w=!1}}},M=(e,t)=>{let n=new Map(e.counts);n.set(t,(n.get(t)??0)+1),e.counts=n},F=(e,t)=>{let r=e.counts.get(e.node)??0;if(r<n||o(e.node))return;let{path:i,names:a}=P(e.parent,e);O(`cycle:${N(a)}`,()=>{let n={kind:`cycle`,path:i,repeats:r,turns:1,root:e.root.name,source:`chain`};return t.traceGroupId!==void 0&&(n.traceGroupId=t.traceGroupId),n})},I=e=>{e.root.steps+=1,!(e.root.steps<=r||e.root.flagged)&&(e.root.flagged=!0,O(`runaway:${e.root.name}`,()=>({kind:`runaway`,path:[e.root.name],repeats:e.root.steps,turns:1,root:e.root.name,source:`chain`})))},L=e=>{if(!a||o(e))return;let t=s(),n=(_.get(e)??[]).filter(e=>t-e<a.perMs);n.push(t),_.set(e,n),n.length<a.count?v.delete(e):v.has(e)||(v.add(e),O(`storm:${e}:${t}`,()=>({kind:`storm`,path:[e],repeats:n.length,turns:1,root:e,source:`chain`})))},R=(e,t)=>{let n=x;return n!==void 0&&n.parent===e&&n.type===t},z=(e,t,n)=>({node:e.type,parent:t,counts:t?.counts??/* @__PURE__ */ new Map,root:t?.root??{name:e.type,steps:0,flagged:!1},inner:n}),B=()=>{b||(b=!0,queueMicrotask(()=>{y||c?.warn(`[nanoeffects] graphLogger got tasks but no emit event: add bus.use(tracePlugin()) for a finer graph.`)}))},V=(e,t,n,r)=>{M(t,e.type),(e.leaf===!0||r||!y)&&(T(e.type),n?E(n.node,e.type):L(e.type)),e.leaf||o(e.type)||(I(t),F(t,e))},H=e=>{let t=e.parent?g.get(e.parent):void 0;e.leaf?(y=!0,x={parent:t,type:e.type}):B();let n=e.emitted===!1,r=!e.leaf&&y&&!n&&!R(t,e.type),i=r&&!t,a=z(e,t,n||r&&!i);g.set(e.ref,a),a.inner||V(e,a,t,i)},U=(e,t)=>{let n=new Map(e.counts),r=e.node;n.set(r,Math.max(0,(n.get(r)??1)-1)),n.set(t.type,(n.get(t.type)??0)+1),e.counts=n,e.node=t.type,e.event=t.event,T(t.type),t.event!==void 0&&E(t.event,t.type),e.inner||F(e,t)},W=e=>{let t=g.get(e.ref);t?U(t,e):H(e)},G=e=>{if(e.phase!==`done`&&e.phase!==`error`)return;let t=l.get(e.type);t&&(t.last=e.phase===`error`?{payload:e.payload,error:e.error}:{payload:e.payload})},K=()=>{let n=J(),r=/* @__PURE__ */ new Set;for(let e of n)q(e,r);let i={nodes:[...l].map(([t,n])=>{let r=e(t);return r===void 0?{name:t,count:n.count}:{name:t,count:n.count,site:r}}),edges:[...f.values()].map(e=>({...e,inCycle:r.has(A(e.from,e.to))})),cycles:n};return t.name!==void 0&&(i.name=t.name),i},q=(e,t)=>{if(e.source===`chain`){for(let[n,r]of e.path.entries()){let i=e.path[n+1];i!==void 0&&t.add(A(r,i))}return}let n=new Set(e.path);for(let e of f.values())n.has(e.from)&&n.has(e.to)&&t.add(A(e.from,e.to))},J=()=>{let e=[...h.values()].filter(e=>e.kind===`cycle`),t=new Set(e.flatMap(e=>e.path)),n=[],r=[...f.values()].filter(e=>!(o(e.from)||o(e.to)));for(let e of i(r))e.every(e=>t.has(e))||n.push({kind:`cycle`,path:e,repeats:e.length,turns:0,root:e[0]??``,source:`graph`});return[...h.values(),...n.filter(t=>!e.some(e=>e.path.join()===t.path.join()))]};return Object.assign(G,{task:W,cycles:()=>J(),toMermaid:e=>u(K(),e),toDot:e=>d(K(),e),toJSON:(e={})=>{let t=K();if(!e.payload)return t;for(let e of t.nodes){let t=l.get(e.name)?.last;t&&(e.last={payload:k(t.payload),...t.error!==void 0&&{error:k(t.error)}})}return t},clear:()=>{l=/* @__PURE__ */ new Map,f=/* @__PURE__ */ new Map,h=/* @__PURE__ */ new Map,g=/* @__PURE__ */ new WeakMap,_=/* @__PURE__ */ new Map,v=/* @__PURE__ */ new Set,x=void 0}})},I=(e={})=>{let t=e.console??globalThis.console,n=e.prefix??`[nanoeffects]`,r=e.source===!0,i=e=>r&&e!==void 0?` (${e})`:``,a=/* @__PURE__ */ new Map;return({id:e,parentId:r,type:o,effect:s,phase:c,payload:l,error:u,traceGroupId:d,source:f})=>{a.has(e)||a.set(e,typeof r==`number`?(a.get(r)??0)+1:0);let p=` `.repeat(a.get(e)??0);c!==`start`&&a.delete(e);let m=d===void 0?``:` [${d}]`,h=i(f),g=`${n}${m} ${p}${o} · ${s} ${c}${h}`;c===`error`?t?.error(g,u):t?.debug(g,l)}},L=(e,t={})=>{let r=t.source===!0,i=e=>{let t=e.name??`<anonymous>`;return r&&e.source!==void 0?`${t} (${e.source})`:t},a={};for(let[t,r]of n(e)??[])typeof t==`string`&&(a[t]=r.map(i));return a},R=e=>{if(e instanceof Error)return`${e.name}: ${e.message}`;let t=k(e);return typeof t==`string`?t:JSON.stringify(t)},z=(e={})=>{let t=e.performance??globalThis.performance,n=/* @__PURE__ */ new Map;return({id:e,parentId:r,rootId:i,traceGroupId:a,type:o,effect:s,phase:c,error:l})=>{if(!t)return;if(c===`start`){n.set(e,t.now());return}let u=`${o} · ${s} (${c})`,d={id:e,parentId:r,...i!==void 0&&{rootId:i},...a!==void 0&&{traceGroupId:a},phase:c,...c===`error`&&{error:R(l)}},f=n.get(e);n.delete(e),typeof f==`number`?t.measure(u,{start:f,end:t.now(),detail:d}):t.mark(u,{detail:d})}},B=e=>{let t=e?.capacity??0,n=Number.isInteger(t)&&t>0?t:0,r=[],i=0,a=e=>{n===0?r.push(e):(r[i%n]=e,i+=1)},o=()=>{r.length=0,i=0},s=()=>{let e=Math.min(i,n),t=i>n?i%n:0,a=[];for(let i=0;i<e;i+=1){let e=r[(t+i)%n];e&&a.push(e)}return a},c=Object.assign(a,{clear:o,events:r,toJSON:()=>(n>0?s():r).map(e=>k(e))});return n>0&&Object.defineProperty(c,"events",{get:s}),c},V=()=>{},H=/* @__PURE__ */ new Set([`cancel`,`drop`,`error`]),U=()=>!1,W=()=>!0,G=e=>{if(typeof e==`function`)return e;if(Array.isArray(e)){let t=new Set(e);return({phase:e})=>t.has(e)}return e?W:U},K=(e={})=>{let t=e.extension??globalThis.__REDUX_DEVTOOLS_EXTENSION__;if(!t)return V;let{starts:n=!1,depth:r=0,maxAge:i}=e,a=G(e.policies),o=e.payload??`essential`,s=e.serialize===!1?e=>e:e.serialize??(e=>k(e)),c=t.connect({name:e.name??`nanoeffects`,...i!==void 0&&{maxAge:i}}),l=/* @__PURE__ */ new Map,u=/* @__PURE__ */ new Map,d=()=>({running:Object.fromEntries(l)});c.init(d());let f=(e,t,n)=>n===0?a({effect:e,phase:t}):n<=r,p=(e,t)=>{e.phase===`start`?(u.set(e.id,t),f(e.effect,`start`,t)&&l.set(e.id,{type:e.type,effect:e.effect,...e.traceGroupId!==void 0&&{traceGroupId:e.traceGroupId}})):(u.delete(e.id),l.delete(e.id))},m=!1,h=!1,g=(e,t)=>{m||h||t!==0||e===`start`||(h=!0,globalThis.console?.warn(`[nanoeffects] reduxDevtools got a task but no emit event: add bus.use(tracePlugin()) to see the emits.`))},_=e=>{let{payload:t,...n}=e;return o===`all`||o===`essential`&&(e.effect===`emit`||H.has(e.phase))?{...n,payload:s(t)}:n},v=(e,t)=>{c.send({..._(e),type:`${e.type}/${e.effect}/${e.phase}`,event:e.type,...e.phase===`error`&&{error:t instanceof Error?t.message:s(t)}},d())};return({error:e,...t})=>{if(t.effect===`emit`){m=!0,c.send({..._(t),event:t.type},d());return}let r=(t.parentId===void 0?-1:u.get(t.parentId)??-1)+1;p(t,r),g(t.phase,r),(t.phase===`error`&&r===0||f(t.effect,t.phase,r))&&(t.phase!==`start`||n)&&v(t,e)}};export{I as consoleLogger,B as createTraceRecorder,r as createTracer,L as describeBus,F as graphLogger,z as performanceLogger,K as reduxDevtools,k as safeSerialize,t as tracePlugin};
|
|
3
|
+
`)},f=e=>e.kind===`storm`?"an emit from a component or a timer repeats: stop it with `equals` of useEmitter, debounce, or a guard":e.kind===`runaway`?`one chain started a huge number of tasks: look for event names that are made from a counter or an id`:e.path.length<=2?`a handler emits its own event: emit a different event for the result`:`handlers feed each other: break the loop with a condition, or emit a different event for the result`,p=(t,n)=>{let r=n===void 0?`[nanoeffects]`:`[nanoeffects ${n}]`,i=t.traceGroupId===void 0?``:` (flow ${t.traceGroupId})`,a=t.kind===`cycle`?`cycle: ${t.path.join(` → `)} (repeated ${t.repeats} times, started at ${t.root})`:`${t.kind}: ${t.path.join(` → `)} (${t.repeats} in a row, started at ${t.root})`,o=[...new Set(t.path)].flatMap(t=>{let n=e(t);return n===void 0?[]:[`${t} at ${n}`]});return`${r} ${a}${i}${o.length>0?`\n ${o.join(`; `)}`:``}\n ${f(t)}`},m=(e,t)=>{if(!e)return;let n=e.connect({name:t});n.init({findings:0});let r=0;return e=>{r+=1,n.send({type:`cycle/detected`,kind:e.kind,path:e.path,repeats:e.repeats,root:e.root},{findings:r})}},h=2e3,g=e=>{let t=Object.getPrototypeOf(e),n=t===null?void 0:Reflect.get(t,`constructor`),r=typeof n==`function`?n.name:``;return typeof r==`string`&&r!==``?r:`Object`},_=e=>{let t=Object.getPrototypeOf(e);return t===null||t===Object.prototype},v=e=>typeof Reflect.get(e,`nodeType`)==`number`&&typeof Reflect.get(e,`nodeName`)==`string`,y=e=>e.length>h?`${e.slice(0,h)}…`:e,b=e=>{switch(typeof e){case`string`:return y(e);case`boolean`:return e;case`number`:return Number.isFinite(e)?e:String(e);case`bigint`:return`${e}n`;case`symbol`:return String(e);case`function`:return`[Function ${e.name||`anonymous`}]`;default:return e===null?null:void 0}},x=e=>{if(e instanceof WeakMap)return`[WeakMap]`;if(e instanceof WeakSet)return`[WeakSet]`;if(e instanceof Promise)return`[Promise]`;if(v(e))return`[DOM ${String(Reflect.get(e,`nodeName`))}]`;if(e instanceof Date)return Number.isNaN(e.getTime())?`[Invalid Date]`:e.toISOString()},S=(e,t,n)=>{if(t.budget<=0)return`[Cut]`;--t.budget;let r=b(e);return r===void 0?typeof e!=`object`||!e?`[undefined]`:C(e,t,n):r},C=(e,t,n)=>{try{let r=x(e);return r===void 0?t.path.has(e)?`[Circular]`:n>=t.maxDepth?Array.isArray(e)?`[Array]`:`[${g(e)}]`:w(e,t,n):r}catch{return`[Unserializable]`}},w=(e,t,n)=>{t.path.add(e);try{return T(e,t,n)}finally{t.path.delete(e)}},T=(e,t,n)=>e instanceof Error?E(e,t,n):Array.isArray(e)?D(e,t,n):e instanceof Map?{$map:D([...e.entries()],t,n)}:e instanceof Set?{$set:D([...e.values()],t,n)}:ArrayBuffer.isView(e)?{$class:g(e),length:Number(Reflect.get(e,`length`))}:O(e,t,n,_(e)?void 0:g(e)),E=(e,t,n)=>{let r={name:y(e.name),message:y(e.message)};return typeof e.stack==`string`&&(r.stack=y(e.stack)),`cause`in e&&(r.cause=S(e.cause,t,n+1)),r},D=(e,t,n)=>{let r=[];for(let[i,a]of e.entries()){if(t.budget<=0){r.push(`[${e.length-i} more]`);break}r.push(S(a,t,n+1))}return r},O=(e,t,n,r)=>{let i={};r!==void 0&&(i.$class=r);for(let r of Object.keys(e)){let a=Object.getOwnPropertyDescriptor(e,r);a&&(i[r]=`value`in a?S(a.value,t,n+1):`[Getter]`)}return i},k=(e,t={})=>{let n={maxDepth:t.maxDepth??6,budget:t.maxSize??1e3,path:/* @__PURE__ */ new Set};try{return S(e,n,0)}catch{return`[Unserializable]`}},A=(e,t)=>`${e}\u0000${t}`,j=e=>{if(typeof e==`function`)return e;let t=new Set(e);return e=>t.has(e)},M=e=>e.event===void 0?[e.node]:[e.event,e.node],N=e=>{let t=e.slice(0,-1),n=t.reduce((e,n,r)=>n<(t[e]??n)?r:e,0);return[...t.slice(n),...t.slice(0,n)].join(`\0`)},P=(e,t)=>{let{node:n}=t,r=[],i=e;for(;i&&i.node!==n;)r.push(i),i=i.parent;let a=r.reverse();return{path:[n,...a.flatMap(M),...M(t)],names:[n,...a.map(e=>e.node),n]}},F=(t={})=>{let n=t.repeats??3,r=t.maxSteps??5e3,a=t.storm===!1?void 0:t.storm??{count:100,perMs:1e3},o=j(t.ignore),s=t.now??(()=>globalThis.performance.now()),c=t.console??globalThis.console,l=/* @__PURE__ */ new Map,f=/* @__PURE__ */ new Map,h=/* @__PURE__ */ new Map,g=/* @__PURE__ */ new WeakMap,_=/* @__PURE__ */ new Map,v=/* @__PURE__ */ new Set,y=!1,b=!1,x,S,C=!1,w=!1,T=e=>{let t=l.get(e)??{count:0};t.count+=1,l.set(e,t)},E=(e,t)=>{let n=A(e,t),r=f.get(n)??{from:e,to:t,count:0,inCycle:!1};r.count+=1,f.set(n,r)},D=e=>{if(t.devtools&&!C){C=!0;let e=t.extension??globalThis.__REDUX_DEVTOOLS_EXTENSION__;S=m(e,t.name??`nanoeffects graph`)}S?.(e),t.onCycle?t.onCycle(e):c?.warn(p(e,t.name))},O=(e,t)=>{let n=h.get(e);if(n){n.turns+=1;return}let r=t();if(h.set(e,r),!w){w=!0;try{D(r)}catch(e){globalThis.console?.error(`[nanoeffects] graph logger report failed`,e)}finally{w=!1}}},M=(e,t)=>{let n=new Map(e.counts);n.set(t,(n.get(t)??0)+1),e.counts=n},F=(e,t)=>{let r=e.counts.get(e.node)??0;if(r<n||o(e.node))return;let{path:i,names:a}=P(e.parent,e);O(`cycle:${N(a)}`,()=>{let n={kind:`cycle`,path:i,repeats:r,turns:1,root:e.root.name,source:`chain`};return t.traceGroupId!==void 0&&(n.traceGroupId=t.traceGroupId),n})},I=e=>{e.root.steps+=1,!(e.root.steps<=r||e.root.flagged)&&(e.root.flagged=!0,O(`runaway:${e.root.name}`,()=>({kind:`runaway`,path:[e.root.name],repeats:e.root.steps,turns:1,root:e.root.name,source:`chain`})))},L=e=>{if(!a||o(e))return;let t=s(),n=(_.get(e)??[]).filter(e=>t-e<a.perMs);n.push(t),_.set(e,n),n.length<a.count?v.delete(e):v.has(e)||(v.add(e),O(`storm:${e}:${t}`,()=>({kind:`storm`,path:[e],repeats:n.length,turns:1,root:e,source:`chain`})))},R=(e,t)=>{let n=x;return n!==void 0&&n.parent===e&&n.type===t},z=(e,t,n)=>({node:e.type,parent:t,counts:t?.counts??/* @__PURE__ */ new Map,root:t?.root??{name:e.type,steps:0,flagged:!1},inner:n}),B=()=>{b||(b=!0,queueMicrotask(()=>{y||c?.warn(`[nanoeffects] graphLogger got tasks but no emit event: add bus.use(tracePlugin()) for a finer graph.`)}))},V=(e,t,n,r)=>{M(t,e.type),(e.leaf===!0||r||!y)&&(T(e.type),n?E(n.node,e.type):L(e.type)),e.leaf||o(e.type)||(I(t),F(t,e))},H=e=>{let t=e.parent?g.get(e.parent):void 0;e.leaf?(y=!0,x={parent:t,type:e.type}):B();let n=e.emitted===!1,r=!e.leaf&&y&&!n&&!R(t,e.type),i=r&&!t,a=z(e,t,n||r&&!i);g.set(e.ref,a),a.inner||V(e,a,t,i)},U=(e,t)=>{let n=new Map(e.counts),r=e.node;n.set(r,Math.max(0,(n.get(r)??1)-1)),n.set(t.type,(n.get(t.type)??0)+1),e.counts=n,e.node=t.type,e.event=t.event,T(t.type),t.event!==void 0&&E(t.event,t.type),e.inner||F(e,t)},W=e=>{let t=g.get(e.ref);t?U(t,e):H(e)},G=e=>{if(e.phase!==`done`&&e.phase!==`error`)return;let t=l.get(e.type);t&&(t.last=e.phase===`error`?{payload:e.payload,error:e.error}:{payload:e.payload})},K=()=>{let n=J(),r=/* @__PURE__ */ new Set;for(let e of n)q(e,r);let i={nodes:[...l].map(([t,n])=>{let r=e(t);return r===void 0?{name:t,count:n.count}:{name:t,count:n.count,site:r}}),edges:[...f.values()].map(e=>({...e,inCycle:r.has(A(e.from,e.to))})),cycles:n};return t.name!==void 0&&(i.name=t.name),i},q=(e,t)=>{if(e.source===`chain`){for(let[n,r]of e.path.entries()){let i=e.path[n+1];i!==void 0&&t.add(A(r,i))}return}let n=new Set(e.path);for(let e of f.values())n.has(e.from)&&n.has(e.to)&&t.add(A(e.from,e.to))},J=()=>{let e=[...h.values()].filter(e=>e.kind===`cycle`),t=new Set(e.flatMap(e=>e.path)),n=[],r=[...f.values()].filter(e=>!(o(e.from)||o(e.to)));for(let e of i(r))e.every(e=>t.has(e))||n.push({kind:`cycle`,path:e,repeats:e.length,turns:0,root:e[0]??``,source:`graph`});return[...h.values(),...n.filter(t=>!e.some(e=>e.path.join()===t.path.join()))]};return Object.assign(G,{task:W,cycles:()=>J(),toMermaid:e=>u(K(),e),toDot:e=>d(K(),e),toJSON:(e={})=>{let t=K();if(!e.payload)return t;for(let e of t.nodes){let t=l.get(e.name)?.last;t&&(e.last={payload:k(t.payload),...t.error!==void 0&&{error:k(t.error)}})}return t},clear:()=>{l=/* @__PURE__ */ new Map,f=/* @__PURE__ */ new Map,h=/* @__PURE__ */ new Map,g=/* @__PURE__ */ new WeakMap,_=/* @__PURE__ */ new Map,v=/* @__PURE__ */ new Set,x=void 0}})},I=(e,t)=>`${e}\u0000${t}`,L=e=>{let t=e.slice(0,-1),n=t.reduce((e,n,r)=>n<(t[e]??n)?r:e,0);return[...t.slice(n),...t.slice(0,n)]},R=e=>{let t=e.kind===`cycle`?L(e.path):[e.root];return[e.kind,e.source,e.traceGroupId??``,...t].join(`\0`)},z=(e,t)=>{let n={name:t.name,count:t.count},r=t.site??e?.site;e&&(n.count+=e.count),r!==void 0&&(n.site=e?.site??r);let i=t.last??e?.last;return i!==void 0&&(n.last=i),n},B=(...e)=>{let t=/* @__PURE__ */ new Map,n=/* @__PURE__ */ new Map,r=/* @__PURE__ */ new Map,i;for(let a of e){i??=a.name;for(let e of a.nodes)t.set(e.name,z(t.get(e.name),e));for(let e of a.edges){let t=I(e.from,e.to),r=n.get(t);n.set(t,{from:e.from,to:e.to,count:e.count+(r?.count??0),inCycle:e.inCycle||(r?.inCycle??!1)})}for(let e of a.cycles){let t=R(e),n=r.get(t);r.set(t,{...e,path:[...e.path],turns:e.turns+(n?.turns??0),repeats:Math.max(e.repeats,n?.repeats??0)})}}let a={nodes:[...t.values()],edges:[...n.values()],cycles:[...r.values()]};return i!==void 0&&(a.name=i),a},V=/ \([^()]*:\d+:\d+\)$/,H=(e,t)=>{let n=new Set(t.nodes.map(e=>e.name)),r=[];for(let[t,i]of Object.entries(e)){let e=n.has(t);for(let a of i){let i=a.replace(V,``);i===`<anonymous>`||n.has(i)||r.push({event:t,listener:i,kind:e?`unrun`:`unemitted`})}}return r},U=e=>`events`in e?e.events:e,W=e=>`tasks`in e?e.tasks:/* @__PURE__ */ new Map,G=(e,t)=>{let n=/* @__PURE__ */ new Map;for(let t of U(e))n.set(t.id,t);let r=W(e),i=[],a=/* @__PURE__ */ new Set,o=t;for(;o!==void 0&&!a.has(o);){a.add(o);let e=n.get(o),t=r.get(o),s=K(o,e,t);if(!s)break;i.push(s),o=e?.parentId??t?.parentId}return i.reverse()},K=(e,t,n)=>{let r=t??{},i=n??{},a=r.type??i.type;if(a===void 0)return;let o=r.source??i.source,s=r.traceGroupId??i.traceGroupId;return{id:e,type:a,...t?{effect:t.effect,phase:t.phase}:{},...o===void 0?{}:{source:o},...s===void 0?{}:{traceGroupId:s}}},q=e=>{let t=/* @__PURE__ */ new Map,n=/* @__PURE__ */ new WeakMap;return{task:({ref:r,parent:i,id:a,type:o,source:s,traceGroupId:c})=>{n.set(r,a);let l=i===void 0?void 0:n.get(i),u={type:o,...l===void 0?{}:{parentId:l},...s===void 0?{}:{source:s},...c===void 0?{}:{traceGroupId:c}};if(t.set(a,u),e>0&&t.size>e){let e=t.keys().next().value;e!==void 0&&t.delete(e)}},tasks:t,clear:()=>t.clear()}},J=(e={})=>{let t=e.console??globalThis.console,n=e.prefix??`[nanoeffects]`,r=e.source===!0,i=e=>r&&e!==void 0?` (${e})`:``,a=/* @__PURE__ */ new Map,o=e.chain===!0?q(1e3):void 0,s=(e,t)=>{if(!o||t!==`error`)return``;let n=[],r=/* @__PURE__ */ new Set,i=e;for(;i!==void 0&&!r.has(i);){r.add(i);let e=o.tasks.get(i);if(!e)break;n.push(e.type),i=e.parentId}return n.length>1?` (${n.reverse().join(` → `)})`:``},c=({id:e,parentId:r,type:o,effect:c,phase:l,payload:u,error:d,traceGroupId:f,source:p})=>{a.has(e)||a.set(e,typeof r==`number`?(a.get(r)??0)+1:0);let m=` `.repeat(a.get(e)??0);l!==`start`&&a.delete(e);let h=f===void 0?``:` [${f}]`,g=i(p),_=s(e,l),v=`${n}${h} ${m}${o} · ${c} ${l}${g}${_}`;l===`error`?t?.error(v,d):t?.debug(v,u)};return o&&(c.task=o.task),c},Y=(e,t={})=>{let r=t.source===!0,i=e=>{let t=e.name??`<anonymous>`;return r&&e.source!==void 0?`${t} (${e.source})`:t},a={};for(let[t,r]of n(e)??[])typeof t==`string`&&(a[t]=r.map(i));return a},X=e=>{if(e instanceof Error)return`${e.name}: ${e.message}`;let t=k(e);return typeof t==`string`?t:JSON.stringify(t)},Z=(e={})=>{let t=e.performance??globalThis.performance,n=/* @__PURE__ */ new Map;return({id:e,parentId:r,rootId:i,traceGroupId:a,type:o,effect:s,phase:c,error:l})=>{if(!t)return;if(c===`start`){n.set(e,t.now());return}let u=`${o} · ${s} (${c})`,d={id:e,parentId:r,...i!==void 0&&{rootId:i},...a!==void 0&&{traceGroupId:a},phase:c,...c===`error`&&{error:X(l)}},f=n.get(e);n.delete(e),typeof f==`number`?t.measure(u,{start:f,end:t.now(),detail:d}):t.mark(u,{detail:d})}},Q=e=>{let t=e?.capacity??0,n=Number.isInteger(t)&&t>0?t:0,r=[],i=q(n),a=0,o=e=>{n===0?r.push(e):(r[a%n]=e,a+=1)},s=()=>{r.length=0,a=0,i.clear()},c=()=>{let e=Math.min(a,n),t=a>n?a%n:0,i=[];for(let a=0;a<e;a+=1){let e=r[(t+a)%n];e&&i.push(e)}return i},l=Object.assign(o,{clear:s,events:r,tasks:i.tasks,task:i.task,toJSON:()=>(n>0?c():r).map(e=>k(e))});return n>0&&Object.defineProperty(l,"events",{get:c}),l},$=()=>{},ee=/* @__PURE__ */ new Set([`cancel`,`drop`,`error`]),te=()=>!1,ne=()=>!0,re=e=>{if(typeof e==`function`)return e;if(Array.isArray(e)){let t=new Set(e);return({phase:e})=>t.has(e)}return e?ne:te},ie=(e={})=>{let t=e.extension??globalThis.__REDUX_DEVTOOLS_EXTENSION__;if(!t)return $;let{starts:n=!1,depth:r=0,maxAge:i}=e,a=re(e.policies),o=e.payload??`essential`,s=e.serialize===!1?e=>e:e.serialize??(e=>k(e)),c=t.connect({name:e.name??`nanoeffects`,...i!==void 0&&{maxAge:i}}),l=/* @__PURE__ */ new Map,u=/* @__PURE__ */ new Map,d=()=>({running:Object.fromEntries(l)});c.init(d());let f=(e,t,n)=>n===0?a({effect:e,phase:t}):n<=r,p=(e,t)=>{e.phase===`start`?(u.set(e.id,t),f(e.effect,`start`,t)&&l.set(e.id,{type:e.type,effect:e.effect,...e.traceGroupId!==void 0&&{traceGroupId:e.traceGroupId}})):(u.delete(e.id),l.delete(e.id))},m=!1,h=!1,g=(e,t)=>{m||h||t!==0||e===`start`||(h=!0,globalThis.console?.warn(`[nanoeffects] reduxDevtools got a task but no emit event: add bus.use(tracePlugin()) to see the emits.`))},_=e=>{let{payload:t,...n}=e;return o===`all`||o===`essential`&&(e.effect===`emit`||ee.has(e.phase))?{...n,payload:s(t)}:n},v=(e,t)=>{c.send({..._(e),type:`${e.type}/${e.effect}/${e.phase}`,event:e.type,...e.phase===`error`&&{error:t instanceof Error?t.message:s(t)}},d())};return({error:e,...t})=>{if(t.effect===`emit`){m=!0,c.send({..._(t),event:t.type},d());return}let r=(t.parentId===void 0?-1:u.get(t.parentId)??-1)+1;p(t,r),g(t.phase,r),(t.phase===`error`&&r===0||f(t.effect,t.phase,r))&&(t.phase!==`start`||n)&&v(t,e)}},ae=()=>typeof globalThis.performance?.now==`function`?globalThis.performance.now():Date.now(),oe=e=>({id:e.id,type:e.type,effect:e.effect,...e.parentId===void 0?{}:{parentId:e.parentId},...e.rootId===void 0?{}:{rootId:e.rootId},...e.source===void 0?{}:{source:e.source}}),se=(e={})=>{let t=e.now??ae,n=e.capacity??0,r=Number.isInteger(n)&&n>0?n:0,i=/* @__PURE__ */ new Map,a=[];return Object.assign(e=>{let n=`${e.id}:${e.effect}`,o=t();if(e.phase===`start`){i.set(n,{...oe(e),startedAt:o});return}let s=i.get(n);if(!s)return;i.delete(n);let{startedAt:c,...l}=s;a.push({...l,phase:e.phase,ms:o-c}),r>0&&a.length>r&&a.shift()},{durations:a,pending:()=>{let e=t();return[...i.values()].map(({startedAt:t,...n})=>({...n,ms:e-t}))},clear:()=>{i.clear(),a.length=0}})};export{G as chainOf,J as consoleLogger,Q as createTraceRecorder,r as createTracer,Y as describeBus,H as findOrphans,F as graphLogger,B as mergeGraphs,Z as performanceLogger,ie as reduxDevtools,k as safeSerialize,se as timingLogger,t as tracePlugin};
|
|
@@ -0,0 +1,2 @@
|
|
|
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=/* @__PURE__ */ new Map,w=(e,t,n)=>{let r=e.name;if(!r&&b&&typeof t==`string`){let e=(x.get(t)??0)+1;x.set(t,e),r=`${t}#${e}`}let i=r===void 0?e.handler:m(e.handler,r,e.source);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)=>{e===void 0?p.clear():t===void 0&&p.delete(k(e));let n=e!==void 0&&t&&y(p,k(e),t);n&&v(p,k(e),n),E(e,n?n.wrapper: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};
|
|
@@ -1,30 +1,16 @@
|
|
|
1
|
-
# Debugging
|
|
1
|
+
# Debugging cases
|
|
2
2
|
|
|
3
3
|
> **Level F.** Read first: [Trace tasks](tracing.md#attach-a-tracer).
|
|
4
4
|
|
|
5
|
-
Read this page
|
|
5
|
+
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).
|
|
6
6
|
|
|
7
7
|
## On this page
|
|
8
8
|
|
|
9
|
-
- [Compare what you can see](#compare-what-you-can-see)
|
|
10
9
|
- [Find who listens to an event](#find-who-listens-to-an-event)
|
|
11
10
|
- [Find which task began a chain](#find-which-task-began-a-chain)
|
|
12
11
|
- [Find out why a request stopped](#find-out-why-a-request-stopped)
|
|
13
12
|
- [Know what is missing](#know-what-is-missing)
|
|
14
13
|
|
|
15
|
-
## Compare what you can see
|
|
16
|
-
|
|
17
|
-
| Question | nanoeffects | RxJS | Effector | mitt / nanoevents |
|
|
18
|
-
| -------- | ----------- | ---- | -------- | ----------------- |
|
|
19
|
-
| 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 |
|
|
20
|
-
| Was a call cancelled or dropped? | Yes: the phases `cancel` and `drop` | An unsubscribe shows no event unless you add one | — | No |
|
|
21
|
-
| A log of every emit | `tracePlugin` reports each emit, also with no listener | `tap` per stream | A logger over the units | A `"*"` listener |
|
|
22
|
-
| 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) |
|
|
23
|
-
| Find a loop | `graphLogger`, then `loopGuardPlugin` cuts it | — | — | No |
|
|
24
|
-
| 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 |
|
|
25
|
-
|
|
26
|
-
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.
|
|
27
|
-
|
|
28
14
|
## Find who listens to an event
|
|
29
15
|
|
|
30
16
|
**Problem.** You read an emit and want to know what it starts.
|
|
@@ -129,7 +115,7 @@ Details: [Trace tasks](tracing.md) and [Cancel stale requests](cancel-stale-requ
|
|
|
129
115
|
|
|
130
116
|
## Know what is missing
|
|
131
117
|
|
|
132
|
-
- **No browser extension or UI of its own.** Use [Redux DevTools](tracing.md#configure-redux-devtools), the console or the Performance panel.
|
|
118
|
+
- **No browser extension or UI of its own.** Use [Redux DevTools](tracing.md#configure-redux-devtools), the console or the Performance panel.
|
|
133
119
|
- **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.
|
|
134
120
|
- **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.
|
|
135
121
|
- **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.
|
package/docs/debugging-loops.md
CHANGED
|
@@ -16,6 +16,7 @@ A loop is two handlers that emit each other's events, or one handler that emits
|
|
|
16
16
|
- [Cut a loop at run time](#cut-a-loop-at-run-time)
|
|
17
17
|
- [Fail a test on a loop](#fail-a-test-on-a-loop)
|
|
18
18
|
- [Export the graph and the payloads](#export-the-graph-and-the-payloads)
|
|
19
|
+
- [Find listeners that never ran](#find-listeners-that-never-ran)
|
|
19
20
|
- [Know how a loop is found](#know-how-a-loop-is-found)
|
|
20
21
|
- [Know the limits](#know-the-limits)
|
|
21
22
|
|
|
@@ -278,6 +279,84 @@ console.log(JSON.stringify(data));
|
|
|
278
279
|
|
|
279
280
|
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.
|
|
280
281
|
|
|
282
|
+
## Find listeners that never ran
|
|
283
|
+
|
|
284
|
+
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.
|
|
285
|
+
|
|
286
|
+
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.
|
|
287
|
+
|
|
288
|
+
1. Each run writes its graph to a file.
|
|
289
|
+
2. A script reads the files and joins them with `mergeGraphs`.
|
|
290
|
+
3. `findOrphans` compares the joined graph with `describeBus`.
|
|
291
|
+
|
|
292
|
+
```js
|
|
293
|
+
// bus.js
|
|
294
|
+
import { createBus } from "nanoeffects";
|
|
295
|
+
import { createTracer, graphLogger, tracePlugin } from "nanoeffects/trace";
|
|
296
|
+
|
|
297
|
+
// graphLogger — the map of what ran
|
|
298
|
+
export const graph = graphLogger({ name: "cart" });
|
|
299
|
+
export const bus = createBus({ trace: createTracer(graph) }).use(tracePlugin());
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
```js
|
|
303
|
+
// handlers.js
|
|
304
|
+
import { bus } from "./bus";
|
|
305
|
+
|
|
306
|
+
// the listener is named by its function; findOrphans matches this name
|
|
307
|
+
bus.on("cart/added", function recalcTotal() {});
|
|
308
|
+
bus.on("cart/cleared", function resetTotal() {});
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
```js
|
|
312
|
+
// graph-setup.js (the setup file of your test runner)
|
|
313
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
314
|
+
import { afterAll } from "vitest";
|
|
315
|
+
import { graph } from "./bus";
|
|
316
|
+
|
|
317
|
+
afterAll(() => {
|
|
318
|
+
mkdirSync("graphs", { recursive: true });
|
|
319
|
+
|
|
320
|
+
// toJSON — the graph of this run as plain data
|
|
321
|
+
const data = graph.toJSON();
|
|
322
|
+
writeFileSync(`graphs/${process.pid}.json`, JSON.stringify(data));
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
```js
|
|
327
|
+
// orphans.js (run it after the tests)
|
|
328
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
329
|
+
import { describeBus, findOrphans, mergeGraphs } from "nanoeffects/trace";
|
|
330
|
+
import { bus } from "./bus";
|
|
331
|
+
import "./handlers";
|
|
332
|
+
|
|
333
|
+
const files = readdirSync("graphs");
|
|
334
|
+
const runs = files.map((file) => JSON.parse(readFileSync(`graphs/${file}`, "utf8")));
|
|
335
|
+
|
|
336
|
+
// mergeGraphs — counts add up, so a listener that ran in any run is a node
|
|
337
|
+
const all = mergeGraphs(...runs);
|
|
338
|
+
|
|
339
|
+
// describeBus — the listeners the bus holds, by event
|
|
340
|
+
const described = describeBus(bus);
|
|
341
|
+
|
|
342
|
+
// findOrphans — the listeners the joined graph never saw run
|
|
343
|
+
console.log(findOrphans(described, all));
|
|
344
|
+
// [{ event: "cart/cleared", listener: "resetTotal", kind: "unemitted" }]
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Each item has a `kind`:
|
|
348
|
+
|
|
349
|
+
| `kind` | Meaning | What to check |
|
|
350
|
+
| ------ | ------- | ------------- |
|
|
351
|
+
| `unemitted` | Nothing emitted the event in any run. | Is the event still used? Is a scenario missing? |
|
|
352
|
+
| `unrun` | The event was emitted, this listener did not run. | A condition or a [policy](policies.md) that never lets it run. |
|
|
353
|
+
|
|
354
|
+
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:
|
|
355
|
+
|
|
356
|
+
- Two listeners with one name are one node, so the second one is never reported. Give each a distinct name or `traceId`.
|
|
357
|
+
- Merge runs of one build. A listener that was renamed shows up as an orphan.
|
|
358
|
+
- `mergeGraphs` does not find a loop that only closes when the edges of two runs are joined. Use `graph.cycles()` in each run.
|
|
359
|
+
|
|
281
360
|
## Know how a loop is found
|
|
282
361
|
|
|
283
362
|
`graphLogger` finds loops in two ways. They give two kinds of finding in `graph.cycles()`, told apart by `source`:
|
package/docs/package.md
CHANGED
|
@@ -30,7 +30,7 @@ Each row is optional. The "when" column says what need brings you to it.
|
|
|
30
30
|
| `nanoeffects/svelte` | `sveltePlugin`: adds `eventStore` to a bus (needs Svelte 5 or newer) | Svelte components |
|
|
31
31
|
| `nanoeffects/vue` | `vuePlugin`: adds `watchEmitter` and `useEventRef` to a bus (needs Vue 3.3 or newer) | Vue components |
|
|
32
32
|
| `nanoeffects/dev` | `devPlugin` (no-op in production), `getLostSignalsCount` and `getLostSignals` for tests | you look for leaks and lost signals in development and in tests |
|
|
33
|
-
| `nanoeffects/trace` | `createTracer`, the loggers (`graphLogger` finds event loops) and `tracePlugin` (names for the trace) | you want to see what ran and why |
|
|
33
|
+
| `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 |
|
|
34
34
|
| `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 |
|
|
35
35
|
| `nanoeffects/guard` | `loopGuardPlugin`: refuses the emit that would pass a limit of a chain | a loop must not take the app down |
|
|
36
36
|
|
|
@@ -1,30 +1,16 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Случаи отладки
|
|
2
2
|
|
|
3
3
|
> **[Уровень F](README.md#виды-проектов)**: платформа, SDK или долгоживущее приложение. Сначала прочитайте: [Трассировка задач](tracing.md#подключение-трассировщика).
|
|
4
4
|
|
|
5
|
-
Прочитайте эту страницу,
|
|
5
|
+
Прочитайте эту страницу, когда что-то идёт не так и нужно понять, кто слушает событие, что начало цепочку или почему остановился запрос. Каждый случай ниже — короткий пример, который можно запустить. О циклах читайте в разделе [Поиск и обрыв циклов событий](debugging-loops.md).
|
|
6
6
|
|
|
7
7
|
## На этой странице
|
|
8
8
|
|
|
9
|
-
- [Сравнение того, что видно](#сравнение-того-что-видно)
|
|
10
9
|
- [Кто слушает событие](#кто-слушает-событие)
|
|
11
10
|
- [Какая задача начала цепочку](#какая-задача-начала-цепочку)
|
|
12
11
|
- [Почему запрос остановился](#почему-запрос-остановился)
|
|
13
12
|
- [Чего не хватает](#чего-не-хватает)
|
|
14
13
|
|
|
15
|
-
## Сравнение того, что видно
|
|
16
|
-
|
|
17
|
-
| Вопрос | nanoeffects | RxJS | Effector | mitt / nanoevents |
|
|
18
|
-
| ------ | ----------- | ---- | -------- | ----------------- |
|
|
19
|
-
| Какая задача запустила какую? | Да: у каждой задачи есть `parentId` и `rootId` ([Трассировка задач](tracing.md)) | Вы добавляете `tap` на каждый поток или берёте инструмент вроде [rxjs-spy](https://github.com/cartant/rxjs-spy) | Да, через [`effector-logger`](https://github.com/effector/logger) и [inspector](https://github.com/effector/inspector) | Нет |
|
|
20
|
-
| Вызов прерван или отброшен? | Да: фазы `cancel` и `drop` | Отписка не даёт события, пока вы его не добавите | — | Нет |
|
|
21
|
-
| Лог каждого emit | `tracePlugin` сообщает о каждом emit, и без подписчиков | `tap` на каждый поток | Логгер над юнитами | Подписка на `"*"` |
|
|
22
|
-
| Карта приложения | `describeBus` до запуска, `graphLogger` после ([Поиск и обрыв циклов событий](debugging-loops.md)) | Нет встроенной | Граф юнитов, имена даёт плагин Babel/SWC | `emitter.all` хранит обработчики каждого события (mitt) |
|
|
23
|
-
| Поиск цикла | `graphLogger`, а `loopGuardPlugin` обрывает его | — | — | Нет |
|
|
24
|
-
| Тест потока | [`createMarbleBus`](testing.md): отправки и фазы диаграммами на поддельных таймерах; [`createTraceRecorder`](tracing.md#последние-события-на-случай-инцидента) хранит события | [Marble-тесты](https://rxjs.dev/guide/testing/marble-testing) | `fork` даёт изолированный scope | Подключение вы пишете сами |
|
|
25
|
-
|
|
26
|
-
Прочерк значит, что эта страница клетку не сравнивает. Читайте таблицу как карту того, где искать, а не как оценку. Effector и RxJS ещё хранят state и потоки, чего nanoeffects не делает, поэтому их инструменты охватывают больше. nanoeffects глубоко решает одну задачу: почему выполнился асинхронный обработчик и почему он остановился.
|
|
27
|
-
|
|
28
14
|
## Кто слушает событие
|
|
29
15
|
|
|
30
16
|
**Проблема.** Вы читаете emit и хотите знать, что он запускает.
|
|
@@ -129,7 +115,7 @@ console.log(phases);
|
|
|
129
115
|
|
|
130
116
|
## Чего не хватает
|
|
131
117
|
|
|
132
|
-
- **Нет расширения для браузера и своего интерфейса.** Используйте [Redux DevTools](tracing.md#настройка-redux-devtools), консоль или панель Performance.
|
|
118
|
+
- **Нет расширения для браузера и своего интерфейса.** Используйте [Redux DevTools](tracing.md#настройка-redux-devtools), консоль или панель Performance.
|
|
133
119
|
- **Нет карты по исходному коду.** `describeBus` показывает обработчики, зарегистрированные к этому моменту, а `graphLogger` — события, которые произошли. Обработчик, который ещё не зарегистрирован, на карте не появится.
|
|
134
120
|
- **Нет перехода из лога в код.** С `tracePlugin({ source: true })` событие несёт `file:line:column` своего обработчика ([Строка кода обработчика](tracing.md#строка-кода-обработчика)), но ссылку в интерфейсе вы делаете сами.
|
|
135
121
|
- **Нет имён от компилятора.** Имена функций дают имя бесплатно; минификатор может их сократить, поэтому используйте `traceId`, где имя должно быть стабильным.
|