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.
@@ -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 const debounce: <P, E extends EventMap, R>(ms: number, fn: Task<P, E, R>, options?: DebounceOptions<P>) => Task<P, E, DebounceResult<R>>;
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 const latest: <P, E extends EventMap, R>(fn: Task<P, E, R>, options?: LatestOptions<P>) => Task<P, E, R | Promise<Awaited<R> | undefined> | undefined>;
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 const leading: <P, E extends EventMap, R>(fn: Task<P, E, R>, options?: LeadingOptions<P>) => Task<P, E, R | Promise<Awaited<R>> | undefined>;
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 const queue: <P, E extends EventMap, R>(fn: Task<P, E, R>, options?: QueueOptions<P>) => Task<P, E, Promise<Awaited<R> | undefined>>;
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 const throttle: <P, E extends EventMap, R>(ms: number, fn: Task<P, E, R>, options?: ThrottleOptions<P>) => Task<P, E, R | Promise<R | undefined> | undefined>;
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. */
@@ -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)}},S=(e,t)=>y(e?x(e,t):t(b)),C=(e,t={})=>S(t.key,t=>w(`latest`,(t,n,r)=>(r(),e(t,n)),t)),w=(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}}},T=(e,t,n={})=>S(n.key,r=>D(e,t,n,r)),E=(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))}},D=(e,n,r,i)=>{let a=r.leading??!1,o=r.trailing??!0;if(!o)return E(e,n,a,i);let s=!1,l=0,u=w(`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}},O=(e,n={})=>S(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}}}),k=(e,t={})=>S(t.key,n=>A(e,t,n)),A=(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()})})},j=(e,t,n={})=>S(n.key,r=>M(e,t,n,r)),M=(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)}},N=Reflect.get(Symbol,`dispose`),P=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 N&&Reflect.set(a,N,()=>i()),a};export{T as debounce,C as latest,O as leading,k as queue,j as throttle,P as withPolicy};
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};
@@ -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 const 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<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 const latest: <P, E extends EventMap, R>(fn: Task<P, E, R>, options?: LatestOptions<P>) => Task<P, E, R | Promise<Awaited<R> | undefined> | undefined>;
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 const 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<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 const 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<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 const 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<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)}},S=(e,t)=>y(e?x(e,t):t(b)),C=(e,t={})=>S(t.key,t=>w(`latest`,(t,n,r)=>(r(),e(t,n)),t)),w=(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}}},T=(e,t,n={})=>S(n.key,r=>D(e,t,n,r)),E=(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))}},D=(e,t,n,r)=>{let a=n.leading??!1,o=n.trailing??!0;if(!o)return E(e,t,a,r);let c=!1,l=0,u=w(`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}},O=(e,t={})=>S(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}}}),k=(e,t={})=>S(t.key,n=>A(e,t,n)),A=(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()})})},j=(e,t,n={})=>S(n.key,r=>M(e,t,n,r)),M=(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{T as debounce,C as latest,O as leading,k as queue,j as throttle,f as withPolicy};
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` gets a task that runs the callback and returns the policy built around
89
- * it. It runs once, and again when an item of `deps` changes: list there what
90
- * `make` reads, such as the wait time. The pending calls of the old function are
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
- * (task) => debounce(wait, task), // the policy, made once per wait
104
+ * debounce(wait), // the policy, made once per wait
103
105
  * [wait],
104
106
  * );
105
107
  * return <input onChange={(event) => onChange(event.target.value)} />;
@@ -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 openResult = async (hit, ctx) => {
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("messages/marked", hit.id);
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.
@@ -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; task runs the callback above
252
- (task) => debounce(wait, task),
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
- - Do not write `usePolicy(debounce(300, callback))`. The policy would be made on every render or keep a stale callback.
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; task runs the callback above
3279
- (task) => debounce(wait, task),
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
- - Do not write `usePolicy(debounce(300, callback))`. The policy would be made on every render or keep a stale callback.
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 openResult = async (hit, ctx) => {
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("messages/marked", hit.id);
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.20.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.6 kB"
210
+ "limit": "2.65 kB"
210
211
  },
211
212
  {
212
213
  "name": "effects (all)",