nanoeffects 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +96 -2
- package/dist/action-rqZEnRIp.d.ts +303 -0
- package/dist/actions.d.ts +58 -0
- package/dist/actions.js +1 -0
- package/dist/bus-CRnlwmQ4.js +1 -0
- package/dist/context-DlA80GOA.js +1 -0
- package/dist/delay-CIHnOh_L.js +1 -0
- package/dist/dev.d.ts +46 -0
- package/dist/dev.js +1 -0
- package/dist/effects.d.ts +284 -0
- package/dist/effects.js +1 -0
- package/dist/event-0RIBGYqS.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/module-BSCdR9D1.d.ts +167 -0
- package/dist/notrace/action-rqZEnRIp.d.ts +303 -0
- package/dist/notrace/actions.d.ts +222 -0
- package/dist/notrace/actions.js +1 -0
- package/dist/notrace/bus-Dx0l9X6h.js +1 -0
- package/dist/notrace/context-yAWj0BFZ.js +1 -0
- package/dist/notrace/delay-Cgvn16oY.js +1 -0
- package/dist/notrace/effects.d.ts +284 -0
- package/dist/notrace/effects.js +1 -0
- package/dist/notrace/event-0RIBGYqS.js +1 -0
- package/dist/notrace/index.d.ts +2 -0
- package/dist/notrace/index.js +1 -0
- package/dist/notrace/policies.d.ts +171 -0
- package/dist/notrace/policies.js +1 -0
- package/dist/notrace/promise-CSLpzwSR.js +1 -0
- package/dist/notrace/redux.d.ts +92 -0
- package/dist/notrace/redux.js +1 -0
- package/dist/observable.d.ts +20 -0
- package/dist/observable.js +1 -0
- package/dist/onAbort-BgMfspK4.js +1 -0
- package/dist/policies.d.ts +171 -0
- package/dist/policies.js +1 -0
- package/dist/production/dev.js +1 -0
- package/dist/promise-CSLpzwSR.js +1 -0
- package/dist/redux.d.ts +92 -0
- package/dist/redux.js +1 -0
- package/dist/trace.d.ts +162 -0
- package/dist/trace.js +1 -0
- package/docs/README.md +15 -0
- package/docs/actions.md +78 -0
- package/docs/context.md +15 -0
- package/docs/effects.md +137 -0
- package/docs/errors.md +23 -0
- package/docs/guide.md +229 -0
- package/docs/hmr.md +36 -0
- package/docs/observable.md +22 -0
- package/docs/policies.md +53 -0
- package/docs/semantics.md +7 -0
- package/docs/stores.md +50 -0
- package/docs/tracing.md +117 -0
- package/package.json +184 -5
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { N as Task, k as EventMap } from "./action-rqZEnRIp.js";
|
|
2
|
+
//#region src/policies/keyed.d.ts
|
|
3
|
+
/** Options shared by every policy that can keep its state per key. */
|
|
4
|
+
type KeyOption<P> = {
|
|
5
|
+
/**
|
|
6
|
+
* Gives each key its own state: calls with different keys do not affect
|
|
7
|
+
* each other. The state of a key is dropped when that key goes idle, so
|
|
8
|
+
* keys can be unbounded (ids, urls).
|
|
9
|
+
*/
|
|
10
|
+
key?: (payload: P) => unknown;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/policies/debounce.d.ts
|
|
14
|
+
type DebounceResult<R> = Promise<R | undefined> | Promise<Awaited<R> | undefined> | undefined;
|
|
15
|
+
type DebounceOptions<P = never> = KeyOption<P> & {
|
|
16
|
+
/** Run the first call of a burst at once. Default `false`. */
|
|
17
|
+
leading?: boolean;
|
|
18
|
+
/** Run the last call of a burst after `ms` of quiet. Default `true`. */
|
|
19
|
+
trailing?: boolean;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Waits for `ms` of quiet, then runs; a new call restarts the wait and drops
|
|
23
|
+
* the previous. With `leading` the first call of a burst runs at once, and the
|
|
24
|
+
* trailing run happens only if more calls follow it. With `trailing: false`
|
|
25
|
+
* only the leading call runs. With `options.key` each key has its own wait.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* bus.on(
|
|
30
|
+
* "input",
|
|
31
|
+
* debounce(300, async (text, { signal }) => {
|
|
32
|
+
* const res = await fetch(`/api?q=${text}`, { signal });
|
|
33
|
+
* showResults(await res.json());
|
|
34
|
+
* }),
|
|
35
|
+
* );
|
|
36
|
+
*
|
|
37
|
+
* bus.emit("input", "r");
|
|
38
|
+
* bus.emit("input", "re"); // only the last call runs, 300 ms after it
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
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>>;
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/policies/latest.d.ts
|
|
44
|
+
type LatestOptions<P = never> = KeyOption<P>;
|
|
45
|
+
/**
|
|
46
|
+
* Aborts the previous run when a new call arrives; stale results are dropped.
|
|
47
|
+
* With `options.key` each key has its own latest run.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```ts
|
|
51
|
+
* bus.on(
|
|
52
|
+
* "search",
|
|
53
|
+
* latest(async (query, { signal }) => {
|
|
54
|
+
* const res = await fetch(`/api?q=${query}`, { signal });
|
|
55
|
+
* showResults(await res.json());
|
|
56
|
+
* }),
|
|
57
|
+
* );
|
|
58
|
+
*
|
|
59
|
+
* bus.emit("search", "re"); // aborted by the next call
|
|
60
|
+
* bus.emit("search", "redux"); // only this one finishes
|
|
61
|
+
*
|
|
62
|
+
* // one latest run per user instead of one for all
|
|
63
|
+
* bus.on("load", latest(loadUser, { key: (user) => user.id }));
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
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>;
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/policies/leading.d.ts
|
|
69
|
+
type LeadingOptions<P = never> = KeyOption<P>;
|
|
70
|
+
/**
|
|
71
|
+
* Drops new calls while the task is still running. The lock is released when
|
|
72
|
+
* the run settles or when its task is aborted, whichever comes first, so a
|
|
73
|
+
* `fn` that ignores the abort signal may overlap with the next run. With
|
|
74
|
+
* `options.key` each key has its own lock.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* bus.on(
|
|
79
|
+
* "save",
|
|
80
|
+
* leading(async (draft) => {
|
|
81
|
+
* await api.save(draft);
|
|
82
|
+
* }),
|
|
83
|
+
* );
|
|
84
|
+
*
|
|
85
|
+
* bus.emit("save", draft); // runs
|
|
86
|
+
* bus.emit("save", draft); // dropped while the first save is in flight
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
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>;
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/policies/queue.d.ts
|
|
92
|
+
type QueueOptions<P = never> = KeyOption<P> & {
|
|
93
|
+
/** How many calls may run at the same time. Default 1. */
|
|
94
|
+
concurrency?: number;
|
|
95
|
+
/** How many calls may wait for a free slot. Default: no limit. */
|
|
96
|
+
limit?: number;
|
|
97
|
+
/**
|
|
98
|
+
* What to drop when a call arrives and `limit` calls already wait: the new
|
|
99
|
+
* call (`"drop-new"`, the default) or the oldest waiting one (`"drop-old"`,
|
|
100
|
+
* which keeps the most recent calls). A dropped call resolves with `undefined`.
|
|
101
|
+
*/
|
|
102
|
+
overflow?: "drop-new" | "drop-old";
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Runs calls one after another, in the order they arrived: a call starts when
|
|
106
|
+
* fewer than `concurrency` calls are running. Every call returns a promise with
|
|
107
|
+
* the result of its own run; an error rejects only that call, and the queue
|
|
108
|
+
* goes on. The first call on an idle queue starts synchronously.
|
|
109
|
+
*
|
|
110
|
+
* With `limit` the number of waiting calls is capped. `limit: 1` with
|
|
111
|
+
* `overflow: "drop-old"` keeps only the latest call while one is running, and
|
|
112
|
+
* runs it when the current one is done.
|
|
113
|
+
*
|
|
114
|
+
* With `options.key` each key has its own line.
|
|
115
|
+
*
|
|
116
|
+
* A call whose task aborts while it waits is skipped and resolves with
|
|
117
|
+
* `undefined`. When the task of a running call aborts, its slot is freed at
|
|
118
|
+
* once and its result is dropped; a `fn` that ignores the abort signal can
|
|
119
|
+
* then overlap with the next run.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* bus.on(
|
|
124
|
+
* "save",
|
|
125
|
+
* queue(async (draft, { signal }) => {
|
|
126
|
+
* await api.save(draft, { signal });
|
|
127
|
+
* }),
|
|
128
|
+
* );
|
|
129
|
+
*
|
|
130
|
+
* bus.emit("save", a); // runs
|
|
131
|
+
* bus.emit("save", b); // waits for a, then runs
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
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>>;
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/policies/throttle.d.ts
|
|
137
|
+
type ThrottleOptions<P = never> = KeyOption<P> & {
|
|
138
|
+
/** Run the first call at once, when the throttle is idle. Default `true`. */
|
|
139
|
+
leading?: boolean;
|
|
140
|
+
/** Run the latest call made while waiting, at the end of the window. Default `true`. */
|
|
141
|
+
trailing?: boolean;
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* At most one run per `ms`, with the semantics of lodash `throttle` (and
|
|
145
|
+
* patronum): a call made when the throttle is idle, that is, at least `ms`
|
|
146
|
+
* after the previous call and the previous run, runs at once (`leading`); calls
|
|
147
|
+
* made while it waits are held, and the latest of them runs when the window
|
|
148
|
+
* closes (`trailing`). Under a steady stream of calls it runs every `ms`.
|
|
149
|
+
*
|
|
150
|
+
* A held call returns a promise that settles with its result; calls superseded
|
|
151
|
+
* by a newer one, or not allowed to wait (`trailing: false`), resolve with
|
|
152
|
+
* `undefined`. With `leading: false` even the first call waits for the end of
|
|
153
|
+
* its window. With `options.key` each key has its own throttle.
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```ts
|
|
157
|
+
* bus.on(
|
|
158
|
+
* "scroll",
|
|
159
|
+
* throttle(200, (position) => {
|
|
160
|
+
* saveScrollPosition(position);
|
|
161
|
+
* }),
|
|
162
|
+
* );
|
|
163
|
+
*
|
|
164
|
+
* bus.emit("scroll", 10); // runs now
|
|
165
|
+
* bus.emit("scroll", 20); // superseded
|
|
166
|
+
* bus.emit("scroll", 30); // runs once the 200 ms window closes
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
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>;
|
|
170
|
+
//#endregion
|
|
171
|
+
export type { DebounceOptions, LatestOptions, LeadingOptions, QueueOptions, ThrottleOptions };
|
package/dist/policies.js
ADDED
|
@@ -0,0 +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-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};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e=e=>typeof e!=`object`||!e?!1:`then`in e&&typeof e.then==`function`,t=(t,n)=>e(t)?Promise.resolve(t).finally(n):(n(),t);export{e as n,t};
|
package/dist/redux.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { k as EventMap, o as Bus, s as BusOptions } from "./action-rqZEnRIp.js";
|
|
2
|
+
//#region src/store/redux/middleware.d.ts
|
|
3
|
+
/** What Redux passes through a middleware: an action is a plain object with a `type`. */
|
|
4
|
+
type ReduxAction = {
|
|
5
|
+
type: string;
|
|
6
|
+
payload?: unknown;
|
|
7
|
+
};
|
|
8
|
+
type NanoeffectsMiddlewareOptions = {
|
|
9
|
+
/** Return `false` for the actions that the bus should not get. Default: all of them. */
|
|
10
|
+
filter?: (action: ReduxAction) => boolean;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* A Redux middleware that sends every action to the bus, after the reducers
|
|
14
|
+
* have handled it, so a handler that reads the store sees the new state. It
|
|
15
|
+
* does not depend on Redux: it only has the shape of a middleware. Values that
|
|
16
|
+
* are not plain actions (the functions of redux-thunk) go through untouched.
|
|
17
|
+
*
|
|
18
|
+
* The other way needs no adapter: give the store to the tasks with
|
|
19
|
+
* `createBus({ context: { store } })` and dispatch from a handler.
|
|
20
|
+
*
|
|
21
|
+
* Take care of loops: a handler must not dispatch the action it handles.
|
|
22
|
+
* Errors of handlers reach the caller of `dispatch`, as for `bus.emit`; set
|
|
23
|
+
* `onError` on the bus to isolate them.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* const store = configureStore({
|
|
28
|
+
* reducer,
|
|
29
|
+
* middleware: (getDefault) => getDefault().concat(nanoeffectsMiddleware(bus)),
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* bus.on(userRequested, latest(async (id, { store, signal }) => {
|
|
33
|
+
* store.dispatch(userLoaded(await fetchUser(id, { signal })));
|
|
34
|
+
* }));
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare const nanoeffectsMiddleware: <E extends EventMap>(bus: Bus<E>, options?: NanoeffectsMiddlewareOptions) => () => (next: (action: unknown) => unknown) => (action: unknown) => unknown;
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/store/redux/effects.d.ts
|
|
40
|
+
/** What Redux gives a middleware: the parts of the store that it may use. */
|
|
41
|
+
type ReduxStoreApi<State = unknown> = {
|
|
42
|
+
dispatch: (action: {
|
|
43
|
+
type: string;
|
|
44
|
+
payload?: unknown;
|
|
45
|
+
}) => unknown;
|
|
46
|
+
getState: () => State;
|
|
47
|
+
};
|
|
48
|
+
type EffectsMiddlewareOptions<E extends EventMap> = Omit<BusOptions<E>, "context"> & NanoeffectsMiddlewareOptions & {
|
|
49
|
+
/** More fields for the context of every task, next to `store`. */
|
|
50
|
+
context?: object;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Creates a bus and the Redux middleware that feeds it: the effects of an app
|
|
54
|
+
* in one place, without sagas. Every task gets `ctx.store`, the `dispatch` and
|
|
55
|
+
* `getState` of the store, as soon as the middleware is added to it. Type it
|
|
56
|
+
* in the event map:
|
|
57
|
+
*
|
|
58
|
+
* ```ts
|
|
59
|
+
* type Events = { userRequested: string } & WithContext<{ store: ReduxStoreApi<RootState> }>;
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* Two things are looked after for you:
|
|
63
|
+
*
|
|
64
|
+
* - **A handler does not loop on its own action.** An action that a handler
|
|
65
|
+
* dispatches through `ctx.store` and that has the type the handler works on
|
|
66
|
+
* does not go back to the bus (a handler of `a` that dispatches `a`). Other
|
|
67
|
+
* handlers of that type do not get it either. Longer cycles, `a` to `b` to
|
|
68
|
+
* `a`, are yours to avoid.
|
|
69
|
+
* - **A failing handler does not break `dispatch`.** Errors go to `onError`,
|
|
70
|
+
* by default to `console.error`, as in Redux Toolkit listeners; reducers
|
|
71
|
+
* still throw to the caller.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* const effects = createEffectsMiddleware<Events>();
|
|
76
|
+
*
|
|
77
|
+
* const store = configureStore({
|
|
78
|
+
* reducer,
|
|
79
|
+
* middleware: (getDefault) => getDefault().concat(effects.middleware),
|
|
80
|
+
* });
|
|
81
|
+
*
|
|
82
|
+
* effects.bus.on(userRequested, latest(async (id, { store, signal }) => {
|
|
83
|
+
* store.dispatch(userLoaded(await fetchUser(id, { signal })));
|
|
84
|
+
* }));
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export declare const createEffectsMiddleware: <E extends EventMap = EventMap>(options?: EffectsMiddlewareOptions<E>) => {
|
|
88
|
+
bus: Bus<E>;
|
|
89
|
+
middleware: (storeApi: ReduxStoreApi) => (next: (action: unknown) => unknown) => (action: unknown) => unknown;
|
|
90
|
+
};
|
|
91
|
+
//#endregion
|
|
92
|
+
export type { EffectsMiddlewareOptions, NanoeffectsMiddlewareOptions, ReduxStoreApi };
|
package/dist/redux.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { E as Tracer, F as TraceEvent, I as TraceLogger, P as Trace } from "./action-rqZEnRIp.js";
|
|
2
|
+
//#region src/trace/console.d.ts
|
|
3
|
+
type ConsoleLoggerOptions = {
|
|
4
|
+
/** Defaults to `globalThis.console`. */
|
|
5
|
+
console?: Pick<Console, "debug" | "error">;
|
|
6
|
+
/** Prepended to every line. */
|
|
7
|
+
prefix?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Trace logger that prints one line per phase, indented by task depth.
|
|
11
|
+
* Failures go to `console.error`, everything else to `console.debug`.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const bus = createBus<Events>({ trace: createTracer(consoleLogger()) });
|
|
16
|
+
* // search · latest start
|
|
17
|
+
* // search · latest cancel
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare const consoleLogger: (options?: ConsoleLoggerOptions) => TraceLogger;
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/trace/performance.d.ts
|
|
23
|
+
/** The part of the User Timing API the logger uses. */
|
|
24
|
+
type PerformanceLike = {
|
|
25
|
+
now: () => number;
|
|
26
|
+
mark: (name: string, options?: {
|
|
27
|
+
detail?: unknown;
|
|
28
|
+
}) => unknown;
|
|
29
|
+
measure: (name: string, options: {
|
|
30
|
+
start: number;
|
|
31
|
+
end: number;
|
|
32
|
+
detail?: unknown;
|
|
33
|
+
}) => unknown;
|
|
34
|
+
};
|
|
35
|
+
type PerformanceLoggerOptions = {
|
|
36
|
+
/** Defaults to `globalThis.performance`. */
|
|
37
|
+
performance?: PerformanceLike;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Trace logger that records User Timing entries, so tasks show up on the
|
|
41
|
+
* Timings track of the browser Performance panel. A task that started is a
|
|
42
|
+
* measure from `start` to its end; a dropped call is a single mark.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* const bus = createBus<Events>({ trace: createTracer(performanceLogger()) });
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare const performanceLogger: (options?: PerformanceLoggerOptions) => TraceLogger;
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region src/trace/recorder.d.ts
|
|
52
|
+
type Recorder<E = Record<string, unknown>> = TraceLogger<E> & {
|
|
53
|
+
/** Every event received so far, in order. */
|
|
54
|
+
readonly events: TraceEvent<E>[];
|
|
55
|
+
clear: () => void;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Trace logger that keeps events in memory; meant for tests.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* const recorder = createTraceRecorder<Events>();
|
|
63
|
+
* const bus = createBus<Events>({ trace: createTracer(recorder) });
|
|
64
|
+
*
|
|
65
|
+
* bus.emit("search", "re");
|
|
66
|
+
* bus.emit("search", "redux");
|
|
67
|
+
* recorder.events.map((e) => e.phase); // ["start", "cancel", "start", "done"]
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export declare const createTraceRecorder: <E = Record<string, unknown>>() => Recorder<E>;
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/trace/redux.d.ts
|
|
73
|
+
type ReduxConnection = {
|
|
74
|
+
init: (state: unknown) => void;
|
|
75
|
+
send: (action: unknown, state: unknown) => void;
|
|
76
|
+
};
|
|
77
|
+
type ReduxExtension = {
|
|
78
|
+
connect: (options?: {
|
|
79
|
+
name?: string;
|
|
80
|
+
maxAge?: number;
|
|
81
|
+
}) => ReduxConnection;
|
|
82
|
+
};
|
|
83
|
+
type ReduxDevtoolsOptions = {
|
|
84
|
+
/** Instance name in the Redux DevTools picker. */
|
|
85
|
+
name?: string;
|
|
86
|
+
/** Defaults to `globalThis.__REDUX_DEVTOOLS_EXTENSION__`. */
|
|
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;
|
|
102
|
+
};
|
|
103
|
+
type ReduxDevtoolsState = {
|
|
104
|
+
/** Tasks that started and have not settled yet, by trace id. */
|
|
105
|
+
running: Record<number, {
|
|
106
|
+
type: string;
|
|
107
|
+
effect: string;
|
|
108
|
+
}>;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
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.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* const bus = createBus<Events>({
|
|
121
|
+
* trace: createTracer(reduxDevtools({ name: "app" })),
|
|
122
|
+
* });
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
export declare const reduxDevtools: (options?: ReduxDevtoolsOptions) => TraceLogger;
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/trace/tracer.d.ts
|
|
128
|
+
type TracerOptions = {
|
|
129
|
+
/** Drops the events it returns `false` for, before any logger sees them. */
|
|
130
|
+
filter?: (event: TraceEvent) => boolean;
|
|
131
|
+
/**
|
|
132
|
+
* Which root tasks to trace: a share between 0 and 1, or a function of the
|
|
133
|
+
* event. It is decided once per root task (a handler call) and every task
|
|
134
|
+
* below it follows, so a trace is never cut in the middle. Default: all.
|
|
135
|
+
*/
|
|
136
|
+
sample?: number | ((event: {
|
|
137
|
+
type: string;
|
|
138
|
+
payload: unknown;
|
|
139
|
+
}) => boolean);
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* Makes the tracer for `createBus({ trace })` from one logger or several.
|
|
143
|
+
* A tracer numbers the tasks it sees, so share one across buses only if the
|
|
144
|
+
* ids should be unique across them. `options.filter` and `options.sample`
|
|
145
|
+
* cut the volume, e.g. for production.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* const bus = createBus<Events>({
|
|
150
|
+
* trace: createTracer([reduxDevtools({ name: "app" }), consoleLogger()]),
|
|
151
|
+
* });
|
|
152
|
+
*
|
|
153
|
+
* // production: trace 5% of the handler calls, and only their failures
|
|
154
|
+
* createTracer(logger, {
|
|
155
|
+
* sample: 0.05,
|
|
156
|
+
* filter: (event) => event.phase === "error",
|
|
157
|
+
* });
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
export declare const createTracer: <E = Record<string, unknown>>(trace: Trace<E>, options?: TracerOptions) => Tracer;
|
|
161
|
+
//#endregion
|
|
162
|
+
export type { ConsoleLoggerOptions, PerformanceLoggerOptions, Recorder, ReduxDevtoolsOptions, ReduxDevtoolsState, ReduxExtension, TracerOptions };
|
package/dist/trace.js
ADDED
|
@@ -0,0 +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{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
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Documentation
|
|
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)
|
|
6
|
+
- [Policies](policies.md): `latest`, `leading`, `debounce`, `throttle`, `queue`, `key`
|
|
7
|
+
- [Effects and cancellation](effects.md): `retry`, `timeout`, `poll`, `take`, `all`, `allSettled`, `race`, `ctx.on`
|
|
8
|
+
- [Actions, commands, jobs and modules](actions.md)
|
|
9
|
+
- [Your own fields in the context](context.md)
|
|
10
|
+
- [Errors and unheard events](errors.md)
|
|
11
|
+
- [Tracing](tracing.md): loggers, Redux DevTools, sampling, the `notrace` build
|
|
12
|
+
- [Redux, Zustand, MobX](stores.md)
|
|
13
|
+
- [Observable: events as streams](observable.md): `createObserve` for RxJS and friends
|
|
14
|
+
- [Hot reload and dev diagnostics](hmr.md): modules replaced by name, `devPlugin`
|
|
15
|
+
- [Semantics](semantics.md)
|
package/docs/actions.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Actions, commands, jobs and modules
|
|
2
|
+
|
|
3
|
+
## Actions, commands and jobs
|
|
4
|
+
|
|
5
|
+
`nanoeffects/actions` has the pieces to describe what a bus does, apart from the code that does it.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { createAction, createCommand, createJob } from "nanoeffects/actions";
|
|
9
|
+
|
|
10
|
+
// an event with a payload, as in Redux Toolkit
|
|
11
|
+
// the payload types come from the event map of the bus: type Events = { "user/loaded": User; "user/failed": Error }
|
|
12
|
+
const userLoaded = createAction("user/loaded");
|
|
13
|
+
const loadFailed = createAction("user/failed");
|
|
14
|
+
|
|
15
|
+
// a request for a result: emit it and get what its handler returns
|
|
16
|
+
const getUser = createCommand("user/get", (id: string, { signal }) => fetchUser(id, { signal }));
|
|
17
|
+
|
|
18
|
+
// a contract without a handler: the code that sends mail is elsewhere
|
|
19
|
+
const sendMail = createCommand<Mail, void>("mail/send");
|
|
20
|
+
|
|
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
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
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)`.
|
|
29
|
+
|
|
30
|
+
**Commands.** `bus.emit(command, payload)` tells the listeners of the command's type, then runs its handler as a task, with a context, and returns the result. The handler can be wrapped in a policy, `createCommand("search", latest(fn))`. Calling `command(payload)` runs its own handler without a bus.
|
|
31
|
+
|
|
32
|
+
**Where handlers are registered.** The command may carry a handler. The bus gets the handlers bound to commands from a registry, `createCommands`, and a handler bound there wins over the command's own. The registry belongs to one bus, as an effector scope does with `fork({ handlers })`, so each bus (each test) can have its own.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { createCommands, bindHandler } from "nanoeffects/actions";
|
|
36
|
+
|
|
37
|
+
const bus = createBus<Events>();
|
|
38
|
+
const commands = createCommands(bus, [bindHandler(sendMail, smtpSend)]); // attached to the bus
|
|
39
|
+
|
|
40
|
+
const unbind = commands.handle(getUser, async () => fakeUser); // later, e.g. in a test
|
|
41
|
+
unbind(); // the command's own handler is back; the latest binding wins
|
|
42
|
+
```
|
|
43
|
+
|
|
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)`.
|
|
47
|
+
|
|
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.
|
|
52
|
+
|
|
53
|
+
## Modules
|
|
54
|
+
|
|
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.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const userModule = createModule<Events>("user", (m) => {
|
|
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
|
|
61
|
+
m.on("login", (user, ctx) => ctx.emit("loaded", user));
|
|
62
|
+
m.spawn(async (_, ctx) => { /* background work, ends with the module */ });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const bus = createBus<Events>();
|
|
66
|
+
const app = bus.use(modulePlugin([userModule]));
|
|
67
|
+
|
|
68
|
+
const unmount = app.mount(orderModule); // later, e.g. after a lazy import
|
|
69
|
+
app.commands.handle(getUser, fakeUser); // in a test
|
|
70
|
+
app.dispose(); // unmounts every module
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- **Unmounting undoes everything** the module did: its subscriptions, its bound commands, its sub-modules and its background tasks, which are aborted.
|
|
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).
|
|
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.
|
|
76
|
+
- A disposed app cannot mount modules.
|
|
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
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Your own fields in the context
|
|
2
|
+
|
|
3
|
+
Add fields to the context of every task of a bus: a store, a client, a logger. Describe them in the event map with `WithContext`, and give the values in `createBus({ context })`:
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
type Events = { save: Draft } & WithContext<{ store: Store }>;
|
|
7
|
+
|
|
8
|
+
const bus = createBus<Events>({ context: { store } });
|
|
9
|
+
bus.on("save", (draft, { store }) => store.put(draft));
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The fields are in the context of handlers, `ctx.run`, listeners added with `ctx.on`, commands, modules and the child tasks of effects and policies, and the compiler checks them: a handler of a bus without `WithContext` has no `store`, and `context` must have the fields that the map names. Two buses can have different fields.
|
|
13
|
+
|
|
14
|
+
- The fields of the library (`signal`, `emit`, `run`, ...) win when the names are the same.
|
|
15
|
+
- `context` can also be a function, `(task) => fields`, called with `{ type, payload }` of each task, for fields that depend on the task.
|