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.
- package/README.md +2 -2
- package/dist/{action-creator-Bd4TBZgl.d.ts → action-creator-BrZF6hXc.d.ts} +0 -4
- package/dist/actions.d.ts +41 -25
- package/dist/actions.js +1 -1
- package/dist/bus-CkTHUAvq.js +1 -0
- package/dist/context-COgyNlrC.js +1 -0
- package/dist/{context-plugin-BVNKqkIx.d.ts → context-plugin--HkNN1ha.d.ts} +1 -1
- package/dist/dev.d.ts +9 -8
- package/dist/dev.js +1 -1
- package/dist/effects.d.ts +2 -2
- package/dist/effects.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/match.d.ts +50 -0
- package/dist/match.js +1 -0
- package/dist/{module-DZ5ngUrM.d.ts → module-DqKegXHt.d.ts} +43 -18
- package/dist/notrace/{action-creator-D-y_-xRS.d.ts → action-creator-DHAEgwuh.d.ts} +0 -4
- package/dist/notrace/actions.d.ts +90 -39
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/bus-BmCTo8BG.js +1 -0
- package/dist/notrace/{context-plugin-DDfsaErA.d.ts → context-plugin-BapQkGwQ.d.ts} +1 -1
- package/dist/notrace/context-yYU3_9lq.js +1 -0
- package/dist/notrace/{delay-B_W1tnkS.js → delay-BosJKFjg.js} +1 -1
- package/dist/notrace/effects.d.ts +2 -2
- package/dist/notrace/effects.js +1 -1
- package/dist/notrace/index.d.ts +2 -2
- package/dist/notrace/index.js +1 -1
- package/dist/notrace/{plugin-CDAngHwg.d.ts → plugin-Cxadz6uU.d.ts} +2 -2
- package/dist/notrace/policies.d.ts +30 -2
- package/dist/notrace/policies.js +1 -1
- package/dist/notrace/redux.d.ts +1 -1
- package/dist/observable.d.ts +1 -1
- package/dist/oop.d.ts +131 -0
- package/dist/oop.js +1 -0
- package/dist/{plugin-DQNPBcxx.d.ts → plugin-DkXCvsmB.d.ts} +2 -2
- package/dist/policies.d.ts +3 -2
- package/dist/policies.js +1 -1
- package/dist/react.d.ts +33 -1
- package/dist/react.js +1 -1
- package/dist/redux.d.ts +1 -1
- package/dist/scope.d.ts +5 -6
- package/dist/scope.js +1 -1
- package/dist/signal.d.ts +51 -0
- package/dist/signal.js +1 -0
- package/dist/trace.d.ts +1 -1
- package/dist/{types-BJ0fzV1p.d.ts → types-KavbvHix.d.ts} +13 -3
- package/dist/with-policy-CqUS-VWW.d.ts +30 -0
- package/dist/with-policy-DBV9dh2F.js +1 -0
- package/docs/README.md +7 -0
- package/docs/actions.md +152 -39
- package/docs/cancel-stale-requests.md +111 -0
- package/docs/debounce-and-abort.md +106 -0
- package/docs/guide.md +143 -19
- package/docs/hmr.md +6 -5
- package/docs/migrating-from-effector.md +51 -18
- package/docs/migrating-from-mitt.md +97 -0
- package/docs/migrating-from-rxjs.md +17 -6
- package/docs/motivation.md +41 -0
- package/docs/observable.md +124 -10
- package/docs/oop.md +286 -0
- package/docs/package.md +6 -2
- package/docs/plugins.md +247 -51
- package/docs/policies.md +33 -0
- package/docs/prevent-double-submit.md +128 -0
- package/docs/query-libraries.md +124 -0
- package/docs/react.md +61 -5
- package/docs/scope.md +60 -22
- package/docs/stores.md +263 -109
- package/llms-full.txt +2153 -355
- package/llms.txt +10 -3
- package/package.json +43 -4
- package/skills/nanoeffects/SKILL.md +52 -0
- package/dist/bus-BAs9x7IQ.js +0 -1
- package/dist/context-CK0ZomWt.js +0 -1
- package/dist/notrace/bus-Bdk0YyFQ.js +0 -1
- 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-
|
|
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
|
|
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-
|
|
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-
|
|
2
|
-
import { n as Scoped } from "./types-
|
|
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
|
-
*
|
|
39
|
-
*
|
|
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
|
|
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};
|
package/dist/signal.d.ts
ADDED
|
@@ -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-
|
|
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-
|
|
2
|
-
import { t as BoundOf } from "./plugin-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
commandsPlugin,
|
|
72
|
-
createCommand,
|
|
73
|
-
} from "nanoeffects/actions";
|
|
87
|
+
import { bindHandler, commandsPlugin } from "nanoeffects/actions";
|
|
88
|
+
import { sendMail } from "./user";
|
|
74
89
|
|
|
75
|
-
const
|
|
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
|
|
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.
|
|
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
|
-
-
|
|
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`).
|
|
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
|
-
|
|
116
|
-
import {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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(
|
|
170
|
-
|
|
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).
|