nanoeffects 0.7.0 → 0.9.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 (76) hide show
  1. package/README.md +2 -2
  2. package/dist/{action-creator-Bd4TBZgl.d.ts → action-creator-BrZF6hXc.d.ts} +0 -4
  3. package/dist/actions.d.ts +41 -25
  4. package/dist/actions.js +1 -1
  5. package/dist/bus-CkTHUAvq.js +1 -0
  6. package/dist/context-COgyNlrC.js +1 -0
  7. package/dist/{context-plugin-BVNKqkIx.d.ts → context-plugin--HkNN1ha.d.ts} +1 -1
  8. package/dist/dev.d.ts +9 -8
  9. package/dist/dev.js +1 -1
  10. package/dist/effects.d.ts +2 -2
  11. package/dist/effects.js +1 -1
  12. package/dist/index.d.ts +2 -2
  13. package/dist/index.js +1 -1
  14. package/dist/match.d.ts +50 -0
  15. package/dist/match.js +1 -0
  16. package/dist/{module-DZ5ngUrM.d.ts → module-DqKegXHt.d.ts} +43 -18
  17. package/dist/notrace/{action-creator-D-y_-xRS.d.ts → action-creator-DHAEgwuh.d.ts} +0 -4
  18. package/dist/notrace/actions.d.ts +90 -39
  19. package/dist/notrace/actions.js +1 -1
  20. package/dist/notrace/bus-BmCTo8BG.js +1 -0
  21. package/dist/notrace/{context-plugin-DDfsaErA.d.ts → context-plugin-BapQkGwQ.d.ts} +1 -1
  22. package/dist/notrace/context-yYU3_9lq.js +1 -0
  23. package/dist/notrace/{delay-B_W1tnkS.js → delay-BosJKFjg.js} +1 -1
  24. package/dist/notrace/effects.d.ts +2 -2
  25. package/dist/notrace/effects.js +1 -1
  26. package/dist/notrace/index.d.ts +2 -2
  27. package/dist/notrace/index.js +1 -1
  28. package/dist/notrace/{plugin-CDAngHwg.d.ts → plugin-Cxadz6uU.d.ts} +2 -2
  29. package/dist/notrace/policies.d.ts +30 -2
  30. package/dist/notrace/policies.js +1 -1
  31. package/dist/notrace/redux.d.ts +1 -1
  32. package/dist/observable.d.ts +1 -1
  33. package/dist/oop.d.ts +131 -0
  34. package/dist/oop.js +1 -0
  35. package/dist/{plugin-DQNPBcxx.d.ts → plugin-DkXCvsmB.d.ts} +2 -2
  36. package/dist/policies.d.ts +3 -2
  37. package/dist/policies.js +1 -1
  38. package/dist/react.d.ts +33 -1
  39. package/dist/react.js +1 -1
  40. package/dist/redux.d.ts +1 -1
  41. package/dist/scope.d.ts +5 -6
  42. package/dist/scope.js +1 -1
  43. package/dist/signal.d.ts +51 -0
  44. package/dist/signal.js +1 -0
  45. package/dist/trace.d.ts +1 -1
  46. package/dist/{types-BJ0fzV1p.d.ts → types-KavbvHix.d.ts} +13 -3
  47. package/dist/with-policy-CqUS-VWW.d.ts +30 -0
  48. package/dist/with-policy-DBV9dh2F.js +1 -0
  49. package/docs/README.md +7 -0
  50. package/docs/actions.md +152 -39
  51. package/docs/cancel-stale-requests.md +111 -0
  52. package/docs/debounce-and-abort.md +106 -0
  53. package/docs/guide.md +143 -19
  54. package/docs/hmr.md +6 -5
  55. package/docs/migrating-from-effector.md +51 -18
  56. package/docs/migrating-from-mitt.md +97 -0
  57. package/docs/migrating-from-rxjs.md +17 -6
  58. package/docs/motivation.md +41 -0
  59. package/docs/observable.md +124 -10
  60. package/docs/oop.md +286 -0
  61. package/docs/package.md +6 -2
  62. package/docs/plugins.md +247 -51
  63. package/docs/policies.md +33 -0
  64. package/docs/prevent-double-submit.md +128 -0
  65. package/docs/query-libraries.md +124 -0
  66. package/docs/react.md +61 -5
  67. package/docs/scope.md +60 -22
  68. package/docs/stores.md +263 -109
  69. package/llms-full.txt +2153 -355
  70. package/llms.txt +10 -3
  71. package/package.json +43 -4
  72. package/skills/nanoeffects/SKILL.md +52 -0
  73. package/dist/bus-BAs9x7IQ.js +0 -1
  74. package/dist/context-CK0ZomWt.js +0 -1
  75. package/dist/notrace/bus-Bdk0YyFQ.js +0 -1
  76. package/dist/notrace/context-CctVmHLC.js +0 -1
package/dist/react.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { B as EventMap, T as EventKey, h as Bus, r as FreeActionCreator, t as ActionCreator } from "./action-creator-Bd4TBZgl.js";
1
+ import { B as EventMap, T as EventKey, W as Task, h as Bus, r as FreeActionCreator, t as ActionCreator } from "./action-creator-BrZF6hXc.js";
2
+ import { t as PolicyFunction } from "./with-policy-CqUS-VWW.js";
2
3
  //#region src/react/plugin.d.ts
3
4
  /** What `reactPlugin` adds to the bus. */
4
5
  /** Tells whether two values are the same: `Object.is` unless the caller gives another. */
@@ -47,4 +48,35 @@ type ReactApi<E extends EventMap> = {
47
48
  */
48
49
  export declare const reactPlugin: () => <E extends EventMap>(bus: Bus<E>) => Bus<E> & ReactApi<E>;
49
50
  //#endregion
51
+ //#region src/react/use-policy.d.ts
52
+ /**
53
+ * A policy (`debounce`, `throttle`, `latest`, ...) as one stable function for a
54
+ * component. A policy made in the body of a component is made again on every
55
+ * render and loses its timer; made once, it keeps the callback of the first
56
+ * render. This hook keeps the policy across renders and calls the `callback` of
57
+ * the latest render, so an inline callback sees fresh props and state.
58
+ *
59
+ * `make` gets a task that runs the callback and returns the policy built around
60
+ * it. It runs once, and again when an item of `deps` changes: list there what
61
+ * `make` reads, such as the wait time. The pending calls of the old function are
62
+ * cancelled then, and when the component unmounts. Needs `react` 18 or newer.
63
+ *
64
+ * @example
65
+ * ```tsx
66
+ * import { debounce } from "nanoeffects/policies";
67
+ * import { usePolicy } from "nanoeffects/react";
68
+ *
69
+ * const Search = ({ wait }: { wait: number }) => {
70
+ * const [results, setResults] = useState([]);
71
+ * const onChange = usePolicy(
72
+ * async (query: string) => setResults(await search(query)), // latest render's callback
73
+ * (task) => debounce(wait, task), // the policy, made once per wait
74
+ * [wait],
75
+ * );
76
+ * return <input onChange={(event) => onChange(event.target.value)} />;
77
+ * };
78
+ * ```
79
+ */
80
+ export declare const usePolicy: <P, R, S = R>(callback: Task<P, EventMap, R>, make: (task: Task<P, EventMap, R>) => Task<P, EventMap, S>, deps?: readonly unknown[]) => PolicyFunction<P, S>;
81
+ //#endregion
50
82
  export type { ReactApi };
package/dist/react.js CHANGED
@@ -1 +1 @@
1
- import{useEffect as e,useRef as t}from"react";const n=()=>n=>{function r(r,i,a){let o=t(i),s=t({event:r,equals:a});e(()=>{s.current={event:r,equals:a}}),e(()=>{let{event:e,equals:t}=s.current,r=t??Object.is,a=o.current;if(r(a,i))return;o.current=i;let c=typeof e==`function`?e(i):{type:e,payload:i};n.emit(c)},[i])}return Object.assign(n,{useEmitter:r})};export{n as reactPlugin};
1
+ import{t as e}from"./with-policy-DBV9dh2F.js";import{useEffect as t,useMemo as n,useRef as r}from"react";const i=()=>e=>{function n(n,i,a){let o=r(i),s=r({event:n,equals:a});t(()=>{s.current={event:n,equals:a}}),t(()=>{let{event:t,equals:n}=s.current,r=n??Object.is,a=o.current;if(r(a,i))return;o.current=i;let c=typeof t==`function`?t(i):{type:t,payload:i};e.emit(c)},[i])}return Object.assign(e,{useEmitter:n})},a=(i,a,o=[])=>{let s=r(i);t(()=>{s.current=i});let c=n(()=>e(a((e,t)=>s.current(e,t))),o);return t(()=>()=>c.cancel(),[c]),c};export{i as reactPlugin,a as usePolicy};
package/dist/redux.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { B as EventMap, h as Bus } from "./action-creator-Bd4TBZgl.js";
1
+ import { B as EventMap, h as Bus } from "./action-creator-BrZF6hXc.js";
2
2
  //#region src/store/redux/middleware.d.ts
3
3
  /** What Redux passes through a middleware: an action is a plain object with a `type`. */
4
4
  type ReduxAction = {
package/dist/scope.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { B as EventMap, a as EmitScope, d as Effects, f as Signaled, h as Bus, o as Scope, u as Effect } from "./action-creator-Bd4TBZgl.js";
2
- import { n as Scoped } from "./types-BJ0fzV1p.js";
1
+ import { B as EventMap, a as EmitScope, d as Effects, f as Signaled, h as Bus, o as Scope, u as Effect } from "./action-creator-BrZF6hXc.js";
2
+ import { n as ScopeSlots, r as Scoped } from "./types-KavbvHix.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
@@ -35,9 +35,8 @@ export declare function effect<A extends unknown[], R>(fn: (ctx: Signaled, ...ar
35
35
  * Fields of the library (`signal`, `emit`, `run`, …) are never replaced. Returns
36
36
  * the same bus.
37
37
  *
38
- * Use it after `effectsPlugin`, which assigns its effects over those of a scope
39
- * when it comes later, and before `modulePlugin`, which mounts its modules when
40
- * it is used. Without this plugin a scope is ignored.
38
+ * It works in any order with the other plugins. Without this plugin a scope is
39
+ * ignored.
41
40
  *
42
41
  * @example
43
42
  * ```ts
@@ -53,6 +52,6 @@ export declare function effect<A extends unknown[], R>(fn: (ctx: Signaled, ...ar
53
52
  * bus.emit(getUser("1"), { http: fakeHttp }); // this call only
54
53
  * ```
55
54
  */
56
- export declare const scopePlugin: () => <E extends EventMap>(bus: Bus<E>) => Bus<E>;
55
+ export declare const scopePlugin: () => <E extends EventMap>(bus: Bus<E> & ScopeSlots) => Bus<E> & ScopeSlots;
57
56
  //#endregion
58
57
  export type { Effect, Effects, EmitScope, Scope, Scoped, Signaled };
package/dist/scope.js CHANGED
@@ -1 +1 @@
1
- import{t as e}from"./scope-key-B7l7mUS5.js";const t=/* @__PURE__ */ new WeakSet;function n(e){return t.add(e),e}const r=/* @__PURE__ */ new Set([`signal`,`abort`,`emit`,`on`,`off`,`run`,`type`,`payload`]),i=[],a=Symbol(`nanoeffects.scope.call`),o=Symbol(`nanoeffects.scope.module`),s=Symbol(`nanoeffects.scope.watched`),c=e=>e,l=e=>typeof e==`function`&&t.has(e),u=(e,t)=>(...n)=>Reflect.apply(t,null,[e,...n]),d=(e,t)=>{if(!t)return;for(let n in t)if(n!==`$effects`&&!r.has(n)){let r=t[n];e[n]=l(r)?u(e,r):r}let n=t.$effects;if(n)for(let t in n){let i=n[t];i&&!r.has(t)&&(e[t]=u(e,i))}},f=(e,t,n)=>{d(e,n);for(let n of t)d(e,n)},p=e=>typeof e==`object`&&!!e,m=e=>p(e),h=e=>(p(e)||typeof e==`function`)&&`type`in e,g=e=>h(e)?e.type:e,_=(e,t)=>p(e)&&h(e)?e.payload:t[0],v=t=>p(t)||typeof t==`function`?Reflect.get(t,e):void 0,y=(e,t)=>{let n=v(e),r=p(e)&&m(t[0])?t[0]:void 0;if(n&&r)return[n,r];let i=n??r;return i?[i]:void 0},b=()=>e=>{let t,n,r,l=e=>{let n=e.run;if(n[s])return;let r=(r,...i)=>{let s=t;t=void 0;try{return n((t,n)=>{let i=e[a];return i&&u(n,i,e[o]),r(t,n)},...i)}finally{t=s}};r[s]=!0,e.run=r},u=(e,t,n)=>{e[a]=t,n&&(e[o]=n),f(e,t,n),l(e)},d=e.addContext;e.addContext=e=>{d?.(e);let i=c(e);t&&i.type===n&&Object.is(i.payload,r)&&u(i,t,void 0)},e.applyScope=(e,t)=>{let n=c(e);u(n,n[a]??i,t)},e.fitScope=(t,n)=>typeof n==`function`?(r,i)=>(e.applyScope?.(i,t),Reflect.apply(n,null,[r,i])):n;let m=e.emit;return e.emit=(e,...i)=>{let a=y(e,i);if(!(a||t))return m(e,...i);let o=t,s=n,c=r;t=a,n=g(e),r=_(e,i);try{return p(e)?m(e):m(e,...i)}finally{t=o,n=s,r=c}},e};export{n as effect,b as scopePlugin};
1
+ import{t as e}from"./scope-key-B7l7mUS5.js";const t=/* @__PURE__ */ new WeakSet;function n(e){return t.add(e),e}const r=/* @__PURE__ */ new Set([`signal`,`abort`,`emit`,`on`,`off`,`run`,`type`,`payload`]),i=[],a=Symbol(`nanoeffects.scope.call`),o=Symbol(`nanoeffects.scope.module`),s=Symbol(`nanoeffects.scope.watched`),c=e=>e,l=e=>typeof e==`function`&&t.has(e),u=(e,t)=>(...n)=>Reflect.apply(t,null,[e,...n]),d=(e,t)=>{if(!t)return;for(let n in t)if(n!==`$effects`&&!r.has(n)){let r=t[n];e[n]=l(r)?u(e,r):r}let n=t.$effects;if(n)for(let t in n){let i=n[t];i&&!r.has(t)&&(e[t]=u(e,i))}},f=(e,t,n)=>{d(e,n);for(let n of t)d(e,n)},p=e=>typeof e==`object`&&!!e,m=e=>p(e),h=e=>(p(e)||typeof e==`function`)&&`type`in e,g=e=>h(e)?e.type:e,_=(e,t)=>p(e)&&h(e)?e.payload:t[0],v=t=>p(t)||typeof t==`function`?Reflect.get(t,e):void 0,y=(e,t)=>{let n=v(e),r=p(e)&&m(t[0])?t[0]:void 0;if(n&&r)return[n,r];let i=n??r;return i?[i]:void 0},b=()=>e=>{let t,n,r,l=e=>{let n=e.run;if(n[s])return;let r=(r,...i)=>{let s=t;t=void 0;try{return n((t,n)=>{let i=e[a];return i&&u(n,i,e[o]),r(t,n)},...i)}finally{t=s}};r[s]=!0,e.run=r},u=(e,t,n)=>{e[a]=t,n&&(e[o]=n),f(e,t,n),l(e)},d=e.addContext;e.addContext=e=>{d?.(e);let i=c(e);t&&i.type===n&&Object.is(i.payload,r)&&u(i,t,void 0)},e.applyScope=(e,t)=>{let n=c(e);u(n,n[a]??i,t)},e.fitScope=(t,n)=>typeof n==`function`?(r,i)=>(e.applyScope?.(i,t),Reflect.apply(n,null,[r,i])):n;let p=e.emit;return e.emit=(e,...i)=>{let a=y(e,i);if(!(a||t))return p(e,...i);let o=t,s=n,c=r;t=a,n=g(e),r=_(e,i);try{return p(e,...i)}finally{t=o,n=s,r=c}},e};export{n as effect,b as scopePlugin};
@@ -0,0 +1,51 @@
1
+ import { B as EventMap, C as Emit, E as EventLike, a as EmitScope, c as WithScope, h as Bus, v as CommandCall } from "./action-creator-BrZF6hXc.js";
2
+ //#region src/signal/plugin.d.ts
3
+ /** What the second argument of `emit` takes besides the fields of a scope. */
4
+ type SignalOption = {
5
+ /** Aborts the tasks that this `emit` starts, with its reason, when it aborts. */
6
+ signal?: AbortSignal;
7
+ };
8
+ /** `emit` of a bus with `signalPlugin`: the call forms take `{ signal }` as their second argument. */
9
+ type SignalEmit<E extends EventMap> = Emit<E> & {
10
+ <P, R, F>(call: CommandCall<P, R> & WithScope<F>, options: SignalOption & EmitScope<E, F>): R;
11
+ <P, R>(call: CommandCall<P, R>, options: SignalOption & EmitScope<E>): R;
12
+ (event: EventLike<E>, options: SignalOption & EmitScope<E>): void;
13
+ };
14
+ /** A bus with `signalPlugin`: the same bus, with the `emit` that takes a signal. */
15
+ type SignalBus<E extends EventMap> = Bus<E> & {
16
+ emit: SignalEmit<E>;
17
+ };
18
+ /**
19
+ * Lets `bus.emit(call, { signal })` tie the tasks that this emit starts to an
20
+ * outside `AbortSignal`: when it aborts, their `ctx.signal` aborts with the same
21
+ * reason, so a handler, `latest`, `retry` and `timeout` stop as for any abort. It
22
+ * is made for libraries that own the request and give you a signal, such as the
23
+ * `queryFn` of React Query. The tasks that count are those made while the call
24
+ * to `emit` runs (the command or job and the listeners of its type, and their
25
+ * `ctx.run` children); a separate `emit` that a handler makes later is not
26
+ * linked. `emit` returns what it returns without the plugin.
27
+ *
28
+ * For a command or a job the link ends when its promise settles; a handler
29
+ * that returns without a promise is finished with the call, so it is not
30
+ * linked after it. For an event the link stays until the signal aborts, so
31
+ * give each request its own signal.
32
+ *
33
+ * `{ signal }` can share the object with the fields of a scope (see
34
+ * `scopePlugin`); `signal` is never put on `ctx` by a scope. Without this
35
+ * plugin the signal is ignored. Returns the same bus, typed with the new `emit`.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * import { createBus } from "nanoeffects";
40
+ * import { signalPlugin } from "nanoeffects/signal";
41
+ *
42
+ * const bus = createBus().use(signalPlugin()); // emit takes a signal now
43
+ *
44
+ * // React Query aborts `signal` when the component unmounts or the key changes
45
+ * const queryFn = ({ signal }: { signal: AbortSignal }) =>
46
+ * bus.emit(getUser("1"), { signal });
47
+ * ```
48
+ */
49
+ export declare const signalPlugin: () => <E extends EventMap>(bus: Bus<E>) => SignalBus<E>;
50
+ //#endregion
51
+ export type { SignalBus, SignalEmit, SignalOption };
package/dist/signal.js ADDED
@@ -0,0 +1 @@
1
+ import{t as e}from"./onAbort-BgMfspK4.js";import{n as t}from"./promise-CSLpzwSR.js";import{n}from"./bus-CkTHUAvq.js";const r=e=>typeof e==`object`&&!!e,i=(e,t)=>{let n=t[0];if(!r(n)||!r(e)&&typeof e!=`function`)return;let i=Reflect.get(n,`signal`);return i instanceof AbortSignal?i:void 0},a=(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},o=()=>e=>{let t,n,r=e.addContext;e.addContext=e=>{r?.(e),t?.push({target:e}),n?.aborted&&e.abort(n.reason)};let o=e.emit;return e.emit=(e,...r)=>{let s=i(e,r);if(!(s||t))return o(e,...r);let c=t,l=n,u=[];t=s?u:void 0,n=s;let d;try{d=o(e,...r)}finally{t=c,n=l}return s?a(s,u,e,d):d},e};export{o as signalPlugin};
package/dist/trace.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { G as Trace, K as TraceEvent, L as Tracer, q as TraceLogger } from "./action-creator-Bd4TBZgl.js";
1
+ import { G as Trace, K as TraceEvent, L as Tracer, q as TraceLogger } from "./action-creator-BrZF6hXc.js";
2
2
  //#region src/trace/console.d.ts
3
3
  type ConsoleLoggerOptions = {
4
4
  /** Defaults to `globalThis.console`. */
@@ -1,6 +1,16 @@
1
- import { B as EventMap, d as Effects, o as Scope, s as WithFields } from "./action-creator-Bd4TBZgl.js";
2
- import { t as BoundOf } from "./plugin-DQNPBcxx.js";
1
+ import { B as EventMap, d as Effects, o as Scope, s as WithFields } from "./action-creator-BrZF6hXc.js";
2
+ import { t as BoundOf } from "./plugin-DkXCvsmB.js";
3
3
  //#region src/scope/types.d.ts
4
+ /**
5
+ * The slots `scopePlugin` fills for `modulePlugin`. They are not on `Bus`: a bus
6
+ * has them in its type once the plugin returned it.
7
+ */
8
+ interface ScopeSlots {
9
+ /** Puts the fields of a module's scope on the context of one of its tasks. Without it a module's scope is ignored. */
10
+ applyScope?: ((ctx: object, scope: Scope) => void) | undefined;
11
+ /** Wraps a handler so it runs with the fields of a module's scope. */
12
+ fitScope?: ((scope: Scope, fn: unknown) => unknown) | undefined;
13
+ }
4
14
  type BoundEffects<E extends EventMap, S extends Scope> = S extends {
5
15
  $effects: infer H extends Effects;
6
16
  } ? BoundOf<H, E> : unknown;
@@ -12,4 +22,4 @@ type FieldsOfScope<E extends EventMap, S extends Scope> = [keyof S] extends [nev
12
22
  */
13
23
  type Scoped<E extends EventMap, S extends Scope> = WithFields<E, FieldsOfScope<E, S>>;
14
24
  //#endregion
15
- export { Scoped as n, FieldsOfScope as t };
25
+ export { ScopeSlots as n, Scoped as r, FieldsOfScope as t };
@@ -0,0 +1,30 @@
1
+ import { B as EventMap, H as PayloadOf, W as Task } from "./action-creator-BrZF6hXc.js";
2
+ //#region src/policies/with-policy.d.ts
3
+ /** What `withPolicy` returns: the task as a plain function that can be cancelled. */
4
+ type PolicyFunction<P, R> = {
5
+ (...args: PayloadOf<P>): Promise<Awaited<R> | undefined>;
6
+ /** Aborts every call that has not finished. Calls made after it work as before. */
7
+ cancel: (reason?: unknown) => void;
8
+ /** Same as `cancel`; present where `Symbol.dispose` exists, for `using`. */
9
+ [Symbol.dispose]?: () => void;
10
+ };
11
+ /**
12
+ * Makes a policy (or any task) a plain function that needs no bus and no
13
+ * context. The returned function keeps the state of the policy between calls,
14
+ * so a debounced one waits across calls. `cancel()` aborts what is pending or
15
+ * running; calls after it work again. The callback keeps the `(payload, ctx)`
16
+ * shape; `ctx.signal`, `ctx.abort` and `ctx.run` work, `ctx.emit` and `ctx.on`
17
+ * do nothing because there is no bus.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * const onChange = withPolicy(debounce(300, (query: string) => search(query)));
22
+ *
23
+ * onChange("re");
24
+ * onChange("redux"); // only this one runs, 300 ms after it
25
+ * onChange.cancel(); // or nothing runs at all
26
+ * ```
27
+ */
28
+ declare const withPolicy: <P, R>(task: Task<P, EventMap, R>) => PolicyFunction<P, R>;
29
+ //#endregion
30
+ export { withPolicy as n, PolicyFunction as t };
@@ -0,0 +1 @@
1
+ import{n as e}from"./context-COgyNlrC.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};
package/docs/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  The [Guide](guide.md) is the ladder: small bus → policies for async UI → effects when you need them → commands and modules for domain logic off the UI.
6
6
 
7
+ Why the library exists, and when to pick it over Redux, Effector or RxJS (SDKs, shared libraries, Module Federation): [Why nanoeffects exists](motivation.md).
8
+
7
9
  Code for every guide step: [`examples/guide.ts`](../examples/guide.ts) (type-checked and tested).
8
10
 
9
11
  ## Jump by project need
@@ -16,6 +18,10 @@ Code for every guide step: [`examples/guide.ts`](../examples/guide.ts) (type-che
16
18
  | Domain logic off the UI | [Actions, commands, modules](actions.md) |
17
19
  | Already on Redux / Zustand / MobX / Pinia | [Stores](stores.md) |
18
20
  | A React form with async checks | [Build a sign-up form in React](react.md) |
21
+ | Old responses overwrite new ones | [Cancel stale requests](cancel-stale-requests.md) |
22
+ | Debounced input that must also cancel the old request | [Debounce and abort](debounce-and-abort.md) |
23
+ | A second click sends a second request | [Prevent a double submit](prevent-double-submit.md) |
24
+ | Using mitt, nanoevents or EventEmitter | [Coming from mitt](migrating-from-mitt.md) |
19
25
  | Moving from redux-saga | [Coming from redux-saga](migrating-from-saga.md) |
20
26
  | Using RxJS | [Use nanoeffects next to RxJS](migrating-from-rxjs.md) |
21
27
  | Moving from Effector | [Coming from Effector](migrating-from-effector.md) |
@@ -27,6 +33,7 @@ Code for every guide step: [`examples/guide.ts`](../examples/guide.ts) (type-che
27
33
  - [Actions, commands, jobs and modules](actions.md)
28
34
  - [Sign-up form in React](react.md) — one form: races, double submit, retry
29
35
  - [Context fields](context.md) — store or client on every task
36
+ - [Use the bus from a class](oop.md) — `extendBus`, `@WithBus`, `@On`, and `dispose()` for the subscriptions of an instance
30
37
  - [TypeScript in many files](typescript.md) — keep the types of the bus in commands and modules
31
38
  - [Errors and unheard events](errors.md)
32
39
  - [Package entries](package.md) — which import holds what
package/docs/actions.md CHANGED
@@ -8,14 +8,28 @@ Recipe without a UI framework: [`examples/modules.ts`](../examples/modules.ts).
8
8
 
9
9
  ## Return a value from `emit` with a command
10
10
 
11
+ The bus lives in one file and its events in another, so any module can import both without a cycle.
12
+
11
13
  ```js
14
+ // bus.js
12
15
  import { createBus } from "nanoeffects";
13
- import { createCommand } from "nanoeffects/actions";
14
16
 
15
- const bus = createBus();
17
+ // createBus — one bus for the app
18
+ export const bus = createBus();
19
+ ```
20
+
21
+ ```js
22
+ // math.js
23
+ import { createCommand } from "nanoeffects/actions";
16
24
 
17
25
  // createCommand — emit runs the handler and returns its result
18
- const double = createCommand("math/double", (value) => value * 2);
26
+ export const double = createCommand("math/double", (value) => value * 2);
27
+ ```
28
+
29
+ ```js
30
+ import { bus } from "./bus";
31
+ import { double } from "./math";
32
+
19
33
  bus.emit(double(21)); // 42
20
34
  ```
21
35
 
@@ -24,27 +38,30 @@ bus.emit(double(21)); // 42
24
38
  `nanoeffects/actions` has the pieces to name what a bus does, apart from the code that does it.
25
39
 
26
40
  ```js
41
+ // user.js
27
42
  import { createAction, createCommand, createJob } from "nanoeffects/actions";
28
43
 
29
44
  // createAction — event with a payload (like RTK createAction)
30
- const userLoaded = createAction("user/loaded");
31
- const loadFailed = createAction("user/failed");
45
+ export const userLoaded = createAction("user/loaded");
46
+ export const loadFailed = createAction("user/failed");
32
47
 
33
48
  // createCommand with a handler — emit returns what the handler returns
34
- const getUser = createCommand("user/get", (id, { signal }) => {
49
+ export const getUser = createCommand("user/get", (id, { signal }) => {
35
50
  return fetchUser(id, { signal });
36
51
  });
37
52
 
38
53
  // createCommand without a handler — a contract; register the work elsewhere
39
- const sendMail = createCommand("mail/send");
54
+ export const sendMail = createCommand("mail/send");
40
55
 
41
56
  // createJob — command that also emits success / fail actions
42
- const loadUser = createJob("user/load", fetchUser, {
57
+ export const loadUser = createJob("user/load", fetchUser, {
43
58
  success: userLoaded,
44
59
  fail: "user/failed",
45
60
  });
46
61
  ```
47
62
 
63
+ These are the events of the user domain, in a file of their own (`user.js`). The bus is made in another file (`bus.js` below), and neither imports the other.
64
+
48
65
  ### Actions
49
66
 
50
67
  `createAction(type)` returns a creator. Without generics it keeps the literal type; the bus checks the payload against its event map. Without an event map, give the payload type: `createAction<User>("user/loaded")`.
@@ -65,22 +82,22 @@ Calling a command builds `{ type, payload }` like `createAction`. `bus.emit(comm
65
82
  **Where handlers are registered.** The command may carry a handler. The bus can also take handlers from a registry (`commandsPlugin`). A handler bound there wins over the command’s own. Each bus (each test) has its own registry.
66
83
 
67
84
  ```js
85
+ // bus.js
68
86
  import { createBus } from "nanoeffects";
69
- import {
70
- bindHandler,
71
- commandsPlugin,
72
- createCommand,
73
- } from "nanoeffects/actions";
87
+ import { bindHandler, commandsPlugin } from "nanoeffects/actions";
88
+ import { sendMail } from "./user";
74
89
 
75
- const sendMail = createCommand("mail/send");
76
- const getUser = createCommand("user/get");
77
-
78
- const bus = createBus();
90
+ export const bus = createBus();
79
91
 
80
92
  // commandsPlugin — registry of handlers for this bus
81
- const commands = bus.use(
93
+ export const commands = bus.use(
82
94
  commandsPlugin([bindHandler(sendMail, smtpSend)]),
83
95
  );
96
+ ```
97
+
98
+ ```js
99
+ import { commands } from "./bus";
100
+ import { getUser } from "./user";
84
101
 
85
102
  // later, e.g. in a test — latest binding wins
86
103
  const unbind = commands.handle(getUser, async () => fakeUser);
@@ -89,19 +106,88 @@ unbind();
89
106
 
90
107
  A command with no handler anywhere throws `No handler for command "..."` from `emit` (the call itself only builds the action).
91
108
 
92
- The registry lives in `nanoeffects/actions`, so a bus without one carries none of that code. Without a registry, `emit` runs the command’s own handler. `bus.use(commandsPlugin())` sets `bus.commands`. A second plugin on the same bus replaces it. There is no `commands` option in `createBus`; assign a custom runner with `bus.commands = { run }`.
109
+ The registry lives in `nanoeffects/actions`, so a bus without one carries none of that code. Without a registry, `emit` runs the command’s own handler. `bus.use(commandsPlugin())` sets `bus.commands`. A second `commandsPlugin` binds its list in that registry and returns it. There is no `commands` option in `createBus`; assign a custom runner with `bus.commands = { run }`.
93
110
 
94
111
  ### Jobs
95
112
 
96
- `createJob(type, handler, { success, fail })` makes a command that also sends its outcome as actions, when nobody waits for the result. It returns the command with `loadUser.success` and `loadUser.fail` on it.
113
+ `createJob(type, handler, { pending, success, fail, cancel })` makes a command that also sends its outcome as actions, when nobody waits for the result. It returns the command with `loadUser.pending`, `loadUser.success`, `loadUser.fail` and `loadUser.cancel` on it.
97
114
 
115
+ - When the handler starts, emits `loadUser.pending(payload)`.
98
116
  - On success, emits `loadUser.success(result)`.
99
117
  - On failure, emits `loadUser.fail(error)`.
100
- - `success` / `fail` are a string (job makes an action creator) or an existing action creator. Missing ones become `<type>/success` or `<type>/fail`.
118
+ - When the task is aborted before the handler settles, emits `loadUser.cancel(reason)` instead of `success` or `fail`. After `pending`, exactly one of the three follows.
119
+ - `pending` / `success` / `fail` / `cancel` are a string (job makes an action creator) or an existing action creator. Missing ones become `<type>/pending`, `<type>/success`, `<type>/fail` or `<type>/cancel`.
101
120
  - `bus.emit(loadUser(id))` returns a promise of the result. Handler errors are always handled: they go to `fail` and the promise resolves with `undefined`. For a result that must reject, use a command.
102
- - Outcome actions are emitted as the task that ran the job (also for a handler bound with `commands.handle`). Nothing is sent after abort.
103
- - A missing handler is not an outcome: `emit` throws `No handler for command "..."`.
121
+ - Outcome actions are emitted as the task that ran the job (also for a handler bound with `commands.handle`). `success` and `fail` are not sent after abort; `cancel` is, once the handler settles.
122
+ - A missing handler is not an outcome: `emit` throws `No handler for command "..."` and nothing is sent.
104
123
  - Calling the job only builds `{ type, payload }`; the handler runs on `emit`.
124
+ - 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.
125
+
126
+ ## Show a loading state with a job
127
+
128
+ **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`.
129
+
130
+ **When.** The UI shows a loading state for a request, and the request can be aborted (a page is left, `ctx.abort()` is called). Skip it when nothing shows the state.
131
+
132
+ A job sends one action when the handler starts and one when it ends, whatever way it ends. A store sets the flag on `pending` and clears it on `success`, `fail` and `cancel`.
133
+
134
+ ```js
135
+ // bus.js
136
+ import { createBus } from "nanoeffects";
137
+
138
+ // createBus — the bus the app shares
139
+ export const bus = createBus();
140
+ ```
141
+
142
+ ```js
143
+ // user.js
144
+ import { createJob } from "nanoeffects/actions";
145
+
146
+ // createJob — a command that also sends pending, success, fail and cancel
147
+ export const loadUser = createJob("user/load", fetchUser);
148
+ ```
149
+
150
+ ```js
151
+ // user-store.js
152
+ import { bus } from "./bus";
153
+ import { loadUser } from "./user";
154
+
155
+ export const state = { loading: false, user: null };
156
+
157
+ // pending — the handler starts: show the spinner
158
+ bus.on(loadUser.pending, () => {
159
+ state.loading = true;
160
+ });
161
+
162
+ // success — the handler returned a user
163
+ bus.on(loadUser.success, (user) => {
164
+ state.loading = false;
165
+ state.user = user;
166
+ });
167
+
168
+ // fail — the handler threw
169
+ bus.on(loadUser.fail, () => {
170
+ state.loading = false;
171
+ });
172
+
173
+ // cancel — the task was aborted: the only way to clear the flag then
174
+ bus.on(loadUser.cancel, () => {
175
+ state.loading = false;
176
+ });
177
+ ```
178
+
179
+ ```js
180
+ // page.js
181
+ import { bus } from "./bus";
182
+ import { loadUser } from "./user";
183
+
184
+ // emit — runs the job; state.loading is true as soon as it returns
185
+ bus.emit(loadUser("1"));
186
+ ```
187
+
188
+ `cancel` is sent when the handler settles, not at the moment of the abort. A handler that stops on `ctx.signal` (as `fetch` does) settles at once. A handler that ignores the signal sends `cancel` when it finishes, and one that never finishes sends nothing.
189
+
190
+ **Limit: policies.** A policy such as `latest`, `leading`, `throttle` or `queue` wraps the handler, inside the job. The job does not know what the policy decides, so `pending` is sent for every call. A call that the policy drops, and a run that `latest` replaces, then close with `success(undefined)`, not with `cancel`. A flag set by `pending` is exact for a job without a policy, and for an abort of the task or of its parent. With a policy, count the calls or use the result of `emit`.
105
191
 
106
192
  ## Mount domains with modules
107
193
 
@@ -112,18 +198,20 @@ The registry lives in `nanoeffects/actions`, so a bus without one carries none o
112
198
  `bus.use(modulePlugin(modules))` assembles an app from modules and returns `mount`, `unmount`, `isMounted`, `dispose`, `commands`, and `createModule`: the same as `createModule` below, with the context fields of the bus typed (see [TypeScript](typescript.md)).
113
199
 
114
200
  ```js
115
- import { createBus } from "nanoeffects";
116
- import {
117
- createCommand,
118
- createModule,
119
- modulePlugin,
120
- } from "nanoeffects/actions";
201
+ // user.js
202
+ import { createCommand } from "nanoeffects/actions";
203
+
204
+ export const getUser = createCommand("user/get");
205
+ export const saveUser = createCommand("user/save", persistUser);
206
+ ```
121
207
 
122
- const getUser = createCommand("user/get");
123
- const saveUser = createCommand("user/save", persistUser);
208
+ ```js
209
+ // user-module.js
210
+ import { createModule } from "nanoeffects/actions";
211
+ import { getUser, saveUser } from "./user";
124
212
 
125
213
  // createModule — one domain; unmount undoes everything below
126
- const userModule = createModule("user", (m) => {
214
+ export const userModule = createModule("user", (m) => {
127
215
  m.handle(getUser, fetchUser);
128
216
  m.use(saveUser, authModule);
129
217
  m.on("login", (user, ctx) => {
@@ -133,10 +221,22 @@ const userModule = createModule("user", (m) => {
133
221
  /* background work; ends when the module unmounts */
134
222
  });
135
223
  });
224
+ ```
136
225
 
137
- const bus = createBus();
226
+ ```js
227
+ // bus.js
228
+ import { createBus } from "nanoeffects";
229
+ import { modulePlugin } from "nanoeffects/actions";
230
+ import { userModule } from "./user-module";
231
+
232
+ export const bus = createBus();
138
233
  // modulePlugin — mount modules on the bus
139
- const app = bus.use(modulePlugin([userModule]));
234
+ export const app = bus.use(modulePlugin([userModule]));
235
+ ```
236
+
237
+ ```js
238
+ import { app } from "./bus";
239
+ import { getUser } from "./user";
140
240
 
141
241
  const unmount = app.mount(orderModule); // later, e.g. after a lazy import
142
242
  app.commands.handle(getUser, fakeUser); // in a test
@@ -147,6 +247,7 @@ app.dispose(); // unmounts every module
147
247
  - **A module is known by its name.** Mounting a module whose name is taken replaces the mounted one (see [Hot reload](hmr.md)).
148
248
  - **Registry.** If `bus.use(commandsPlugin())` ran first, that registry is used; otherwise the module plugin attaches its own. Either way it is `bus.commands` and `app.commands`.
149
249
  - A disposed app cannot mount modules.
250
+ - **The given modules are mounted on the first use** of the app or the bus (the first `bus.emit`, or a call of `mount`, `unmount`, `isMounted`, `dispose`, or the read of `commands`), so the plugins you use after `modulePlugin` still reach them. The `bus.on` calls made in between are held and made right after, so the listeners of a module stay ahead of them. See [using plugins in any order](plugins.md#use-them-in-any-order).
150
251
  - Module context = task context plus `handle`, `use`, and `spawn`:
151
252
  - `handle` — give a command a handler (contract, or override).
152
253
  - `use` — bring sub-modules and commands that carry a handler; the returned function undoes that call.
@@ -158,17 +259,29 @@ app.dispose(); // unmounts every module
158
259
  A command 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 `createCommand`, `createAction`, `createModule` and `createJob`, and turn it on with `scopePlugin`:
159
260
 
160
261
  ```js
262
+ // bus.js
161
263
  import { createBus } from "nanoeffects";
162
- import { createCommand } from "nanoeffects/actions";
163
264
  import { scopePlugin } from "nanoeffects/scope";
164
265
 
165
266
  // scopePlugin — makes the scope of a command work on this bus
166
- const bus = createBus().use(scopePlugin());
267
+ export const bus = createBus().use(scopePlugin());
268
+ ```
269
+
270
+ ```js
271
+ // user.js
272
+ import { createCommand } from "nanoeffects/actions";
167
273
 
168
274
  // the third argument — fields of this command only
169
- const getUser = createCommand("user/get", (id, { api }) => api.getUser(id), {
170
- api,
171
- });
275
+ export const getUser = createCommand(
276
+ "user/get",
277
+ (id, { api }) => api.getUser(id),
278
+ { api },
279
+ );
280
+ ```
281
+
282
+ ```js
283
+ import { bus } from "./bus";
284
+ import { getUser } from "./user";
172
285
 
173
286
  // a test replaces them for one call
174
287
  const fakeApi = { getUser: async (id) => ({ id, name: "test" }) };
@@ -0,0 +1,111 @@
1
+ # Cancel stale requests and fix race conditions
2
+
3
+ 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
+
5
+ ## See the problem
6
+
7
+ A user types `ab`, then `abc`. Two requests start. If the one for `ab` is slower, it finishes last and its results replace the right ones.
8
+
9
+ ```js
10
+ import { createBus } from "nanoeffects";
11
+
12
+ const bus = createBus(); // the bus: events and handlers
13
+
14
+ // Without a policy every "search" starts its own request, and the slowest one wins.
15
+ bus.on("search", async (query) => {
16
+ const items = await api.search(query);
17
+ bus.emit("results", items);
18
+ });
19
+ ```
20
+
21
+ Checking a counter by hand (`if (id !== lastId) return`) hides the old result, but the old request still runs. Calling `controller.abort()` by hand works, but you must keep the controller somewhere and abort it before each new call.
22
+
23
+ ## Keep only the latest request
24
+
25
+ Wrap the handler in `latest`. A new `"search"` aborts the task of the previous one, and an aborted task delivers no result.
26
+
27
+ ```js
28
+ import { createBus } from "nanoeffects";
29
+ import { latest } from "nanoeffects/policies";
30
+
31
+ const bus = createBus(); // the bus: events and handlers
32
+
33
+ // latest: a newer "search" aborts the older run
34
+ bus.on(
35
+ "search",
36
+ latest(async (query, { signal }) => {
37
+ // signal: pass it on so the browser cancels the request, not only drops the result
38
+ const items = await api.search(query, { signal });
39
+ bus.emit("results", items);
40
+ }),
41
+ );
42
+
43
+ bus.on("results", (items) => render(items));
44
+ ```
45
+
46
+ What to replace: `api.search` is your own request function. It must accept `{ signal }` and give it to `fetch`. Without it, `latest` still ignores the old result, but the request keeps running.
47
+
48
+ Result: `emit("search", "ab")` followed by `emit("search", "abc")` shows only the results for `abc`, and the request for `ab` is cancelled.
49
+
50
+ ## Wait for a pause in typing
51
+
52
+ Add `debounce` outside `latest`. The search starts after 300 ms without a new keystroke.
53
+
54
+ ```js
55
+ import { createBus } from "nanoeffects";
56
+ import { debounce, latest } from "nanoeffects/policies";
57
+
58
+ const bus = createBus(); // the bus: events and handlers
59
+
60
+ // latest: a newer search aborts the request in flight
61
+ const searchLatest = latest(async (query, { signal }) => {
62
+ const items = await api.search(query, { signal });
63
+ bus.emit("results", items);
64
+ });
65
+
66
+ // debounce: run after 300 ms without a new "typed"
67
+ bus.on("typed", debounce(300, searchLatest));
68
+ ```
69
+
70
+ ## Retry a failed request and limit its time
71
+
72
+ Use `retry` and `timeout` around the request. The pause grows with each attempt.
73
+
74
+ ```js
75
+ import { createBus } from "nanoeffects";
76
+ import { latest } from "nanoeffects/policies";
77
+ import { backoff, retry, timeout } from "nanoeffects/effects";
78
+
79
+ const bus = createBus(); // the bus: events and handlers
80
+
81
+ // timeout: abort one attempt after 5 s
82
+ const searchOnce = timeout(5000, (query, { signal }) => api.search(query, { signal }));
83
+
84
+ // retry: up to 4 attempts, backoff makes the pause longer each time
85
+ const searchWithRetry = retry(searchOnce, {
86
+ times: 4,
87
+ delay: backoff({ base: 200, max: 5000, jitter: true }),
88
+ });
89
+
90
+ // latest: a newer search aborts the whole retry chain
91
+ bus.on(
92
+ "search",
93
+ latest(async (query, ctx) => {
94
+ const items = await ctx.run(searchWithRetry, query); // ctx.run: a child task, aborted with this one
95
+ ctx.emit("results", items);
96
+ }),
97
+ );
98
+ ```
99
+
100
+ ## Pick another policy for another race
101
+
102
+ | Situation | Policy |
103
+ | --------- | ------ |
104
+ | Only the newest result matters (search, filter, tab switch) | `latest` |
105
+ | A second click while the first call runs must be ignored (submit, pay) | `leading` |
106
+ | Every call must run, one after another (save, upload) | `queue` |
107
+ | Calls come in bursts and you want one at the end | `debounce` |
108
+ | Calls come in bursts and you want one per period | `throttle` |
109
+ | Each user or item has its own race | add `{ key }`, for example `latest(handler, { key })` |
110
+
111
+ All policies and their options: [Policies](policies.md). The same step by step in a form: [Build a sign-up form in React](react.md).