nanoeffects 0.29.0 → 0.31.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.
Files changed (127) hide show
  1. package/README.md +6 -5
  2. package/dist/{action-creator-jeEqIKgK.d.ts → action-creator-6rlBTmH9.d.ts} +27 -2
  3. package/dist/actions.d.ts +3 -3
  4. package/dist/actions.js +1 -1
  5. package/dist/{bus-0uYn_kRj.js → bus-DFlmNJ6h.js} +1 -1
  6. package/dist/context-B5Z2RpJt.js +1 -0
  7. package/dist/{context-types-Bme1mzkn.d.ts → context-types-DKNc5mkS.d.ts} +1 -1
  8. package/dist/context.d.ts +2 -2
  9. package/dist/dev.d.ts +49 -3
  10. package/dist/dev.js +2 -1
  11. package/dist/effects.d.ts +2 -2
  12. package/dist/effects.js +1 -1
  13. package/dist/emit-key-C6FL7Ewn.js +1 -0
  14. package/dist/guard.d.ts +59 -0
  15. package/dist/guard.js +1 -0
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +1 -1
  18. package/dist/match.d.ts +1 -1
  19. package/dist/{module-BWMW83qk.d.ts → module-G39MaVOt.d.ts} +2 -2
  20. package/dist/notrace/{action-creator-xpQ_I2h8.d.ts → action-creator-BrMSO3RP.d.ts} +1 -0
  21. package/dist/notrace/actions.d.ts +2 -2
  22. package/dist/notrace/effects.d.ts +2 -2
  23. package/dist/notrace/index.d.ts +1 -1
  24. package/dist/notrace/{plugin-6LhNx0th.d.ts → plugin-7GUL2EW5.d.ts} +1 -1
  25. package/dist/notrace/policies.d.ts +1 -1
  26. package/dist/notrace/redux.d.ts +1 -1
  27. package/dist/observable.d.ts +1 -1
  28. package/dist/oop.d.ts +1 -1
  29. package/dist/oop.js +1 -1
  30. package/dist/{plugin-BaErevLy.d.ts → plugin-BWNP23hr.d.ts} +2 -2
  31. package/dist/policies.d.ts +2 -2
  32. package/dist/policies.js +1 -1
  33. package/dist/production/dev.js +1 -1
  34. package/dist/react.d.ts +2 -2
  35. package/dist/react.js +1 -1
  36. package/dist/redux.d.ts +1 -1
  37. package/dist/scope.d.ts +2 -2
  38. package/dist/signal.d.ts +22 -2
  39. package/dist/signal.js +1 -1
  40. package/dist/site-DLvUoXs3.js +1 -0
  41. package/dist/svelte.d.ts +1 -1
  42. package/dist/trace.d.ts +151 -2
  43. package/dist/trace.js +3 -1
  44. package/dist/{types-D5xsR37j.d.ts → types-N_cdRr1r.d.ts} +2 -2
  45. package/dist/vue.d.ts +1 -1
  46. package/dist/{with-policy-C6BIiPIi.js → with-policy-CeM1rtES.js} +1 -1
  47. package/dist/{with-policy-BZVRBUOm.d.ts → with-policy-CthiBp_z.d.ts} +1 -1
  48. package/docs/README.md +62 -31
  49. package/docs/actions.md +44 -27
  50. package/docs/cancel-stale-requests.md +2 -0
  51. package/docs/chat-example.md +3 -1
  52. package/docs/chat-widget.md +2 -0
  53. package/docs/context.md +322 -9
  54. package/docs/debounce-and-abort.md +3 -1
  55. package/docs/debugging-loops.md +318 -0
  56. package/docs/effects.md +255 -138
  57. package/docs/errors.md +4 -2
  58. package/docs/examples.md +60 -0
  59. package/docs/guide.md +39 -22
  60. package/docs/hmr.md +35 -1
  61. package/docs/loop-guard.md +109 -0
  62. package/docs/migrating-from-effector.md +4 -2
  63. package/docs/migrating-from-mitt.md +6 -4
  64. package/docs/migrating-from-rxjs.md +2 -0
  65. package/docs/migrating-from-saga.md +2 -0
  66. package/docs/motivation.md +14 -21
  67. package/docs/observable.md +2 -0
  68. package/docs/oop.md +3 -1
  69. package/docs/package.md +4 -1
  70. package/docs/pass-the-signal.md +346 -0
  71. package/docs/plugins.md +22 -5
  72. package/docs/policies.md +73 -46
  73. package/docs/prevent-double-submit.md +3 -1
  74. package/docs/query-libraries.md +3 -1
  75. package/docs/react.md +42 -13
  76. package/docs/recipes.md +528 -0
  77. package/docs/ru/README.md +62 -0
  78. package/docs/ru/actions.md +412 -0
  79. package/docs/ru/cancel-stale-requests.md +106 -0
  80. package/docs/ru/chat-example.md +259 -0
  81. package/docs/ru/chat-widget.md +131 -0
  82. package/docs/ru/context.md +402 -0
  83. package/docs/ru/debounce-and-abort.md +105 -0
  84. package/docs/ru/debugging-loops.md +318 -0
  85. package/docs/ru/effects.md +538 -0
  86. package/docs/ru/errors.md +36 -0
  87. package/docs/ru/examples.md +60 -0
  88. package/docs/ru/guide.md +530 -0
  89. package/docs/ru/hmr.md +83 -0
  90. package/docs/ru/loop-guard.md +109 -0
  91. package/docs/ru/migrating-from-effector.md +339 -0
  92. package/docs/ru/migrating-from-mitt.md +99 -0
  93. package/docs/ru/migrating-from-rxjs.md +197 -0
  94. package/docs/ru/migrating-from-saga.md +97 -0
  95. package/docs/ru/motivation.md +47 -0
  96. package/docs/ru/observable.md +233 -0
  97. package/docs/ru/oop.md +431 -0
  98. package/docs/ru/package.md +42 -0
  99. package/docs/ru/pass-the-signal.md +346 -0
  100. package/docs/ru/plugins.md +398 -0
  101. package/docs/ru/policies.md +201 -0
  102. package/docs/ru/prevent-double-submit.md +122 -0
  103. package/docs/ru/query-libraries.md +392 -0
  104. package/docs/ru/react.md +309 -0
  105. package/docs/ru/recipes.md +528 -0
  106. package/docs/ru/semantics.md +19 -0
  107. package/docs/ru/server.md +204 -0
  108. package/docs/ru/starter.md +118 -0
  109. package/docs/ru/stores.md +673 -0
  110. package/docs/ru/svelte.md +78 -0
  111. package/docs/ru/tracing.md +466 -0
  112. package/docs/ru/typescript.md +170 -0
  113. package/docs/ru/vue.md +92 -0
  114. package/docs/semantics.md +2 -0
  115. package/docs/server.md +2 -0
  116. package/docs/starter.md +2 -0
  117. package/docs/stores.md +2 -0
  118. package/docs/svelte.md +3 -1
  119. package/docs/tracing.md +58 -1
  120. package/docs/typescript.md +2 -0
  121. package/docs/vue.md +3 -1
  122. package/llms-full.txt +2087 -418
  123. package/llms.txt +11 -8
  124. package/package.json +23 -6
  125. package/dist/context-DtUnzC-1.js +0 -1
  126. package/docs/scope.md +0 -278
  127. package/docs/stability.md +0 -90
package/dist/scope.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { H as EventMap, I as WithSignal, c as Scope, f as Effect, m as Signaled, p as Effects, s as EmitScope, v as Bus } from "./action-creator-jeEqIKgK.js";
2
- import { n as ScopeSlots, r as Scoped } from "./types-D5xsR37j.js";
1
+ import { H as EventMap, I as WithSignal, c as Scope, f as Effect, m as Signaled, p as Effects, s as EmitScope, v as Bus } from "./action-creator-6rlBTmH9.js";
2
+ import { n as ScopeSlots, r as Scoped } from "./types-N_cdRr1r.js";
3
3
  //#region src/scope/effect.d.ts
4
4
  /**
5
5
  * Marks a function whose first parameter is `{ signal }` as an effect, so that
package/dist/signal.d.ts CHANGED
@@ -1,5 +1,25 @@
1
- import { A as HandledCall, C as Emit, E as EventLike, H as EventMap, s as EmitScope, u as WithScope, v as Bus } from "./action-creator-jeEqIKgK.js";
2
- import { n as FromSignal } from "./context-types-Bme1mzkn.js";
1
+ import { A as HandledCall, C as Emit, E as EventLike, H as EventMap, s as EmitScope, u as WithScope, v as Bus } from "./action-creator-6rlBTmH9.js";
2
+ import { n as FromSignal } from "./context-types-DKNc5mkS.js";
3
+ //#region src/signal/fetch.d.ts
4
+ /**
5
+ * A `fetch` that carries a given signal, so the request stops when the signal aborts and the caller never passes
6
+ * it. Give it to `fromSignal` and every task gets a `ctx.fetch` made from its own signal. A call that has a
7
+ * `signal` of its own stops with whichever aborts first.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * const bus = createBus().use(
12
+ * contextPlugin({ fetch: fromSignal(signalFetch) }),
13
+ * );
14
+ *
15
+ * bus.on("search", latest(async (query, ctx) => {
16
+ * const response = await ctx.fetch(`/api?q=${query}`); // stops when the policy cancels the handler
17
+ * ctx.emit("results", await response.json());
18
+ * }));
19
+ * ```
20
+ */
21
+ export declare const signalFetch: (signal: AbortSignal) => typeof fetch;
22
+ //#endregion
3
23
  //#region src/signal/from-signal.d.ts
4
24
  /**
5
25
  * Marks a field of `contextPlugin` as a value that needs the signal of its task: a
package/dist/signal.js CHANGED
@@ -1 +1 @@
1
- import{t as e}from"./onAbort-BgMfspK4.js";import{n as t}from"./promise-CSLpzwSR.js";import{n}from"./bus-0uYn_kRj.js";import{t as r}from"./signal-field-key-CWT-3OXC.js";const i=e=>({[r]:e}),a=(t,n)=>(r,i,a)=>{let o=a?.signal;if(!o)return t(r,i,a);let s=t(r,n?(e,t)=>(c(),i(e,t)):i,a),c=e({signal:o},s);return()=>{c(),s()}},o=e=>typeof e==`object`&&!!e,s=(e,t)=>{let n=t[0];if(!o(n)||!o(e)&&typeof e!=`function`)return;let r=Reflect.get(n,`signal`);return r instanceof AbortSignal?r:void 0},c=(r,i,a,o)=>{let s=e({signal:r},e=>{for(let{target:t}of i)t.abort(e)});return t(o)?o.then(s,s):n(a)&&s(),o},l=()=>e=>{let t,n,r=e.addContext;e.addContext=e=>{r?.(e),t?.push({target:e}),n?.aborted&&e.abort(n.reason)};let i=e.emit;e.emit=(e,...r)=>{let a=s(e,r);if(!(a||t))return i(e,...r);let o=t,l=n,u=[];t=a?u:void 0,n=a;let d;try{d=i(e,...r)}finally{t=o,n=l}return a?c(a,u,e,d):d};let o=e.on,l=e.once;return e.on=a(o,!1),e.once=a(l,!0),e};export{i as fromSignal,l as signalPlugin};
1
+ import{t as e}from"./onAbort-BgMfspK4.js";import{n as t}from"./promise-CSLpzwSR.js";import{n}from"./bus-DFlmNJ6h.js";import{t as r}from"./signal-field-key-CWT-3OXC.js";const i=e=>(t,n)=>{let r=n?.signal,i=r?AbortSignal.any([e,r]):e;return fetch(t,{...n,signal:i})},a=e=>({[r]:e}),o=(t,n)=>(r,i,a)=>{let o=a?.signal;if(!o)return t(r,i,a);let s=t(r,n?(e,t)=>(c(),i(e,t)):i,a),c=e({signal:o},s);return()=>{c(),s()}},s=e=>typeof e==`object`&&!!e,c=(e,t)=>{let n=t[0];if(!s(n)||!s(e)&&typeof e!=`function`)return;let r=Reflect.get(n,`signal`);return r instanceof AbortSignal?r:void 0},l=(r,i,a,o)=>{let s=e({signal:r},e=>{for(let{target:t}of i)t.abort(e)});return t(o)?o.then(s,s):n(a)&&s(),o},u=()=>e=>{let t,n,r=e.addContext;e.addContext=e=>{r?.(e),t?.push({target:e}),n?.aborted&&e.abort(n.reason)};let i=e.emit;e.emit=(e,...r)=>{let a=c(e,r);if(!(a||t))return i(e,...r);let o=t,s=n,u=[];t=a?u:void 0,n=a;let d;try{d=i(e,...r)}finally{t=o,n=s}return a?l(a,u,e,d):d};let a=e.on,s=e.once;return e.on=o(a,!1),e.once=o(s,!0),e};export{a as fromSignal,i as signalFetch,u as signalPlugin};
@@ -0,0 +1 @@
1
+ const e=/* @__PURE__ */ new Map,t=(t,n)=>{let r=e.get(t);e.set(t,{site:r?.site??n,count:(r?.count??0)+1});let i=!1;return()=>{if(i)return;i=!0;let r=(e.get(t)?.count??1)-1;r<=0?e.delete(t):e.set(t,{site:e.get(t)?.site??n,count:r})}},n=t=>e.get(t)?.site;export{n,t};
package/dist/svelte.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-jeEqIKgK.js";
1
+ import { H as EventMap, T as EventKey, a as FreeActionCreator, t as ActionCreator, v as Bus } from "./action-creator-6rlBTmH9.js";
2
2
  import { t as StateOf } from "./state-of-C19H6BoV.js";
3
3
  import { Readable } from "svelte/store";
4
4
  //#region src/svelte/plugin.d.ts
package/dist/trace.d.ts CHANGED
@@ -1,4 +1,141 @@
1
- import { B as Tracer, H as EventMap, J as Trace, K as Phase, V as EffectName, X as TraceLogger, Y as TraceEvent, v as Bus } from "./action-creator-jeEqIKgK.js";
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-6rlBTmH9.js";
2
+ //#region src/graph/serialize.d.ts
3
+ type JsonValue = null | boolean | number | string | JsonValue[] | {
4
+ [key: string]: JsonValue;
5
+ };
6
+ type SerializeOptions = {
7
+ /** How deep to go before a value becomes a marker like `"[Object]"`. Default 6. */
8
+ maxDepth?: number;
9
+ /** How many values, in all, to visit before the rest is cut. Default 1000. */
10
+ maxSize?: number;
11
+ };
12
+ /**
13
+ * Makes any value a plain JSON value: never throws, never loops, never calls
14
+ * a getter or `toJSON`, and cuts a value that is too deep or too big.
15
+ * `Error` keeps its name, message, stack and cause; a cycle is `"[Circular]"`;
16
+ * `Map` and `Set` become `{ $map }` and `{ $set }`; an instance of a class
17
+ * keeps its own fields and its name as `$class`.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * safeSerialize({ user: new User("Ann"), tags: new Set(["a"]) });
22
+ * // { user: { $class: "User", name: "Ann" }, tags: { $set: ["a"] } }
23
+ * ```
24
+ */
25
+ export declare const safeSerialize: (value: unknown, options?: SerializeOptions) => JsonValue;
26
+ //#endregion
27
+ //#region src/graph/types.d.ts
28
+ /** A loop that the logger found, or an event that is emitted too often. */
29
+ type Finding = {
30
+ /** `cycle`: a node repeats in a chain; `storm`: too many emits from outside any task; `runaway`: one root started too many tasks. */
31
+ kind: "cycle" | "storm" | "runaway";
32
+ /** The names from the first turn of the loop back to it: `a`, `b`, `a`. For a storm, the event. */
33
+ path: string[];
34
+ /** How many times the repeating node was in the chain when it was found. */
35
+ repeats: number;
36
+ /** How many times the loop has gone round since it was found, the first time included. */
37
+ turns: number;
38
+ /** The first node of the chain, or the event of a storm. */
39
+ root: string;
40
+ traceGroupId?: string;
41
+ /** `chain`: it ran as one chain; `graph`: the edges of different runs close it. */
42
+ source: "chain" | "graph";
43
+ };
44
+ type GraphLoggerOptions = {
45
+ /** Put before every report line and in the title of the exports. */
46
+ name?: string;
47
+ /** How many times a node may be in one chain before it is a cycle. Default 3. */
48
+ repeats?: number;
49
+ /** How many tasks one root may start before the chain is called runaway. Default 5000. */
50
+ maxSteps?: number;
51
+ /** How many emits of one event from outside any task, in how many ms, make a storm. Default 100 in 1000; `false` turns it off. */
52
+ storm?: false | {
53
+ count: number;
54
+ perMs: number;
55
+ };
56
+ /** Names that are not counted as repeats, steps or storms (they stay in the graph). */
57
+ ignore?: readonly string[] | ((name: string) => boolean);
58
+ /** Receives each finding once, in place of `console.warn`. */
59
+ onCycle?: (finding: Finding) => void;
60
+ /** Also send each finding to Redux DevTools as `cycle/detected`. */
61
+ devtools?: boolean;
62
+ /** The clock, in ms. Default `performance.now`. */
63
+ now?: () => number;
64
+ /** Where the default report goes. Default `globalThis.console`. */
65
+ console?: Pick<Console, "warn">;
66
+ /** The Redux DevTools extension. Default `globalThis.__REDUX_DEVTOOLS_EXTENSION__`. */
67
+ extension?: DevtoolsExtension;
68
+ };
69
+ type DevtoolsExtension = {
70
+ connect: (options?: {
71
+ name?: string;
72
+ }) => {
73
+ init: (state: unknown) => void;
74
+ send: (action: unknown, state: unknown) => void;
75
+ };
76
+ };
77
+ type NodeSnapshot = {
78
+ name: string;
79
+ count: number;
80
+ /** Where the listener was made, when `devPlugin` is used. */
81
+ site?: string;
82
+ last?: {
83
+ payload: JsonValue;
84
+ error?: JsonValue;
85
+ };
86
+ };
87
+ type EdgeSnapshot = {
88
+ from: string;
89
+ to: string;
90
+ count: number;
91
+ inCycle: boolean;
92
+ };
93
+ type GraphSnapshot = {
94
+ name?: string;
95
+ nodes: NodeSnapshot[];
96
+ edges: EdgeSnapshot[];
97
+ cycles: Finding[];
98
+ };
99
+ type ExportOptions = {
100
+ /** Put the nodes in groups: `"prefix"` is the part of the name before the first `/`, or a function of the name. */
101
+ group?: "prefix" | ((name: string) => string | undefined);
102
+ };
103
+ type JsonOptions = {
104
+ /** Add the last payload and error of each node, made safe. Default `false`. */
105
+ payload?: boolean;
106
+ };
107
+ //#endregion
108
+ //#region src/graph/logger.d.ts
109
+ type GraphLogger = TraceLogger & {
110
+ /** The loops found so far: the ones that ran as one chain, and the cycles that the edges of different runs close. */
111
+ cycles: () => Finding[];
112
+ /** The graph as a Mermaid flowchart; the edges of a cycle are red. */
113
+ toMermaid: (options?: ExportOptions) => string;
114
+ /** The graph as a Graphviz digraph. */
115
+ toDot: (options?: ExportOptions) => string;
116
+ /** The graph as plain data that `JSON.stringify` takes. */
117
+ toJSON: (options?: JsonOptions) => GraphSnapshot;
118
+ /** Forgets the graph, the findings and the counters. */
119
+ clear: () => void;
120
+ };
121
+ /**
122
+ * A trace logger that builds the map of which event leads to which, finds loops
123
+ * while the app runs, and writes the map as Mermaid, Graphviz or JSON. Pass it to
124
+ * `createTracer` like any logger; it needs no other setup, and works best with
125
+ * `tracePlugin` (it sees every emit and the names of listeners).
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * const graph = graphLogger({ name: "cart" });
130
+ * const bus = createBus({ trace: createTracer(graph) }).use(tracePlugin());
131
+ *
132
+ * // a loop is written to the console once; read the rest when you want
133
+ * graph.cycles();
134
+ * graph.toMermaid();
135
+ * ```
136
+ */
137
+ export declare const graphLogger: (options?: GraphLoggerOptions) => GraphLogger;
138
+ //#endregion
2
139
  //#region src/trace/console.d.ts
3
140
  type ConsoleLoggerOptions = {
4
141
  /** Defaults to `globalThis.console`. */
@@ -86,6 +223,11 @@ type Recorder<E = Record<string, unknown>> = TraceLogger<E> & {
86
223
  */
87
224
  readonly events: TraceEvent<E>[];
88
225
  clear: () => void;
226
+ /**
227
+ * The events as plain data that `JSON.stringify` takes: the payload and the error are made safe, so a
228
+ * circular object, an `Error`, a `Map` or a class instance cannot break it. `JSON.stringify(recorder)` uses it.
229
+ */
230
+ toJSON: () => JsonValue[];
89
231
  };
90
232
  /**
91
233
  * Trace logger that keeps events in memory. With no option it keeps all of
@@ -175,6 +317,13 @@ type ReduxDevtoolsOptions = {
175
317
  payload?: PayloadMode;
176
318
  /** The number of actions that DevTools keeps; its own default is 50. */
177
319
  maxAge?: number;
320
+ /**
321
+ * Makes the payload and the error of an action plain data before they reach the extension. The default
322
+ * keeps any value safe: an `Error`, a `Map`, a class instance, a circular object, a function. Pass your
323
+ * own function to change what is sent, or `false` to send values as they are (the extension then
324
+ * serializes them, and can fail on a circular one).
325
+ */
326
+ serialize?: false | ((value: unknown) => unknown);
178
327
  };
179
328
  type ReduxDevtoolsState = {
180
329
  /** Tasks that started and have not settled yet, by trace id. */
@@ -239,4 +388,4 @@ type TracerOptions = {
239
388
  */
240
389
  export declare const createTracer: <E = Record<string, unknown>>(trace: Trace<E>, options?: TracerOptions) => Tracer;
241
390
  //#endregion
242
- export type { ConsoleLoggerOptions, EffectName, PayloadMode, PerformanceLoggerOptions, Phase, PolicyStage, PolicyStages, Recorder, ReduxDevtoolsOptions, ReduxDevtoolsState, ReduxExtension, Trace, TraceEvent, TraceLogger, Tracer, TracerOptions };
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 };
package/dist/trace.js CHANGED
@@ -1 +1,3 @@
1
- import{n as e,t}from"./onAbort-BgMfspK4.js";import{a as n,c as r,n as i,o as a,r as o,s}from"./context-DtUnzC-1.js";import{n as c}from"./promise-CSLpzwSR.js";import{t as l}from"./origin-key-BbWN3gmN.js";import{t as u}from"./scope-key-B7l7mUS5.js";const d=(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)}},f=(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:String(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 p;const m=()=>p,h=(e,t)=>{let n=p;p=e;try{return t()}finally{p=n}},g=(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()}},_=e=>typeof e==`string`?e:Reflect.get(Object(e),`type`),v=(e,t)=>typeof e==`object`?Reflect.get(Object(e),`payload`):t[0],y=()=>e=>{if(!(r&&e.trace))return e;let{trace:t}=e,i=(e,n)=>{let r=t.task({type:_(e),payload:v(e,n)},o());t.report(r,`emit`,`done`)},s=e.emit;e.emit=(e,...t)=>h(e,()=>(i(e,t),s(e,...t))),Reflect.set(e,l,(t,n)=>{g(e,t,n)});let c=/* @__PURE__ */ new WeakMap,u=(e,t,n)=>{let r=c.get(t)??/* @__PURE__ */ new Map,i=_(e);r.set(i,[...r.get(i)??[],n]),c.set(t,r)},d=(e,t)=>(c.get(t)?.get(_(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,y=(e,t)=>{t===void 0?m(e):m(e,d(e,t)??t)},b=e.on,x=e.once;return e.on=p(b,!1),e.once=p(x,!0),e.off=y,e},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});return n>0&&Object.defineProperty(c,"events",{get:s}),c},x=()=>{},S=/* @__PURE__ */ new Set([`cancel`,`drop`,`error`]),C=()=>!1,w=()=>!0,T=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:C},E=(e={})=>{let t=e.extension??globalThis.__REDUX_DEVTOOLS_EXTENSION__;if(!t)return x;let{starts:n=!1,depth:r=0,maxAge:i}=e,a=T(e.policies),o=e.payload??`essential`,s=t.connect({name:e.name??`nanoeffects`,...i!==void 0&&{maxAge:i}}),c=/* @__PURE__ */ new Map,l=/* @__PURE__ */ new Map,u=()=>({running:Object.fromEntries(c)});s.init(u());let d=(e,t,n)=>n===0?a({effect:e,phase:t}):n<=r,f=(e,t)=>{e.phase===`start`?(l.set(e.id,t),d(e.effect,`start`,t)&&c.set(e.id,{type:e.type,effect:e.effect,...e.traceGroupId!==void 0&&{traceGroupId:e.traceGroupId}})):(l.delete(e.id),c.delete(e.id))},p=!1,m=!1,h=(e,t)=>{p||m||t!==0||e===`start`||(m=!0,globalThis.console?.warn(`[nanoeffects] reduxDevtools got a task but no emit event: add bus.use(tracePlugin()) to see the emits.`))},g=e=>{let{payload:t,...n}=e;return o===`all`||o===`essential`&&(e.effect===`emit`||S.has(e.phase))?{...n,payload:t}:n},_=(e,t)=>{s.send({...g(e),type:`${e.type}/${e.effect}/${e.phase}`,event:e.type,...e.phase===`error`&&{error:t instanceof Error?t.message:t}},u())};return({error:e,...t})=>{if(t.effect===`emit`){p=!0,s.send({...g(t),event:t.type},u());return}let r=(t.parentId===void 0?-1:l.get(t.parentId)??-1)+1;f(t,r),h(t.phase,r),(t.phase===`error`&&r===0||d(t.effect,t.phase,r))&&(t.phase!==`start`||n)&&_(t,e)}},D=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)}}},O=()=>{},k=e=>{if(typeof e.traceGroupId==`string`)return e.traceGroupId;let t=m();if(typeof t!=`object`&&typeof t!=`function`||t===null)return;let n=Reflect.get(t,u),r=typeof n==`object`&&n?Reflect.get(n,`traceGroupId`):void 0;return typeof r==`string`?r:void 0},A=(e,n={})=>{let r=D(e),{filter:i,sample:a=1}=n,o=0,s=e=>typeof a==`function`?a({type:e.type,payload:e.payload}):Math.random()<a,l={task:(e,t)=>({id:++o,root:t?.root??o,parent:t?.id,type:e.type,payload:e.payload,sampled:t?.sampled??s(e),group:k(e)??t?.group}),rename:(e,t)=>{e.type=t},report:(e,t,n,a)=>{if(!e.sampled)return;let o={id:e.id,type:e.type,effect:t,phase:n,payload:e.payload};typeof e.parent==`number`&&(o.parentId=e.parent),e.root!==e.id&&(o.rootId=e.root),e.group!==void 0&&(o.traceGroupId=e.group),n===`error`&&(o.error=a),(!i||i(o))&&r?.(o)},settler:(e,t)=>{if(!e.sampled)return O;let n=!1;return(r,i)=>{n||(n=!0,l.report(e,t,r,i))}},run:(e,n,r,i)=>{if(!e.sampled)return i();let a=l.settler(e,n);l.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 l};export{d as consoleLogger,b as createTraceRecorder,A as createTracer,f as performanceLogger,E as reduxDevtools,y as tracePlugin};
1
+ import{n as e,t}from"./onAbort-BgMfspK4.js";import{a as n,c as r,n as i,o as a,r as o,s}from"./context-B5Z2RpJt.js";import{n as c}from"./promise-CSLpzwSR.js";import{t as l}from"./origin-key-BbWN3gmN.js";import{n as u}from"./site-DLvUoXs3.js";import{t as d}from"./scope-key-B7l7mUS5.js";const f=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},p=(e,t)=>{if(t===`prefix`){let t=e.indexOf(`/`);return t>0?e.slice(0,t):void 0}return t?.(e)},m=e=>e.replaceAll(`"`,`#quot;`).replaceAll(`<`,`#lt;`).replaceAll(`>`,`#gt;`).replaceAll(/\r?\n/g,` `),h=e=>e.replaceAll(`\\`,`\\\\`).replaceAll(`"`,`\\"`).replaceAll(/\r?\n/g,`\\n`),g=e=>new Map(e.nodes.map((e,t)=>[e.name,`n${t}`])),_=(e,t)=>{let n=/* @__PURE__ */ new Map;for(let{name:r}of e.nodes){let e=p(r,t);e!==void 0&&n.set(e,[...n.get(e)??[],r])}return n},v=(e,t={})=>{let n=g(e),r=[`flowchart LR`],i=/* @__PURE__ */ new Set;for(let[a,o]of _(e,t.group)){r.push(` subgraph g_${r.length}["${m(a)}"]`);for(let e of o)i.add(e),r.push(` ${n.get(e)}["${m(e)}"]`);r.push(` end`)}for(let{name:t}of e.nodes)i.has(t)||r.push(` ${n.get(t)}["${m(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
+ `)},y=(e,t={})=>{let n=g(e),r=[`digraph events {`];e.name!==void 0&&r.push(` label="${h(e.name)}";`);let i=/* @__PURE__ */ new Set;for(let[a,o]of _(e,t.group)){r.push(` subgraph "cluster_${h(a)}" {`,` label="${h(a)}";`);for(let e of o)i.add(e),r.push(` ${n.get(e)} [label="${h(e)}"];`);r.push(` }`)}for(let{name:t}of e.nodes)i.has(t)||r.push(` ${n.get(t)} [label="${h(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
+ `)},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,5 +1,5 @@
1
- import { H as EventMap, c as Scope, l as WithFields, p as Effects } from "./action-creator-jeEqIKgK.js";
2
- import { t as BoundOf } from "./plugin-BaErevLy.js";
1
+ import { H as EventMap, c as Scope, l as WithFields, p as Effects } from "./action-creator-6rlBTmH9.js";
2
+ import { t as BoundOf } from "./plugin-BWNP23hr.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-jeEqIKgK.js";
1
+ import { H as EventMap, T as EventKey, a as FreeActionCreator, t as ActionCreator, v as Bus } from "./action-creator-6rlBTmH9.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-DtUnzC-1.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
+ import{n as e}from"./context-B5Z2RpJt.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-jeEqIKgK.js";
1
+ import { G as PayloadOf, H as EventMap, q as Task } from "./action-creator-6rlBTmH9.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> = {
package/docs/README.md CHANGED
@@ -1,26 +1,66 @@
1
1
  # Documentation
2
2
 
3
- ## Start here
3
+ nanoeffects moves business logic and side effects out of UI components into plain `async` functions. Policies (`latest`, `debounce`, `queue`) and effects (`retry`, `timeout`, `poll`) handle races, retries and cancellation.
4
4
 
5
- The [Guide](guide.md) is the ladder: small bus → policies for async UI → effects when you need them → handlers and modules for domain logic off the UI.
5
+ ```js
6
+ import { createBus } from "nanoeffects";
7
+ import { latest } from "nanoeffects/policies";
6
8
 
7
- Why the library exists (logic and side effects outside React, Vue and Svelte components, as plain `async` functions), and when to pick it over redux-saga, RxJS, Redux or Effector (including SDKs and Module Federation): [Why nanoeffects exists](motivation.md).
9
+ const bus = createBus(); // event bus
8
10
 
9
- Code for every guide step: [`examples/guide.ts`](../examples/guide.ts) (type-checked and tested).
11
+ // The search logic lives outside the component; latest aborts the previous request
12
+ bus.on(
13
+ "search",
14
+ latest(async (query, { signal }) => {
15
+ const items = await api.search(query, { signal });
16
+ bus.emit("found", items);
17
+ }),
18
+ );
19
+
20
+ // The component only listens for the result and sends events
21
+ bus.on("found", (items) => render(items));
22
+ bus.emit("search", "nano");
23
+ ```
24
+
25
+ The same example with a UI framework: [React](react.md), [Vue](vue.md), [Svelte](svelte.md). Ready projects: [Start a project from a template](starter.md).
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ npm install nanoeffects
31
+ ```
32
+
33
+ To start a new project from a template ([Start a project from a template](starter.md)):
34
+
35
+ ```bash
36
+ npm create nanoeffects@latest my-app
37
+ ```
10
38
 
11
- ## Start with three imports
39
+ ## Where to start
12
40
 
13
- Most apps need only these. Everything else is optional; read it when you hit its need.
41
+ 1. Follow the [Guide](guide.md#1-create-a-bus): from the first bus to modules.
42
+ 2. Common async UI problems (stale responses, double clicks, retries) are in [Common tasks](recipes.md).
14
43
 
15
- | Import | Gives you |
16
- | ------ | --------- |
17
- | `nanoeffects` | `createBus`: `on`, `emit`, `ctx` |
18
- | `nanoeffects/policies` | `latest`, `debounce`, ... for races in async UI |
19
- | `nanoeffects/effects` | `retry`, `timeout`, `poll`, ... for an unreliable network |
44
+ ## The ladder, from A to F
20
45
 
21
- The other entries and plugins: [Package entries](package.md) and [Plugins](plugins.md#pick-a-plugin-by-task).
46
+ Read the docs from the level of your project and stop when it has what it needs. Each level adds one idea. A page says at its top which level it is for and what to read first.
22
47
 
23
- ## Jump by project need
48
+ | Level | Your project | You can stop here when | Read |
49
+ | ----- | ------------ | ---------------------- | ---- |
50
+ | **A** | A small app, widget or MVP with rare races | Events and handlers do the job | [Guide](guide.md#1-create-a-bus) |
51
+ | **B** | A SPA with async UI: search, sending messages, unmounting components | A newer request must replace an older one | [Policies](policies.md), [Guide](guide.md#3-abort-the-previous-run-with-latest) |
52
+ | **C** | Complex business logic | Retries, timeouts and waiting are handled | [Effects](effects.md), [Guide](guide.md#5-add-retry-and-timeout) |
53
+ | **D** | Domain logic that must live off the UI | Features are actions and modules, not components | [Actions, jobs and modules](actions.md), [Guide](guide.md#7-name-events-three-ways) |
54
+ | **E** | An existing Redux, Zustand, MobX or Pinia app, or a data library | The bus runs the async work next to your store | [Stores](stores.md), [Query libraries](query-libraries.md) |
55
+ | **F** | A platform, an SDK or a long-lived app | You can see what ran and reload modules | [Tracing](tracing.md), [Hot reload](hmr.md), [Semantics](semantics.md) |
56
+
57
+ A small app never needs level B or higher. Move up when the first problem of that level appears, not before.
58
+
59
+ Code for every guide step: [`examples/guide.ts`](../examples/guide.ts) (type-checked and tested).
60
+
61
+ ## Jump by need
62
+
63
+ For a problem, start from [Common tasks](recipes.md). For a project shape:
24
64
 
25
65
  | Project shape | First page |
26
66
  | ------------- | ---------- |
@@ -35,29 +75,20 @@ The other entries and plugins: [Package entries](package.md) and [Plugins](plugi
35
75
  | A Vue app | [Use nanoeffects in a Vue app](vue.md) |
36
76
  | A Svelte app | [Use nanoeffects in a Svelte app](svelte.md) |
37
77
  | Old responses overwrite new ones | [Cancel stale requests](cancel-stale-requests.md) |
38
- | Debounced input that must also cancel the old request | [Debounce and abort](debounce-and-abort.md) |
78
+ | Debounced input that must also cancel the old request | [Pause between requests](debounce-and-abort.md) |
39
79
  | A second click sends a second request | [Prevent a double submit](prevent-double-submit.md) |
40
- | Using mitt, nanoevents or EventEmitter | [Coming from mitt](migrating-from-mitt.md) |
80
+ | Using mitt or nanoevents | [Coming from mitt](migrating-from-mitt.md) |
41
81
  | Moving from redux-saga | [Coming from redux-saga](migrating-from-saga.md) |
42
82
  | Using RxJS | [Use nanoeffects next to RxJS](migrating-from-rxjs.md) |
43
83
  | Moving from Effector | [Coming from Effector](migrating-from-effector.md) |
44
84
 
45
85
  ## Reference
46
86
 
47
- - [Policies](policies.md) — `latest`, `leading`, `debounce`, `throttle`, `queue`, `key`
48
- - [Effects and cancellation](effects.md) — `retry`, `timeout`, `poll`, `take`, `all`, …
49
- - [Actions, jobs and modules](actions.md)
50
- - [Sign-up form in React](react.md) — one form: races, double submit, retry
51
- - [nanoeffects in a Vue app](vue.md) — `watchEmitter` and `useEventRef`
52
- - [nanoeffects in a Svelte app](svelte.md) — `eventStore`
53
- - [Context fields](context.md) — store or client on every task
54
- - [Use the bus from a class](oop.md) — `extendBus`, `@WithBus`, `@On`, and `dispose()` for the subscriptions of an instance
55
- - [TypeScript in many files](typescript.md) — keep the types of the bus in actions and modules
56
- - [Errors and unheard events](errors.md)
57
- - [Package entries](package.md) — which import holds what
58
- - [Stability](stability.md) — which entries are stable, what is breaking, supported versions
59
- - [Plugins](plugins.md) — every plugin of a bus, what it returns, and the order to use them in
60
- - [Tracing](tracing.md) — loggers, Redux DevTools, `notrace`
61
- - [Observable](observable.md) — events as TC39 Observables
87
+ Look these up when you need the exact rule.
88
+
89
+ - [Package entries](package.md): which import holds what
90
+ - [Plugins](plugins.md): every plugin of a bus, what it returns, and the order to use them in
91
+ - [Semantics](semantics.md): the exact rules of emit, abort and ordering
92
+ - [Tracing](tracing.md): loggers, Redux DevTools, `notrace`
93
+ - [Observable](observable.md): events as TC39 Observables
62
94
  - [Hot reload and dev diagnostics](hmr.md)
63
- - [Semantics](semantics.md)
package/docs/actions.md CHANGED
@@ -1,6 +1,23 @@
1
1
  # Actions, jobs and modules
2
2
 
3
- Use this page when **domain logic lives on the bus**, off the UI: the UI emits and listens; handlers and modules own the work.
3
+ > **Level D.** Read first: [Guide](guide.md#1-create-a-bus) steps 1 to 6.
4
+
5
+ When a project has a lot of business logic, you need to split it. One way is to split it by domains, or entities. In a shop, the domains could be products and product categories. Then the code that belongs to the actions of one domain goes into one file (`product.js`), its handlers into another, and the module that mounts them into a third. A domain can then be mounted, replaced or unmounted without touching the others. For the shop the files could look like this:
6
+
7
+ ```text
8
+ src/
9
+ bus.js
10
+ product/
11
+ product.js
12
+ product-handlers.js
13
+ product-module.js
14
+ category/
15
+ category.js
16
+ category-handlers.js
17
+ category-module.js
18
+ ```
19
+
20
+ The examples below show the content of such files.
4
21
 
5
22
  If you only need `on` / `emit` or policies, stay in the [Guide](guide.md) until you need a result from `emit` or a mountable domain.
6
23
 
@@ -10,37 +27,37 @@ A chat with two entities and three features, jobs under policies and effects, an
10
27
 
11
28
  A whole project with two domains, a job and React as a thin layer: `npm create nanoeffects@latest my-app -- --template domains-ts` (see [Start a project from a template](starter.md#start-with-domains)).
12
29
 
13
- ## Pick the word: event, action, job, module
30
+ ## Action, job and module
14
31
 
15
- Four names, one idea each. Take the first row that fits.
32
+ Four forms, one idea each. Take the first row that fits.
16
33
 
17
- | Word | What it is | Use it when |
34
+ | Form | What it is | Use it when |
18
35
  | ---- | ---------- | ----------- |
19
- | Event | A name and a payload; `createAction("x")` with no handler. | The UI or a handler tells that something happened. |
20
- | Action with a handler | The same, plus work: `createAction("x", handler)`. `emit` returns the result. | The caller needs a value back. |
21
- | Job | An action with a handler that also emits `pending`, `success`, `fail` and `cancel`. | The UI shows a loading state. |
22
- | Module | A group of listeners, handlers and tasks, mounted and unmounted with one call. | One domain must start and stop as a unit. |
36
+ | `createAction(type)` | An action with no handler: a name and a payload. | The UI or a handler tells that something happened. |
37
+ | `createAction(type, handler)` | An action with a handler: `emit` runs the handler and returns its result. | You want the result of the handler from `emit`. |
38
+ | `createJob(type, handler)` | An action with a handler that returns its result as the actions `success` and `fail`, and also sends `pending` and `cancel`. | You want the result of the work of a handler and, if you need it, to subscribe to the state of that work. |
39
+ | `createModule(name, setup)` | A group of actions and jobs with their handlers, mounted and unmounted with one call. | One domain must start and stop as a unit. |
23
40
 
24
- Start with events. Add a handler when you need a result, a job when you need a status, a module when you need a unit.
41
+ Start with actions. Add a handler when you need a result, a job when you need a status, a module when you need a unit.
25
42
 
26
43
  ## On this page
27
44
 
28
- - [Pick the word: event, action, job, module](#pick-the-word-event-action-job-module)
29
- - [Return a value from `emit` with a handler](#return-a-value-from-emit-with-a-handler)
30
- - [Describe events, handlers and jobs](#describe-events-handlers-and-jobs)
45
+ - [Action, job and module](#action-job-and-module)
46
+ - [Get the result of `emit`](#get-the-result-of-emit)
47
+ - [Actions, handlers and jobs](#actions-handlers-and-jobs)
31
48
  - [Actions](#actions)
32
49
  - [Bind a handler later](#bind-a-handler-later)
33
- - [Say what a bound handler returns](#say-what-a-bound-handler-returns)
50
+ - [The result type of a bound handler](#the-result-type-of-a-bound-handler)
34
51
  - [Jobs](#jobs)
35
- - [Show a loading state with a job](#show-a-loading-state-with-a-job)
36
- - [Mount domains with modules](#mount-domains-with-modules)
37
- - [Give an action or a module its own fields](#give-an-action-or-a-module-its-own-fields)
52
+ - [The state of a job](#the-state-of-a-job)
53
+ - [Modules](#modules)
54
+ - [Extend the context of actions and modules](#extend-the-context-of-actions-and-modules)
38
55
 
39
- ## Return a value from `emit` with a handler
56
+ ## Get the result of `emit`
40
57
 
41
- Give `createAction` a handler, and `emit` runs it and returns its result. There is one creator for an event and for an action with work behind it; the handler is the only difference.
58
+ When you want an answer from a handler at the `emit` call, give `createAction` the handler. A handler is a function `(payload, ctx) => result` that runs when the action is emitted. `emit` runs it and returns its result. There is one creator for an action with and without a handler; the handler is the only difference.
42
59
 
43
- The bus lives in one file and its events in another, so any module can import both without a cycle.
60
+ The bus lives in one file and its actions in another, so any module can import both without a cycle.
44
61
 
45
62
  ```js
46
63
  // bus.js
@@ -65,7 +82,7 @@ import { double } from "./math";
65
82
  bus.emit(double(21)); // 42
66
83
  ```
67
84
 
68
- ## Describe events, handlers and jobs
85
+ ## Actions, handlers and jobs
69
86
 
70
87
  `nanoeffects/actions` has the pieces to name what a bus does, apart from the code that does it.
71
88
 
@@ -100,7 +117,7 @@ These are the events of the user domain, in a file of their own (`user.js`). The
100
117
  - `userLoaded.match(action)` — narrows an action
101
118
  - `createAction(type, handler)` — a function as the second argument is the handler; `bus.emit(action(payload))` tells the listeners, runs the handler as a task and returns its result
102
119
  - `createAction(type, { prepare })` — build the payload from several arguments
103
- - `createAction(type, { handle, prepare, ...scope })` — everything in one object: `handle` and `prepare` are taken out, and every other field is the [scope](scope.md)
120
+ - `createAction(type, { handle, prepare, ...scope })` — everything in one object: `handle` and `prepare` are taken out, and every other field is the [scope](context.md#scope-fields-for-an-action-a-module-and-a-call)
104
121
  - Works as the event of its type: `bus.emit(userLoaded(user))`, `bus.on(userLoaded, fn)`
105
122
 
106
123
  A scope goes after a handler as the last argument, `createAction(type, handler, scope)`; `prepare` with a handler goes in that last object: `createAction(type, handler, { prepare, ...scope })`.
@@ -150,7 +167,7 @@ A handler bound in the registry makes `bus.emit(sendMail(mail))` run it and retu
150
167
 
151
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.
152
169
 
153
- ### Say what a bound handler returns
170
+ ### The result type of a bound handler
154
171
 
155
172
  TypeScript cannot see which handler the registry has, so the type of `emit` for an action without a handler is `void`. Tell it what the bound handler returns with `Call<Payload, Result>`. This is a promise of yours: nothing checks it against the handler.
156
173
 
@@ -194,7 +211,7 @@ On a bus without an event map you can say it where you call: `const id = bus.emi
194
211
  - Calling the job only builds `{ type, payload }`; the handler runs on `emit`.
195
212
  - The outcomes are events like any other: with `onUnhandled` set on the bus, it is called for a `pending` or a `cancel` that nobody listens to.
196
213
 
197
- ## Show a loading state with a job
214
+ ## The state of a job
198
215
 
199
216
  **Problem.** A spinner needs a flag that is `true` while a request runs. Setting it by hand in the handler (`loading = true`, then `try` / `finally`) breaks when the task is aborted: `ctx.emit` does nothing after an abort, so a flag that is cleared through the bus stays `true`.
200
217
 
@@ -267,7 +284,7 @@ bus.emit(loadUser("1"));
267
284
 
268
285
  A flag is exact when calls do not overlap, or when `latest` replaces them. If several runs can go at once (`queue` with `concurrency` above 1, or a job without a policy called many times), keep a counter: add one on `pending` and take one off on `success`, `fail` and `cancel`. A policy that you write yourself, or that you hide in your own function, does not report to the job; `pending` is then sent at the call, as for a job with no policy.
269
286
 
270
- ## Mount domains with modules
287
+ ## Modules
271
288
 
272
289
  **Problem.** A domain needs several subscriptions and handler bindings. With plain `bus.on` / `actions.handle` you must remember every unsubscribe.
273
290
 
@@ -360,9 +377,9 @@ If the `setup` of one module throws, the modules of that call that were already
360
377
  - `spawn` — background task, aborted on unmount.
361
378
  - An action that carries a handler keeps answering after unmount (`use` only tracks registration). Use `handle` for a handler that must live only as long as the module.
362
379
 
363
- ## Give an action or a module its own fields
380
+ ## Extend the context of actions and modules
364
381
 
365
- An action with a handler can need its own client, a module its own logger. Pass a scope, a plain object whose keys go on `ctx` (`$effects` binds functions to the task), as the last argument of `createAction`, `createAction`, `createModule` and `createJob`, and turn it on with `scopePlugin`:
382
+ An action with a handler can need its own client, a module its own logger. Pass a scope, a plain object whose keys go on `ctx` (`$effects` binds functions to the task), as the last argument of `createAction`, `createModule` and `createJob`, and turn it on with `scopePlugin`:
366
383
 
367
384
  ```js
368
385
  // bus.js
@@ -394,4 +411,4 @@ const fakeApi = { getUser: async (id) => ({ id, name: "test" }) };
394
411
  bus.emit(getUser("1"), { api: fakeApi });
395
412
  ```
396
413
 
397
- The handler gets the fields typed. Without `scopePlugin` the scope is ignored. The full picture, with jobs, actions, modules and which value wins, is in [scopes](scope.md).
414
+ The handler gets the fields typed. Without `scopePlugin` the scope is ignored. The full picture, with jobs, actions, modules and which value wins, is in [scopes](context.md#scope-fields-for-an-action-a-module-and-a-call).
@@ -1,5 +1,7 @@
1
1
  # Cancel stale requests and fix race conditions
2
2
 
3
+ > **Level B.** Read first: the bus from the [README](../README.md).
4
+
3
5
  Read this page if an older response can overwrite a newer one: a search box that shows the results of the previous query, a filter that flickers, a form that submits twice. It shows the cause, the fix with `latest`, and how to add a pause, a retry and a timeout. It needs only the bus from the [README](../README.md).
4
6
 
5
7
  ## See the problem
@@ -1,5 +1,7 @@
1
1
  # Build a chat with entities, features, modules and a small server
2
2
 
3
+ > **Level D.** Read first: [Actions, jobs and modules](actions.md).
4
+
3
5
  Use this page when your app has several features that react to each other and you want to see where each piece of logic goes. The example is a chat with rooms, a history that loads, messages that can fail to send, a search across all rooms, and new messages that arrive on their own. A small Fastify server in the example answers every request, and it misbehaves on a known schedule, so each problem on this page really happens.
4
6
 
5
7
  It is a level D example. Start with [Actions, jobs and modules](actions.md) for the API, or with the `domains-ts` template in [Start a project from a template](starter.md#start-with-domains) for a smaller project.
@@ -88,7 +90,7 @@ import { latest } from "nanoeffects/policies";
88
90
  export const fetchHistory = latest((roomId, { signal }) => api.loadHistory(roomId, { signal }));
89
91
  ```
90
92
 
91
- The module gives the job its work and keeps the state from the outcomes, so an aborted load clears the flag too (see [Show a loading state with a job](actions.md#show-a-loading-state-with-a-job)):
93
+ The module gives the job its work and keeps the state from the outcomes, so an aborted load clears the flag too (see [Show a loading state with a job](actions.md#the-state-of-a-job)):
92
94
 
93
95
  ```js
94
96
  // message-module.js