nanoeffects 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -61
- package/dist/{action-BM-UdmJr.d.ts → action-rqZEnRIp.d.ts} +12 -11
- package/dist/actions.d.ts +47 -185
- package/dist/actions.js +1 -1
- package/dist/bus-CRnlwmQ4.js +1 -0
- package/dist/context-DlA80GOA.js +1 -0
- package/dist/dev.d.ts +46 -0
- package/dist/dev.js +1 -0
- package/dist/effects.d.ts +61 -21
- package/dist/effects.js +1 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -1
- package/dist/module-BSCdR9D1.d.ts +167 -0
- package/dist/notrace/{action-BM-UdmJr.d.ts → action-rqZEnRIp.d.ts} +12 -11
- package/dist/notrace/actions.d.ts +71 -45
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/bus-Dx0l9X6h.js +1 -0
- package/dist/notrace/context-yAWj0BFZ.js +1 -0
- package/dist/notrace/{delay-Cko_Q9hl.js → delay-Cgvn16oY.js} +1 -1
- package/dist/notrace/effects.d.ts +61 -21
- package/dist/notrace/effects.js +1 -1
- package/dist/notrace/index.d.ts +1 -2
- package/dist/notrace/index.js +1 -1
- package/dist/notrace/policies.d.ts +1 -1
- package/dist/notrace/policies.js +1 -1
- package/dist/notrace/redux.d.ts +1 -1
- package/dist/notrace/redux.js +1 -1
- package/dist/observable.d.ts +1 -1
- package/dist/policies.d.ts +1 -1
- package/dist/policies.js +1 -1
- package/dist/production/dev.js +1 -0
- package/dist/redux.d.ts +1 -1
- package/dist/redux.js +1 -1
- package/dist/trace.d.ts +22 -4
- package/dist/trace.js +1 -1
- package/docs/README.md +4 -1
- package/docs/actions.md +21 -11
- package/docs/context.md +0 -1
- package/docs/effects.md +49 -31
- package/docs/errors.md +4 -4
- package/docs/guide.md +229 -0
- package/docs/hmr.md +20 -58
- package/docs/policies.md +4 -4
- package/docs/tracing.md +21 -8
- package/package.json +25 -5
- package/dist/bus-RUiw8f9p.js +0 -1
- package/dist/context-BXQ6Einm.js +0 -1
- package/dist/index-CaxrMD1A.d.ts +0 -1
- package/dist/notrace/bus-CFxgpsRL.js +0 -1
- package/dist/notrace/context-DGPIFXxI.js +0 -1
- package/dist/notrace/index-CaxrMD1A.d.ts +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as Keys, k as EventMap, u as Context } from "./action-
|
|
1
|
+
import { A as Keys, N as Task, k as EventMap, u as Context } from "./action-rqZEnRIp.js";
|
|
2
2
|
//#region src/utils/delay.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Resolves after `ms`, or rejects early if `ctx.signal` aborts. `ms` can be a
|
|
@@ -129,30 +129,36 @@ type PollOptions = {
|
|
|
129
129
|
waitForFn?: boolean;
|
|
130
130
|
};
|
|
131
131
|
/**
|
|
132
|
-
* Calls
|
|
133
|
-
* `ms` (with `leading` also at once), and the next ticks keep a steady
|
|
134
|
-
* whatever the duration of
|
|
135
|
-
* (`waitForFn` starts each pause when the call ends instead).
|
|
136
|
-
*
|
|
132
|
+
* Calls a function every `ms`, like patronum `interval`: the first call comes
|
|
133
|
+
* after `ms` (with `leading` also at once), and the next ticks keep a steady
|
|
134
|
+
* rate whatever the duration of the function, so a slow one may overlap with
|
|
135
|
+
* the next call (`waitForFn` starts each pause when the call ends instead). An
|
|
136
|
+
* error stops the polling and rejects; an abort of the caller ends it without
|
|
137
|
+
* an error and clears the timer. Two forms:
|
|
138
|
+
*
|
|
139
|
+
* - `poll(ms, task, options)` wraps a Task `(payload, ctx) => result` and
|
|
140
|
+
* returns a Task. Each call gets a context of its own, a child of the
|
|
141
|
+
* caller's: `bus.on("watch", poll(10_000, refresh))`.
|
|
142
|
+
* - `poll(ctx, fn, ms, options)` is for one call inside a body; `fn` gets the
|
|
143
|
+
* number of the call, starting at 0.
|
|
137
144
|
*
|
|
138
145
|
* `ms` can be a function of the number of the call that the pause leads to, for
|
|
139
146
|
* a growing or random pause: `(call) => Math.min(1000 * 2 ** call, 60_000)`.
|
|
140
147
|
*
|
|
141
|
-
* It stops when `ctx` aborts: the result then resolves, it does not reject, and
|
|
142
|
-
* the pending timer is cleared. If a call throws, polling stops and the result
|
|
143
|
-
* rejects.
|
|
144
|
-
*
|
|
145
148
|
* @example
|
|
146
149
|
* ```ts
|
|
150
|
+
* bus.on("watch", poll(10_000, (_, { signal }) => refresh({ signal })));
|
|
151
|
+
*
|
|
147
152
|
* bus.on("watch", async (_, ctx) => {
|
|
148
|
-
* await poll(ctx, () => refresh(),
|
|
149
|
-
* cleanup(); // the task was aborted
|
|
153
|
+
* await poll(ctx, () => refresh(), 10_000);
|
|
150
154
|
* });
|
|
151
155
|
* ```
|
|
152
156
|
*/
|
|
153
|
-
|
|
157
|
+
type PollMs = number | ((call: number) => number);
|
|
158
|
+
export declare function poll<P, E extends EventMap, R>(ms: PollMs, task: Task<P, E, R>, options?: PollOptions): Task<P, E, Promise<void>>;
|
|
159
|
+
export declare function poll(ctx: {
|
|
154
160
|
signal: AbortSignal;
|
|
155
|
-
}, fn: (tick: number) => unknown, ms:
|
|
161
|
+
}, fn: (tick: number) => unknown, ms: PollMs, options?: PollOptions): Promise<void>;
|
|
156
162
|
//#endregion
|
|
157
163
|
//#region src/effects/race.d.ts
|
|
158
164
|
type RaceStarter<T> = Starter<T>;
|
|
@@ -192,9 +198,33 @@ type RetryOptions = {
|
|
|
192
198
|
/** Return `false` to stop retrying and throw `error`. Default: always retry. */
|
|
193
199
|
shouldRetry?: (error: unknown, attempt: number) => boolean;
|
|
194
200
|
};
|
|
195
|
-
|
|
201
|
+
/**
|
|
202
|
+
* Calls a function until it succeeds or `times` attempts are used, then throws
|
|
203
|
+
* the last error. If the caller aborts, retrying stops and the result rejects
|
|
204
|
+
* with the abort reason. Two forms:
|
|
205
|
+
*
|
|
206
|
+
* - `retry(task, options)` wraps a Task `(payload, ctx) => result` and returns
|
|
207
|
+
* a Task. Each attempt gets a context of its own, a child of the caller's, so
|
|
208
|
+
* it can go straight into `on` and into `ctx.run`:
|
|
209
|
+
* `bus.on("load", latest(retry(load, { times: 4 })))`. The number of the
|
|
210
|
+
* attempt is not passed; use the other form when it is needed.
|
|
211
|
+
* - `retry(ctx, fn, options)` is for one call inside a body. `fn` gets the
|
|
212
|
+
* number of the attempt, starting at 1.
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```ts
|
|
216
|
+
* bus.on("load", latest(retry(loadUser, { times: 4, delay: (attempt) => 2 ** attempt * 100 })));
|
|
217
|
+
*
|
|
218
|
+
* bus.on("load", async (id, ctx) => {
|
|
219
|
+
* const user = await retry(ctx, () => fetchUser(id, { signal: ctx.signal }), { times: 4 });
|
|
220
|
+
* });
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
type RetryFn<T> = (attempt: number) => T | PromiseLike<T>;
|
|
224
|
+
export declare function retry<P, E extends EventMap, R>(task: Task<P, E, R>, options?: RetryOptions): Task<P, E, Promise<Awaited<R>>>;
|
|
225
|
+
export declare function retry<T>(ctx: {
|
|
196
226
|
signal?: AbortSignal;
|
|
197
|
-
}, fn:
|
|
227
|
+
}, fn: RetryFn<T>, options?: RetryOptions): Promise<T>;
|
|
198
228
|
//#endregion
|
|
199
229
|
//#region src/effects/take.d.ts
|
|
200
230
|
type TakeOptions<P> = {
|
|
@@ -226,19 +256,29 @@ export declare const take: <E extends EventMap, K extends Keys<E>>(ctx: Context<
|
|
|
226
256
|
//#endregion
|
|
227
257
|
//#region src/effects/timeout.d.ts
|
|
228
258
|
/**
|
|
229
|
-
* Runs
|
|
230
|
-
* with a `TimeoutError` (a `DOMException`, like `AbortSignal.timeout`), even
|
|
231
|
-
*
|
|
259
|
+
* Runs a function; if it takes longer than `ms`, aborts its signal and rejects
|
|
260
|
+
* with a `TimeoutError` (a `DOMException`, like `AbortSignal.timeout`), even if
|
|
261
|
+
* it ignores the signal. If the caller aborts, rejects with its reason. Two
|
|
262
|
+
* forms:
|
|
263
|
+
*
|
|
264
|
+
* - `timeout(ms, task)` wraps a Task `(payload, ctx) => result` and returns a
|
|
265
|
+
* Task. The Task gets a context of its own whose `signal` the time limit
|
|
266
|
+
* aborts: `bus.on("load", latest(timeout(5000, load)))`.
|
|
267
|
+
* - `timeout(ctx, ms, starter)` is for one call inside a body; `starter` gets
|
|
268
|
+
* `{ signal, abort }`.
|
|
232
269
|
*
|
|
233
270
|
* @example
|
|
234
271
|
* ```ts
|
|
272
|
+
* bus.on("load", timeout(5000, (id, { signal }) => fetchData(id, { signal })));
|
|
273
|
+
*
|
|
235
274
|
* bus.on("load", async (id, ctx) => {
|
|
236
275
|
* const data = await timeout(ctx, 5000, ({ signal }) => fetchData(id, { signal }));
|
|
237
276
|
* });
|
|
238
277
|
* ```
|
|
239
278
|
*/
|
|
240
|
-
export declare
|
|
279
|
+
export declare function timeout<P, E extends EventMap, R>(ms: number, task: Task<P, E, R>): Task<P, E, Promise<Awaited<R>>>;
|
|
280
|
+
export declare function timeout<T>(ctx: {
|
|
241
281
|
signal?: AbortSignal;
|
|
242
|
-
}, ms: number, starter: Starter<T>)
|
|
282
|
+
}, ms: number, starter: Starter<T>): Promise<T>;
|
|
243
283
|
//#endregion
|
|
244
284
|
export type { CancellableExecutor, CancellableHandle, EffectContext, PollOptions, RaceStarter, RetryOptions, TakeOptions };
|
package/dist/notrace/effects.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a as e,c as t,i as n,o as r,s as i,t as a}from"./context-
|
|
1
|
+
import{a as e,c as t,i as n,o as r,s as i,t as a}from"./context-yAWj0BFZ.js";import{n as o,r as s,t as c}from"./delay-Cgvn16oY.js";const l=(e,t)=>t?.signal?new Promise((n,r)=>{let a=i(t,r);e.then(n,r).finally(a)}):e,u=async(e,t,o,s)=>{t?.signal?.throwIfAborted();let{ctx:c,unlink:u}=n(t??{}),d=a(c,e);r(c,e,`start`),i(c,()=>d(`cancel`));try{let e=await l(o(c),t);return d(`done`),s?.(!1),e}catch(e){throw d(`error`,e),s?.(!0),e}finally{u()}},d=async(t,r)=>{let{ctx:i,unlink:a}=n(t);try{return await e(i,`run`,()=>r(i))}finally{a()}},f=e=>e,p=(e,t)=>n=>e(t,f(n)),m=e=>Array.isArray(e)?Array(e.length):{},h=(t,r)=>{let i=Object.entries(r).map(([r,i])=>{let a=n(t),o={signal:a.ctx.signal,abort:a.ctx.abort},s=()=>i(o);return{key:r,task:new Promise(t=>t(e(a.ctx,`run`,s))).finally(a.unlink),cancel:a.ctx.abort}});return{tasks:i.map(({key:e,task:t})=>[e,t]),cancelAll:()=>{for(let e of i)e.cancel()}}},g=(e,t,n,r,i)=>{let a=()=>{};return u(e,t,async e=>{let t=h(e,n);return a=t.cancelAll,r(t.tasks)},e=>{(e||i===`end`)&&a()})};function _(e,t){return g(`all`,e,t,async e=>{let n=await Promise.all(e.map(([,e])=>e)),r=m(t);for(let[t,[i]]of e.entries())r[i]=n[t];return r},`failure`)}const v=e=>e.then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}));function y(e,t){return g(`allSettled`,e,t,async e=>{let n=await Promise.all(e.map(([,e])=>v(e))),r=m(t);for(let[t,[i]]of e.entries())r[i]=n[t];return r},`failure`)}const b=(e,n)=>e.signal.aborted?Promise.reject(e.signal.reason):new Promise((r,a)=>{let o=[],s=`pending`,c=e=>{try{e()}catch(e){t(`cancel cleanup failed`,e)}},l=i(e,e=>{s=`cancelled`;for(let e of o.reverse())c(e);a(e)}),u=e=>(...t)=>{s===`pending`&&(s=`settled`,l(),o.length=0,e(...t))},d=e=>{s===`pending`?o.push(e):s===`cancelled`&&c(e)};try{n({resolve:u(r),reject:u(a),onCancel:d})}catch(e){u(a)(e)}}),x=(e,t,n,r)=>{if(e.signal.aborted)return Promise.resolve();let a=r.leading??!1,c=r.waitForFn??!1;return u(`poll`,e,e=>new Promise((r,l)=>{let u=0,f=null,p=()=>{f!==null&&(o(f),f=null)},m=e=>{try{f=s(h,typeof n==`function`?n(e):n)}catch(e){p(),l(e)}},h=()=>{f=null,c||m(u+1),g()},g=()=>{let n=u++;d(e,e=>t(e,n)).then(()=>{c&&!e.signal.aborted&&m(u)},e=>{p(),l(e)})};i(e,p),a?(g(),c||m(u)):m(0)})).catch(t=>{if(!e.signal.aborted)throw t})};function S(e,t,n,r={}){if(typeof e!=`object`){let r=t,i=n??{};return(t,n)=>x(n,p(r,t),e,i)}let i=t;return x(e,(e,t)=>i(t),n,r)}function C(e,t){let n=m(t);return Object.keys(t).length===0?Promise.resolve(n):g(`race`,e,t,async e=>{let[t,r]=await Promise.race(e.map(([e,t])=>t.then(t=>[e,t])));return n[t]=r,n},`end`)}const w=(e,t,n)=>typeof e==`function`?e(t,n):e,T=e=>({times:e.times??3,delay:e.delay??0,shouldRetry:e.shouldRetry??(()=>!0)}),E=(e,t,n)=>u(`retry`,e,async e=>{let{times:r,delay:i,shouldRetry:a}=T(n);for(let n=1;;n++)try{return await d(e,e=>t(e,n))}catch(t){if(e.signal.throwIfAborted(),n>=r||!a(t,n))throw t;let o=w(i,n,t);o>0&&await c(e,o)}});function D(e,t,n={}){if(typeof e==`function`){let n=e;return(e,r)=>E(r,p(n,e),t??{})}let r=t;return E(e,(e,t)=>r(t),n)}const O=(e,t,n={})=>u(`take`,e,r=>new Promise((a,o)=>{let{predicate:s}=n,c=[],l=()=>{for(let e of c)e()},u=e=>{l(),o(e)};c.push(e.on(t,e=>{(!s||s(e))&&(l(),a(e))})),c.push(i(r,u)),c.push(i({signal:n.signal},u))})),k=(t,r,i)=>u(`timeout`,t,async t=>{let a=n(t),c=s(()=>a.ctx.abort(new DOMException(`Timed out after ${r}ms`,`TimeoutError`)),r);try{return await l(Promise.resolve(e(a.ctx,`run`,()=>i(a.ctx))),a.ctx)}finally{o(c),a.unlink()}});function A(e,t,n){if(typeof e==`number`){let n=t;return(t,r)=>k(r,e,p(n,t))}let r=t,i=n;return k(e,r,e=>i({signal:e.signal,abort:e.abort}))}export{_ as all,y as allSettled,b as cancellable,c as delay,i as onAbort,S as poll,C as race,D as retry,O as take,A as timeout};
|
package/dist/notrace/index.d.ts
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
import { A as Keys, C as WithPayload, D as anyEventType, E as Tracer, F as TraceEvent, I as TraceLogger, L as UnsubscribeCallback, M as Phase, N as Task, O as EffectName, P as Trace, S as WithContext, T as createBus, _ as EventPayload, a as AnyEvent, b as Off, c as Command, d as ContextOf, f as ContextOption, g as EventLike, h as EventKey, i as Action, j as PayloadOf, k as EventMap, l as Commands, m as Emittable, o as Bus, p as Emit, s as BusOptions, u as Context, v as Handler, w as WithType, x as On, y as HandlerRef } from "./action-
|
|
2
|
-
import "./index-CaxrMD1A.js";
|
|
1
|
+
import { A as Keys, C as WithPayload, D as anyEventType, E as Tracer, F as TraceEvent, I as TraceLogger, L as UnsubscribeCallback, M as Phase, N as Task, O as EffectName, P as Trace, S as WithContext, T as createBus, _ as EventPayload, a as AnyEvent, b as Off, c as Command, d as ContextOf, f as ContextOption, g as EventLike, h as EventKey, i as Action, j as PayloadOf, k as EventMap, l as Commands, m as Emittable, o as Bus, p as Emit, s as BusOptions, u as Context, v as Handler, w as WithType, x as On, y as HandlerRef } from "./action-rqZEnRIp.js";
|
|
3
2
|
export { type Action, type AnyEvent, type Bus, type BusOptions, type Command, type Commands, type Context, type ContextOf, type ContextOption, type EffectName, type Emit, type Emittable, type EventKey, type EventLike, type EventMap, type EventPayload, type Handler, type HandlerRef, type Keys, type Off, type On, type PayloadOf, type Phase, type Task, type Trace, type TraceEvent, type TraceLogger, type Tracer, type UnsubscribeCallback, type WithContext, type WithPayload, type WithType, anyEventType, createBus };
|
package/dist/notrace/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{n as anyEventType,t as createBus}from"./bus-
|
|
1
|
+
export{n as anyEventType,t as createBus}from"./bus-Dx0l9X6h.js";
|
package/dist/notrace/policies.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a as e,i as t,o as n,s as r,t as i}from"./context-
|
|
1
|
+
import{a as e,i as t,o as n,s as r,t as i}from"./context-yAWj0BFZ.js";import{n as a,r as o,t as s}from"./delay-Cgvn16oY.js";import{n as c,t as l}from"./promise-CSLpzwSR.js";const u=()=>{},d=(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)}},f=(e,t)=>e?d(e,t):t(u),p=(e,t={})=>f(t.key,t=>m(`latest`,(t,n,r)=>(r(),e(t,n)),t)),m=(e,a,o=u)=>{let s=null;return(l,u)=>{if(s?.abort(),u.signal.aborted)return;let{ctx:d,unlink:f}=t(u);s=d;let p=i(d,e),m=!1,h=()=>{d.signal.aborted||(m=!0,n(d,e,`start`))};r(d,()=>p(m?`cancel`:`drop`));let g=()=>{f(),s===d&&(s=null,o())},_=e=>{if(!d.signal.aborted)return p(m?`done`:`drop`),e};try{let e=a(l,d,h);return c(e)?Promise.resolve(e).then(_,e=>{if(!d.signal.aborted)throw p(`error`,e),e}).finally(g):(g(),_(e))}catch(e){if(g(),d.signal.aborted)return;throw p(`error`,e),e}}},h=(e,t,n={})=>f(n.key,r=>_(e,t,n,r)),g=(t,r,i,s)=>{let c=s!==u,l=null,d=null;return(u,f)=>{if(f.signal.aborted)return;let p=Date.now(),m=l===null||p-l>=t||p-l<0;return l=p,d!==null&&a(d),d=c?o(s,t):null,i&&m?Promise.resolve(e(f,`debounce`,()=>r(u,f))):(n(f,`debounce`,`drop`),Promise.resolve(void 0))}},_=(t,n,i,a)=>{let o=i.leading??!1,c=i.trailing??!0;if(!c)return g(t,n,o,a);let l=!1,u=0,d=m(`debounce`,([e,i],a,o)=>{let d=u;return r(a,()=>{u===d&&(l=!1)}),s(a,t).then(()=>{if(l=!1,c&&!i)return o(),n(e,a)})},a);return(t,r)=>{if(r.signal.aborted)return;let i=o&&!l;l=!0,u++;let a=d([t,i],r);return i?Promise.resolve(e(r,`debounce`,()=>n(t,r))):a}},v=(t,i={})=>f(i.key,i=>{let a=null;return(o,s)=>{if(s.signal.aborted)return;if(a){n(s,`leading`,`drop`);return}let c={};a=c;let u=()=>{d(),a===c&&(a=null,i())},d=r(s,u);try{return l(e(s,`leading`,()=>t(o,s)),u)}catch(e){throw u(),e}}}),y=(e,t={})=>f(t.key,n=>b(e,t,n)),b=(t,i,a)=>{let o=i.concurrency??1,s=i.limit??1/0,c=[],l=0,d=()=>{for(;l<o;){let e=c.shift();if(!e)return;e.start()}};return(f,p)=>new Promise((m,h)=>{if(p.signal.aborted){m(void 0);return}let g=u,_=()=>{g(),n(p,`queue`,`drop`),m(void 0)},v=()=>{if(g(),p.signal.aborted){_();return}l++;let n=!0,i=()=>{n&&(n=!1,l--,d(),l===0&&c.length===0&&a())},o=r(p,()=>{i(),m(void 0)}),s;try{s=Promise.resolve(e(p,`queue`,()=>t(f,p)))}catch(e){s=Promise.reject(e)}let u=()=>{o(),i()};s.then(e=>{u(),m(p.signal.aborted?void 0:e)},e=>{u(),p.signal.aborted?m(void 0):h(e)})};if(l<o){v();return}if(c.length>=s){if(i.overflow!==`drop-old`||s<=0){_();return}c.shift()?.drop()}let y={start:v,drop:_};c.push(y),g=r(p,()=>{let e=c.indexOf(y);e>=0&&c.splice(e,1),_()})})},x=(e,t,n={})=>f(n.key,r=>S(e,t,n,r)),S=(t,r,i,s)=>{let c=i.leading??!0,l=i.trailing??!0,d=s!==u,f=null,p=0,m=null,h=null,g=null,_=!1,v=e=>f===null||e-f>=t||e-f<0||e-p>=t,y=e=>n(e,`throttle`,`drop`),b=()=>{let e=g;g=null,e&&(y(e.ctx),e.resolve())},x=(e,t)=>(b(),new Promise((n,r)=>{g={payload:e,ctx:t,resolve:n,reject:r}})),S=(t,n,i)=>{b(),p=i,_=!0;try{return e(n,`throttle`,()=>r(t,n))}finally{_=!1}},C=()=>{d&&(h=o(w,t))},w=()=>{if(h=null,!d||m!==null||g)return;let e=Date.now(),n=Math.max(t-(e-(f??-1/0)),t-(e-p));n<=0?s():h=o(w,n)},T=e=>{m=null;let t=g;if(l&&t&&!t.ctx.signal.aborted){g=null;try{t.resolve(S(t.payload,t.ctx,e))}catch(e){t.reject(e)}}else b();w()},E=()=>{let e=Date.now();v(e)?T(e):m=o(E,Math.min(t-(e-(f??e)),t-(e-p)))},D=()=>{h!==null&&(a(h),h=null)},O=(e,t)=>{if(!l)y(t);else return x(e,t)},k=(e,t,n,r)=>{if(!r)y(t);else if(p=n,C(),!c)y(t);else return S(e,t,n)},A=(e,n,r,i)=>i&&m!==null?(a(m),m=o(E,t),S(e,n,r)):m===null&&(m=o(E,t),i&&(p=r,c))?S(e,n,r):O(e,n);return(e,t)=>{if(t.signal.aborted)return;let n=Date.now(),r=!_&&v(n);return f=n,D(),l?A(e,t,n,r):k(e,t,n,r)}};export{h as debounce,p as latest,v as leading,y as queue,x as throttle};
|
package/dist/notrace/redux.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { k as EventMap, o as Bus, s as BusOptions } from "./action-
|
|
1
|
+
import { k as EventMap, o as Bus, s as BusOptions } from "./action-rqZEnRIp.js";
|
|
2
2
|
//#region src/store/redux/middleware.d.ts
|
|
3
3
|
/** What Redux passes through a middleware: an action is a plain object with a `type`. */
|
|
4
4
|
type ReduxAction = {
|
package/dist/notrace/redux.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{c as e}from"./context-
|
|
1
|
+
import{c as e}from"./context-yAWj0BFZ.js";import{t}from"./bus-Dx0l9X6h.js";const n=e=>typeof e==`object`&&!!e&&`type`in e,r=(e,t={})=>{let r=e.emit;return()=>e=>i=>{let a=e(i);return n(i)&&(t.filter?.(i)??!0)&&r(i),a}},i=(n={})=>{let{filter:i,context:a,onError:o,...s}=n,c,l=/* @__PURE__ */ new WeakMap,u=e=>({...a,store:c&&{getState:c.getState,dispatch:t=>{typeof t==`object`&&t&&l.set(t,e.type);let n=t;return c?.dispatch(n)}}}),d=t({...s,onError:o??((t,{type:n})=>{e(`handler of "${n}" failed`,t)}),context:u}),f=r(d,{filter:e=>l.get(e)!==e.type&&(i?.(e)??!0)});return{bus:d,middleware:e=>(c=e,f())}};export{i as createEffectsMiddleware,r as nanoeffectsMiddleware};
|
package/dist/observable.d.ts
CHANGED
package/dist/policies.d.ts
CHANGED
package/dist/policies.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as e}from"./onAbort-BgMfspK4.js";import{a as t,i as n,o as r,t as i}from"./context-
|
|
1
|
+
import{t as e}from"./onAbort-BgMfspK4.js";import{a as t,i as n,o as r,t as i}from"./context-DlA80GOA.js";import{n as a,r as o,t as s}from"./delay-CIHnOh_L.js";import{n as c,t as l}from"./promise-CSLpzwSR.js";const u=()=>{},d=(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)}},f=(e,t)=>e?d(e,t):t(u),p=(e,t={})=>f(t.key,t=>m(`latest`,(t,n,r)=>(r(),e(t,n)),t)),m=(t,a,o=u)=>{let s=null;return(l,u)=>{if(s?.abort(),u.signal.aborted)return;let{ctx:d,unlink:f}=n(u);s=d;let p=i(d,t),m=!1,h=()=>{d.signal.aborted||(m=!0,r(d,t,`start`))};e(d,()=>p(m?`cancel`:`drop`));let g=()=>{f(),s===d&&(s=null,o())},_=e=>{if(!d.signal.aborted)return p(m?`done`:`drop`),e};try{let e=a(l,d,h);return c(e)?Promise.resolve(e).then(_,e=>{if(!d.signal.aborted)throw p(`error`,e),e}).finally(g):(g(),_(e))}catch(e){if(g(),d.signal.aborted)return;throw p(`error`,e),e}}},h=(e,t,n={})=>f(n.key,r=>_(e,t,n,r)),g=(e,n,i,s)=>{let c=s!==u,l=null,d=null;return(u,f)=>{if(f.signal.aborted)return;let p=Date.now(),m=l===null||p-l>=e||p-l<0;return l=p,d!==null&&a(d),d=c?o(s,e):null,i&&m?Promise.resolve(t(f,`debounce`,()=>n(u,f))):(r(f,`debounce`,`drop`),Promise.resolve(void 0))}},_=(n,r,i,a)=>{let o=i.leading??!1,c=i.trailing??!0;if(!c)return g(n,r,o,a);let l=!1,u=0,d=m(`debounce`,([t,i],a,o)=>{let d=u;return e(a,()=>{u===d&&(l=!1)}),s(a,n).then(()=>{if(l=!1,c&&!i)return o(),r(t,a)})},a);return(e,n)=>{if(n.signal.aborted)return;let i=o&&!l;l=!0,u++;let a=d([e,i],n);return i?Promise.resolve(t(n,`debounce`,()=>r(e,n))):a}},v=(n,i={})=>f(i.key,i=>{let a=null;return(o,s)=>{if(s.signal.aborted)return;if(a){r(s,`leading`,`drop`);return}let c={};a=c;let u=()=>{d(),a===c&&(a=null,i())},d=e(s,u);try{return l(t(s,`leading`,()=>n(o,s)),u)}catch(e){throw u(),e}}}),y=(e,t={})=>f(t.key,n=>b(e,t,n)),b=(n,i,a)=>{let o=i.concurrency??1,s=i.limit??1/0,c=[],l=0,d=()=>{for(;l<o;){let e=c.shift();if(!e)return;e.start()}};return(f,p)=>new Promise((m,h)=>{if(p.signal.aborted){m(void 0);return}let g=u,_=()=>{g(),r(p,`queue`,`drop`),m(void 0)},v=()=>{if(g(),p.signal.aborted){_();return}l++;let r=!0,i=()=>{r&&(r=!1,l--,d(),l===0&&c.length===0&&a())},o=e(p,()=>{i(),m(void 0)}),s;try{s=Promise.resolve(t(p,`queue`,()=>n(f,p)))}catch(e){s=Promise.reject(e)}let u=()=>{o(),i()};s.then(e=>{u(),m(p.signal.aborted?void 0:e)},e=>{u(),p.signal.aborted?m(void 0):h(e)})};if(l<o){v();return}if(c.length>=s){if(i.overflow!==`drop-old`||s<=0){_();return}c.shift()?.drop()}let y={start:v,drop:_};c.push(y),g=e(p,()=>{let e=c.indexOf(y);e>=0&&c.splice(e,1),_()})})},x=(e,t,n={})=>f(n.key,r=>S(e,t,n,r)),S=(e,n,i,s)=>{let c=i.leading??!0,l=i.trailing??!0,d=s!==u,f=null,p=0,m=null,h=null,g=null,_=!1,v=t=>f===null||t-f>=e||t-f<0||t-p>=e,y=e=>r(e,`throttle`,`drop`),b=()=>{let e=g;g=null,e&&(y(e.ctx),e.resolve())},x=(e,t)=>(b(),new Promise((n,r)=>{g={payload:e,ctx:t,resolve:n,reject:r}})),S=(e,r,i)=>{b(),p=i,_=!0;try{return t(r,`throttle`,()=>n(e,r))}finally{_=!1}},C=()=>{d&&(h=o(w,e))},w=()=>{if(h=null,!d||m!==null||g)return;let t=Date.now(),n=Math.max(e-(t-(f??-1/0)),e-(t-p));n<=0?s():h=o(w,n)},T=e=>{m=null;let t=g;if(l&&t&&!t.ctx.signal.aborted){g=null;try{t.resolve(S(t.payload,t.ctx,e))}catch(e){t.reject(e)}}else b();w()},E=()=>{let t=Date.now();v(t)?T(t):m=o(E,Math.min(e-(t-(f??t)),e-(t-p)))},D=()=>{h!==null&&(a(h),h=null)},O=(e,t)=>{if(!l)y(t);else return x(e,t)},k=(e,t,n,r)=>{if(!r)y(t);else if(p=n,C(),!c)y(t);else return S(e,t,n)},A=(t,n,r,i)=>i&&m!==null?(a(m),m=o(E,e),S(t,n,r)):m===null&&(m=o(E,e),i&&(p=r,c))?S(t,n,r):O(t,n);return(e,t)=>{if(t.signal.aborted)return;let n=Date.now(),r=!_&&v(n);return f=n,D(),l?A(e,t,n,r):k(e,t,n,r)}};export{h as debounce,p as latest,v as leading,y as queue,x as throttle};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e=()=>{},t=()=>e,n=()=>t;export{n as devPlugin};
|
package/dist/redux.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { k as EventMap, o as Bus, s as BusOptions } from "./action-
|
|
1
|
+
import { k as EventMap, o as Bus, s as BusOptions } from "./action-rqZEnRIp.js";
|
|
2
2
|
//#region src/store/redux/middleware.d.ts
|
|
3
3
|
/** What Redux passes through a middleware: an action is a plain object with a `type`. */
|
|
4
4
|
type ReduxAction = {
|
package/dist/redux.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{n as e}from"./onAbort-BgMfspK4.js";import{t}from"./bus-
|
|
1
|
+
import{n as e}from"./onAbort-BgMfspK4.js";import{t}from"./bus-CRnlwmQ4.js";const n=e=>typeof e==`object`&&!!e&&`type`in e,r=(e,t={})=>{let r=e.emit;return()=>e=>i=>{let a=e(i);return n(i)&&(t.filter?.(i)??!0)&&r(i),a}},i=(n={})=>{let{filter:i,context:a,onError:o,...s}=n,c,l=/* @__PURE__ */ new WeakMap,u=e=>({...a,store:c&&{getState:c.getState,dispatch:t=>{typeof t==`object`&&t&&l.set(t,e.type);let n=t;return c?.dispatch(n)}}}),d=t({...s,onError:o??((t,{type:n})=>{e(`handler of "${n}" failed`,t)}),context:u}),f=r(d,{filter:e=>l.get(e)!==e.type&&(i?.(e)??!0)});return{bus:d,middleware:e=>(c=e,f())}};export{i as createEffectsMiddleware,r as nanoeffectsMiddleware};
|
package/dist/trace.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { E as Tracer, F as TraceEvent, I as TraceLogger, P as Trace } from "./action-
|
|
1
|
+
import { E as Tracer, F as TraceEvent, I as TraceLogger, P as Trace } from "./action-rqZEnRIp.js";
|
|
2
2
|
//#region src/trace/console.d.ts
|
|
3
3
|
type ConsoleLoggerOptions = {
|
|
4
4
|
/** Defaults to `globalThis.console`. */
|
|
@@ -77,6 +77,7 @@ type ReduxConnection = {
|
|
|
77
77
|
type ReduxExtension = {
|
|
78
78
|
connect: (options?: {
|
|
79
79
|
name?: string;
|
|
80
|
+
maxAge?: number;
|
|
80
81
|
}) => ReduxConnection;
|
|
81
82
|
};
|
|
82
83
|
type ReduxDevtoolsOptions = {
|
|
@@ -84,6 +85,20 @@ type ReduxDevtoolsOptions = {
|
|
|
84
85
|
name?: string;
|
|
85
86
|
/** Defaults to `globalThis.__REDUX_DEVTOOLS_EXTENSION__`. */
|
|
86
87
|
extension?: ReduxExtension;
|
|
88
|
+
/**
|
|
89
|
+
* Also send the start of every task as an action. Default `false`: a task
|
|
90
|
+
* is one action, sent when it ends, and the tasks that are running are in
|
|
91
|
+
* the state.
|
|
92
|
+
*/
|
|
93
|
+
starts?: boolean;
|
|
94
|
+
/**
|
|
95
|
+
* How deep in the tree of tasks to show: `0` is the handler tasks only, `1`
|
|
96
|
+
* also the effects they start (`retry`, `timeout`, ...), and so on. The
|
|
97
|
+
* attempts and the starters under them are hidden at the default, `1`.
|
|
98
|
+
*/
|
|
99
|
+
depth?: number;
|
|
100
|
+
/** The number of actions that DevTools keeps; its own default is 50. */
|
|
101
|
+
maxAge?: number;
|
|
87
102
|
};
|
|
88
103
|
type ReduxDevtoolsState = {
|
|
89
104
|
/** Tasks that started and have not settled yet, by trace id. */
|
|
@@ -93,9 +108,12 @@ type ReduxDevtoolsState = {
|
|
|
93
108
|
}>;
|
|
94
109
|
};
|
|
95
110
|
/**
|
|
96
|
-
* Trace logger that sends
|
|
97
|
-
* `type/effect/phase`, with the running tasks as state.
|
|
98
|
-
*
|
|
111
|
+
* Trace logger that sends the tasks to Redux DevTools as actions
|
|
112
|
+
* `type/effect/phase`, with the running tasks as state. By default it is quiet:
|
|
113
|
+
* a task is one action, when it ends, and only the handler tasks and the
|
|
114
|
+
* effects they start are shown (see `starts` and `depth` to show more).
|
|
115
|
+
* Read-only: there is no time travel. Does nothing when the extension is not
|
|
116
|
+
* installed.
|
|
99
117
|
*
|
|
100
118
|
* @example
|
|
101
119
|
* ```ts
|
package/dist/trace.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{n as e,t}from"./onAbort-BgMfspK4.js";import{n}from"./promise-CSLpzwSR.js";const r=(e={})=>{let t=e.console??globalThis.console,n=e.prefix??`[nanoeffects]`,r=/* @__PURE__ */ new Map;return({id:e,parent:i,type:a,effect:o,phase:s,payload:c,error:l})=>{r.has(e)||r.set(e,typeof i==`number`?(r.get(i)??0)+1:0);let u=` `.repeat(r.get(e)??0);s!==`start`&&r.delete(e);let d=`${n} ${u}${a} · ${o} ${s}`;s===`error`?t?.error(d,l):t?.debug(d,c)}},i=(e={})=>{let t=e.performance??globalThis.performance,n=/* @__PURE__ */ new Map;return({id:e,parent:r,type:i,effect:a,phase:o,error:s})=>{if(!t)return;if(o===`start`){n.set(e,t.now());return}let c=`${i} · ${a} (${o})`,l={id:e,parent:r,phase:o,...o===`error`&&{error:String(s)}},u=n.get(e);n.delete(e),typeof u==`number`?t.measure(c,{start:u,end:t.now(),detail:l}):t.mark(c,{detail:l})}},a=()=>{let e=[];return Object.assign(t=>{e.push(t)},{events:e,clear:()=>{e.length=0}})},o=()=>{},s=(e={})=>{let t=e.extension??globalThis.__REDUX_DEVTOOLS_EXTENSION__;if(!t)return o;let
|
|
1
|
+
import{n as e,t}from"./onAbort-BgMfspK4.js";import{n}from"./promise-CSLpzwSR.js";const r=(e={})=>{let t=e.console??globalThis.console,n=e.prefix??`[nanoeffects]`,r=/* @__PURE__ */ new Map;return({id:e,parent:i,type:a,effect:o,phase:s,payload:c,error:l})=>{r.has(e)||r.set(e,typeof i==`number`?(r.get(i)??0)+1:0);let u=` `.repeat(r.get(e)??0);s!==`start`&&r.delete(e);let d=`${n} ${u}${a} · ${o} ${s}`;s===`error`?t?.error(d,l):t?.debug(d,c)}},i=(e={})=>{let t=e.performance??globalThis.performance,n=/* @__PURE__ */ new Map;return({id:e,parent:r,type:i,effect:a,phase:o,error:s})=>{if(!t)return;if(o===`start`){n.set(e,t.now());return}let c=`${i} · ${a} (${o})`,l={id:e,parent:r,phase:o,...o===`error`&&{error:String(s)}},u=n.get(e);n.delete(e),typeof u==`number`?t.measure(c,{start:u,end:t.now(),detail:l}):t.mark(c,{detail:l})}},a=()=>{let e=[];return Object.assign(t=>{e.push(t)},{events:e,clear:()=>{e.length=0}})},o=()=>{},s=(e={})=>{let t=e.extension??globalThis.__REDUX_DEVTOOLS_EXTENSION__;if(!t)return o;let{starts:n=!1,depth:r=1,maxAge:i}=e,a=t.connect({name:e.name??`nanoeffects`,...i!==void 0&&{maxAge:i}}),s=/* @__PURE__ */ new Map,c=/* @__PURE__ */ new Map,l=()=>({running:Object.fromEntries(s)});return a.init(l()),({error:e,...t})=>{let i=(t.parent===void 0?-1:c.get(t.parent)??-1)+1,o=i<=r;t.phase===`start`?(c.set(t.id,i),o&&s.set(t.id,{type:t.type,effect:t.effect})):(c.delete(t.id),s.delete(t.id)),o&&(t.phase!==`start`||n)&&a.send({...t,type:`${t.type}/${t.effect}/${t.phase}`,event:t.type,...t.phase===`error`&&{error:e instanceof Error?e.message:e}},l())}},c=t=>{let n=typeof t==`function`?[t]:[...t??[]];return n.length===0?null:t=>{for(let r of n)try{r(t)}catch(t){e(`trace logger failed`,t)}}},l=()=>{},u=(e,r={})=>{let i=c(e),{filter:a,sample:o=1}=r,s=0,u=e=>typeof o==`function`?o({type:e.type,payload:e.payload}):Math.random()<o,d={task:(e,t)=>({id:++s,parent:t?.id,type:e.type,payload:e.payload,sampled:t?.sampled??u(e)}),report:(e,t,n,r)=>{if(!e.sampled)return;let o={id:e.id,type:e.type,effect:t,phase:n,payload:e.payload};typeof e.parent==`number`&&(o.parent=e.parent),n===`error`&&(o.error=r),(!a||a(o))&&i?.(o)},settler:(e,t)=>{if(!e.sampled)return l;let n=!1;return(r,i)=>{n||(n=!0,d.report(e,t,r,i))}},run:(e,r,i,a)=>{if(!e.sampled)return a();let o=d.settler(e,r);d.report(e,r,`start`),t({signal:i},()=>o(`cancel`));let s;try{s=a()}catch(e){throw o(`error`,e),e}return n(s)?Promise.resolve(s).then(e=>(o(`done`),e),e=>{throw o(`error`,e),e}):(o(`done`),s)}};return d};export{r as consoleLogger,a as createTraceRecorder,u as createTracer,i as performanceLogger,s as reduxDevtools};
|
package/docs/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Documentation
|
|
2
2
|
|
|
3
|
+
Start with the [Guide](guide.md): from a bare bus to modules, one step at a time. The pages below are the reference.
|
|
4
|
+
|
|
5
|
+
- [Guide](guide.md)
|
|
3
6
|
- [Policies](policies.md): `latest`, `leading`, `debounce`, `throttle`, `queue`, `key`
|
|
4
7
|
- [Effects and cancellation](effects.md): `retry`, `timeout`, `poll`, `take`, `all`, `allSettled`, `race`, `ctx.on`
|
|
5
8
|
- [Actions, commands, jobs and modules](actions.md)
|
|
@@ -8,5 +11,5 @@
|
|
|
8
11
|
- [Tracing](tracing.md): loggers, Redux DevTools, sampling, the `notrace` build
|
|
9
12
|
- [Redux, Zustand, MobX](stores.md)
|
|
10
13
|
- [Observable: events as streams](observable.md): `createObserve` for RxJS and friends
|
|
11
|
-
- [Hot reload](hmr.md)
|
|
14
|
+
- [Hot reload and dev diagnostics](hmr.md): modules replaced by name, `devPlugin`
|
|
12
15
|
- [Semantics](semantics.md)
|
package/docs/actions.md
CHANGED
|
@@ -18,8 +18,11 @@ const getUser = createCommand("user/get", (id: string, { signal }) => fetchUser(
|
|
|
18
18
|
// a contract without a handler: the code that sends mail is elsewhere
|
|
19
19
|
const sendMail = createCommand<Mail, void>("mail/send");
|
|
20
20
|
|
|
21
|
-
// a command whose outcome is sent on as
|
|
22
|
-
const loadUser = createJob("user/load", fetchUser, {
|
|
21
|
+
// a command whose outcome is sent on as actions: the command and its two actions together
|
|
22
|
+
const { command: loadUser, successAction, failAction } = createJob("user/load", fetchUser, {
|
|
23
|
+
success: userLoaded, // an action creator that exists: used and returned as it is
|
|
24
|
+
fail: "user/failed", // a string: the job makes the action creator
|
|
25
|
+
});
|
|
23
26
|
```
|
|
24
27
|
|
|
25
28
|
**Actions.** `createAction(type)` returns a creator, with no generics: it keeps the literal type, and the bus checks the payload against its event map (`bus.emit(userLoaded(5))` is an error when `"user/loaded"` is a `User`). Without an event map give the payload type, `createAction<User>("user/loaded")`. The creator: `userLoaded(user)` is `{ type, payload }`, `userLoaded.type` is the type, `userLoaded.match(action)` narrows an action. `createAction(type, prepare)` builds the payload from several arguments. A creator works as the event of its type: `bus.emit(userLoaded(user))`, `bus.on(userLoaded, fn)`.
|
|
@@ -31,30 +34,36 @@ const loadUser = createJob("user/load", fetchUser, { done: userLoaded, fail: loa
|
|
|
31
34
|
```ts
|
|
32
35
|
import { createCommands, bindHandler } from "nanoeffects/actions";
|
|
33
36
|
|
|
34
|
-
const
|
|
35
|
-
const
|
|
37
|
+
const bus = createBus<Events>();
|
|
38
|
+
const commands = createCommands(bus, [bindHandler(sendMail, smtpSend)]); // attached to the bus
|
|
36
39
|
|
|
37
40
|
const unbind = commands.handle(getUser, async () => fakeUser); // later, e.g. in a test
|
|
38
41
|
unbind(); // the command's own handler is back; the latest binding wins
|
|
39
42
|
```
|
|
40
43
|
|
|
41
|
-
A command with no handler anywhere throws `No handler for command "..."` from `emit`. The registry lives in `nanoeffects/actions`, so a bus without
|
|
44
|
+
A command with no handler anywhere throws `No handler for command "..."` from `emit`. The registry lives in `nanoeffects/actions`, so a bus without one carries none of this code; without it `emit(command, payload)` simply runs the command's own handler. `createCommands(bus)` sets the registry as `bus.commands`, and a second call on the same bus replaces it. There is no `commands` option in `createBus`; a custom runner is assigned, `bus.commands = { run }`.
|
|
45
|
+
|
|
46
|
+
**Jobs.** `createJob(type, handler, { success, fail })` makes a command that sends its outcome on as actions, for when nobody waits for the result, and returns `{ command, successAction, failAction }`. When the handler succeeds, `successAction(result)` is emitted on the bus, and when it fails, `failAction(error)`.
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
- `success` and `fail` are a string, from which the job makes an action creator with `createAction` (the payload is the result, or the error), or an action creator that exists already, which is used and returned as it is. A missing one is made with the type `<type>/success` or `<type>/fail`.
|
|
49
|
+
- `bus.emit(command, payload)` returns a promise of the result. An error of the handler always counts as handled: it goes to `failAction` and the promise resolves with `undefined`. For a result that must reject, use a command.
|
|
50
|
+
- The actions are emitted as the task that ran the job, also for a handler bound with `commands.handle`, and are not sent once the task is aborted. A missing handler is not an outcome: `emit` throws `No handler for command "..."`.
|
|
51
|
+
- Calling `command(payload)` directly runs only the handler.
|
|
44
52
|
|
|
45
53
|
## Modules
|
|
46
54
|
|
|
47
|
-
`
|
|
55
|
+
`bus.use(modulePlugin(modules))`, with `modulePlugin` from `nanoeffects/actions`, assembles an app from modules and returns `mount`, `unmount`, `isMounted`, `dispose` and `commands`, the registry of command handlers. A module is what a domain of the app needs: the commands it handles, the events it listens to, its sub-modules and its background tasks.
|
|
48
56
|
|
|
49
57
|
```ts
|
|
50
58
|
const userModule = createModule<Events>("user", (m) => {
|
|
51
|
-
m.handle(getUser, fetchUser);
|
|
59
|
+
m.handle(getUser, fetchUser); // a contract gets its handler here
|
|
60
|
+
m.use(saveUser, authModule); // a command that carries a handler, and a sub-module
|
|
52
61
|
m.on("login", (user, ctx) => ctx.emit("loaded", user));
|
|
53
62
|
m.spawn(async (_, ctx) => { /* background work, ends with the module */ });
|
|
54
63
|
});
|
|
55
64
|
|
|
56
65
|
const bus = createBus<Events>();
|
|
57
|
-
const app =
|
|
66
|
+
const app = bus.use(modulePlugin([userModule]));
|
|
58
67
|
|
|
59
68
|
const unmount = app.mount(orderModule); // later, e.g. after a lazy import
|
|
60
69
|
app.commands.handle(getUser, fakeUser); // in a test
|
|
@@ -63,6 +72,7 @@ app.dispose(); // unmounts every module
|
|
|
63
72
|
|
|
64
73
|
- **Unmounting undoes everything** the module did: its subscriptions, its bound commands, its sub-modules and its background tasks, which are aborted.
|
|
65
74
|
- **A module is known by its name.** Mounting a module whose name is taken replaces the mounted one. That is what hot reload needs: see [hmr.md](hmr.md).
|
|
66
|
-
- **The registry.** If
|
|
75
|
+
- **The registry.** If `createCommands(bus)` was called, that registry is used; otherwise the module plugin attaches its own. Either way it is `bus.commands` and `app.commands`, so the commands that modules bind are the ones the bus runs.
|
|
67
76
|
- A disposed app cannot mount modules.
|
|
68
|
-
- A module context is a task context plus `handle` (
|
|
77
|
+
- A module context is a task context plus `handle` (give a command a handler: for a contract, or to override), `use` (bring sub-modules and commands that carry a handler: `m.use(getUser, saveUser, subModule)`, in any order; the returned function undoes just that call) and `spawn` (a background task, aborted on unmount).
|
|
78
|
+
- A command that carries a handler keeps answering after its module is unmounted: the handler is its own, and `use` only brings the command and tracks it in the registry. Use `handle` for a handler that should live only as long as the module.
|
package/docs/context.md
CHANGED
|
@@ -13,4 +13,3 @@ The fields are in the context of handlers, `ctx.run`, listeners added with `ctx.
|
|
|
13
13
|
|
|
14
14
|
- The fields of the library (`signal`, `emit`, `run`, ...) win when the names are the same.
|
|
15
15
|
- `context` can also be a function, `(task) => fields`, called with `{ type, payload }` of each task, for fields that depend on the task.
|
|
16
|
-
- Several stores are fields of one object: `WithContext<{ stores: { user: UserStore; cart: CartStore } }>`.
|
package/docs/effects.md
CHANGED
|
@@ -8,13 +8,13 @@ An abort only raises a flag: your code decides to stop. `nanoeffects/effects` ha
|
|
|
8
8
|
import { cancellable } from "nanoeffects/effects";
|
|
9
9
|
|
|
10
10
|
bus.on(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
"wait",
|
|
12
|
+
latest((_, ctx) =>
|
|
13
|
+
cancellable(ctx, ({ resolve, onCancel }) => {
|
|
14
|
+
const timer = setTimeout(resolve, 5000);
|
|
15
|
+
onCancel(() => clearTimeout(timer));
|
|
16
|
+
}),
|
|
17
|
+
),
|
|
18
18
|
);
|
|
19
19
|
```
|
|
20
20
|
|
|
@@ -28,8 +28,8 @@ bus.on(
|
|
|
28
28
|
import { onAbort } from "nanoeffects/effects";
|
|
29
29
|
|
|
30
30
|
bus.on("connect", (url, ctx) => {
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
const socket = new WebSocket(url);
|
|
32
|
+
onAbort(ctx, () => socket.close());
|
|
33
33
|
});
|
|
34
34
|
```
|
|
35
35
|
|
|
@@ -39,8 +39,8 @@ bus.on("connect", (url, ctx) => {
|
|
|
39
39
|
|
|
40
40
|
```ts
|
|
41
41
|
const { user, posts } = await allSettled(ctx, {
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
user: ({ signal }) => fetchUser(id, { signal }),
|
|
43
|
+
posts: ({ signal }) => fetchPosts(id, { signal }),
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
if (user.status === "fulfilled") render(user.value);
|
|
@@ -52,7 +52,7 @@ A task context has `on` and `off`, like the bus. A subscription made with `ctx.o
|
|
|
52
52
|
|
|
53
53
|
```ts
|
|
54
54
|
bus.on("watch", (_, ctx) => {
|
|
55
|
-
|
|
55
|
+
ctx.on("tick", (n) => console.log(n)); // gone when this task aborts
|
|
56
56
|
});
|
|
57
57
|
```
|
|
58
58
|
|
|
@@ -66,13 +66,13 @@ bus.on("watch", (_, ctx) => {
|
|
|
66
66
|
import { take, timeout } from "nanoeffects/effects";
|
|
67
67
|
|
|
68
68
|
bus.on("login", async (user, ctx) => {
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
const reason = await take(ctx, "logout"); // resolves on the next logout
|
|
70
|
+
cleanup(user, reason);
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
// wait for a matching event, but not longer than 5 s
|
|
73
|
+
const id = await timeout(ctx, 5000, ({ signal }) =>
|
|
74
|
+
take(ctx, "saved", { signal, predicate: (id) => id === draft.id }),
|
|
75
|
+
);
|
|
76
76
|
});
|
|
77
77
|
```
|
|
78
78
|
|
|
@@ -82,25 +82,43 @@ bus.on("login", async (user, ctx) => {
|
|
|
82
82
|
|
|
83
83
|
## retry, timeout, poll
|
|
84
84
|
|
|
85
|
+
These three have two forms. As a **Task wrapper** they take a function `(payload, ctx) => result` and return one, so they go straight into `on`, next to the policies, with no `ctx` in their arguments. As a **call inside a body** they take `ctx` first.
|
|
86
|
+
|
|
85
87
|
```ts
|
|
86
88
|
import { poll, retry, timeout } from "nanoeffects/effects";
|
|
87
89
|
|
|
90
|
+
const loadUser = (id: string, { signal }) => fetchUser(id, { signal });
|
|
91
|
+
|
|
92
|
+
// wrappers: up to 4 attempts with a backoff, each within 5 s; a newer call supersedes the older
|
|
93
|
+
bus.on(
|
|
94
|
+
"load",
|
|
95
|
+
latest(
|
|
96
|
+
retry(timeout(5000, loadUser), {
|
|
97
|
+
times: 4,
|
|
98
|
+
delay: (attempt) => 2 ** attempt * 100,
|
|
99
|
+
shouldRetry: (error) => !isFatal(error),
|
|
100
|
+
}),
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
// calls refresh every 10 s until the task is aborted
|
|
105
|
+
bus.on("watch", poll(10_000, (_, { signal }) => refresh({ signal })));
|
|
106
|
+
|
|
107
|
+
// a wrapped Task runs as a child of a handler with ctx.run: the only place where ctx is written
|
|
108
|
+
bus.on("open", async (id, ctx) => {
|
|
109
|
+
const user = await ctx.run(retry(timeout(5000, loadUser)), id);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// one call inside a body: ctx first
|
|
88
113
|
bus.on("load", async (id, ctx) => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
delay: (attempt) => 2 ** attempt * 100,
|
|
93
|
-
shouldRetry: (error) => !isFatal(error),
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
// aborts the starter's signal and rejects with a TimeoutError after 5 s
|
|
97
|
-
const data = await timeout(ctx, 5000, ({ signal }) => fetchData(id, { signal }));
|
|
98
|
-
|
|
99
|
-
// calls refresh every 10 s; resolves when the task aborts
|
|
100
|
-
await poll(ctx, () => refresh(), 10_000);
|
|
114
|
+
const user = await retry(ctx, () => fetchUser(id, { signal: ctx.signal }), { times: 4 });
|
|
115
|
+
const data = await timeout(ctx, 5000, ({ signal }) => fetchData(id, { signal }));
|
|
116
|
+
await poll(ctx, () => refresh(), 10_000);
|
|
101
117
|
});
|
|
102
118
|
```
|
|
103
119
|
|
|
120
|
+
Use the wrapper form for a whole handler or a whole `ctx.run`, and the `ctx` form for one call in the middle of a body. The forms are told apart by the first argument: a function (`retry`), a number (`timeout`), a number or a function (`poll`) is a wrapper; an object is `ctx`. Each attempt or call of a wrapper gets a context of its own, a child of the caller's, so an abort of the caller reaches it and an abort of one attempt (a timeout) does not reach the caller. The attempt number is passed only in the `ctx` form.
|
|
121
|
+
|
|
104
122
|
- `retry`: `times` is the total number of attempts (default 3), `delay` is a number or `(attempt, error) => ms`, `shouldRetry(error, attempt)` can stop early. `fn` gets the attempt number, starting at 1. On abort it rejects with the abort reason.
|
|
105
123
|
- `timeout`: the error is a `DOMException` named `TimeoutError`, like `AbortSignal.timeout`. It rejects even if the starter ignores its signal.
|
|
106
124
|
- `poll`: like patronum `interval`. The first call comes after `ms` (`leading: true` also calls at once), and the next ticks keep a steady rate whatever the duration of `fn`, so a slow `fn` can overlap with the next call; `waitForFn: true` starts each pause when the call ends instead, so calls never overlap. `fn` gets the call number from 0. An error from `fn` stops polling and rejects; an abort ends it normally and clears the timer.
|
|
@@ -114,6 +132,6 @@ await delay(ctx, () => 200 + Math.random() * 200); // 200 to 400 ms
|
|
|
114
132
|
|
|
115
133
|
- All three are traced: the effect is a group (`retry`, `timeout`, `poll`) and each attempt or call is a `run` child.
|
|
116
134
|
|
|
117
|
-
|
|
135
|
+
The other effects wait inside a body, so `ctx` is their first argument: `delay(ctx, ms)`, `all(ctx, effects)`, `allSettled(ctx, effects)`, `race(ctx, effects)`, `cancellable(ctx, executor)`, `onAbort(ctx, fn)`, `take(ctx, event, options)`, and `retry`, `timeout` and `poll` in their `ctx` form. It is required; outside a task pass `{}` or any object with a `signal`.
|
|
118
136
|
|
|
119
137
|
For `fetch` and other APIs that take a signal, pass `ctx.signal`. A timeout without `race`: `AbortSignal.any([ctx.signal, AbortSignal.timeout(5000)])`.
|
package/docs/errors.md
CHANGED
|
@@ -6,7 +6,7 @@ Pass `onError` to isolate handlers instead. Then every handler runs, and thrown
|
|
|
6
6
|
|
|
7
7
|
```ts
|
|
8
8
|
const bus = createBus<Events>({
|
|
9
|
-
|
|
9
|
+
onError: (error, { type, payload }) => report(error, type, payload),
|
|
10
10
|
});
|
|
11
11
|
```
|
|
12
12
|
|
|
@@ -16,8 +16,8 @@ A typo in an event name, `emit("serch")`, does nothing and says nothing. `onUnha
|
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
18
|
const bus = createBus<Events>({
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
onUnhandled: import.meta.env.DEV
|
|
20
|
+
? ({ type }) => console.warn(`Nobody listens to "${type}"`)
|
|
21
|
+
: void 0,
|
|
22
22
|
});
|
|
23
23
|
```
|