nanoeffects 0.31.2 → 0.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{action-creator-6rlBTmH9.d.ts → action-creator-DprJV-pN.d.ts} +6 -2
- package/dist/actions.d.ts +3 -3
- package/dist/actions.js +1 -1
- package/dist/{bus-DFlmNJ6h.js → bus-Cmk4U1hQ.js} +1 -1
- package/dist/caller-DULg_ZYV.js +1 -0
- package/dist/context-Dp_QRXzb.js +1 -0
- package/dist/{context-types-DKNc5mkS.d.ts → context-types-BiTMp_lW.d.ts} +1 -1
- package/dist/context.d.ts +2 -2
- package/dist/dev.d.ts +2 -2
- package/dist/dev.js +2 -2
- package/dist/effects.d.ts +2 -2
- package/dist/effects.js +1 -1
- package/dist/guard.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/match.d.ts +1 -1
- package/dist/match.js +1 -1
- package/dist/{module-G39MaVOt.d.ts → module-DPKjzfRk.d.ts} +2 -2
- package/dist/notrace/{action-creator-BrMSO3RP.d.ts → action-creator-BWd9FWt6.d.ts} +2 -2
- package/dist/notrace/actions.d.ts +2 -2
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/effects.d.ts +2 -2
- package/dist/notrace/index.d.ts +1 -1
- package/dist/notrace/{plugin-7GUL2EW5.d.ts → plugin-DmAYT3Xk.d.ts} +1 -1
- package/dist/notrace/policies.d.ts +1 -1
- package/dist/notrace/redux.d.ts +1 -1
- package/dist/observable.d.ts +1 -1
- package/dist/oop.d.ts +1 -1
- package/dist/oop.js +1 -1
- package/dist/{plugin-BWNP23hr.d.ts → plugin-2d3uuW7o.d.ts} +2 -2
- package/dist/policies.d.ts +2 -2
- package/dist/policies.js +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/redux.d.ts +1 -1
- package/dist/scope.d.ts +2 -2
- package/dist/signal.d.ts +2 -2
- package/dist/signal.js +1 -1
- package/dist/svelte.d.ts +1 -1
- package/dist/testing.d.ts +80 -0
- package/dist/testing.js +1 -0
- package/dist/trace.d.ts +39 -3
- package/dist/trace.js +3 -3
- package/dist/tracer-mMDm5yE7.js +2 -0
- package/dist/{types-N_cdRr1r.d.ts → types-DxPZDzZG.d.ts} +2 -2
- package/dist/vue.d.ts +1 -1
- package/dist/{with-policy-CeM1rtES.js → with-policy-BmL0eRU3.js} +1 -1
- package/dist/{with-policy-CthiBp_z.d.ts → with-policy-Y12d-yx6.d.ts} +1 -1
- package/docs/debugging-compared.md +136 -0
- package/docs/migrating-from-rxjs.md +1 -1
- package/docs/package.md +1 -0
- package/docs/recipes.md +35 -0
- package/docs/ru/debugging-compared.md +136 -0
- package/docs/ru/migrating-from-rxjs.md +1 -1
- package/docs/ru/package.md +1 -0
- package/docs/ru/recipes.md +35 -0
- package/docs/ru/testing.md +92 -0
- package/docs/ru/tracing.md +85 -1
- package/docs/testing.md +92 -0
- package/docs/tracing.md +85 -1
- package/llms-full.txt +358 -2
- package/llms.txt +2 -0
- package/package.json +12 -3
- package/dist/context-B5Z2RpJt.js +0 -1
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { H as EventMap, v as Bus, y as BusOptions } from "./action-creator-DprJV-pN.js";
|
|
2
|
+
//#region src/testing/clock.d.ts
|
|
3
|
+
/** What the runner needs from time: move it forward, and read it. */
|
|
4
|
+
type Clock = {
|
|
5
|
+
/** Moves time forward by `ms` and lets the timers and the promises of that time run. */
|
|
6
|
+
advance(ms: number): void | Promise<void>;
|
|
7
|
+
/** The time now, in milliseconds. */
|
|
8
|
+
now(): number;
|
|
9
|
+
};
|
|
10
|
+
/** The part of `vi` (Vitest) and `jest` (Jest) that `fakeTimersClock` uses. */
|
|
11
|
+
type FakeTimers = {
|
|
12
|
+
advanceTimersByTimeAsync(ms: number): Promise<unknown>;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* A clock over fake timers. The test turns them on itself (`vi.useFakeTimers()`), and leaves `Date`
|
|
16
|
+
* faked, which is the default, because the clock reads `Date.now()`.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { vi } from "vitest";
|
|
21
|
+
* import { fakeTimersClock } from "nanoeffects/testing";
|
|
22
|
+
*
|
|
23
|
+
* vi.useFakeTimers();
|
|
24
|
+
* const clock = fakeTimersClock(vi);
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare const fakeTimersClock: (timers: FakeTimers) => Clock;
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/testing/runner.d.ts
|
|
30
|
+
type MarbleOptions<E extends EventMap> = Omit<BusOptions<E>, "trace"> & {
|
|
31
|
+
/** Moves time and tells it. `fakeTimersClock(vi)` is one. */
|
|
32
|
+
clock: Clock;
|
|
33
|
+
/** The length of a frame in milliseconds. Default 10. */
|
|
34
|
+
frame?: number;
|
|
35
|
+
};
|
|
36
|
+
/** What to emit for one event: a diagram, and the value of each character (the character itself by default). */
|
|
37
|
+
type EmitMarble = {
|
|
38
|
+
marble: string;
|
|
39
|
+
values?: Record<string, unknown>;
|
|
40
|
+
};
|
|
41
|
+
type RunOptions = {
|
|
42
|
+
/** Event name → what to emit. */
|
|
43
|
+
emits?: Record<string, EmitMarble>;
|
|
44
|
+
/** How many frames to run. Default: the length of the longest diagram of `emits`. */
|
|
45
|
+
frames?: number;
|
|
46
|
+
};
|
|
47
|
+
type MarbleResult = {
|
|
48
|
+
/** The number of frames that were run. */
|
|
49
|
+
frames: number;
|
|
50
|
+
/** The phases of every traced task as diagrams, by `type:effect`. */
|
|
51
|
+
rows(): Record<string, string>;
|
|
52
|
+
/**
|
|
53
|
+
* Throws when a row differs, with both diagrams. A key with `:` is one row (`search:run`), a key
|
|
54
|
+
* without it is the effect (`latest`) and must match one row. Returns the result.
|
|
55
|
+
*/
|
|
56
|
+
expect(expected: Record<string, string>): MarbleResult;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* A bus that records its trace, and a `run` that moves time frame by frame, emits what the
|
|
60
|
+
* diagrams say at the start of each frame, and gives the phases of the tasks back as diagrams.
|
|
61
|
+
* Register the listeners on `bus`, then `run`. It needs a tracer, so there is no `notrace` build.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* const marbles = createMarbleBus({ clock: fakeTimersClock(vi) });
|
|
66
|
+
* marbles.bus.on("search/changed", latest(search), { traceId: "search" });
|
|
67
|
+
*
|
|
68
|
+
* const result = await marbles.run({
|
|
69
|
+
* emits: { "search/changed": { marble: "a-b---", values: { a: "x", b: "xy" } } },
|
|
70
|
+
* frames: 12,
|
|
71
|
+
* });
|
|
72
|
+
* result.expect({ latest: "s-c-d-" });
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
export declare const createMarbleBus: <E extends EventMap = EventMap>(options: MarbleOptions<E>) => {
|
|
76
|
+
bus: Bus<E>;
|
|
77
|
+
run: ({ emits, frames }?: RunOptions) => Promise<MarbleResult>;
|
|
78
|
+
};
|
|
79
|
+
//#endregion
|
|
80
|
+
export type { Clock, EmitMarble, FakeTimers, MarbleOptions, MarbleResult, RunOptions };
|
package/dist/testing.js
ADDED
|
@@ -0,0 +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};
|
package/dist/trace.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as Tracer, H as EventMap, J as TaskInfo, K as Phase, V as EffectName, X as TraceEvent, Y as Trace, Z as TraceLogger, v as Bus } from "./action-creator-
|
|
1
|
+
import { B as Tracer, H as EventMap, J as TaskInfo, K as Phase, V as EffectName, X as TraceEvent, Y as Trace, Z as TraceLogger, v as Bus } from "./action-creator-DprJV-pN.js";
|
|
2
2
|
//#region src/graph/serialize.d.ts
|
|
3
3
|
type JsonValue = null | boolean | number | string | JsonValue[] | {
|
|
4
4
|
[key: string]: JsonValue;
|
|
@@ -142,6 +142,8 @@ type ConsoleLoggerOptions = {
|
|
|
142
142
|
console?: Pick<Console, "debug" | "error">;
|
|
143
143
|
/** Prepended to every line. */
|
|
144
144
|
prefix?: string;
|
|
145
|
+
/** Append where the listener was registered, to the lines of events that have it. Default `false`. */
|
|
146
|
+
source?: boolean;
|
|
145
147
|
};
|
|
146
148
|
/**
|
|
147
149
|
* Trace logger that prints one line per phase, indented by task depth.
|
|
@@ -156,6 +158,32 @@ type ConsoleLoggerOptions = {
|
|
|
156
158
|
*/
|
|
157
159
|
export declare const consoleLogger: (options?: ConsoleLoggerOptions) => TraceLogger;
|
|
158
160
|
//#endregion
|
|
161
|
+
//#region src/trace/describe.d.ts
|
|
162
|
+
type DescribeBusOptions = {
|
|
163
|
+
/** Add where each listener was registered, for the ones `tracePlugin({ source: true })` recorded. Default `false`. */
|
|
164
|
+
source?: boolean;
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* The listeners that each event has right now, by name and in the order they were
|
|
168
|
+
* added: the `traceId` of a listener, else its function name, else `"<anonymous>"`.
|
|
169
|
+
* Reads what `tracePlugin` keeps, so it returns `{}` on a bus without the plugin or
|
|
170
|
+
* a tracer, and in the `notrace` build. It changes nothing on the bus. With `{ source: true }`
|
|
171
|
+
* each name is followed by `(file:line:column)` of its registration, when it was recorded.
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```ts
|
|
175
|
+
* import { createBus } from "nanoeffects";
|
|
176
|
+
* import { consoleLogger, createTracer, describeBus, tracePlugin } from "nanoeffects/trace";
|
|
177
|
+
*
|
|
178
|
+
* const trace = createTracer(consoleLogger());
|
|
179
|
+
* const bus = createBus({ trace }).use(tracePlugin());
|
|
180
|
+
* bus.on("name/changed", function saveName() {});
|
|
181
|
+
*
|
|
182
|
+
* describeBus(bus); // { "name/changed": ["saveName"] }
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
export declare const describeBus: <E extends EventMap>(bus: Bus<E>, options?: DescribeBusOptions) => Record<string, string[]>;
|
|
186
|
+
//#endregion
|
|
159
187
|
//#region src/trace/performance.d.ts
|
|
160
188
|
/** The part of the User Timing API the logger uses. */
|
|
161
189
|
type PerformanceLike = {
|
|
@@ -186,6 +214,14 @@ type PerformanceLoggerOptions = {
|
|
|
186
214
|
export declare const performanceLogger: (options?: PerformanceLoggerOptions) => TraceLogger;
|
|
187
215
|
//#endregion
|
|
188
216
|
//#region src/trace/plugin.d.ts
|
|
217
|
+
type TracePluginOptions = {
|
|
218
|
+
/**
|
|
219
|
+
* Record where each `bus.on` and `bus.once` was called (`file:line:column`) and put it on the
|
|
220
|
+
* events of the listener. Reads one stack at registration, never on emit. An anonymous listener
|
|
221
|
+
* is reported too, under the type of its event. Default `false`.
|
|
222
|
+
*/
|
|
223
|
+
source?: boolean;
|
|
224
|
+
};
|
|
189
225
|
/**
|
|
190
226
|
* Names and groups what the trace shows, on a bus that has a tracer:
|
|
191
227
|
*
|
|
@@ -213,7 +249,7 @@ export declare const performanceLogger: (options?: PerformanceLoggerOptions) =>
|
|
|
213
249
|
* bus.on("name/changed", saveName, { traceId: "saveNotifier" });
|
|
214
250
|
* ```
|
|
215
251
|
*/
|
|
216
|
-
export declare const tracePlugin: () => <E extends EventMap>(bus: Bus<E>) => Bus<E>;
|
|
252
|
+
export declare const tracePlugin: (options?: TracePluginOptions) => <E extends EventMap>(bus: Bus<E>) => Bus<E>;
|
|
217
253
|
//#endregion
|
|
218
254
|
//#region src/trace/recorder.d.ts
|
|
219
255
|
type Recorder<E = Record<string, unknown>> = TraceLogger<E> & {
|
|
@@ -388,4 +424,4 @@ type TracerOptions = {
|
|
|
388
424
|
*/
|
|
389
425
|
export declare const createTracer: <E = Record<string, unknown>>(trace: Trace<E>, options?: TracerOptions) => Tracer;
|
|
390
426
|
//#endregion
|
|
391
|
-
export type { ConsoleLoggerOptions, EffectName, ExportOptions, Finding, GraphLogger, GraphLoggerOptions, GraphSnapshot, JsonOptions, JsonValue, PayloadMode, PerformanceLoggerOptions, Phase, PolicyStage, PolicyStages, Recorder, ReduxDevtoolsOptions, ReduxDevtoolsState, ReduxExtension, SerializeOptions, TaskInfo, Trace, TraceEvent, TraceLogger, Tracer, TracerOptions };
|
|
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 };
|
package/dist/trace.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import{n as e
|
|
2
|
-
`)},
|
|
3
|
-
`)},b=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`,x=(e,t)=>{let n=t===void 0?`[nanoeffects]`:`[nanoeffects ${t}]`,r=e.traceGroupId===void 0?``:` (flow ${e.traceGroupId})`,i=e.kind===`cycle`?`cycle: ${e.path.join(` → `)} (repeated ${e.repeats} times, started at ${e.root})`:`${e.kind}: ${e.path.join(` → `)} (${e.repeats} in a row, started at ${e.root})`,a=[...new Set(e.path)].flatMap(e=>{let t=u(e);return t===void 0?[]:[`${e} at ${t}`]});return`${n} ${i}${r}${a.length>0?`\n ${a.join(`; `)}`:``}\n ${b(e)}`},S=(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})}},C=2e3,w=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`},T=e=>{let t=Object.getPrototypeOf(e);return t===null||t===Object.prototype},E=e=>typeof Reflect.get(e,`nodeType`)==`number`&&typeof Reflect.get(e,`nodeName`)==`string`,D=e=>e.length>C?`${e.slice(0,C)}…`:e,O=e=>{switch(typeof e){case`string`:return D(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}},k=e=>{if(e instanceof WeakMap)return`[WeakMap]`;if(e instanceof WeakSet)return`[WeakSet]`;if(e instanceof Promise)return`[Promise]`;if(E(e))return`[DOM ${String(Reflect.get(e,`nodeName`))}]`;if(e instanceof Date)return Number.isNaN(e.getTime())?`[Invalid Date]`:e.toISOString()},A=(e,t,n)=>{if(t.budget<=0)return`[Cut]`;--t.budget;let r=O(e);return r===void 0?typeof e!=`object`||!e?`[undefined]`:j(e,t,n):r},j=(e,t,n)=>{try{let r=k(e);return r===void 0?t.path.has(e)?`[Circular]`:n>=t.maxDepth?Array.isArray(e)?`[Array]`:`[${w(e)}]`:M(e,t,n):r}catch{return`[Unserializable]`}},M=(e,t,n)=>{t.path.add(e);try{return N(e,t,n)}finally{t.path.delete(e)}},N=(e,t,n)=>e instanceof Error?P(e,t,n):Array.isArray(e)?F(e,t,n):e instanceof Map?{$map:F([...e.entries()],t,n)}:e instanceof Set?{$set:F([...e.values()],t,n)}:ArrayBuffer.isView(e)?{$class:w(e),length:Number(Reflect.get(e,`length`))}:I(e,t,n,T(e)?void 0:w(e)),P=(e,t,n)=>{let r={name:D(e.name),message:D(e.message)};return typeof e.stack==`string`&&(r.stack=D(e.stack)),`cause`in e&&(r.cause=A(e.cause,t,n+1)),r},F=(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(A(a,t,n+1))}return r},I=(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?A(a.value,t,n+1):`[Getter]`)}return i},L=(e,t={})=>{let n={maxDepth:t.maxDepth??6,budget:t.maxSize??1e3,path:/* @__PURE__ */ new Set};try{return A(e,n,0)}catch{return`[Unserializable]`}},R=(e,t)=>`${e}\u0000${t}`,z=e=>{if(typeof e==`function`)return e;let t=new Set(e);return e=>t.has(e)},B=e=>e.event===void 0?[e.node]:[e.event,e.node],V=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`)},H=(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(B),...B(t)],names:[n,...a.map(e=>e.node),n]}},U=(e={})=>{let t=e.repeats??3,n=e.maxSteps??5e3,r=e.storm===!1?void 0:e.storm??{count:100,perMs:1e3},i=z(e.ignore),a=e.now??(()=>globalThis.performance.now()),o=e.console??globalThis.console,s=/* @__PURE__ */ new Map,c=/* @__PURE__ */ new Map,l=/* @__PURE__ */ new Map,d=/* @__PURE__ */ new WeakMap,p=/* @__PURE__ */ new Map,m=/* @__PURE__ */ new Set,h=!1,g=!1,_,b,C=!1,w=!1,T=e=>{let t=s.get(e)??{count:0};t.count+=1,s.set(e,t)},E=(e,t)=>{let n=R(e,t),r=c.get(n)??{from:e,to:t,count:0,inCycle:!1};r.count+=1,c.set(n,r)},D=t=>{if(e.devtools&&!C){C=!0;let t=e.extension??globalThis.__REDUX_DEVTOOLS_EXTENSION__;b=S(t,e.name??`nanoeffects graph`)}b?.(t),e.onCycle?e.onCycle(t):o?.warn(x(t,e.name))},O=(e,t)=>{let n=l.get(e);if(n){n.turns+=1;return}let r=t();if(l.set(e,r),!w){w=!0;try{D(r)}catch(e){globalThis.console?.error(`[nanoeffects] graph logger report failed`,e)}finally{w=!1}}},k=(e,t)=>{let n=new Map(e.counts);n.set(t,(n.get(t)??0)+1),e.counts=n},A=(e,n)=>{let r=e.counts.get(e.node)??0;if(r<t||i(e.node))return;let{path:a,names:o}=H(e.parent,e);O(`cycle:${V(o)}`,()=>{let t={kind:`cycle`,path:a,repeats:r,turns:1,root:e.root.name,source:`chain`};return n.traceGroupId!==void 0&&(t.traceGroupId=n.traceGroupId),t})},j=e=>{e.root.steps+=1,!(e.root.steps<=n||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`})))},M=e=>{if(!r||i(e))return;let t=a(),n=(p.get(e)??[]).filter(e=>t-e<r.perMs);n.push(t),p.set(e,n),n.length<r.count?m.delete(e):m.has(e)||(m.add(e),O(`storm:${e}:${t}`,()=>({kind:`storm`,path:[e],repeats:n.length,turns:1,root:e,source:`chain`})))},N=(e,t)=>{let n=_;return n!==void 0&&n.parent===e&&n.type===t},P=(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}),F=()=>{g||(g=!0,queueMicrotask(()=>{h||o?.warn(`[nanoeffects] graphLogger got tasks but no emit event: add bus.use(tracePlugin()) for a finer graph.`)}))},I=(e,t,n,r)=>{k(t,e.type),(e.leaf===!0||r||!h)&&(T(e.type),n?E(n.node,e.type):M(e.type)),e.leaf||i(e.type)||(j(t),A(t,e))},B=e=>{let t=e.parent?d.get(e.parent):void 0;e.leaf?(h=!0,_={parent:t,type:e.type}):F();let n=e.emitted===!1,r=!e.leaf&&h&&!n&&!N(t,e.type),i=r&&!t,a=P(e,t,n||r&&!i);d.set(e.ref,a),a.inner||I(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||A(e,t)},W=e=>{let t=d.get(e.ref);t?U(t,e):B(e)},G=e=>{if(e.phase!==`done`&&e.phase!==`error`)return;let t=s.get(e.type);t&&(t.last=e.phase===`error`?{payload:e.payload,error:e.error}:{payload:e.payload})},K=()=>{let t=J(),n=/* @__PURE__ */ new Set;for(let e of t)q(e,n);let r={nodes:[...s].map(([e,t])=>{let n=u(e);return n===void 0?{name:e,count:t.count}:{name:e,count:t.count,site:n}}),edges:[...c.values()].map(e=>({...e,inCycle:n.has(R(e.from,e.to))})),cycles:t};return e.name!==void 0&&(r.name=e.name),r},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(R(r,i))}return}let n=new Set(e.path);for(let e of c.values())n.has(e.from)&&n.has(e.to)&&t.add(R(e.from,e.to))},J=()=>{let e=[...l.values()].filter(e=>e.kind===`cycle`),t=new Set(e.flatMap(e=>e.path)),n=[],r=[...c.values()].filter(e=>!(i(e.from)||i(e.to)));for(let e of f(r))e.every(e=>t.has(e))||n.push({kind:`cycle`,path:e,repeats:e.length,turns:0,root:e[0]??``,source:`graph`});return[...l.values(),...n.filter(t=>!e.some(e=>e.path.join()===t.path.join()))]};return Object.assign(G,{task:W,cycles:()=>J(),toMermaid:e=>v(K(),e),toDot:e=>y(K(),e),toJSON:(e={})=>{let t=K();if(!e.payload)return t;for(let e of t.nodes){let t=s.get(e.name)?.last;t&&(e.last={payload:L(t.payload),...t.error!==void 0&&{error:L(t.error)}})}return t},clear:()=>{s=/* @__PURE__ */ new Map,c=/* @__PURE__ */ new Map,l=/* @__PURE__ */ new Map,d=/* @__PURE__ */ new WeakMap,p=/* @__PURE__ */ new Map,m=/* @__PURE__ */ new Set,_=void 0}})},W=(e={})=>{let t=e.console??globalThis.console,n=e.prefix??`[nanoeffects]`,r=/* @__PURE__ */ new Map;return({id:e,parentId:i,type:a,effect:o,phase:s,payload:c,error:l,traceGroupId:u})=>{r.has(e)||r.set(e,typeof i==`number`?(r.get(i)??0)+1:0);let d=` `.repeat(r.get(e)??0);s!==`start`&&r.delete(e);let f=u===void 0?``:` [${u}]`,p=`${n}${f} ${d}${a} · ${o} ${s}`;s===`error`?t?.error(p,l):t?.debug(p,c)}},G=e=>{if(e instanceof Error)return`${e.name}: ${e.message}`;let t=L(e);return typeof t==`string`?t:JSON.stringify(t)},K=(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:G(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})}};let q;const J=()=>q,Y=(e,t)=>{let n=q;q=e;try{return t()}finally{q=n}},X=(e,t,n={})=>{let{trace:r}=e;if(!r){e.emit(t);return}let{traceId:a,traceGroupId:o}=n,{ctx:c,unlink:l}=i({event:{type:a??t.type,payload:t,...o===void 0?{}:{traceGroupId:o}},bus:{emit:e.emit,on:e.on,off:e.off,ctx:e.ctx,addContext:e.addContext},tracer:r});s(c,`run`,`start`);try{c.emit(t),s(c,`run`,`done`)}catch(e){throw s(c,`run`,`error`,e),e}finally{l()}},Z=e=>typeof e==`string`?e:Reflect.get(Object(e),`type`),Q=(e,t)=>typeof e==`object`?Reflect.get(Object(e),`payload`):t[0],$=()=>e=>{if(!(r&&e.trace))return e;let{trace:t}=e,i=(e,n)=>{let r=t.task({type:Z(e),payload:Q(e,n),leaf:!0},o());t.report(r,`emit`,`done`)},s=e.emit;e.emit=(e,...t)=>Y(e,()=>(i(e,t),s(e,...t))),Reflect.set(e,l,(t,n)=>{X(e,t,n)});let c=/* @__PURE__ */ new WeakMap,u=(e,t,n)=>{let r=c.get(t)??/* @__PURE__ */ new Map,i=Z(e);r.set(i,[...r.get(i)??[],n]),c.set(t,r)},d=(e,t)=>(c.get(t)?.get(Z(e)))?.pop(),f=(e,t)=>(r,i)=>(n(i,t),a(i,`run`,()=>e(r,i))),p=(e,t)=>(n,r,i)=>{let a=Reflect.get(Object(i),`traceId`);if(typeof a!=`string`)return e(n,r,i);let o=t?(e,t)=>(d(n,r),f(r,a)(e,t)):f(r,a);u(n,r,o);let s=e(n,o,i);return()=>{s()}},m=e.off,h=(e,t)=>{t===void 0?m(e):m(e,d(e,t)??t)},g=e.on,_=e.once;return e.on=p(g,!1),e.once=p(_,!0),e.off=h,e},ee=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=>L(e))});return n>0&&Object.defineProperty(c,"events",{get:s}),c},te=()=>{},ne=/* @__PURE__ */ new Set([`cancel`,`drop`,`error`]),re=()=>!1,ie=()=>!0,ae=e=>{if(typeof e==`function`)return e;if(Array.isArray(e)){let t=new Set(e);return({phase:e})=>t.has(e)}return e?ie:re},oe=(e={})=>{let t=e.extension??globalThis.__REDUX_DEVTOOLS_EXTENSION__;if(!t)return te;let{starts:n=!1,depth:r=0,maxAge:i}=e,a=ae(e.policies),o=e.payload??`essential`,s=e.serialize===!1?e=>e:e.serialize??(e=>L(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`||ne.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)}},se=t=>{let n=typeof t==`function`?[t]:[...t??[]];return n.length===0?null:t=>{for(let r of n)try{r(t)}catch(t){e(`trace logger failed`,t)}}},ce=()=>{},le=e=>{if(typeof e.traceGroupId==`string`)return e.traceGroupId;let t=J();if(typeof t!=`object`&&typeof t!=`function`||t===null)return;let n=Reflect.get(t,d),r=typeof n==`object`&&n?Reflect.get(n,`traceGroupId`):void 0;return typeof r==`string`?r:void 0},ue=(n,r={})=>{let i=n,a=se(i),{filter:s,sample:l=1}=r,u=0,d=(Array.isArray(i)?i:[i]).flatMap(e=>e.task?[e.task]:[]),f=t=>{let n={ref:t,id:t.id,type:t.type};t.parentRef&&(n.parent=t.parentRef),t.event!==void 0&&(n.event=t.event),t.leaf&&(n.leaf=!0),n.emitted=t.emitted,t.group!==void 0&&(n.traceGroupId=t.group);for(let t of d)try{t(n)}catch(t){e(`trace logger failed`,t)}},p=e=>typeof l==`function`?l({type:e.type,payload:e.payload}):Math.random()<l,m={task:(e,t)=>{let n={id:++u,root:t?.root??u,parent:t?.id,parentRef:d.length>0?t:void 0,leaf:e.leaf,emitted:o()===t,type:e.type,payload:e.payload,sampled:t?.sampled??p(e),group:le(e)??t?.group};return d.length>0&&n.sampled&&f(n),n},rename:(e,t)=>{e.event??=e.type,e.type=t,d.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),n===`error`&&(i.error=r),(!s||s(i))&&a?.(i)},settler:(e,t)=>{if(!e.sampled)return ce;let n=!1;return(r,i)=>{n||(n=!0,m.report(e,t,r,i))}},run:(e,n,r,i)=>{if(!e.sampled)return i();let a=m.settler(e,n);m.report(e,n,`start`),t({signal:r},()=>a(`cancel`));let o;try{o=i()}catch(e){throw a(`error`,e),e}return c(o)?Promise.resolve(o).then(e=>(a(`done`),e),e=>{throw a(`error`,e),e}):(a(`done`),o)}};return m};export{W as consoleLogger,ee as createTraceRecorder,ue as createTracer,U as graphLogger,K as performanceLogger,oe as reduxDevtools,L as safeSerialize,$ as tracePlugin};
|
|
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(`
|
|
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};
|
|
@@ -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=(e,t)=>e!==void 0||!b?e:typeof t==`string`?t:void 0,w=(e,t,n)=>{let r=x(e.name,t),i=typeof r==`string`?m(e.handler,r,e.source):e.handler;return n?(n,r)=>(v(p,t,e),i(n,r)):i},T=(n,r)=>{let i=(e,i,a)=>{let o=k(e),s=b?O(t(),2):void 0,c={handler:i,wrapper:i,name:g(i,a),source:s};c.wrapper=w(c,o,r),_(p,o,c);let l=n(e,c.wrapper,a);return()=>{v(p,o,c),l()}};return(t,n,r)=>e(()=>i(t,n,r))},E=i.off,D=(e,t)=>{if(e===void 0)p.clear();else if(t===void 0)p.delete(k(e));else{let n=y(p,k(e),t);n&&v(p,k(e),n),E(e,n?.wrapper??t);return}E(e,t)},j=i.on,M=i.once;return i.on=T(j,!1),i.once=T(M,!0),i.off=D,i},M=e=>{let t=typeof e==`function`?[e]:[...e??[]];return t.length===0?null:e=>{for(let n of t)try{n(e)}catch(e){r(`trace logger failed`,e)}}},N=()=>{},P=e=>{if(typeof e.traceGroupId==`string`)return e.traceGroupId;let t=x();if(typeof t!=`object`&&typeof t!=`function`||t===null)return;let n=Reflect.get(t,p),r=typeof n==`object`&&n?Reflect.get(n,`traceGroupId`):void 0;return typeof r==`string`?r:void 0},F=(e,t={})=>{let n=e,a=M(n),{filter:o,sample:s=1}=t,c=0,u=(Array.isArray(n)?n:[n]).flatMap(e=>e.task?[e.task]:[]),f=e=>{let t={ref:e,id:e.id,type:e.type};e.parentRef&&(t.parent=e.parentRef),e.event!==void 0&&(t.event=e.event),e.leaf&&(t.leaf=!0),t.emitted=e.emitted,e.group!==void 0&&(t.traceGroupId=e.group),e.source!==void 0&&(t.source=e.source);for(let e of u)try{e(t)}catch(e){r(`trace logger failed`,e)}},p=e=>typeof s==`function`?s({type:e.type,payload:e.payload}):Math.random()<s,m={task:(e,t)=>{let n={id:++c,root:t?.root??c,parent:t?.id,parentRef:u.length>0?t:void 0,leaf:e.leaf,emitted:l()===t,type:e.type,payload:e.payload,sampled:t?.sampled??p(e),group:P(e)??t?.group};return u.length>0&&n.sampled&&f(n),n},rename:(e,t,n)=>{e.event??=e.type,e.type=t,n!==void 0&&(e.source=n),u.length>0&&e.sampled&&f(e)},report:(e,t,n,r)=>{if(!e.sampled)return;let i={id:e.id,type:e.type,effect:t,phase:n,payload:e.payload};typeof e.parent==`number`&&(i.parentId=e.parent),e.root!==e.id&&(i.rootId=e.root),e.group!==void 0&&(i.traceGroupId=e.group),e.source!==void 0&&(i.source=e.source),n===`error`&&(i.error=r),(!o||o(i))&&a?.(i)},settler:(e,t)=>{if(!e.sampled)return N;let n=!1;return(r,i)=>{n||(n=!0,m.report(e,t,r,i))}},run:(e,t,n,r)=>{if(!e.sampled)return r();let a=m.settler(e,t);m.report(e,t,`start`),i({signal:n},()=>a(`cancel`));let o;try{o=r()}catch(e){throw a(`error`,e),e}return d(o)?Promise.resolve(o).then(e=>(a(`done`),e),e=>{throw a(`error`,e),e}):(a(`done`),o)}};return m};export{j as n,g as r,F as t};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { H as EventMap, c as Scope, l as WithFields, p as Effects } from "./action-creator-
|
|
2
|
-
import { t as BoundOf } from "./plugin-
|
|
1
|
+
import { H as EventMap, c as Scope, l as WithFields, p as Effects } from "./action-creator-DprJV-pN.js";
|
|
2
|
+
import { t as BoundOf } from "./plugin-2d3uuW7o.js";
|
|
3
3
|
//#region src/scope/types.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* The slots `scopePlugin` fills for `modulePlugin`. They are not on `Bus`: a bus
|
package/dist/vue.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { H as EventMap, T as EventKey, a as FreeActionCreator, t as ActionCreator, v as Bus } from "./action-creator-
|
|
1
|
+
import { H as EventMap, T as EventKey, a as FreeActionCreator, t as ActionCreator, v as Bus } from "./action-creator-DprJV-pN.js";
|
|
2
2
|
import { t as OriginOptions } from "./origin-key-CxK0SkeG.js";
|
|
3
3
|
import { t as StateOf } from "./state-of-C19H6BoV.js";
|
|
4
4
|
import { ShallowRef, WatchSource, WatchStopHandle } from "vue";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{n as e}from"./context-
|
|
1
|
+
import{n as e}from"./context-Dp_QRXzb.js";const t=Reflect.get(Symbol,`dispose`),n=n=>{let r=new AbortController,i=async(...t)=>{let i=t[0],{ctx:a,unlink:o}=e({event:{type:`withPolicy`,payload:i},parent:{signal:r.signal}});try{return await n(i,a)}finally{o()}},a=e=>{r.abort(e),r=new AbortController},o=Object.assign(i,{cancel:a});return t&&Reflect.set(o,t,()=>a()),o};export{n as t};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { G as PayloadOf, H as EventMap, q as Task } from "./action-creator-
|
|
1
|
+
import { G as PayloadOf, H as EventMap, q as Task } from "./action-creator-DprJV-pN.js";
|
|
2
2
|
//#region src/policies/with-policy.d.ts
|
|
3
3
|
/** What `withPolicy` returns: the task as a plain function that can be cancelled. */
|
|
4
4
|
type PolicyFunction<P, R> = {
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Debugging compared with RxJS, Effector and mitt
|
|
2
|
+
|
|
3
|
+
> **Level F.** Read first: [Trace tasks](tracing.md#attach-a-tracer).
|
|
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).
|
|
6
|
+
|
|
7
|
+
## On this page
|
|
8
|
+
|
|
9
|
+
- [Compare what you can see](#compare-what-you-can-see)
|
|
10
|
+
- [Find who listens to an event](#find-who-listens-to-an-event)
|
|
11
|
+
- [Find which task began a chain](#find-which-task-began-a-chain)
|
|
12
|
+
- [Find out why a request stopped](#find-out-why-a-request-stopped)
|
|
13
|
+
- [Know what is missing](#know-what-is-missing)
|
|
14
|
+
|
|
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
|
+
## Find who listens to an event
|
|
29
|
+
|
|
30
|
+
**Problem.** You read an emit and want to know what it starts.
|
|
31
|
+
|
|
32
|
+
Name the listeners with functions or a `traceId`, use `tracePlugin`, and call `describeBus`:
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
import { createBus } from "nanoeffects";
|
|
36
|
+
import { consoleLogger, createTracer, describeBus, tracePlugin } from "nanoeffects/trace";
|
|
37
|
+
|
|
38
|
+
function saveName(name) {
|
|
39
|
+
console.log("save", name);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function audit(name) {
|
|
43
|
+
console.log("audit", name);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const trace = createTracer(consoleLogger());
|
|
47
|
+
|
|
48
|
+
const bus = createBus({ trace })
|
|
49
|
+
.use(tracePlugin());
|
|
50
|
+
|
|
51
|
+
bus.on("name/changed", saveName);
|
|
52
|
+
bus.on("name/changed", audit);
|
|
53
|
+
|
|
54
|
+
// describeBus: the listeners of each event, in the order they run
|
|
55
|
+
console.log(describeBus(bus));
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
{ "name/changed": ["saveName", "audit"] }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Details: [List who listens](tracing.md#list-who-listens).
|
|
63
|
+
|
|
64
|
+
## Find which task began a chain
|
|
65
|
+
|
|
66
|
+
**Problem.** A request ran and you do not know what asked for it.
|
|
67
|
+
|
|
68
|
+
Every task of a trace has a `rootId` and a `parentId`. The recorder keeps them, so you can walk up from the request to the emit that began it:
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
import { createBus } from "nanoeffects";
|
|
72
|
+
import { createTraceRecorder, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
73
|
+
|
|
74
|
+
// createTraceRecorder: keeps the last events in memory
|
|
75
|
+
const recorder = createTraceRecorder();
|
|
76
|
+
const trace = createTracer(recorder);
|
|
77
|
+
|
|
78
|
+
const bus = createBus({ trace })
|
|
79
|
+
.use(tracePlugin());
|
|
80
|
+
|
|
81
|
+
function loadUser(id, ctx) {
|
|
82
|
+
ctx.emit("user/loading", id);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
bus.on("user/open", loadUser);
|
|
86
|
+
bus.emit("user/open", 7);
|
|
87
|
+
|
|
88
|
+
// each event says which task is its parent
|
|
89
|
+
for (const event of recorder.events) {
|
|
90
|
+
console.log(event.id, event.parentId, event.type, event.phase);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Details: [Show which subscription began a chain](tracing.md#show-which-subscription-began-a-chain) and [Keep the chain: `ctx.emit`, not `bus.emit`](tracing.md#keep-the-chain-ctxemit-not-busemit).
|
|
95
|
+
|
|
96
|
+
## Find out why a request stopped
|
|
97
|
+
|
|
98
|
+
**Problem.** A search request never finished and no error appeared.
|
|
99
|
+
|
|
100
|
+
A policy that stops a call reports `cancel` or `drop` in the trace. Pick the call and read its last phase:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
import { createBus } from "nanoeffects";
|
|
104
|
+
import { latest } from "nanoeffects/policies";
|
|
105
|
+
import { createTraceRecorder, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
106
|
+
|
|
107
|
+
const recorder = createTraceRecorder();
|
|
108
|
+
const trace = createTracer(recorder);
|
|
109
|
+
|
|
110
|
+
const bus = createBus({ trace })
|
|
111
|
+
.use(tracePlugin());
|
|
112
|
+
|
|
113
|
+
// latest: a new search stops the one that is still running
|
|
114
|
+
const search = latest(async (query, ctx) => {
|
|
115
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
116
|
+
ctx.emit("search/loaded", query);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
bus.on("search/changed", search, { traceId: "search" });
|
|
120
|
+
bus.emit("search/changed", "a");
|
|
121
|
+
bus.emit("search/changed", "ab");
|
|
122
|
+
|
|
123
|
+
// a "cancel" phase on the first call says that the second one stopped it
|
|
124
|
+
const phases = recorder.events.map((event) => `${event.type}:${event.phase}`);
|
|
125
|
+
console.log(phases);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Details: [Trace tasks](tracing.md) and [Cancel stale requests](cancel-stale-requests.md).
|
|
129
|
+
|
|
130
|
+
## Know what is missing
|
|
131
|
+
|
|
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.
|
|
133
|
+
- **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
|
+
- **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
|
+
- **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.
|
|
136
|
+
- **No replay and no time travel.** The recorder keeps what happened and cannot run it again.
|
|
@@ -194,4 +194,4 @@ The subscription is not tied to a task. Call `subscription.unsubscribe()` when t
|
|
|
194
194
|
- **Abort replaces unsubscribe for work in flight.** A stopped task is stopped through its `signal`. Pass it to `fetch` or to the client.
|
|
195
195
|
- **Errors are not part of the stream.** A failed task rejects its own promise. Without `onError` on the bus nobody else hears it ([Errors](errors.md)).
|
|
196
196
|
- **No completion.** An event bus does not end. A task ends when its work ends or its signal aborts.
|
|
197
|
-
- **
|
|
197
|
+
- **Marble tests are a smaller tool.** [`createMarbleBus`](testing.md) writes the emits and the phases of tasks as diagrams on fake timers. It has no hot or cold sources and no `|`.
|
package/docs/package.md
CHANGED
|
@@ -31,6 +31,7 @@ Each row is optional. The "when" column says what need brings you to it.
|
|
|
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
33
|
| `nanoeffects/trace` | `createTracer`, the loggers (`graphLogger` finds event loops) and `tracePlugin` (names for the trace) | you want to see what ran and why |
|
|
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 |
|
|
34
35
|
| `nanoeffects/guard` | `loopGuardPlugin`: refuses the emit that would pass a limit of a chain | a loop must not take the app down |
|
|
35
36
|
|
|
36
37
|
The `...Plugin` names above are used with `bus.use`. [Plugins](plugins.md) lists all of them, what each returns, and the order to use them in.
|
package/docs/recipes.md
CHANGED
|
@@ -29,6 +29,7 @@ Each task below starts with the problem, then a short solution. Where the page h
|
|
|
29
29
|
- [Do not leave subscriptions after a widget closes](#do-not-leave-subscriptions-after-a-widget-closes)
|
|
30
30
|
- [Stop work on a server](#stop-work-on-a-server)
|
|
31
31
|
- [Debugging: what ran and why](#debugging-what-ran-and-why)
|
|
32
|
+
- [Test a policy with fake timers](#test-a-policy-with-fake-timers)
|
|
32
33
|
- [Other libraries in the project](#other-libraries-in-the-project)
|
|
33
34
|
- [Classes and TypeScript](#classes-and-typescript)
|
|
34
35
|
- [Use the bus in a class](#use-the-bus-in-a-class)
|
|
@@ -480,6 +481,40 @@ bus.emit("search", "nano"); // the log shows: search · emit, then results · em
|
|
|
480
481
|
|
|
481
482
|
More: [Trace tasks](tracing.md). To reload modules without a page refresh, see [Hot reload](hmr.md).
|
|
482
483
|
|
|
484
|
+
## Test a policy with fake timers
|
|
485
|
+
|
|
486
|
+
A test of `latest` or `debounce` is full of `advanceTimersByTime` numbers, and nobody can tell from them what the test proves. Write the emits and the phases as diagrams instead.
|
|
487
|
+
|
|
488
|
+
```js
|
|
489
|
+
import { it, vi } from "vitest";
|
|
490
|
+
import { latest } from "nanoeffects/policies";
|
|
491
|
+
import { createMarbleBus, fakeTimersClock } from "nanoeffects/testing";
|
|
492
|
+
|
|
493
|
+
// a search that takes 50 ms
|
|
494
|
+
function search(query) {
|
|
495
|
+
return new Promise((resolve) => setTimeout(resolve, 50));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
it("latest cancels the search that is running", async () => {
|
|
499
|
+
vi.useFakeTimers();
|
|
500
|
+
|
|
501
|
+
// createMarbleBus: a bus that records its trace; a frame is 10 ms
|
|
502
|
+
const marbles = createMarbleBus({ clock: fakeTimersClock(vi) });
|
|
503
|
+
marbles.bus.on("search/changed", latest(search), { traceId: "search" });
|
|
504
|
+
|
|
505
|
+
// "a-b": two emits, at frame 0 and frame 2
|
|
506
|
+
const result = await marbles.run({
|
|
507
|
+
emits: { "search/changed": { marble: "a-b" } },
|
|
508
|
+
frames: 12,
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// s start, c cancel, d done
|
|
512
|
+
result.expect({ "search/changed:latest": "s-(cs)----d----" });
|
|
513
|
+
});
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
More: [Test time with diagrams](testing.md).
|
|
517
|
+
|
|
483
518
|
## Other libraries in the project
|
|
484
519
|
|
|
485
520
|
- **Redux Toolkit, Zustand, MobX or Pinia.** [Connect a store](stores.md).
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Отладка в сравнении с RxJS, Effector и mitt
|
|
2
|
+
|
|
3
|
+
> **[Уровень F](README.md#виды-проектов)**: платформа, SDK или долгоживущее приложение. Сначала прочитайте: [Трассировка задач](tracing.md#подключение-трассировщика).
|
|
4
|
+
|
|
5
|
+
Прочитайте эту страницу, если знаете RxJS, Effector или mitt и хотите понять, что nanoeffects даёт, когда что-то идёт не так, а чего не даёт. Сначала сравнивается, что показывает каждый инструмент, затем разбираются три вопроса, которые вы задаёте отладчику. Это не руководство по переходу: для него есть страницы про [RxJS](migrating-from-rxjs.md), [Effector](migrating-from-effector.md) и [mitt](migrating-from-mitt.md).
|
|
6
|
+
|
|
7
|
+
## На этой странице
|
|
8
|
+
|
|
9
|
+
- [Сравнение того, что видно](#сравнение-того-что-видно)
|
|
10
|
+
- [Кто слушает событие](#кто-слушает-событие)
|
|
11
|
+
- [Какая задача начала цепочку](#какая-задача-начала-цепочку)
|
|
12
|
+
- [Почему запрос остановился](#почему-запрос-остановился)
|
|
13
|
+
- [Чего не хватает](#чего-не-хватает)
|
|
14
|
+
|
|
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
|
+
## Кто слушает событие
|
|
29
|
+
|
|
30
|
+
**Проблема.** Вы читаете emit и хотите знать, что он запускает.
|
|
31
|
+
|
|
32
|
+
Назовите обработчики функциями или через `traceId`, подключите `tracePlugin` и вызовите `describeBus`:
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
import { createBus } from "nanoeffects";
|
|
36
|
+
import { consoleLogger, createTracer, describeBus, tracePlugin } from "nanoeffects/trace";
|
|
37
|
+
|
|
38
|
+
function saveName(name) {
|
|
39
|
+
console.log("save", name);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function audit(name) {
|
|
43
|
+
console.log("audit", name);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const trace = createTracer(consoleLogger());
|
|
47
|
+
|
|
48
|
+
const bus = createBus({ trace })
|
|
49
|
+
.use(tracePlugin());
|
|
50
|
+
|
|
51
|
+
bus.on("name/changed", saveName);
|
|
52
|
+
bus.on("name/changed", audit);
|
|
53
|
+
|
|
54
|
+
// describeBus: обработчики каждого события в порядке запуска
|
|
55
|
+
console.log(describeBus(bus));
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
{ "name/changed": ["saveName", "audit"] }
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Подробнее: [Список подписчиков события](tracing.md#список-подписчиков-события).
|
|
63
|
+
|
|
64
|
+
## Какая задача начала цепочку
|
|
65
|
+
|
|
66
|
+
**Проблема.** Запрос выполнился, а вы не знаете, что его запросило.
|
|
67
|
+
|
|
68
|
+
У каждой задачи трассировки есть `rootId` и `parentId`. Recorder хранит их, поэтому от запроса можно подняться к emit, который начал цепочку:
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
import { createBus } from "nanoeffects";
|
|
72
|
+
import { createTraceRecorder, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
73
|
+
|
|
74
|
+
// createTraceRecorder: хранит последние события в памяти
|
|
75
|
+
const recorder = createTraceRecorder();
|
|
76
|
+
const trace = createTracer(recorder);
|
|
77
|
+
|
|
78
|
+
const bus = createBus({ trace })
|
|
79
|
+
.use(tracePlugin());
|
|
80
|
+
|
|
81
|
+
function loadUser(id, ctx) {
|
|
82
|
+
ctx.emit("user/loading", id);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
bus.on("user/open", loadUser);
|
|
86
|
+
bus.emit("user/open", 7);
|
|
87
|
+
|
|
88
|
+
// каждое событие говорит, какая задача его родитель
|
|
89
|
+
for (const event of recorder.events) {
|
|
90
|
+
console.log(event.id, event.parentId, event.type, event.phase);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Подробнее: [Какая подписка начала цепочку](tracing.md#какая-подписка-начала-цепочку) и [Сохранение цепочки: `ctx.emit` вместо `bus.emit`](tracing.md#сохранение-цепочки-ctxemit-вместо-busemit).
|
|
95
|
+
|
|
96
|
+
## Почему запрос остановился
|
|
97
|
+
|
|
98
|
+
**Проблема.** Запрос поиска не завершился, а ошибки нет.
|
|
99
|
+
|
|
100
|
+
Политика, которая останавливает вызов, пишет в трассировку `cancel` или `drop`. Найдите вызов и прочитайте его последнюю фазу:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
import { createBus } from "nanoeffects";
|
|
104
|
+
import { latest } from "nanoeffects/policies";
|
|
105
|
+
import { createTraceRecorder, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
106
|
+
|
|
107
|
+
const recorder = createTraceRecorder();
|
|
108
|
+
const trace = createTracer(recorder);
|
|
109
|
+
|
|
110
|
+
const bus = createBus({ trace })
|
|
111
|
+
.use(tracePlugin());
|
|
112
|
+
|
|
113
|
+
// latest: новый поиск останавливает тот, что ещё выполняется
|
|
114
|
+
const search = latest(async (query, ctx) => {
|
|
115
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
116
|
+
ctx.emit("search/loaded", query);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
bus.on("search/changed", search, { traceId: "search" });
|
|
120
|
+
bus.emit("search/changed", "a");
|
|
121
|
+
bus.emit("search/changed", "ab");
|
|
122
|
+
|
|
123
|
+
// фаза "cancel" у первого вызова говорит, что его остановил второй
|
|
124
|
+
const phases = recorder.events.map((event) => `${event.type}:${event.phase}`);
|
|
125
|
+
console.log(phases);
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Подробнее: [Трассировка задач](tracing.md) и [Отмена устаревших запросов](cancel-stale-requests.md).
|
|
129
|
+
|
|
130
|
+
## Чего не хватает
|
|
131
|
+
|
|
132
|
+
- **Нет расширения для браузера и своего интерфейса.** Используйте [Redux DevTools](tracing.md#настройка-redux-devtools), консоль или панель Performance. У Effector для этого есть inspector.
|
|
133
|
+
- **Нет карты по исходному коду.** `describeBus` показывает обработчики, зарегистрированные к этому моменту, а `graphLogger` — события, которые произошли. Обработчик, который ещё не зарегистрирован, на карте не появится.
|
|
134
|
+
- **Нет перехода из лога в код.** С `tracePlugin({ source: true })` событие несёт `file:line:column` своего обработчика ([Строка кода обработчика](tracing.md#строка-кода-обработчика)), но ссылку в интерфейсе вы делаете сами.
|
|
135
|
+
- **Нет имён от компилятора.** Имена функций дают имя бесплатно; минификатор может их сократить, поэтому используйте `traceId`, где имя должно быть стабильным.
|
|
136
|
+
- **Нет повтора и путешествий во времени.** Recorder хранит то, что случилось, и не умеет запускать это заново.
|