nanoeffects 0.32.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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};
package/docs/actions.md CHANGED
@@ -143,15 +143,21 @@ export const sendMail = createAction("mail/send");
143
143
  ```js
144
144
  // bus.js
145
145
  import { createBus } from "nanoeffects";
146
- import { actionsPlugin, setHandler } from "nanoeffects/actions";
147
- import { sendMail } from "./mail";
146
+ import { actionsPlugin } from "nanoeffects/actions";
148
147
 
149
148
  export const bus = createBus();
150
149
 
151
- // actionsPlugin — the registry of handlers of this bus; setHandler pairs an action with its work
152
- export const actions = bus.use(
153
- actionsPlugin([setHandler(sendMail, smtpSend)]),
154
- );
150
+ // actionsPlugin — the registry of handlers of this bus
151
+ export const actions = bus.use(actionsPlugin());
152
+ ```
153
+
154
+ ```js
155
+ // mail-handlers.js
156
+ import { actions } from "./bus";
157
+ import { sendMail } from "./mail";
158
+
159
+ // actions.handle — binds the work to the action on this bus
160
+ actions.handle(sendMail, smtpSend);
155
161
  ```
156
162
 
157
163
  ```js
@@ -165,7 +171,7 @@ unbind();
165
171
 
166
172
  A handler bound in the registry makes `bus.emit(sendMail(mail))` run it and return its result, the same as for an action that carries its handler. The listeners of the type are told first. With no handler bound, `sendMail` is an event again.
167
173
 
168
- The registry lives in `nanoeffects/actions`, so a bus without one carries none of that code. Without a registry, `emit` runs the handler that the action carries. `bus.use(actionsPlugin())` sets `bus.actions`. A second `actionsPlugin` binds its list in that registry and returns it. There is no `actions` option in `createBus`; assign a custom runner with `bus.actions = { run }`, and add `bound(type)` to it to answer for actions that have no handler of their own.
174
+ The registry lives in `nanoeffects/actions`, so a bus without one carries none of that code. Without a registry, `emit` runs the handler that the action carries. `bus.use(actionsPlugin())` sets `bus.actions`. A second `actionsPlugin` returns that registry. There is no `actions` option in `createBus`; assign a custom runner with `bus.actions = { run }`, and add `bound(type)` to it to answer for actions that have no handler of their own.
169
175
 
170
176
  ### The result type of a bound handler
171
177
 
@@ -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/guide.md CHANGED
@@ -377,7 +377,7 @@ Use strings for the plain case. Use objects when other code already builds actio
377
377
 
378
378
  **Problem.** The handler of an action is in the file of the feature, but a test needs a fake, or another layer owns the work. If the handler sits in `createAction`, the event file is tied to the work.
379
379
 
380
- **What to do.** Create the action without a handler: it is an event. Bind the real handler with `setHandler` when the bus is made. Later, `actions.handle` binds a newer handler that wins until you remove it.
380
+ **What to do.** Create the action without a handler: it is an event. Bind the real handler with `actions.handle` when the bus is made. A later `actions.handle` binds a newer handler that wins until you remove it.
381
381
 
382
382
  ```js
383
383
  // user.js
@@ -389,16 +389,16 @@ export const getUser = createAction("user/get"); // no handler yet: an event
389
389
  ```js
390
390
  // bus.js
391
391
  import { createBus } from "nanoeffects";
392
- import { actionsPlugin, setHandler } from "nanoeffects/actions";
392
+ import { actionsPlugin } from "nanoeffects/actions";
393
393
  import { getUser } from "./user";
394
394
 
395
395
  export const bus = createBus();
396
396
 
397
- // setHandler — binds the real handler to the action
398
- const realUser = setHandler(getUser, (id) => api.user(id));
399
-
400
397
  // actionsPlugin — the registry of bound handlers of this bus
401
- export const actions = bus.use(actionsPlugin([realUser]));
398
+ export const actions = bus.use(actionsPlugin());
399
+
400
+ // actions.handle — binds the real handler to the action
401
+ actions.handle(getUser, (id) => api.user(id));
402
402
  ```
403
403
 
404
404
  ```js
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
 
@@ -141,15 +141,21 @@ export const sendMail = createAction("mail/send");
141
141
  ```js
142
142
  // bus.js
143
143
  import { createBus } from "nanoeffects";
144
- import { actionsPlugin, setHandler } from "nanoeffects/actions";
145
- import { sendMail } from "./mail";
144
+ import { actionsPlugin } from "nanoeffects/actions";
146
145
 
147
146
  export const bus = createBus();
148
147
 
149
- // actionsPlugin — реестр обработчиков этой шины; setHandler связывает action с его работой
150
- export const actions = bus.use(
151
- actionsPlugin([setHandler(sendMail, smtpSend)]),
152
- );
148
+ // actionsPlugin — реестр обработчиков этой шины
149
+ export const actions = bus.use(actionsPlugin());
150
+ ```
151
+
152
+ ```js
153
+ // mail-handlers.js
154
+ import { actions } from "./bus";
155
+ import { sendMail } from "./mail";
156
+
157
+ // actions.handle — привязывает работу к action на этой шине
158
+ actions.handle(sendMail, smtpSend);
153
159
  ```
154
160
 
155
161
  ```js
@@ -163,7 +169,7 @@ unbind();
163
169
 
164
170
  Обработчик, привязанный в реестре, заставляет `bus.emit(sendMail(mail))` запустить его и вернуть его результат, как и для action со своим обработчиком. Подписки на этот тип получают сообщение первыми. Если обработчик не привязан, `sendMail` снова просто событие.
165
171
 
166
- Реестр живёт в `nanoeffects/actions`, поэтому шина без него этого кода не содержит. Без реестра `emit` запускает собственный обработчик action. `bus.use(actionsPlugin())` задаёт `bus.actions`. Второй `actionsPlugin` привязывает свой список в том же реестре и возвращает его. В `createBus` нет параметра `actions`. Собственный исполнитель назначьте через `bus.actions = { run }`, а добавив ему `bound(type)`, вы сможете отвечать за actions без собственного обработчика.
172
+ Реестр живёт в `nanoeffects/actions`, поэтому шина без него этого кода не содержит. Без реестра `emit` запускает собственный обработчик action. `bus.use(actionsPlugin())` задаёт `bus.actions`. Второй `actionsPlugin` возвращает тот же реестр. В `createBus` нет параметра `actions`. Собственный исполнитель назначьте через `bus.actions = { run }`, а добавив ему `bound(type)`, вы сможете отвечать за actions без собственного обработчика.
167
173
 
168
174
  ### Тип результата привязанного обработчика
169
175
 
@@ -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`, где имя должно быть стабильным.
@@ -16,6 +16,7 @@
16
16
  - [Обрыв цикла во время работы](#обрыв-цикла-во-время-работы)
17
17
  - [Падение теста при цикле](#падение-теста-при-цикле)
18
18
  - [Экспорт графа и payload](#экспорт-графа-и-payload)
19
+ - [Подписки, которые не запускались](#подписки-которые-не-запускались)
19
20
  - [Как находится цикл](#как-находится-цикл)
20
21
  - [Ограничения](#ограничения)
21
22
 
@@ -278,6 +279,84 @@ console.log(JSON.stringify(data));
278
279
 
279
280
  Payload может быть экземпляром класса, `Map`, объектом с циклической ссылкой или `Error`, которые обычный `JSON.stringify` не примет. Экспорт превращает их в обычный JSON: у `Error` остаются имя, сообщение, стек и причина, цикл становится `"[Circular]"`, `Map` — `{ "$map": [...] }`, а экземпляр класса сохраняет собственные поля и имя класса в `$class`. Геттеры не вызываются, а слишком глубокое или слишком большое значение обрезается.
280
281
 
282
+ ## Подписки, которые не запускались
283
+
284
+ Граф одного запуска показывает только то, что сделал этот запуск. Он не скажет, какие подписки есть в приложении, но ни один запуск до них не дошёл: мёртвый код или сценарий, который никто не тестирует. Чтобы их найти, объедините графы нескольких запусков и сравните результат с подписками шины.
285
+
286
+ Что нужно заранее: шина использует `tracePlugin` и `graphLogger` (см. [Поиск цикла](#поиск-цикла)), а у каждой подписки есть имя функции или `traceId`. У анонимной подписки нет узла в графе, поэтому о ней не сообщается.
287
+
288
+ 1. Каждый запуск пишет свой граф в файл.
289
+ 2. Скрипт читает файлы и объединяет их через `mergeGraphs`.
290
+ 3. `findOrphans` сравнивает объединённый граф с `describeBus`.
291
+
292
+ ```js
293
+ // bus.js
294
+ import { createBus } from "nanoeffects";
295
+ import { createTracer, graphLogger, tracePlugin } from "nanoeffects/trace";
296
+
297
+ // graphLogger — карта того, что выполнилось
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
+ // подписка названа по функции; findOrphans сопоставляет это имя
307
+ bus.on("cart/added", function recalcTotal() {});
308
+ bus.on("cart/cleared", function resetTotal() {});
309
+ ```
310
+
311
+ ```js
312
+ // graph-setup.js (setup-файл вашего тест-раннера)
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 — граф этого запуска как обычные данные
321
+ const data = graph.toJSON();
322
+ writeFileSync(`graphs/${process.pid}.json`, JSON.stringify(data));
323
+ });
324
+ ```
325
+
326
+ ```js
327
+ // orphans.js (запустите после тестов)
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 — счётчики складываются, поэтому подписка, выполнявшаяся в любом запуске, становится узлом
337
+ const all = mergeGraphs(...runs);
338
+
339
+ // describeBus — подписки, которые держит шина, по событиям
340
+ const described = describeBus(bus);
341
+
342
+ // findOrphans — подписки, которые объединённый граф ни разу не видел
343
+ console.log(findOrphans(described, all));
344
+ // [{ event: "cart/cleared", listener: "resetTotal", kind: "unemitted" }]
345
+ ```
346
+
347
+ У каждого элемента есть `kind`:
348
+
349
+ | `kind` | Что значит | Что проверить |
350
+ | ------ | ---------- | ------------- |
351
+ | `unemitted` | Ни в одном запуске никто не отправил это событие. | Нужно ли ещё событие? Не хватает ли сценария? |
352
+ | `unrun` | Событие отправлялось, а эта подписка не запускалась. | Условие или [политика](policies.md), из-за которых она не запускается. |
353
+
354
+ Читайте список как подсказку, а не как ошибку. При малом числе запусков в нём много ложных записей, и сигналом он становится, когда запуски покрывают основные сценарии. Ограничения:
355
+
356
+ - Две подписки с одним именем дают один узел, поэтому вторая никогда не попадёт в список. Дайте каждой своё имя или `traceId`.
357
+ - Объединяйте запуски одной сборки. Переименованная подписка попадёт в список.
358
+ - `mergeGraphs` не находит цикл, который замыкается только при объединении рёбер разных запусков. Смотрите `graph.cycles()` в каждом запуске.
359
+
281
360
  ## Как находится цикл
282
361
 
283
362
  `graphLogger` находит циклы двумя способами. Они дают два вида находок в `graph.cycles()`, которые различает поле `source`:
package/docs/ru/guide.md CHANGED
@@ -376,7 +376,7 @@ bus.emit(double(21)); // 42
376
376
 
377
377
  **Проблема.** Обработчик action лежит в файле фичи, а тесту нужна подделка, или работа принадлежит другому слою. Если положить обработчик в `createAction`, файл события окажется связан с работой.
378
378
 
379
- **Что делать.** Создайте action без обработчика: это событие. Настоящий обработчик привяжите через `setHandler` при создании шины. Позже `actions.handle` привязывает новый обработчик, который главнее, пока вы его не снимете.
379
+ **Что делать.** Создайте action без обработчика: это событие. Настоящий обработчик привяжите через `actions.handle` при создании шины. Следующий `actions.handle` привязывает новый обработчик, который главнее, пока вы его не снимете.
380
380
 
381
381
  ```js
382
382
  // user.js
@@ -388,16 +388,16 @@ export const getUser = createAction("user/get"); // обработчика по
388
388
  ```js
389
389
  // bus.js
390
390
  import { createBus } from "nanoeffects";
391
- import { actionsPlugin, setHandler } from "nanoeffects/actions";
391
+ import { actionsPlugin } from "nanoeffects/actions";
392
392
  import { getUser } from "./user";
393
393
 
394
394
  export const bus = createBus();
395
395
 
396
- // setHandler — привязывает настоящий обработчик к action
397
- const realUser = setHandler(getUser, (id) => api.user(id));
398
-
399
396
  // actionsPlugin — реестр привязанных обработчиков этой шины
400
- export const actions = bus.use(actionsPlugin([realUser]));
397
+ export const actions = bus.use(actionsPlugin());
398
+
399
+ // actions.handle — привязывает настоящий обработчик к action
400
+ actions.handle(getUser, (id) => api.user(id));
401
401
  ```
402
402
 
403
403
  ```js
@@ -26,7 +26,7 @@
26
26
  | `nanoeffects/svelte` | `sveltePlugin`: добавляет `eventStore` на шину (нужен Svelte 5 или новее) | компоненты Svelte |
27
27
  | `nanoeffects/vue` | `vuePlugin`: добавляет `watchEmitter` и `useEventRef` на шину (нужен Vue 3.3 или новее) | компоненты Vue |
28
28
  | `nanoeffects/dev` | `devPlugin` (в production ничего не делает), `getLostSignalsCount` и `getLostSignals` для тестов | нужно находить утечки подписок и потерянные сигналы при разработке и в тестах |
29
- | `nanoeffects/trace` | `createTracer`, логгеры (`graphLogger` находит циклы событий) и `tracePlugin` (имена для трассировки) | нужно видеть, что выполнилось и почему |
29
+ | `nanoeffects/trace` | `createTracer`, логгеры (`graphLogger` находит циклы событий; `mergeGraphs` и `findOrphans` находят подписки, до которых не дошёл ни один запуск) и `tracePlugin` (имена для трассировки) | нужно видеть, что выполнилось и почему |
30
30
  | `nanoeffects/testing` | `createMarbleBus`, `fakeTimersClock`: шина для тестов, где отправки и фазы записаны диаграммами | вы тестируете `latest`, `debounce`, `queue`, `retry` или `timeout` на поддельных таймерах |
31
31
  | `nanoeffects/guard` | `loopGuardPlugin`: отклоняет emit, который превысил бы предел цепочки | цикл не должен ронять приложение |
32
32