nanoeffects 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/notrace/policies.d.ts +50 -5
- package/dist/notrace/policies.js +1 -1
- package/dist/policies.d.ts +50 -5
- package/dist/policies.js +1 -1
- package/dist/react.d.ts +6 -4
- package/docs/chat-example.md +8 -3
- package/docs/policies.md +27 -0
- package/docs/query-libraries.md +2 -0
- package/docs/react.md +5 -3
- package/docs/starter.md +12 -1
- package/llms-full.txt +54 -7
- package/llms.txt +1 -1
- package/package.json +3 -2
|
@@ -9,6 +9,14 @@ type KeyOption<P> = {
|
|
|
9
9
|
*/
|
|
10
10
|
key?: (payload: P) => unknown;
|
|
11
11
|
};
|
|
12
|
+
/** Options of a policy called without a task: `key` is left out. */
|
|
13
|
+
type Unkeyed<O> = Omit<O, "key"> & {
|
|
14
|
+
key?: undefined;
|
|
15
|
+
};
|
|
16
|
+
/** Options of a policy called without a task, with a `key` that tells the payload type. */
|
|
17
|
+
type Keyed<O, P> = O & {
|
|
18
|
+
key: (payload: P) => unknown;
|
|
19
|
+
};
|
|
12
20
|
//#endregion
|
|
13
21
|
//#region src/policies/debounce.d.ts
|
|
14
22
|
type DebounceResult<R> = Promise<R | undefined> | Promise<Awaited<R> | undefined> | undefined;
|
|
@@ -18,6 +26,10 @@ type DebounceOptions<P = never> = KeyOption<P> & {
|
|
|
18
26
|
/** Run the last call of a burst after `ms` of quiet. Default `true`. */
|
|
19
27
|
trailing?: boolean;
|
|
20
28
|
};
|
|
29
|
+
/** What `debounce(ms)` returns: it turns a task into a `debounce` task. */
|
|
30
|
+
type DebounceWrapper = <P, E extends EventMap, R>(fn: Task<P, E, R>) => Task<P, E, DebounceResult<R>>;
|
|
31
|
+
/** What `debounce(ms, { key })` returns: the payload type comes from `key`. */
|
|
32
|
+
type KeyedDebounceWrapper<P> = <E extends EventMap, R>(fn: Task<P, E, R>) => Task<P, E, DebounceResult<R>>;
|
|
21
33
|
/**
|
|
22
34
|
* Waits for `ms` of quiet, then runs; a new call restarts the wait and drops
|
|
23
35
|
* the previous. With `leading` the first call of a burst runs at once, and the
|
|
@@ -38,14 +50,24 @@ type DebounceOptions<P = never> = KeyOption<P> & {
|
|
|
38
50
|
* bus.emit("input", "re"); // only the last call runs, 300 ms after it
|
|
39
51
|
* ```
|
|
40
52
|
*/
|
|
41
|
-
export declare
|
|
53
|
+
export declare function debounce<P, E extends EventMap, R>(ms: number, fn: Task<P, E, R>, options?: DebounceOptions<P>): Task<P, E, DebounceResult<R>>;
|
|
54
|
+
export declare function debounce(ms: number, options?: Unkeyed<DebounceOptions>): DebounceWrapper;
|
|
55
|
+
export declare function debounce<P>(ms: number, options: Keyed<DebounceOptions<P>, P>): KeyedDebounceWrapper<P>;
|
|
42
56
|
//#endregion
|
|
43
57
|
//#region src/policies/latest.d.ts
|
|
44
58
|
type LatestOptions<P = never> = KeyOption<P>;
|
|
59
|
+
type LatestResult<R> = R | Promise<Awaited<R> | undefined> | undefined;
|
|
60
|
+
/** What `latest()` returns: it turns a task into a `latest` task. */
|
|
61
|
+
type LatestWrapper = <P, E extends EventMap, R>(fn: Task<P, E, R>) => Task<P, E, LatestResult<R>>;
|
|
62
|
+
/** What `latest({ key })` returns: the payload type comes from `key`. */
|
|
63
|
+
type KeyedLatestWrapper<P> = <E extends EventMap, R>(fn: Task<P, E, R>) => Task<P, E, LatestResult<R>>;
|
|
45
64
|
/**
|
|
46
65
|
* Aborts the previous run when a new call arrives; stale results are dropped.
|
|
47
66
|
* With `options.key` each key has its own latest run.
|
|
48
67
|
*
|
|
68
|
+
* Called without the task, `latest()` returns a function that takes the task,
|
|
69
|
+
* so `usePolicy(callback, latest())` has no `(task) => ...` to get wrong.
|
|
70
|
+
*
|
|
49
71
|
* @example
|
|
50
72
|
* ```ts
|
|
51
73
|
* bus.on(
|
|
@@ -61,12 +83,21 @@ type LatestOptions<P = never> = KeyOption<P>;
|
|
|
61
83
|
*
|
|
62
84
|
* // one latest run per user instead of one for all
|
|
63
85
|
* bus.on("load", latest(loadUser, { key: (user) => user.id }));
|
|
86
|
+
*
|
|
87
|
+
* // without the task: for a hook that builds the task itself
|
|
88
|
+
* const search = usePolicy(runSearch, latest());
|
|
64
89
|
* ```
|
|
65
90
|
*/
|
|
66
|
-
export declare
|
|
91
|
+
export declare function latest<P, E extends EventMap, R>(fn: Task<P, E, R>, options?: LatestOptions<P>): Task<P, E, LatestResult<R>>;
|
|
92
|
+
export declare function latest(options?: Unkeyed<LatestOptions>): LatestWrapper;
|
|
93
|
+
export declare function latest<P>(options: Keyed<LatestOptions<P>, P>): KeyedLatestWrapper<P>;
|
|
67
94
|
//#endregion
|
|
68
95
|
//#region src/policies/leading.d.ts
|
|
69
96
|
type LeadingOptions<P = never> = KeyOption<P>;
|
|
97
|
+
/** What `leading()` returns: it turns a task into a `leading` task. */
|
|
98
|
+
type LeadingWrapper = <P, E extends EventMap, R>(fn: Task<P, E, R>) => Task<P, E, R | Promise<Awaited<R>> | undefined>;
|
|
99
|
+
/** What `leading({ key })` returns: the payload type comes from `key`. */
|
|
100
|
+
type KeyedLeadingWrapper<P> = <E extends EventMap, R>(fn: Task<P, E, R>) => Task<P, E, R | Promise<Awaited<R>> | undefined>;
|
|
70
101
|
/**
|
|
71
102
|
* Drops new calls while the task is still running. The lock is released when
|
|
72
103
|
* the run settles or when its task is aborted, whichever comes first, so a
|
|
@@ -86,7 +117,9 @@ type LeadingOptions<P = never> = KeyOption<P>;
|
|
|
86
117
|
* bus.emit("save", draft); // dropped while the first save is in flight
|
|
87
118
|
* ```
|
|
88
119
|
*/
|
|
89
|
-
export declare
|
|
120
|
+
export declare function leading<P, E extends EventMap, R>(fn: Task<P, E, R>, options?: LeadingOptions<P>): Task<P, E, R | Promise<Awaited<R>> | undefined>;
|
|
121
|
+
export declare function leading(options?: Unkeyed<LeadingOptions>): LeadingWrapper;
|
|
122
|
+
export declare function leading<P>(options: Keyed<LeadingOptions<P>, P>): KeyedLeadingWrapper<P>;
|
|
90
123
|
//#endregion
|
|
91
124
|
//#region src/policies/queue.d.ts
|
|
92
125
|
type QueueOptions<P = never> = KeyOption<P> & {
|
|
@@ -101,6 +134,10 @@ type QueueOptions<P = never> = KeyOption<P> & {
|
|
|
101
134
|
*/
|
|
102
135
|
overflow?: "drop-new" | "drop-old";
|
|
103
136
|
};
|
|
137
|
+
/** What `queue()` returns: it turns a task into a `queue` task. */
|
|
138
|
+
type QueueWrapper = <P, E extends EventMap, R>(fn: Task<P, E, R>) => Task<P, E, Promise<Awaited<R> | undefined>>;
|
|
139
|
+
/** What `queue({ key })` returns: the payload type comes from `key`. */
|
|
140
|
+
type KeyedQueueWrapper<P> = <E extends EventMap, R>(fn: Task<P, E, R>) => Task<P, E, Promise<Awaited<R> | undefined>>;
|
|
104
141
|
/**
|
|
105
142
|
* Runs calls one after another, in the order they arrived: a call starts when
|
|
106
143
|
* fewer than `concurrency` calls are running. Every call returns a promise with
|
|
@@ -131,7 +168,9 @@ type QueueOptions<P = never> = KeyOption<P> & {
|
|
|
131
168
|
* bus.emit("save", b); // waits for a, then runs
|
|
132
169
|
* ```
|
|
133
170
|
*/
|
|
134
|
-
export declare
|
|
171
|
+
export declare function queue<P, E extends EventMap, R>(fn: Task<P, E, R>, options?: QueueOptions<P>): Task<P, E, Promise<Awaited<R> | undefined>>;
|
|
172
|
+
export declare function queue(options?: Unkeyed<QueueOptions>): QueueWrapper;
|
|
173
|
+
export declare function queue<P>(options: Keyed<QueueOptions<P>, P>): KeyedQueueWrapper<P>;
|
|
135
174
|
//#endregion
|
|
136
175
|
//#region src/policies/throttle.d.ts
|
|
137
176
|
type ThrottleOptions<P = never> = KeyOption<P> & {
|
|
@@ -140,6 +179,10 @@ type ThrottleOptions<P = never> = KeyOption<P> & {
|
|
|
140
179
|
/** Run the latest call made while waiting, at the end of the window. Default `true`. */
|
|
141
180
|
trailing?: boolean;
|
|
142
181
|
};
|
|
182
|
+
/** What `throttle(ms)` returns: it turns a task into a `throttle` task. */
|
|
183
|
+
type ThrottleWrapper = <P, E extends EventMap, R>(fn: Task<P, E, R>) => Task<P, E, R | Promise<R | undefined> | undefined>;
|
|
184
|
+
/** What `throttle(ms, { key })` returns: the payload type comes from `key`. */
|
|
185
|
+
type KeyedThrottleWrapper<P> = <E extends EventMap, R>(fn: Task<P, E, R>) => Task<P, E, R | Promise<R | undefined> | undefined>;
|
|
143
186
|
/**
|
|
144
187
|
* At most one run per `ms`, with the semantics of lodash `throttle` (and
|
|
145
188
|
* patronum): a call made when the throttle is idle, that is, at least `ms`
|
|
@@ -166,7 +209,9 @@ type ThrottleOptions<P = never> = KeyOption<P> & {
|
|
|
166
209
|
* bus.emit("scroll", 30); // runs once the 200 ms window closes
|
|
167
210
|
* ```
|
|
168
211
|
*/
|
|
169
|
-
export declare
|
|
212
|
+
export declare function throttle<P, E extends EventMap, R>(ms: number, fn: Task<P, E, R>, options?: ThrottleOptions<P>): Task<P, E, R | Promise<R | undefined> | undefined>;
|
|
213
|
+
export declare function throttle(ms: number, options?: Unkeyed<ThrottleOptions>): ThrottleWrapper;
|
|
214
|
+
export declare function throttle<P>(ms: number, options: Keyed<ThrottleOptions<P>, P>): KeyedThrottleWrapper<P>;
|
|
170
215
|
//#endregion
|
|
171
216
|
//#region src/policies/with-policy.d.ts
|
|
172
217
|
/** What `withPolicy` returns: the task as a plain function that can be cancelled. */
|
package/dist/notrace/policies.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a as e,c as t,i as n,n as r,o as i,t as a}from"./context-yYU3_9lq.js";import{n as o,r as s,t as c}from"./delay-BosJKFjg.js";import{n as l,t as u}from"./promise-CSLpzwSR.js";import{n as d,t as f}from"./lifecycle-key-8iX8RUVD.js";const p=e=>Reflect.get(e,f),m=e=>{p(e)?.start()},h=(t,n,r)=>(m(t),e(t,n,r)),g=(e,t)=>{i(e,t,`drop`),p(e)?.drop()},_=(e,t)=>{p(e)?.done(t)},v=(e,t,n)=>{let r=p(e);t?r?.cancel(n):r?.drop()},y=e=>(Reflect.set(e,d,!0),e),b=()=>{},x=(e,t)=>{let n=/* @__PURE__ */ new Map;return(r,i)=>{let a=e(r),o=n.get(a);if(!o){let e=t(()=>{n.get(a)===e&&n.delete(a)});o=e,n.set(a,e)}return o(r,i)}},
|
|
1
|
+
import{a as e,c as t,i as n,n as r,o as i,t as a}from"./context-yYU3_9lq.js";import{n as o,r as s,t as c}from"./delay-BosJKFjg.js";import{n as l,t as u}from"./promise-CSLpzwSR.js";import{n as d,t as f}from"./lifecycle-key-8iX8RUVD.js";const p=e=>Reflect.get(e,f),m=e=>{p(e)?.start()},h=(t,n,r)=>(m(t),e(t,n,r)),g=(e,t)=>{i(e,t,`drop`),p(e)?.drop()},_=(e,t)=>{p(e)?.done(t)},v=(e,t,n)=>{let r=p(e);t?r?.cancel(n):r?.drop()},y=e=>(Reflect.set(e,d,!0),e),b=()=>{},x=(e,t,n)=>typeof e==`function`?n(e,t??{}):t=>n(t,e??{}),S=(e,t)=>{let n=/* @__PURE__ */ new Map;return(r,i)=>{let a=e(r),o=n.get(a);if(!o){let e=t(()=>{n.get(a)===e&&n.delete(a)});o=e,n.set(a,e)}return o(r,i)}},C=(e,t)=>y(e?S(e,t):t(b));function w(e,t){return x(e,t,T)}const T=(e,t)=>C(t.key,t=>E(`latest`,(t,n,r)=>(r(),e(t,n)),t)),E=(e,r,o=b)=>{let s=null;return(c,u)=>{if(s?.abort(),u.signal.aborted)return;let{ctx:d,unlink:f}=n(u);s=d;let p=a(d,e),h=!1,g=()=>{d.signal.aborted||(h=!0,m(d),i(d,e,`start`))};t(d,e=>{v(d,h,e),p(h?`cancel`:`drop`)});let _=()=>{f(),s===d&&(s=null,o())},y=e=>{if(!d.signal.aborted)return p(h?`done`:`drop`),e};try{let e=r(c,d,g);return l(e)?Promise.resolve(e).then(y,e=>{if(!d.signal.aborted)throw p(`error`,e),e}).finally(_):(_(),y(e))}catch(e){if(_(),d.signal.aborted)return;throw p(`error`,e),e}}};function D(e,t,n){return x(t,n,(t,n)=>O(e,t,n))}const O=(e,t,n)=>C(n.key,r=>A(e,t,n,r)),k=(e,t,n,r)=>{let i=r!==b,a=null,c=null;return(l,u)=>{if(u.signal.aborted)return;let d=Date.now(),f=a===null||d-a>=e||d-a<0;return a=d,c!==null&&o(c),c=i?s(r,e):null,n&&f?Promise.resolve(h(u,`debounce`,()=>t(l,u))):(g(u,`debounce`),Promise.resolve(void 0))}},A=(e,n,r,i)=>{let a=r.leading??!1,o=r.trailing??!0;if(!o)return k(e,n,a,i);let s=!1,l=0,u=E(`debounce`,([r,i],a,u)=>{let d=l;return t(a,()=>{l===d&&(s=!1)}),c(a,e).then(()=>{if(s=!1,o&&!i)return u(),n(r,a)})},i);return(e,t)=>{if(t.signal.aborted)return;let r=a&&!s;s=!0,l++;let i=u([e,r],t);return r?Promise.resolve(h(t,`debounce`,()=>n(e,t))):i}};function j(e,t){return x(e,t,M)}const M=(e,n)=>C(n.key,n=>{let r=null;return(i,a)=>{if(a.signal.aborted)return;if(r){g(a,`leading`);return}let o={};r=o;let s=()=>{c(),r===o&&(r=null,n())},c=t(a,s);try{return u(h(a,`leading`,()=>e(i,a)),s)}catch(e){throw s(),e}}});function N(e,t){return x(e,t,P)}const P=(e,t)=>C(t.key,n=>F(e,t,n)),F=(e,n,r)=>{let i=n.concurrency??1,a=n.limit??1/0,o=[],s=0,c=()=>{for(;s<i;){let e=o.shift();if(!e)return;e.start()}};return(l,u)=>new Promise((d,f)=>{if(u.signal.aborted){d(void 0);return}let p=b,m=()=>{p(),g(u,`queue`),d(void 0)},v=()=>{if(p(),u.signal.aborted){m();return}s++;let n=!0,i=()=>{n&&(n=!1,s--,c(),s===0&&o.length===0&&r())},a=t(u,()=>{i(),d(void 0)}),g;try{g=Promise.resolve(h(u,`queue`,()=>e(l,u)))}catch(e){g=Promise.reject(e)}let v=()=>{a(),i()};g.then(e=>{_(u,{value:e}),v(),d(u.signal.aborted?void 0:e)},e=>{_(u,{error:e}),v(),u.signal.aborted?d(void 0):f(e)})};if(s<i){v();return}if(o.length>=a){if(n.overflow!==`drop-old`||a<=0){m();return}o.shift()?.drop()}let y={start:v,drop:m};o.push(y),p=t(u,()=>{let e=o.indexOf(y);e>=0&&o.splice(e,1),m()})})};function I(e,t,n){return x(t,n,(t,n)=>L(e,t,n))}const L=(e,t,n)=>C(n.key,r=>R(e,t,n,r)),R=(e,t,n,r)=>{let i=n.leading??!0,a=n.trailing??!0,c=r!==b,l=null,u=0,d=null,f=null,p=null,m=!1,_=t=>l===null||t-l>=e||t-l<0||t-u>=e,v=e=>g(e,`throttle`),y=()=>{let e=p;p=null,e&&(v(e.ctx),e.resolve())},x=(e,t)=>(y(),new Promise((n,r)=>{p={payload:e,ctx:t,resolve:n,reject:r}})),S=(e,n,r)=>{y(),u=r,m=!0;try{return h(n,`throttle`,()=>t(e,n))}finally{m=!1}},C=()=>{c&&(f=s(w,e))},w=()=>{if(f=null,!c||d!==null||p)return;let t=Date.now(),n=Math.max(e-(t-(l??-1/0)),e-(t-u));n<=0?r():f=s(w,n)},T=e=>{d=null;let t=p;if(a&&t&&!t.ctx.signal.aborted){p=null;try{t.resolve(S(t.payload,t.ctx,e))}catch(e){t.reject(e)}}else y();w()},E=()=>{let t=Date.now();_(t)?T(t):d=s(E,Math.min(e-(t-(l??t)),e-(t-u)))},D=()=>{f!==null&&(o(f),f=null)},O=(e,t)=>{if(!a)v(t);else return x(e,t)},k=(e,t,n,r)=>{if(!r)v(t);else if(u=n,C(),!i)v(t);else return S(e,t,n)},A=(t,n,r,a)=>a&&d!==null?(o(d),d=s(E,e),S(t,n,r)):d===null&&(d=s(E,e),a&&(u=r,i))?S(t,n,r):O(t,n);return(e,t)=>{if(t.signal.aborted)return;let n=Date.now(),r=!m&&_(n);return l=n,D(),a?A(e,t,n,r):k(e,t,n,r)}},z=Reflect.get(Symbol,`dispose`),B=e=>{let t=new AbortController,n=async(...n)=>{let i=n[0],{ctx:a,unlink:o}=r({event:{type:`withPolicy`,payload:i},parent:{signal:t.signal}});try{return await e(i,a)}finally{o()}},i=e=>{t.abort(e),t=new AbortController},a=Object.assign(n,{cancel:i});return z&&Reflect.set(a,z,()=>i()),a};export{D as debounce,w as latest,j as leading,N as queue,I as throttle,B as withPolicy};
|
package/dist/policies.d.ts
CHANGED
|
@@ -10,6 +10,14 @@ type KeyOption<P> = {
|
|
|
10
10
|
*/
|
|
11
11
|
key?: (payload: P) => unknown;
|
|
12
12
|
};
|
|
13
|
+
/** Options of a policy called without a task: `key` is left out. */
|
|
14
|
+
type Unkeyed<O> = Omit<O, "key"> & {
|
|
15
|
+
key?: undefined;
|
|
16
|
+
};
|
|
17
|
+
/** Options of a policy called without a task, with a `key` that tells the payload type. */
|
|
18
|
+
type Keyed<O, P> = O & {
|
|
19
|
+
key: (payload: P) => unknown;
|
|
20
|
+
};
|
|
13
21
|
//#endregion
|
|
14
22
|
//#region src/policies/debounce.d.ts
|
|
15
23
|
type DebounceResult<R> = Promise<R | undefined> | Promise<Awaited<R> | undefined> | undefined;
|
|
@@ -19,6 +27,10 @@ type DebounceOptions<P = never> = KeyOption<P> & {
|
|
|
19
27
|
/** Run the last call of a burst after `ms` of quiet. Default `true`. */
|
|
20
28
|
trailing?: boolean;
|
|
21
29
|
};
|
|
30
|
+
/** What `debounce(ms)` returns: it turns a task into a `debounce` task. */
|
|
31
|
+
type DebounceWrapper = <P, E extends EventMap, R>(fn: Task<P, E, R>) => Task<P, E, DebounceResult<R>>;
|
|
32
|
+
/** What `debounce(ms, { key })` returns: the payload type comes from `key`. */
|
|
33
|
+
type KeyedDebounceWrapper<P> = <E extends EventMap, R>(fn: Task<P, E, R>) => Task<P, E, DebounceResult<R>>;
|
|
22
34
|
/**
|
|
23
35
|
* Waits for `ms` of quiet, then runs; a new call restarts the wait and drops
|
|
24
36
|
* the previous. With `leading` the first call of a burst runs at once, and the
|
|
@@ -39,14 +51,24 @@ type DebounceOptions<P = never> = KeyOption<P> & {
|
|
|
39
51
|
* bus.emit("input", "re"); // only the last call runs, 300 ms after it
|
|
40
52
|
* ```
|
|
41
53
|
*/
|
|
42
|
-
export declare
|
|
54
|
+
export declare function debounce<P, E extends EventMap, R>(ms: number, fn: Task<P, E, R>, options?: DebounceOptions<P>): Task<P, E, DebounceResult<R>>;
|
|
55
|
+
export declare function debounce(ms: number, options?: Unkeyed<DebounceOptions>): DebounceWrapper;
|
|
56
|
+
export declare function debounce<P>(ms: number, options: Keyed<DebounceOptions<P>, P>): KeyedDebounceWrapper<P>;
|
|
43
57
|
//#endregion
|
|
44
58
|
//#region src/policies/latest.d.ts
|
|
45
59
|
type LatestOptions<P = never> = KeyOption<P>;
|
|
60
|
+
type LatestResult<R> = R | Promise<Awaited<R> | undefined> | undefined;
|
|
61
|
+
/** What `latest()` returns: it turns a task into a `latest` task. */
|
|
62
|
+
type LatestWrapper = <P, E extends EventMap, R>(fn: Task<P, E, R>) => Task<P, E, LatestResult<R>>;
|
|
63
|
+
/** What `latest({ key })` returns: the payload type comes from `key`. */
|
|
64
|
+
type KeyedLatestWrapper<P> = <E extends EventMap, R>(fn: Task<P, E, R>) => Task<P, E, LatestResult<R>>;
|
|
46
65
|
/**
|
|
47
66
|
* Aborts the previous run when a new call arrives; stale results are dropped.
|
|
48
67
|
* With `options.key` each key has its own latest run.
|
|
49
68
|
*
|
|
69
|
+
* Called without the task, `latest()` returns a function that takes the task,
|
|
70
|
+
* so `usePolicy(callback, latest())` has no `(task) => ...` to get wrong.
|
|
71
|
+
*
|
|
50
72
|
* @example
|
|
51
73
|
* ```ts
|
|
52
74
|
* bus.on(
|
|
@@ -62,12 +84,21 @@ type LatestOptions<P = never> = KeyOption<P>;
|
|
|
62
84
|
*
|
|
63
85
|
* // one latest run per user instead of one for all
|
|
64
86
|
* bus.on("load", latest(loadUser, { key: (user) => user.id }));
|
|
87
|
+
*
|
|
88
|
+
* // without the task: for a hook that builds the task itself
|
|
89
|
+
* const search = usePolicy(runSearch, latest());
|
|
65
90
|
* ```
|
|
66
91
|
*/
|
|
67
|
-
export declare
|
|
92
|
+
export declare function latest<P, E extends EventMap, R>(fn: Task<P, E, R>, options?: LatestOptions<P>): Task<P, E, LatestResult<R>>;
|
|
93
|
+
export declare function latest(options?: Unkeyed<LatestOptions>): LatestWrapper;
|
|
94
|
+
export declare function latest<P>(options: Keyed<LatestOptions<P>, P>): KeyedLatestWrapper<P>;
|
|
68
95
|
//#endregion
|
|
69
96
|
//#region src/policies/leading.d.ts
|
|
70
97
|
type LeadingOptions<P = never> = KeyOption<P>;
|
|
98
|
+
/** What `leading()` returns: it turns a task into a `leading` task. */
|
|
99
|
+
type LeadingWrapper = <P, E extends EventMap, R>(fn: Task<P, E, R>) => Task<P, E, R | Promise<Awaited<R>> | undefined>;
|
|
100
|
+
/** What `leading({ key })` returns: the payload type comes from `key`. */
|
|
101
|
+
type KeyedLeadingWrapper<P> = <E extends EventMap, R>(fn: Task<P, E, R>) => Task<P, E, R | Promise<Awaited<R>> | undefined>;
|
|
71
102
|
/**
|
|
72
103
|
* Drops new calls while the task is still running. The lock is released when
|
|
73
104
|
* the run settles or when its task is aborted, whichever comes first, so a
|
|
@@ -87,7 +118,9 @@ type LeadingOptions<P = never> = KeyOption<P>;
|
|
|
87
118
|
* bus.emit("save", draft); // dropped while the first save is in flight
|
|
88
119
|
* ```
|
|
89
120
|
*/
|
|
90
|
-
export declare
|
|
121
|
+
export declare function leading<P, E extends EventMap, R>(fn: Task<P, E, R>, options?: LeadingOptions<P>): Task<P, E, R | Promise<Awaited<R>> | undefined>;
|
|
122
|
+
export declare function leading(options?: Unkeyed<LeadingOptions>): LeadingWrapper;
|
|
123
|
+
export declare function leading<P>(options: Keyed<LeadingOptions<P>, P>): KeyedLeadingWrapper<P>;
|
|
91
124
|
//#endregion
|
|
92
125
|
//#region src/policies/queue.d.ts
|
|
93
126
|
type QueueOptions<P = never> = KeyOption<P> & {
|
|
@@ -102,6 +135,10 @@ type QueueOptions<P = never> = KeyOption<P> & {
|
|
|
102
135
|
*/
|
|
103
136
|
overflow?: "drop-new" | "drop-old";
|
|
104
137
|
};
|
|
138
|
+
/** What `queue()` returns: it turns a task into a `queue` task. */
|
|
139
|
+
type QueueWrapper = <P, E extends EventMap, R>(fn: Task<P, E, R>) => Task<P, E, Promise<Awaited<R> | undefined>>;
|
|
140
|
+
/** What `queue({ key })` returns: the payload type comes from `key`. */
|
|
141
|
+
type KeyedQueueWrapper<P> = <E extends EventMap, R>(fn: Task<P, E, R>) => Task<P, E, Promise<Awaited<R> | undefined>>;
|
|
105
142
|
/**
|
|
106
143
|
* Runs calls one after another, in the order they arrived: a call starts when
|
|
107
144
|
* fewer than `concurrency` calls are running. Every call returns a promise with
|
|
@@ -132,7 +169,9 @@ type QueueOptions<P = never> = KeyOption<P> & {
|
|
|
132
169
|
* bus.emit("save", b); // waits for a, then runs
|
|
133
170
|
* ```
|
|
134
171
|
*/
|
|
135
|
-
export declare
|
|
172
|
+
export declare function queue<P, E extends EventMap, R>(fn: Task<P, E, R>, options?: QueueOptions<P>): Task<P, E, Promise<Awaited<R> | undefined>>;
|
|
173
|
+
export declare function queue(options?: Unkeyed<QueueOptions>): QueueWrapper;
|
|
174
|
+
export declare function queue<P>(options: Keyed<QueueOptions<P>, P>): KeyedQueueWrapper<P>;
|
|
136
175
|
//#endregion
|
|
137
176
|
//#region src/policies/throttle.d.ts
|
|
138
177
|
type ThrottleOptions<P = never> = KeyOption<P> & {
|
|
@@ -141,6 +180,10 @@ type ThrottleOptions<P = never> = KeyOption<P> & {
|
|
|
141
180
|
/** Run the latest call made while waiting, at the end of the window. Default `true`. */
|
|
142
181
|
trailing?: boolean;
|
|
143
182
|
};
|
|
183
|
+
/** What `throttle(ms)` returns: it turns a task into a `throttle` task. */
|
|
184
|
+
type ThrottleWrapper = <P, E extends EventMap, R>(fn: Task<P, E, R>) => Task<P, E, R | Promise<R | undefined> | undefined>;
|
|
185
|
+
/** What `throttle(ms, { key })` returns: the payload type comes from `key`. */
|
|
186
|
+
type KeyedThrottleWrapper<P> = <E extends EventMap, R>(fn: Task<P, E, R>) => Task<P, E, R | Promise<R | undefined> | undefined>;
|
|
144
187
|
/**
|
|
145
188
|
* At most one run per `ms`, with the semantics of lodash `throttle` (and
|
|
146
189
|
* patronum): a call made when the throttle is idle, that is, at least `ms`
|
|
@@ -167,6 +210,8 @@ type ThrottleOptions<P = never> = KeyOption<P> & {
|
|
|
167
210
|
* bus.emit("scroll", 30); // runs once the 200 ms window closes
|
|
168
211
|
* ```
|
|
169
212
|
*/
|
|
170
|
-
export declare
|
|
213
|
+
export declare function throttle<P, E extends EventMap, R>(ms: number, fn: Task<P, E, R>, options?: ThrottleOptions<P>): Task<P, E, R | Promise<R | undefined> | undefined>;
|
|
214
|
+
export declare function throttle(ms: number, options?: Unkeyed<ThrottleOptions>): ThrottleWrapper;
|
|
215
|
+
export declare function throttle<P>(ms: number, options: Keyed<ThrottleOptions<P>, P>): KeyedThrottleWrapper<P>;
|
|
171
216
|
//#endregion
|
|
172
217
|
export { type DebounceOptions, type LatestOptions, type LeadingOptions, type PolicyFunction, type QueueOptions, type ThrottleOptions, withPolicy };
|
package/dist/policies.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{i as e,o as t,s as n,t as r,u as i}from"./context-CJGW6OWj.js";import{n as a,r as o,t as s}from"./delay-BnBdGPh7.js";import{n as c,t as l}from"./promise-CSLpzwSR.js";import{n as u,t as d}from"./lifecycle-key-8iX8RUVD.js";import{t as f}from"./with-policy-CW50Edgk.js";const p=e=>Reflect.get(e,d),m=e=>{p(e)?.start()},h=(e,n,r)=>(m(e),t(e,n,r)),g=(e,t)=>{n(e,t,`drop`),p(e)?.drop()},_=(e,t)=>{p(e)?.done(t)},v=(e,t,n)=>{let r=p(e);t?r?.cancel(n):r?.drop()},y=e=>(Reflect.set(e,u,!0),e),b=()=>{},x=(e,t)=>{let n=/* @__PURE__ */ new Map;return(r,i)=>{let a=e(r),o=n.get(a);if(!o){let e=t(()=>{n.get(a)===e&&n.delete(a)});o=e,n.set(a,e)}return o(r,i)}},
|
|
1
|
+
import{i as e,o as t,s as n,t as r,u as i}from"./context-CJGW6OWj.js";import{n as a,r as o,t as s}from"./delay-BnBdGPh7.js";import{n as c,t as l}from"./promise-CSLpzwSR.js";import{n as u,t as d}from"./lifecycle-key-8iX8RUVD.js";import{t as f}from"./with-policy-CW50Edgk.js";const p=e=>Reflect.get(e,d),m=e=>{p(e)?.start()},h=(e,n,r)=>(m(e),t(e,n,r)),g=(e,t)=>{n(e,t,`drop`),p(e)?.drop()},_=(e,t)=>{p(e)?.done(t)},v=(e,t,n)=>{let r=p(e);t?r?.cancel(n):r?.drop()},y=e=>(Reflect.set(e,u,!0),e),b=()=>{},x=(e,t,n)=>typeof e==`function`?n(e,t??{}):t=>n(t,e??{}),S=(e,t)=>{let n=/* @__PURE__ */ new Map;return(r,i)=>{let a=e(r),o=n.get(a);if(!o){let e=t(()=>{n.get(a)===e&&n.delete(a)});o=e,n.set(a,e)}return o(r,i)}},C=(e,t)=>y(e?S(e,t):t(b));function w(e,t){return x(e,t,T)}const T=(e,t)=>C(t.key,t=>E(`latest`,(t,n,r)=>(r(),e(t,n)),t)),E=(t,a,o=b)=>{let s=null;return(l,u)=>{if(s?.abort(),u.signal.aborted)return;let{ctx:d,unlink:f}=e(u);s=d;let p=r(d,t),h=!1,g=()=>{d.signal.aborted||(h=!0,m(d),n(d,t,`start`))};i(d,e=>{v(d,h,e),p(h?`cancel`:`drop`)});let _=()=>{f(),s===d&&(s=null,o())},y=e=>{if(!d.signal.aborted)return p(h?`done`:`drop`),e};try{let e=a(l,d,g);return c(e)?Promise.resolve(e).then(y,e=>{if(!d.signal.aborted)throw p(`error`,e),e}).finally(_):(_(),y(e))}catch(e){if(_(),d.signal.aborted)return;throw p(`error`,e),e}}};function D(e,t,n){return x(t,n,(t,n)=>O(e,t,n))}const O=(e,t,n)=>C(n.key,r=>A(e,t,n,r)),k=(e,t,n,r)=>{let i=r!==b,s=null,c=null;return(l,u)=>{if(u.signal.aborted)return;let d=Date.now(),f=s===null||d-s>=e||d-s<0;return s=d,c!==null&&a(c),c=i?o(r,e):null,n&&f?Promise.resolve(h(u,`debounce`,()=>t(l,u))):(g(u,`debounce`),Promise.resolve(void 0))}},A=(e,t,n,r)=>{let a=n.leading??!1,o=n.trailing??!0;if(!o)return k(e,t,a,r);let c=!1,l=0,u=E(`debounce`,([n,r],a,u)=>{let d=l;return i(a,()=>{l===d&&(c=!1)}),s(a,e).then(()=>{if(c=!1,o&&!r)return u(),t(n,a)})},r);return(e,n)=>{if(n.signal.aborted)return;let r=a&&!c;c=!0,l++;let i=u([e,r],n);return r?Promise.resolve(h(n,`debounce`,()=>t(e,n))):i}};function j(e,t){return x(e,t,M)}const M=(e,t)=>C(t.key,t=>{let n=null;return(r,a)=>{if(a.signal.aborted)return;if(n){g(a,`leading`);return}let o={};n=o;let s=()=>{c(),n===o&&(n=null,t())},c=i(a,s);try{return l(h(a,`leading`,()=>e(r,a)),s)}catch(e){throw s(),e}}});function N(e,t){return x(e,t,P)}const P=(e,t)=>C(t.key,n=>F(e,t,n)),F=(e,t,n)=>{let r=t.concurrency??1,a=t.limit??1/0,o=[],s=0,c=()=>{for(;s<r;){let e=o.shift();if(!e)return;e.start()}};return(l,u)=>new Promise((d,f)=>{if(u.signal.aborted){d(void 0);return}let p=b,m=()=>{p(),g(u,`queue`),d(void 0)},v=()=>{if(p(),u.signal.aborted){m();return}s++;let t=!0,r=()=>{t&&(t=!1,s--,c(),s===0&&o.length===0&&n())},a=i(u,()=>{r(),d(void 0)}),g;try{g=Promise.resolve(h(u,`queue`,()=>e(l,u)))}catch(e){g=Promise.reject(e)}let v=()=>{a(),r()};g.then(e=>{_(u,{value:e}),v(),d(u.signal.aborted?void 0:e)},e=>{_(u,{error:e}),v(),u.signal.aborted?d(void 0):f(e)})};if(s<r){v();return}if(o.length>=a){if(t.overflow!==`drop-old`||a<=0){m();return}o.shift()?.drop()}let y={start:v,drop:m};o.push(y),p=i(u,()=>{let e=o.indexOf(y);e>=0&&o.splice(e,1),m()})})};function I(e,t,n){return x(t,n,(t,n)=>L(e,t,n))}const L=(e,t,n)=>C(n.key,r=>R(e,t,n,r)),R=(e,t,n,r)=>{let i=n.leading??!0,s=n.trailing??!0,c=r!==b,l=null,u=0,d=null,f=null,p=null,m=!1,_=t=>l===null||t-l>=e||t-l<0||t-u>=e,v=e=>g(e,`throttle`),y=()=>{let e=p;p=null,e&&(v(e.ctx),e.resolve())},x=(e,t)=>(y(),new Promise((n,r)=>{p={payload:e,ctx:t,resolve:n,reject:r}})),S=(e,n,r)=>{y(),u=r,m=!0;try{return h(n,`throttle`,()=>t(e,n))}finally{m=!1}},C=()=>{c&&(f=o(w,e))},w=()=>{if(f=null,!c||d!==null||p)return;let t=Date.now(),n=Math.max(e-(t-(l??-1/0)),e-(t-u));n<=0?r():f=o(w,n)},T=e=>{d=null;let t=p;if(s&&t&&!t.ctx.signal.aborted){p=null;try{t.resolve(S(t.payload,t.ctx,e))}catch(e){t.reject(e)}}else y();w()},E=()=>{let t=Date.now();_(t)?T(t):d=o(E,Math.min(e-(t-(l??t)),e-(t-u)))},D=()=>{f!==null&&(a(f),f=null)},O=(e,t)=>{if(!s)v(t);else return x(e,t)},k=(e,t,n,r)=>{if(!r)v(t);else if(u=n,C(),!i)v(t);else return S(e,t,n)},A=(t,n,r,s)=>s&&d!==null?(a(d),d=o(E,e),S(t,n,r)):d===null&&(d=o(E,e),s&&(u=r,i))?S(t,n,r):O(t,n);return(e,t)=>{if(t.signal.aborted)return;let n=Date.now(),r=!m&&_(n);return l=n,D(),s?A(e,t,n,r):k(e,t,n,r)}};export{D as debounce,w as latest,j as leading,N as queue,I as throttle,f as withPolicy};
|
package/dist/react.d.ts
CHANGED
|
@@ -85,9 +85,11 @@ export declare const reactPlugin: () => <E extends EventMap>(bus: Bus<E>) => Bus
|
|
|
85
85
|
* render. This hook keeps the policy across renders and calls the `callback` of
|
|
86
86
|
* the latest render, so an inline callback sees fresh props and state.
|
|
87
87
|
*
|
|
88
|
-
* `make`
|
|
89
|
-
*
|
|
90
|
-
*
|
|
88
|
+
* `make` builds the policy around the callback. Pass a policy called without
|
|
89
|
+
* its task (`debounce(300)`, `latest()`), or a function `(task) => policy` for
|
|
90
|
+
* a combined policy such as `(task) => throttle(100, latest(task))`; `task`
|
|
91
|
+
* runs the callback. `make` runs once, and again when an item of `deps`
|
|
92
|
+
* changes: list there what it reads, such as the wait time. The pending calls of the old function are
|
|
91
93
|
* cancelled then, and when the component unmounts. Needs `react` 18 or newer.
|
|
92
94
|
*
|
|
93
95
|
* @example
|
|
@@ -99,7 +101,7 @@ export declare const reactPlugin: () => <E extends EventMap>(bus: Bus<E>) => Bus
|
|
|
99
101
|
* const [results, setResults] = useState([]);
|
|
100
102
|
* const onChange = usePolicy(
|
|
101
103
|
* async (query: string) => setResults(await search(query)), // latest render's callback
|
|
102
|
-
*
|
|
104
|
+
* debounce(wait), // the policy, made once per wait
|
|
103
105
|
* [wait],
|
|
104
106
|
* );
|
|
105
107
|
* return <input onChange={(event) => onChange(event.target.value)} />;
|
package/docs/chat-example.md
CHANGED
|
@@ -44,7 +44,7 @@ Each domain is a folder. The components only emit events and read values; they n
|
|
|
44
44
|
| `src/domains/incoming/` | New messages, asked for at an interval. |
|
|
45
45
|
| `src/ui/` | The components. |
|
|
46
46
|
|
|
47
|
-
A domain folder has its events (`rooms.ts`, which imports no bus), its work (`rooms-handlers.ts`), the state it keeps (`rooms-store.ts`) and a module (`rooms-module.ts`) that wires them. A domain reacts to another domain by listening to its events, never by importing its handlers or its store.
|
|
47
|
+
A domain folder has its events (`rooms.ts`, which imports no bus), its work (`rooms-handlers.ts`), the state it keeps (`rooms-store.ts`) and a module (`rooms-module.ts`) that wires them. A domain reacts to another domain by listening to its events, never by importing its handlers or its store. Events and commands both live in the events file. An event (`createAction`) says that something happened, and any domain may listen: `rooms/selected` is heard by three. A command (`createCommand`) is an order with exactly one handler, as `search/openResult` and `messages/mark` are. The handler is given to `createCommand`, so the command works wherever it is called and can return a result, and the module binds it with `module.use` for as long as it is mounted. A job is a command that also sends its outcomes, but its handler is bound in the module (`module.handle`): a job usually runs under a policy, and the type of its outcomes is exact only when the handler is not part of the type.
|
|
48
48
|
|
|
49
49
|
## Load a history that must not race
|
|
50
50
|
|
|
@@ -173,19 +173,24 @@ export const runSearch = debounce(
|
|
|
173
173
|
`take` waits for the next event of a name. Start it before the emit that causes the event, so nothing is missed:
|
|
174
174
|
|
|
175
175
|
```js
|
|
176
|
+
import { createCommand } from "nanoeffects/actions";
|
|
176
177
|
import { take } from "nanoeffects/effects";
|
|
177
178
|
|
|
179
|
+
// createCommand — an order for the messages domain, with the handler that does it
|
|
180
|
+
const markMessage = createCommand("messages/mark", (messageId) => console.log("mark", messageId));
|
|
181
|
+
|
|
178
182
|
// the room that is open, kept from the rooms/selected event
|
|
179
183
|
let selectedRoom = "general";
|
|
180
184
|
|
|
181
|
-
export const
|
|
185
|
+
export const goToResult = async (hit, ctx) => {
|
|
182
186
|
if (hit.roomId !== selectedRoom) {
|
|
183
187
|
// take — wait for the next event of that name; started before the emit so nothing is missed
|
|
184
188
|
const loaded = take(ctx, "messages/loadHistory/success");
|
|
185
189
|
ctx.emit("rooms/selected", hit.roomId);
|
|
186
190
|
await loaded;
|
|
187
191
|
}
|
|
188
|
-
ctx.emit
|
|
192
|
+
// ctx.emit with a command — the messages domain marks the message
|
|
193
|
+
ctx.emit(markMessage(hit.id));
|
|
189
194
|
};
|
|
190
195
|
```
|
|
191
196
|
|
package/docs/policies.md
CHANGED
|
@@ -36,6 +36,33 @@ Every policy accepts `key` in its options (see [Isolate state by key](#isolate-s
|
|
|
36
36
|
| `throttle(ms, fn, options?)` | at most one run per `ms` |
|
|
37
37
|
| `queue(fn, options?)` | one after another, in order |
|
|
38
38
|
|
|
39
|
+
## Call a policy without its function
|
|
40
|
+
|
|
41
|
+
**Problem.** Some code builds the function for you and only asks which policy to use, as [`usePolicy`](react.md#keep-a-debounced-handler-between-renders) does. If you must pass your own function there, it is easy to pass the wrong one.
|
|
42
|
+
|
|
43
|
+
**When.** You pass a policy to `usePolicy`. With a bus, `withPolicy` or your own code, call the policy with its function as above.
|
|
44
|
+
|
|
45
|
+
Leave the function out and the policy returns a wrapper. The wrapper takes a function and gives back the same policy as the direct call:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
import { debounce } from "nanoeffects/policies";
|
|
49
|
+
|
|
50
|
+
const runSearch = async (query) => {
|
|
51
|
+
await api.search(query);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// debounce(300) — no function yet: returns a wrapper
|
|
55
|
+
const wrap = debounce(300);
|
|
56
|
+
|
|
57
|
+
// the wrapper takes the function; same as debounce(300, runSearch)
|
|
58
|
+
const search = wrap(runSearch);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- It works for `latest()`, `leading()`, `debounce(ms)`, `throttle(ms)` and `queue()`, with the options as the last argument: `queue({ concurrency: 2 })`, `debounce(300, { leading: true })`.
|
|
62
|
+
- Each time you apply a wrapper to a function, you get a new policy with its own state.
|
|
63
|
+
- To combine policies, nest them as before: `throttle(100, latest(fn))`.
|
|
64
|
+
- With `key`, annotate the payload of the key function in TypeScript: `latest({ key: (user: User) => user.id })`. Without the annotation the wrapper cannot know the payload type.
|
|
65
|
+
|
|
39
66
|
## Configure debounce and throttle
|
|
40
67
|
|
|
41
68
|
`debounce` and `throttle` take `leading` and `trailing`. Behavior matches lodash (and patronum); tests compare random call sequences to lodash itself.
|
package/docs/query-libraries.md
CHANGED
|
@@ -122,3 +122,5 @@ Send it from the handler with `ctx.emit(userLoaded(user))`. Do not replace the c
|
|
|
122
122
|
- **A handler that returns without a promise is finished with the call**, so a signal that aborts later does not reach it. Commands that fetch are async, so this only matters for a sync handler that sets up a callback.
|
|
123
123
|
- **Give each request its own signal for an event.** For a command or a job the link ends when it settles. For `bus.emit(action, { signal })` it stays until the signal aborts.
|
|
124
124
|
- **The signal reaches the tasks that this `emit` starts**, with the listeners of the command's type and the `ctx.run` children. A separate `bus.emit` that the handler makes is not linked: pass `{ signal: ctx.signal }` to it.
|
|
125
|
+
|
|
126
|
+
A whole small app, with a search, a filter, pages and a detail panel, is in the [Rick and Morty example](../examples/rickandmorty).
|
package/docs/react.md
CHANGED
|
@@ -248,8 +248,8 @@ export const Search = ({ wait }) => {
|
|
|
248
248
|
const found = await api.search(query, { signal });
|
|
249
249
|
setItems(found);
|
|
250
250
|
},
|
|
251
|
-
// the second argument builds the policy once
|
|
252
|
-
|
|
251
|
+
// debounce(wait) — the second argument builds the policy once around the callback above
|
|
252
|
+
debounce(wait),
|
|
253
253
|
// deps — list what the second argument reads; a new wait makes a new policy
|
|
254
254
|
[wait],
|
|
255
255
|
);
|
|
@@ -265,7 +265,9 @@ export const Search = ({ wait }) => {
|
|
|
265
265
|
|
|
266
266
|
- `onChange` keeps its identity while `wait` stays the same, so it is safe in `useEffect` and in props.
|
|
267
267
|
- A pending call is cancelled on unmount and when `deps` change.
|
|
268
|
-
-
|
|
268
|
+
- Write the policy without its callback: `debounce(300)`, not `debounce(300, callback)`. `usePolicy` puts the callback in itself, always the one of the latest render.
|
|
269
|
+
- To combine policies, write a function of the task: `(task) => throttle(100, latest(task))`. Here `task` is the callback that `usePolicy` keeps current; pass it, not your own function.
|
|
270
|
+
- `deps` stay your job: `debounce(wait)` is made again on every render, but only the first one (and the one after `deps` change) is used.
|
|
269
271
|
- Without a React component, use [`withPolicy`](policies.md#use-a-policy-without-a-bus).
|
|
270
272
|
|
|
271
273
|
## Where to go next
|
package/docs/starter.md
CHANGED
|
@@ -48,6 +48,7 @@ Pass `--template` to skip the question. With npm, put `--` before the option.
|
|
|
48
48
|
| `react-ts` | The `react` app in TypeScript | You build a React app in TypeScript |
|
|
49
49
|
| `domains-ts` | Commands, a job and modules, with the logic split into domain folders and React as a thin layer | You build an app with more than one feature and want the logic off the UI |
|
|
50
50
|
| `vue-ts` | The `react-ts` app in Vue 3, with `watchEmitter` and `useEventRef` from `nanoeffects/vue` | You build a Vue 3 app |
|
|
51
|
+
| `svelte-ts` | The `react-ts` app in Svelte 5, with a Svelte store made from an event | You build a Svelte app |
|
|
51
52
|
|
|
52
53
|
```bash
|
|
53
54
|
npm create nanoeffects@latest my-app -- --template react
|
|
@@ -71,6 +72,16 @@ npm create nanoeffects@latest my-app -- --template vue-ts
|
|
|
71
72
|
|
|
72
73
|
`npm run build` type-checks the `.vue` files with `vue-tsc`, which needs TypeScript 5: the template depends on it.
|
|
73
74
|
|
|
75
|
+
### Start with Svelte
|
|
76
|
+
|
|
77
|
+
`svelte-ts` is the search app of `react-ts` in Svelte 5. It needs no part of nanoeffects for Svelte: a Svelte store is any object with `subscribe`, so a small helper of the project, `src/event-store.ts`, makes a store from an event with `readable` and `bus.on`. In a component, `$found` reads it, and Svelte subscribes and unsubscribes for you. The query goes to the bus from the `oninput` handler, so nothing is emitted when the component mounts. The store listens only while something reads it, and is [not a store of the app](react.md#show-the-last-value-of-an-event-as-state): a value that several components share belongs in a store of your own.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm create nanoeffects@latest my-app -- --template svelte-ts
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`npm run build` type-checks the `.svelte` files with `svelte-check`, which needs TypeScript 5: the template depends on it.
|
|
84
|
+
|
|
74
85
|
### Start with domains
|
|
75
86
|
|
|
76
87
|
`domains-ts` is not the search box of the other templates grown in TypeScript: it shows where logic goes when an app has several features. Each domain is a folder under `src/domains/`. It has the events of the domain, the work that handles them, and a module that mounts the work. The React components only emit events and read values.
|
|
@@ -93,7 +104,7 @@ The commands, jobs and modules themselves are explained in [Actions, commands, j
|
|
|
93
104
|
|
|
94
105
|
## Read the files
|
|
95
106
|
|
|
96
|
-
Every template except `domains-ts` splits the code the way [the guide](guide.md) does: one job per file. In the TypeScript templates the files end in `.ts` (and `.tsx` for React components, `.vue` for Vue components).
|
|
107
|
+
Every template except `domains-ts` splits the code the way [the guide](guide.md) does: one job per file. In the TypeScript templates the files end in `.ts` (and `.tsx` for React components, `.vue` for Vue components, `.svelte` for Svelte components).
|
|
97
108
|
|
|
98
109
|
| File | Job |
|
|
99
110
|
| ---- | --- |
|
package/llms-full.txt
CHANGED
|
@@ -553,6 +553,7 @@ Pass `--template` to skip the question. With npm, put `--` before the option.
|
|
|
553
553
|
| `react-ts` | The `react` app in TypeScript | You build a React app in TypeScript |
|
|
554
554
|
| `domains-ts` | Commands, a job and modules, with the logic split into domain folders and React as a thin layer | You build an app with more than one feature and want the logic off the UI |
|
|
555
555
|
| `vue-ts` | The `react-ts` app in Vue 3, with `watchEmitter` and `useEventRef` from `nanoeffects/vue` | You build a Vue 3 app |
|
|
556
|
+
| `svelte-ts` | The `react-ts` app in Svelte 5, with a Svelte store made from an event | You build a Svelte app |
|
|
556
557
|
|
|
557
558
|
```bash
|
|
558
559
|
npm create nanoeffects@latest my-app -- --template react
|
|
@@ -576,6 +577,16 @@ npm create nanoeffects@latest my-app -- --template vue-ts
|
|
|
576
577
|
|
|
577
578
|
`npm run build` type-checks the `.vue` files with `vue-tsc`, which needs TypeScript 5: the template depends on it.
|
|
578
579
|
|
|
580
|
+
### Start with Svelte
|
|
581
|
+
|
|
582
|
+
`svelte-ts` is the search app of `react-ts` in Svelte 5. It needs no part of nanoeffects for Svelte: a Svelte store is any object with `subscribe`, so a small helper of the project, `src/event-store.ts`, makes a store from an event with `readable` and `bus.on`. In a component, `$found` reads it, and Svelte subscribes and unsubscribes for you. The query goes to the bus from the `oninput` handler, so nothing is emitted when the component mounts. The store listens only while something reads it, and is [not a store of the app](react.md#show-the-last-value-of-an-event-as-state): a value that several components share belongs in a store of your own.
|
|
583
|
+
|
|
584
|
+
```bash
|
|
585
|
+
npm create nanoeffects@latest my-app -- --template svelte-ts
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
`npm run build` type-checks the `.svelte` files with `svelte-check`, which needs TypeScript 5: the template depends on it.
|
|
589
|
+
|
|
579
590
|
### Start with domains
|
|
580
591
|
|
|
581
592
|
`domains-ts` is not the search box of the other templates grown in TypeScript: it shows where logic goes when an app has several features. Each domain is a folder under `src/domains/`. It has the events of the domain, the work that handles them, and a module that mounts the work. The React components only emit events and read values.
|
|
@@ -598,7 +609,7 @@ The commands, jobs and modules themselves are explained in [Actions, commands, j
|
|
|
598
609
|
|
|
599
610
|
## Read the files
|
|
600
611
|
|
|
601
|
-
Every template except `domains-ts` splits the code the way [the guide](guide.md) does: one job per file. In the TypeScript templates the files end in `.ts` (and `.tsx` for React components, `.vue` for Vue components).
|
|
612
|
+
Every template except `domains-ts` splits the code the way [the guide](guide.md) does: one job per file. In the TypeScript templates the files end in `.ts` (and `.tsx` for React components, `.vue` for Vue components, `.svelte` for Svelte components).
|
|
602
613
|
|
|
603
614
|
| File | Job |
|
|
604
615
|
| ---- | --- |
|
|
@@ -1069,6 +1080,33 @@ Every policy accepts `key` in its options (see [Isolate state by key](#isolate-s
|
|
|
1069
1080
|
| `throttle(ms, fn, options?)` | at most one run per `ms` |
|
|
1070
1081
|
| `queue(fn, options?)` | one after another, in order |
|
|
1071
1082
|
|
|
1083
|
+
## Call a policy without its function
|
|
1084
|
+
|
|
1085
|
+
**Problem.** Some code builds the function for you and only asks which policy to use, as [`usePolicy`](react.md#keep-a-debounced-handler-between-renders) does. If you must pass your own function there, it is easy to pass the wrong one.
|
|
1086
|
+
|
|
1087
|
+
**When.** You pass a policy to `usePolicy`. With a bus, `withPolicy` or your own code, call the policy with its function as above.
|
|
1088
|
+
|
|
1089
|
+
Leave the function out and the policy returns a wrapper. The wrapper takes a function and gives back the same policy as the direct call:
|
|
1090
|
+
|
|
1091
|
+
```js
|
|
1092
|
+
import { debounce } from "nanoeffects/policies";
|
|
1093
|
+
|
|
1094
|
+
const runSearch = async (query) => {
|
|
1095
|
+
await api.search(query);
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
// debounce(300) — no function yet: returns a wrapper
|
|
1099
|
+
const wrap = debounce(300);
|
|
1100
|
+
|
|
1101
|
+
// the wrapper takes the function; same as debounce(300, runSearch)
|
|
1102
|
+
const search = wrap(runSearch);
|
|
1103
|
+
```
|
|
1104
|
+
|
|
1105
|
+
- It works for `latest()`, `leading()`, `debounce(ms)`, `throttle(ms)` and `queue()`, with the options as the last argument: `queue({ concurrency: 2 })`, `debounce(300, { leading: true })`.
|
|
1106
|
+
- Each time you apply a wrapper to a function, you get a new policy with its own state.
|
|
1107
|
+
- To combine policies, nest them as before: `throttle(100, latest(fn))`.
|
|
1108
|
+
- With `key`, annotate the payload of the key function in TypeScript: `latest({ key: (user: User) => user.id })`. Without the annotation the wrapper cannot know the payload type.
|
|
1109
|
+
|
|
1072
1110
|
## Configure debounce and throttle
|
|
1073
1111
|
|
|
1074
1112
|
`debounce` and `throttle` take `leading` and `trailing`. Behavior matches lodash (and patronum); tests compare random call sequences to lodash itself.
|
|
@@ -3022,6 +3060,8 @@ Send it from the handler with `ctx.emit(userLoaded(user))`. Do not replace the c
|
|
|
3022
3060
|
- **Give each request its own signal for an event.** For a command or a job the link ends when it settles. For `bus.emit(action, { signal })` it stays until the signal aborts.
|
|
3023
3061
|
- **The signal reaches the tasks that this `emit` starts**, with the listeners of the command's type and the `ctx.run` children. A separate `bus.emit` that the handler makes is not linked: pass `{ signal: ctx.signal }` to it.
|
|
3024
3062
|
|
|
3063
|
+
A whole small app, with a search, a filter, pages and a detail panel, is in the [Rick and Morty example](../examples/rickandmorty).
|
|
3064
|
+
|
|
3025
3065
|
---
|
|
3026
3066
|
Source: docs/react.md
|
|
3027
3067
|
|
|
@@ -3275,8 +3315,8 @@ export const Search = ({ wait }) => {
|
|
|
3275
3315
|
const found = await api.search(query, { signal });
|
|
3276
3316
|
setItems(found);
|
|
3277
3317
|
},
|
|
3278
|
-
// the second argument builds the policy once
|
|
3279
|
-
|
|
3318
|
+
// debounce(wait) — the second argument builds the policy once around the callback above
|
|
3319
|
+
debounce(wait),
|
|
3280
3320
|
// deps — list what the second argument reads; a new wait makes a new policy
|
|
3281
3321
|
[wait],
|
|
3282
3322
|
);
|
|
@@ -3292,7 +3332,9 @@ export const Search = ({ wait }) => {
|
|
|
3292
3332
|
|
|
3293
3333
|
- `onChange` keeps its identity while `wait` stays the same, so it is safe in `useEffect` and in props.
|
|
3294
3334
|
- A pending call is cancelled on unmount and when `deps` change.
|
|
3295
|
-
-
|
|
3335
|
+
- Write the policy without its callback: `debounce(300)`, not `debounce(300, callback)`. `usePolicy` puts the callback in itself, always the one of the latest render.
|
|
3336
|
+
- To combine policies, write a function of the task: `(task) => throttle(100, latest(task))`. Here `task` is the callback that `usePolicy` keeps current; pass it, not your own function.
|
|
3337
|
+
- `deps` stay your job: `debounce(wait)` is made again on every render, but only the first one (and the one after `deps` change) is used.
|
|
3296
3338
|
- Without a React component, use [`withPolicy`](policies.md#use-a-policy-without-a-bus).
|
|
3297
3339
|
|
|
3298
3340
|
## Where to go next
|
|
@@ -3352,7 +3394,7 @@ Each domain is a folder. The components only emit events and read values; they n
|
|
|
3352
3394
|
| `src/domains/incoming/` | New messages, asked for at an interval. |
|
|
3353
3395
|
| `src/ui/` | The components. |
|
|
3354
3396
|
|
|
3355
|
-
A domain folder has its events (`rooms.ts`, which imports no bus), its work (`rooms-handlers.ts`), the state it keeps (`rooms-store.ts`) and a module (`rooms-module.ts`) that wires them. A domain reacts to another domain by listening to its events, never by importing its handlers or its store.
|
|
3397
|
+
A domain folder has its events (`rooms.ts`, which imports no bus), its work (`rooms-handlers.ts`), the state it keeps (`rooms-store.ts`) and a module (`rooms-module.ts`) that wires them. A domain reacts to another domain by listening to its events, never by importing its handlers or its store. Events and commands both live in the events file. An event (`createAction`) says that something happened, and any domain may listen: `rooms/selected` is heard by three. A command (`createCommand`) is an order with exactly one handler, as `search/openResult` and `messages/mark` are. The handler is given to `createCommand`, so the command works wherever it is called and can return a result, and the module binds it with `module.use` for as long as it is mounted. A job is a command that also sends its outcomes, but its handler is bound in the module (`module.handle`): a job usually runs under a policy, and the type of its outcomes is exact only when the handler is not part of the type.
|
|
3356
3398
|
|
|
3357
3399
|
## Load a history that must not race
|
|
3358
3400
|
|
|
@@ -3481,19 +3523,24 @@ export const runSearch = debounce(
|
|
|
3481
3523
|
`take` waits for the next event of a name. Start it before the emit that causes the event, so nothing is missed:
|
|
3482
3524
|
|
|
3483
3525
|
```js
|
|
3526
|
+
import { createCommand } from "nanoeffects/actions";
|
|
3484
3527
|
import { take } from "nanoeffects/effects";
|
|
3485
3528
|
|
|
3529
|
+
// createCommand — an order for the messages domain, with the handler that does it
|
|
3530
|
+
const markMessage = createCommand("messages/mark", (messageId) => console.log("mark", messageId));
|
|
3531
|
+
|
|
3486
3532
|
// the room that is open, kept from the rooms/selected event
|
|
3487
3533
|
let selectedRoom = "general";
|
|
3488
3534
|
|
|
3489
|
-
export const
|
|
3535
|
+
export const goToResult = async (hit, ctx) => {
|
|
3490
3536
|
if (hit.roomId !== selectedRoom) {
|
|
3491
3537
|
// take — wait for the next event of that name; started before the emit so nothing is missed
|
|
3492
3538
|
const loaded = take(ctx, "messages/loadHistory/success");
|
|
3493
3539
|
ctx.emit("rooms/selected", hit.roomId);
|
|
3494
3540
|
await loaded;
|
|
3495
3541
|
}
|
|
3496
|
-
ctx.emit
|
|
3542
|
+
// ctx.emit with a command — the messages domain marks the message
|
|
3543
|
+
ctx.emit(markMessage(hit.id));
|
|
3497
3544
|
};
|
|
3498
3545
|
```
|
|
3499
3546
|
|
package/llms.txt
CHANGED
|
@@ -15,7 +15,7 @@ Rules that generated code often gets wrong:
|
|
|
15
15
|
## Start here
|
|
16
16
|
|
|
17
17
|
- [Guide](docs/guide.md): bus, handler context, policies, effects, commands and modules, step by step
|
|
18
|
-
- [Start a project from a template](docs/starter.md): `npm create nanoeffects` makes a Vite project (`vanilla`, `react`, `vue-ts` and others, in JavaScript or TypeScript) with a bus, `debounce` and `latest`
|
|
18
|
+
- [Start a project from a template](docs/starter.md): `npm create nanoeffects` makes a Vite project (`vanilla`, `react`, `vue-ts`, `svelte-ts` and others, in JavaScript or TypeScript) with a bus, `debounce` and `latest`
|
|
19
19
|
- [Why nanoeffects exists](docs/motivation.md): no dependencies and no global state for SDKs and Module Federation, and why not Redux, Effector or RxJS
|
|
20
20
|
- [Package entries](docs/package.md): which import holds what
|
|
21
21
|
- [Plugins](docs/plugins.md): every plugin of a bus, what `bus.use` returns, and the order to use them in
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nanoeffects",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Cancel stale async work and fix race conditions in event handlers: a typed event bus where every handler is a task with an AbortSignal, plus latest, debounce, throttle, queue, retry, timeout and poll. About 1.4 kB gzip.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"event-bus",
|
|
@@ -166,6 +166,7 @@
|
|
|
166
166
|
"@reduxjs/toolkit": "^2.13.0",
|
|
167
167
|
"@size-limit/esbuild": "^14.2.0",
|
|
168
168
|
"@size-limit/file": "^14.2.0",
|
|
169
|
+
"@tanstack/react-query": "^5.105.0",
|
|
169
170
|
"@types/react": "^19.3.0",
|
|
170
171
|
"@types/react-dom": "^19.3.0",
|
|
171
172
|
"@vitejs/plugin-react": "^6.1.2",
|
|
@@ -206,7 +207,7 @@
|
|
|
206
207
|
{
|
|
207
208
|
"name": "policies (all)",
|
|
208
209
|
"path": "dist/policies.js",
|
|
209
|
-
"limit": "2.
|
|
210
|
+
"limit": "2.65 kB"
|
|
210
211
|
},
|
|
211
212
|
{
|
|
212
213
|
"name": "effects (all)",
|