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 CHANGED
@@ -1 +1 @@
1
- import{t as e}from"./bus-Cmk4U1hQ.js";import{n as t,t as n}from"./tracer-mMDm5yE7.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};
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-mMDm5yE7.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(`
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 compared with RxJS, Effector and mitt
1
+ # Debugging cases
2
2
 
3
3
  > **Level F.** Read first: [Trace tasks](tracing.md#attach-a-tracer).
4
4
 
5
- Read this page if you know RxJS, Effector or mitt and want to know what nanoeffects gives you when something goes wrong, and what it does not. It compares what each tool shows, then answers three questions you ask in a debugger. It is not a migration guide: for that, see [RxJS](migrating-from-rxjs.md), [Effector](migrating-from-effector.md) and [mitt](migrating-from-mitt.md).
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. Effector has an inspector for this.
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.
@@ -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
- # Отладка в сравнении с RxJS, Effector и mitt
1
+ # Случаи отладки
2
2
 
3
3
  > **[Уровень F](README.md#виды-проектов)**: платформа, SDK или долгоживущее приложение. Сначала прочитайте: [Трассировка задач](tracing.md#подключение-трассировщика).
4
4
 
5
- Прочитайте эту страницу, если знаете RxJS, Effector или mitt и хотите понять, что nanoeffects даёт, когда что-то идёт не так, а чего не даёт. Сначала сравнивается, что показывает каждый инструмент, затем разбираются три вопроса, которые вы задаёте отладчику. Это не руководство по переходу: для него есть страницы про [RxJS](migrating-from-rxjs.md), [Effector](migrating-from-effector.md) и [mitt](migrating-from-mitt.md).
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. У Effector для этого есть inspector.
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`, где имя должно быть стабильным.