nanoeffects 0.3.0 → 0.5.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.
Files changed (60) hide show
  1. package/README.md +53 -55
  2. package/dist/{action-Bdfp-Wvd.d.ts → action-creator-DOWcbXpy.d.ts} +79 -73
  3. package/dist/actions.d.ts +75 -6
  4. package/dist/actions.js +1 -1
  5. package/dist/bus-BAs9x7IQ.js +1 -0
  6. package/dist/{context-DlA80GOA.js → context-CK0ZomWt.js} +1 -1
  7. package/dist/context-plugin-B0sLWOxR.d.ts +34 -0
  8. package/dist/dev.d.ts +2 -2
  9. package/dist/effects.d.ts +153 -56
  10. package/dist/effects.js +1 -1
  11. package/dist/index.d.ts +3 -2
  12. package/dist/index.js +1 -1
  13. package/dist/{module-I1ixyJzx.d.ts → module-DCJl-qfi.d.ts} +24 -9
  14. package/dist/notrace/{action-Bdfp-Wvd.d.ts → action-creator-DOWcbXpy.d.ts} +79 -73
  15. package/dist/notrace/actions.d.ts +97 -13
  16. package/dist/notrace/actions.js +1 -1
  17. package/dist/notrace/bus-Bdk0YyFQ.js +1 -0
  18. package/dist/notrace/{context-yAWj0BFZ.js → context-CctVmHLC.js} +1 -1
  19. package/dist/notrace/context-plugin-B0sLWOxR.d.ts +34 -0
  20. package/dist/notrace/{delay-Cgvn16oY.js → delay-B_W1tnkS.js} +1 -1
  21. package/dist/notrace/effects.d.ts +153 -56
  22. package/dist/notrace/effects.js +1 -1
  23. package/dist/notrace/index.d.ts +3 -2
  24. package/dist/notrace/index.js +1 -1
  25. package/dist/notrace/policies.d.ts +1 -1
  26. package/dist/notrace/policies.js +1 -1
  27. package/dist/notrace/redux.d.ts +18 -20
  28. package/dist/notrace/redux.js +1 -1
  29. package/dist/observable.d.ts +1 -1
  30. package/dist/policies.d.ts +1 -1
  31. package/dist/policies.js +1 -1
  32. package/dist/react.d.ts +50 -0
  33. package/dist/react.js +1 -0
  34. package/dist/redux.d.ts +18 -20
  35. package/dist/redux.js +1 -1
  36. package/dist/trace.d.ts +1 -1
  37. package/docs/README.md +30 -9
  38. package/docs/actions.md +117 -41
  39. package/docs/context.md +48 -7
  40. package/docs/effects.md +229 -73
  41. package/docs/errors.md +23 -10
  42. package/docs/guide.md +238 -129
  43. package/docs/hmr.md +25 -13
  44. package/docs/migrating-from-effector.md +293 -0
  45. package/docs/migrating-from-rxjs.md +127 -0
  46. package/docs/migrating-from-saga.md +93 -0
  47. package/docs/observable.md +11 -9
  48. package/docs/package.md +24 -0
  49. package/docs/policies.md +93 -30
  50. package/docs/react.md +157 -0
  51. package/docs/semantics.md +5 -3
  52. package/docs/stores.md +488 -25
  53. package/docs/tracing.md +69 -54
  54. package/docs/typescript.md +153 -0
  55. package/llms-full.txt +2645 -0
  56. package/llms.txt +48 -0
  57. package/package.json +39 -9
  58. package/dist/bus-CRnlwmQ4.js +0 -1
  59. package/dist/notrace/bus-Dx0l9X6h.js +0 -1
  60. package/dist/notrace/event-0RIBGYqS.js +0 -1
@@ -1,4 +1,4 @@
1
- import { k as EventMap, o as Bus, s as BusOptions } from "./action-Bdfp-Wvd.js";
1
+ import { _ as Bus, o as EventMap } from "./action-creator-DOWcbXpy.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 = {
@@ -8,6 +8,8 @@ type ReduxAction = {
8
8
  type NanoeffectsMiddlewareOptions = {
9
9
  /** Return `false` for the actions that the bus should not get. Default: all of them. */
10
10
  filter?: (action: ReduxAction) => boolean;
11
+ /** Receives an error that `emit` throws, so `dispatch` does not. Default: the error is thrown. */
12
+ onError?: (error: unknown, action: ReduxAction) => void;
11
13
  };
12
14
  //#endregion
13
15
  //#region src/store/redux/effects.d.ts
@@ -19,48 +21,44 @@ type ReduxStoreApi<State = unknown> = {
19
21
  }) => unknown;
20
22
  getState: () => State;
21
23
  };
22
- type EffectsMiddlewareOptions<E extends EventMap> = Omit<BusOptions<E>, "context"> & NanoeffectsMiddlewareOptions & {
23
- /** More fields for the context of every task, next to `store`. */
24
- context?: object;
25
- };
24
+ type EffectsMiddlewareOptions = Pick<NanoeffectsMiddlewareOptions, "filter">;
26
25
  /**
27
- * Creates a bus and the Redux middleware that feeds it: the effects of an app
28
- * in one place, without sagas. Every task gets `ctx.store`, the `dispatch` and
29
- * `getState` of the store, as soon as the middleware is added to it. Type it
30
- * in the event map:
26
+ * Makes the Redux middleware that feeds a bus you already have: the actions of
27
+ * the store reach its handlers, and every task gets `ctx.store`, the `dispatch`
28
+ * and `getState` of the store, as soon as Redux has installed the middleware.
29
+ * Type it in the event map of the bus:
31
30
  *
32
31
  * ```ts
33
32
  * type Events = { userRequested: string } & WithContext<{ store: ReduxStoreApi<RootState> }>;
34
33
  * ```
35
34
  *
36
- * Two things are looked after for you:
35
+ * Use one bus for one store: a second store would replace the first in the
36
+ * contexts. Two things are looked after for you:
37
37
  *
38
38
  * - **A handler does not loop on its own action.** An action that a handler
39
39
  * dispatches through `ctx.store` and that has the type the handler works on
40
40
  * does not go back to the bus (a handler of `a` that dispatches `a`). Other
41
41
  * handlers of that type do not get it either. Longer cycles, `a` to `b` to
42
42
  * `a`, are yours to avoid.
43
- * - **A failing handler does not break `dispatch`.** Errors go to `onError`,
44
- * by default to `console.error`, as in Redux Toolkit listeners; reducers
45
- * still throw to the caller.
43
+ * - **A failing handler does not break `dispatch`.** With `onError` on the bus
44
+ * the bus isolates the handlers. Without it the error is written to
45
+ * `console.error`, as in Redux Toolkit listeners. Reducers still throw to
46
+ * the caller.
46
47
  *
47
48
  * @example
48
49
  * ```ts
49
- * const effects = createEffectsMiddleware<Events>();
50
+ * const bus = createBus<Events>();
50
51
  *
51
52
  * const store = configureStore({
52
53
  * reducer,
53
- * middleware: (getDefault) => getDefault().concat(effects.middleware),
54
+ * middleware: (getDefault) => getDefault().concat(createEffectsMiddleware(bus)),
54
55
  * });
55
56
  *
56
- * effects.bus.on(userRequested, latest(async (id, { store, signal }) => {
57
+ * bus.on(userRequested, latest(async (id, { store, signal }) => {
57
58
  * store.dispatch(userLoaded(await fetchUser(id, { signal })));
58
59
  * }));
59
60
  * ```
60
61
  */
61
- export declare const createEffectsMiddleware: <E extends EventMap = EventMap>(options?: EffectsMiddlewareOptions<E>) => {
62
- bus: Bus<E>;
63
- middleware: (storeApi: ReduxStoreApi) => (next: (action: unknown) => unknown) => (action: unknown) => unknown;
64
- };
62
+ export declare const createEffectsMiddleware: <E extends EventMap>(bus: Bus<E>, options?: EffectsMiddlewareOptions) => (storeApi: ReduxStoreApi) => (next: (action: unknown) => unknown) => (action: unknown) => unknown;
65
63
  //#endregion
66
64
  export type { EffectsMiddlewareOptions, ReduxStoreApi };
@@ -1 +1 @@
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};
1
+ const e=e=>typeof e==`object`&&!!e&&`type`in e,t=(t,n={})=>{let r=t.emit,i=e=>{try{r(e)}catch(t){if(!n.onError)throw t;n.onError(t,e)}};return()=>t=>r=>{let a=t(r);return e(r)&&(n.filter?.(r)??!0)&&i(r),a}},n=(e,n={})=>{let r,i=/* @__PURE__ */ new WeakMap,a=e.addContext;e.addContext=e=>{a?.(e);let t=r;if(!t)return;let{type:n}=e;Object.assign(e,{store:{getState:t.getState,dispatch:e=>{typeof e==`object`&&e&&i.set(e,n);let r=e;return t.dispatch(r)}}})};let o=t(e,{filter:e=>i.get(e)!==e.type&&(n.filter?.(e)??!0),onError:(e,{type:t})=>{globalThis.console?.error(`[nanoeffects] handler of "${t}" failed`,e)}});return e=>(r=e,o())};export{n as createEffectsMiddleware};
@@ -1,4 +1,4 @@
1
- import { A as Keys, k as EventMap, o as Bus } from "./action-Bdfp-Wvd.js";
1
+ import { _ as Bus, o as EventMap, s as Keys } from "./action-creator-DOWcbXpy.js";
2
2
  //#region src/observable.d.ts
3
3
  declare global {
4
4
  interface SymbolConstructor {
@@ -1,4 +1,4 @@
1
- import { N as Task, k as EventMap } from "./action-Bdfp-Wvd.js";
1
+ import { o as EventMap, u as Task } from "./action-creator-DOWcbXpy.js";
2
2
  //#region src/policies/keyed.d.ts
3
3
  /** Options shared by every policy that can keep its state per key. */
4
4
  type KeyOption<P> = {
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-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};
1
+ import{t as e}from"./onAbort-BgMfspK4.js";import{a as t,i as n,o as r,t as i}from"./context-CK0ZomWt.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,50 @@
1
+ import { D as EventKey, _ as Bus, o as EventMap, r as FreeActionCreator, t as ActionCreator } from "./action-creator-DOWcbXpy.js";
2
+ //#region src/react/plugin.d.ts
3
+ /** What `reactPlugin` adds to the bus. */
4
+ /** Tells whether two values are the same: `Object.is` unless the caller gives another. */
5
+ type Equals<V> = (previous: V, next: V) => boolean;
6
+ type ReactApi<E extends EventMap> = {
7
+ /**
8
+ * A React hook: after a render commits with a `value` that differs from the
9
+ * previous one (`Object.is`, or the `equals` you pass), emits `event` with it. It does not emit on mount,
10
+ * not twice under StrictMode, and not for an equal value. Changes before one
11
+ * commit emit once, with the last value. The method does not read `this`, so
12
+ * `export const { useEmitter } = bus` gives a free hook.
13
+ */
14
+ useEmitter: {
15
+ <K extends EventKey<E>>(event: K, value: E[K], equals?: Equals<E[K]>): void;
16
+ /** A creator without a payload type: the payload comes from the event map. */
17
+ <K extends EventKey<E>>(creator: FreeActionCreator<K>, value: E[K], equals?: Equals<E[K]>): void;
18
+ <P, T extends string>(creator: ActionCreator<P, T>, value: P, equals?: Equals<P>): void;
19
+ };
20
+ };
21
+ /**
22
+ * Adds `bus.useEmitter(event, value)`: a hook that tells the bus when a value of
23
+ * a component changes, without the traps of `useEffect(() => emit(value), [value])`
24
+ * (an emit on mount, two on mount under StrictMode, a new `emit` re-running it).
25
+ * Pass `equals` as the third argument for values that `Object.is` tells apart
26
+ * although they are the same, such as a new object with the same fields.
27
+ * Needs `react` 18 or newer. Returns the same bus.
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * import { createBus } from "nanoeffects";
32
+ * import { createAction } from "nanoeffects/actions";
33
+ * import { reactPlugin } from "nanoeffects/react";
34
+ *
35
+ * export const bus = createBus().use(reactPlugin());
36
+ * export const { useEmitter } = bus; // a free hook
37
+ *
38
+ * const nameChanged = createAction<string>("name/changed");
39
+ * bus.on(nameChanged, (name) => console.log(name));
40
+ *
41
+ * const Profile = () => {
42
+ * const [name, setName] = useState("Adam");
43
+ * useEmitter(nameChanged, name); // nameChanged("…") on every change of name
44
+ * return <input value={name} onChange={(event) => setName(event.target.value)} />;
45
+ * };
46
+ * ```
47
+ */
48
+ export declare const reactPlugin: () => <E extends EventMap>(bus: Bus<E>) => Bus<E> & ReactApi<E>;
49
+ //#endregion
50
+ export type { ReactApi };
package/dist/react.js ADDED
@@ -0,0 +1 @@
1
+ import{useEffect as e,useRef as t}from"react";const n=()=>n=>{function r(r,i,a){let o=t(i),s=t({event:r,equals:a});e(()=>{s.current={event:r,equals:a}}),e(()=>{let{event:e,equals:t}=s.current,r=t??Object.is,a=o.current;if(r(a,i))return;o.current=i;let c=typeof e==`function`?e(i):{type:e,payload:i};n.emit(c)},[i])}return Object.assign(n,{useEmitter:r})};export{n as reactPlugin};
package/dist/redux.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { k as EventMap, o as Bus, s as BusOptions } from "./action-Bdfp-Wvd.js";
1
+ import { _ as Bus, o as EventMap } from "./action-creator-DOWcbXpy.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 = {
@@ -8,6 +8,8 @@ type ReduxAction = {
8
8
  type NanoeffectsMiddlewareOptions = {
9
9
  /** Return `false` for the actions that the bus should not get. Default: all of them. */
10
10
  filter?: (action: ReduxAction) => boolean;
11
+ /** Receives an error that `emit` throws, so `dispatch` does not. Default: the error is thrown. */
12
+ onError?: (error: unknown, action: ReduxAction) => void;
11
13
  };
12
14
  //#endregion
13
15
  //#region src/store/redux/effects.d.ts
@@ -19,48 +21,44 @@ type ReduxStoreApi<State = unknown> = {
19
21
  }) => unknown;
20
22
  getState: () => State;
21
23
  };
22
- type EffectsMiddlewareOptions<E extends EventMap> = Omit<BusOptions<E>, "context"> & NanoeffectsMiddlewareOptions & {
23
- /** More fields for the context of every task, next to `store`. */
24
- context?: object;
25
- };
24
+ type EffectsMiddlewareOptions = Pick<NanoeffectsMiddlewareOptions, "filter">;
26
25
  /**
27
- * Creates a bus and the Redux middleware that feeds it: the effects of an app
28
- * in one place, without sagas. Every task gets `ctx.store`, the `dispatch` and
29
- * `getState` of the store, as soon as the middleware is added to it. Type it
30
- * in the event map:
26
+ * Makes the Redux middleware that feeds a bus you already have: the actions of
27
+ * the store reach its handlers, and every task gets `ctx.store`, the `dispatch`
28
+ * and `getState` of the store, as soon as Redux has installed the middleware.
29
+ * Type it in the event map of the bus:
31
30
  *
32
31
  * ```ts
33
32
  * type Events = { userRequested: string } & WithContext<{ store: ReduxStoreApi<RootState> }>;
34
33
  * ```
35
34
  *
36
- * Two things are looked after for you:
35
+ * Use one bus for one store: a second store would replace the first in the
36
+ * contexts. Two things are looked after for you:
37
37
  *
38
38
  * - **A handler does not loop on its own action.** An action that a handler
39
39
  * dispatches through `ctx.store` and that has the type the handler works on
40
40
  * does not go back to the bus (a handler of `a` that dispatches `a`). Other
41
41
  * handlers of that type do not get it either. Longer cycles, `a` to `b` to
42
42
  * `a`, are yours to avoid.
43
- * - **A failing handler does not break `dispatch`.** Errors go to `onError`,
44
- * by default to `console.error`, as in Redux Toolkit listeners; reducers
45
- * still throw to the caller.
43
+ * - **A failing handler does not break `dispatch`.** With `onError` on the bus
44
+ * the bus isolates the handlers. Without it the error is written to
45
+ * `console.error`, as in Redux Toolkit listeners. Reducers still throw to
46
+ * the caller.
46
47
  *
47
48
  * @example
48
49
  * ```ts
49
- * const effects = createEffectsMiddleware<Events>();
50
+ * const bus = createBus<Events>();
50
51
  *
51
52
  * const store = configureStore({
52
53
  * reducer,
53
- * middleware: (getDefault) => getDefault().concat(effects.middleware),
54
+ * middleware: (getDefault) => getDefault().concat(createEffectsMiddleware(bus)),
54
55
  * });
55
56
  *
56
- * effects.bus.on(userRequested, latest(async (id, { store, signal }) => {
57
+ * bus.on(userRequested, latest(async (id, { store, signal }) => {
57
58
  * store.dispatch(userLoaded(await fetchUser(id, { signal })));
58
59
  * }));
59
60
  * ```
60
61
  */
61
- export declare const createEffectsMiddleware: <E extends EventMap = EventMap>(options?: EffectsMiddlewareOptions<E>) => {
62
- bus: Bus<E>;
63
- middleware: (storeApi: ReduxStoreApi) => (next: (action: unknown) => unknown) => (action: unknown) => unknown;
64
- };
62
+ export declare const createEffectsMiddleware: <E extends EventMap>(bus: Bus<E>, options?: EffectsMiddlewareOptions) => (storeApi: ReduxStoreApi) => (next: (action: unknown) => unknown) => (action: unknown) => unknown;
65
63
  //#endregion
66
64
  export type { EffectsMiddlewareOptions, ReduxStoreApi };
package/dist/redux.js CHANGED
@@ -1 +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};
1
+ const e=e=>typeof e==`object`&&!!e&&`type`in e,t=(t,n={})=>{let r=t.emit,i=e=>{try{r(e)}catch(t){if(!n.onError)throw t;n.onError(t,e)}};return()=>t=>r=>{let a=t(r);return e(r)&&(n.filter?.(r)??!0)&&i(r),a}},n=(e,n={})=>{let r,i=/* @__PURE__ */ new WeakMap,a=e.addContext;e.addContext=e=>{a?.(e);let t=r;if(!t)return;let{type:n}=e;Object.assign(e,{store:{getState:t.getState,dispatch:e=>{typeof e==`object`&&e&&i.set(e,n);let r=e;return t.dispatch(r)}}})};let o=t(e,{filter:e=>i.get(e)!==e.type&&(n.filter?.(e)??!0),onError:(e,{type:t})=>{globalThis.console?.error(`[nanoeffects] handler of "${t}" failed`,e)}});return e=>(r=e,o())};export{n as createEffectsMiddleware};
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-Bdfp-Wvd.js";
1
+ import { d as Trace, f as TraceEvent, p as TraceLogger, z as Tracer } from "./action-creator-DOWcbXpy.js";
2
2
  //#region src/trace/console.d.ts
3
3
  type ConsoleLoggerOptions = {
4
4
  /** Defaults to `globalThis.console`. */
package/docs/README.md CHANGED
@@ -1,15 +1,36 @@
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.
3
+ ## Start here
4
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`
5
+ The [Guide](guide.md) is the ladder: small bus → policies for async UI → effects when you need them → commands and modules for domain logic off the UI.
6
+
7
+ Code for every guide step: [`examples/guide.ts`](../examples/guide.ts) (type-checked and tested).
8
+
9
+ ## Jump by project need
10
+
11
+ | Project shape | First page |
12
+ | ------------- | ---------- |
13
+ | Small app / widget (rare races) | [Guide § Create a bus](guide.md#1-create-a-bus) |
14
+ | SPA with async UI | [Policies](policies.md), [Guide § latest](guide.md#3-abort-the-previous-run-with-latest) |
15
+ | Unreliable network / long flows | [Effects](effects.md) — one helper per job, then the rest as reference |
16
+ | Domain logic off the UI | [Actions, commands, modules](actions.md) |
17
+ | Already on Redux / Zustand / MobX / Pinia | [Stores](stores.md) |
18
+ | A React form with async checks | [Build a sign-up form in React](react.md) |
19
+ | Moving from redux-saga | [Coming from redux-saga](migrating-from-saga.md) |
20
+ | Using RxJS | [Use nanoeffects next to RxJS](migrating-from-rxjs.md) |
21
+ | Moving from Effector | [Coming from Effector](migrating-from-effector.md) |
22
+
23
+ ## Reference
24
+
25
+ - [Policies](policies.md) — `latest`, `leading`, `debounce`, `throttle`, `queue`, `key`
26
+ - [Effects and cancellation](effects.md) — `retry`, `timeout`, `poll`, `take`, `all`, …
8
27
  - [Actions, commands, jobs and modules](actions.md)
9
- - [Your own fields in the context](context.md)
28
+ - [Sign-up form in React](react.md) — one form: races, double submit, retry
29
+ - [Context fields](context.md) — store or client on every task
30
+ - [TypeScript in many files](typescript.md) — keep the types of the bus in commands and modules
10
31
  - [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`
32
+ - [Package entries](package.md) — which import holds what
33
+ - [Tracing](tracing.md) — loggers, Redux DevTools, `notrace`
34
+ - [Observable](observable.md) — events as TC39 Observables
35
+ - [Hot reload and dev diagnostics](hmr.md)
15
36
  - [Semantics](semantics.md)
package/docs/actions.md CHANGED
@@ -1,68 +1,141 @@
1
1
  # Actions, commands, jobs and modules
2
2
 
3
- ## Actions, commands and jobs
3
+ Use this page when **domain logic lives on the bus**, off the UI: the UI emits and listens; handlers and modules own the work.
4
4
 
5
- `nanoeffects/actions` has the pieces to describe what a bus does, apart from the code that does it.
5
+ If you only need `on` / `emit` or policies, stay in the [Guide](guide.md) until you need a result from `emit` or a mountable domain.
6
6
 
7
- ```ts
7
+ Recipe without a UI framework: [`examples/modules.ts`](../examples/modules.ts).
8
+
9
+ ## Return a value from `emit` with a command
10
+
11
+ ```js
12
+ import { createBus } from "nanoeffects";
13
+ import { createCommand } from "nanoeffects/actions";
14
+
15
+ const bus = createBus();
16
+
17
+ // createCommand — emit runs the handler and returns its result
18
+ const double = createCommand("math/double", (value) => value * 2);
19
+ bus.emit(double(21)); // 42
20
+ ```
21
+
22
+ ## Describe events, commands, and jobs
23
+
24
+ `nanoeffects/actions` has the pieces to name what a bus does, apart from the code that does it.
25
+
26
+ ```js
8
27
  import { createAction, createCommand, createJob } from "nanoeffects/actions";
9
28
 
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 }
29
+ // createAction — event with a payload (like RTK createAction)
12
30
  const userLoaded = createAction("user/loaded");
13
31
  const loadFailed = createAction("user/failed");
14
32
 
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 }));
33
+ // createCommand with a handler — emit returns what the handler returns
34
+ const getUser = createCommand("user/get", (id, { signal }) => {
35
+ return fetchUser(id, { signal });
36
+ });
17
37
 
18
- // a contract without a handler: the code that sends mail is elsewhere
19
- const sendMail = createCommand<Mail, void>("mail/send");
38
+ // createCommand without a handler — a contract; register the work elsewhere
39
+ const sendMail = createCommand("mail/send");
20
40
 
21
- // a command whose outcome is sent on as actions: they are its properties, loadUser.success and loadUser.fail
41
+ // createJob — command that also emits success / fail actions
22
42
  const loadUser = 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
43
+ success: userLoaded,
44
+ fail: "user/failed",
25
45
  });
26
46
  ```
27
47
 
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)`.
48
+ ### Actions
49
+
50
+ `createAction(type)` returns a creator. Without generics it keeps the literal type; the bus checks the payload against its event map. Without an event map, give the payload type: `createAction<User>("user/loaded")`.
51
+
52
+ - `userLoaded(user)` → `{ type, payload }`
53
+ - `userLoaded.type` — the type string
54
+ - `userLoaded.match(action)` — narrows an action
55
+ - `createAction(type, prepare)` — build the payload from several arguments
56
+ - Works as the event of its type: `bus.emit(userLoaded(user))`, `bus.on(userLoaded, fn)`
29
57
 
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. `command.match(action)` tells whether an action has the command's type and narrows it, as `match` of an action creator does.
58
+ ### Commands
31
59
 
32
- **Where handlers are registered.** The command may carry a handler. The bus gets the handlers bound to commands from a registry, `commandsPlugin`, 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.
60
+ Calling a command builds `{ type, payload }` like `createAction`. `bus.emit(command(payload))` (or `bus.emit(command, payload)`) tells the listeners of the command’s type, then runs its handler as a task and returns the result.
33
61
 
34
- ```ts
35
- import { commandsPlugin, bindHandler } from "nanoeffects/actions";
62
+ - Wrap the handler in a policy: `createCommand("search", latest(fn))`.
63
+ - `command.match(action)` narrows like an action creator’s `match`.
36
64
 
37
- const bus = createBus<Events>();
38
- const commands = bus.use(commandsPlugin([bindHandler(sendMail, smtpSend)])); // attached to the bus
65
+ **Where handlers are registered.** The command may carry a handler. The bus can also take handlers from a registry (`commandsPlugin`). A handler bound there wins over the command’s own. Each bus (each test) has its own registry.
39
66
 
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
67
+ ```js
68
+ import { createBus } from "nanoeffects";
69
+ import {
70
+ bindHandler,
71
+ commandsPlugin,
72
+ createCommand,
73
+ } from "nanoeffects/actions";
74
+
75
+ const sendMail = createCommand("mail/send");
76
+ const getUser = createCommand("user/get");
77
+
78
+ const bus = createBus();
79
+
80
+ // commandsPlugin — registry of handlers for this bus
81
+ const commands = bus.use(
82
+ commandsPlugin([bindHandler(sendMail, smtpSend)]),
83
+ );
84
+
85
+ // later, e.g. in a test — latest binding wins
86
+ const unbind = commands.handle(getUser, async () => fakeUser);
87
+ unbind();
42
88
  ```
43
89
 
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. `bus.use(commandsPlugin())` sets the registry as `bus.commands`, and a second one on the same bus replaces it. There is no `commands` option in `createBus`; a custom runner is assigned, `bus.commands = { run }`.
90
+ A command with no handler anywhere throws `No handler for command "..."` from `emit` (the call itself only builds the action).
91
+
92
+ The registry lives in `nanoeffects/actions`, so a bus without one carries none of that code. Without a registry, `emit` runs the command’s own handler. `bus.use(commandsPlugin())` sets `bus.commands`. A second plugin on the same bus replaces it. There is no `commands` option in `createBus`; assign a custom runner with `bus.commands = { run }`.
93
+
94
+ ### Jobs
95
+
96
+ `createJob(type, handler, { success, fail })` makes a command that also sends its outcome as actions, when nobody waits for the result. It returns the command with `loadUser.success` and `loadUser.fail` on it.
97
+
98
+ - On success, emits `loadUser.success(result)`.
99
+ - On failure, emits `loadUser.fail(error)`.
100
+ - `success` / `fail` are a string (job makes an action creator) or an existing action creator. Missing ones become `<type>/success` or `<type>/fail`.
101
+ - `bus.emit(loadUser(id))` returns a promise of the result. Handler errors are always handled: they go to `fail` and the promise resolves with `undefined`. For a result that must reject, use a command.
102
+ - Outcome actions are emitted as the task that ran the job (also for a handler bound with `commands.handle`). Nothing is sent after abort.
103
+ - A missing handler is not an outcome: `emit` throws `No handler for command "..."`.
104
+ - Calling the job only builds `{ type, payload }`; the handler runs on `emit`.
105
+
106
+ ## Mount domains with modules
107
+
108
+ **Problem.** A domain needs several subscriptions and command bindings. With plain `bus.on` / `commands.handle` you must remember every unsubscribe.
45
109
 
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 the command itself, with the two actions on it: `loadUser.success` and `loadUser.fail`. When the handler succeeds, `loadUser.success(result)` is emitted on the bus, and when it fails, `loadUser.fail(error)`.
110
+ **When.** You want one mount / one unmount for that domain. Skip modules while a few top-level `bus.on` calls are enough.
47
111
 
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 `loadUser.fail` 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.
112
+ `bus.use(modulePlugin(modules))` assembles an app from modules and returns `mount`, `unmount`, `isMounted`, `dispose`, `commands`, and `createModule`: the same as `createModule` below, with the context fields of the bus typed (see [TypeScript](typescript.md)).
52
113
 
53
- ## Modules
114
+ ```js
115
+ import { createBus } from "nanoeffects";
116
+ import {
117
+ createCommand,
118
+ createModule,
119
+ modulePlugin,
120
+ } from "nanoeffects/actions";
54
121
 
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.
122
+ const getUser = createCommand("user/get");
123
+ const saveUser = createCommand("user/save", persistUser);
56
124
 
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 */ });
125
+ // createModule — one domain; unmount undoes everything below
126
+ const userModule = createModule("user", (m) => {
127
+ m.handle(getUser, fetchUser);
128
+ m.use(saveUser, authModule);
129
+ m.on("login", (user, ctx) => {
130
+ ctx.emit("loaded", user);
131
+ });
132
+ m.spawn(async (_, ctx) => {
133
+ /* background work; ends when the module unmounts */
134
+ });
63
135
  });
64
136
 
65
- const bus = createBus<Events>();
137
+ const bus = createBus();
138
+ // modulePlugin — mount modules on the bus
66
139
  const app = bus.use(modulePlugin([userModule]));
67
140
 
68
141
  const unmount = app.mount(orderModule); // later, e.g. after a lazy import
@@ -70,9 +143,12 @@ app.commands.handle(getUser, fakeUser); // in a test
70
143
  app.dispose(); // unmounts every module
71
144
  ```
72
145
 
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 `bus.use(commandsPlugin())` 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.
146
+ - **Unmounting undoes everything** the module did: subscriptions, bound commands, sub-modules, and background tasks (aborted).
147
+ - **A module is known by its name.** Mounting a module whose name is taken replaces the mounted one (see [Hot reload](hmr.md)).
148
+ - **Registry.** If `bus.use(commandsPlugin())` ran first, that registry is used; otherwise the module plugin attaches its own. Either way it is `bus.commands` and `app.commands`.
76
149
  - 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.
150
+ - Module context = task context plus `handle`, `use`, and `spawn`:
151
+ - `handle` — give a command a handler (contract, or override).
152
+ - `use` — bring sub-modules and commands that carry a handler; the returned function undoes that call.
153
+ - `spawn` — background task, aborted on unmount.
154
+ - A command that carries a handler keeps answering after unmount (`use` only tracks registration). Use `handle` for a handler that must live only as long as the module.
package/docs/context.md CHANGED
@@ -1,15 +1,56 @@
1
- # Your own fields in the context
1
+ # Put your own fields on every task
2
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 })`:
3
+ Optional: put a store, client, or logger on every task. Skip until many handlers need the same dependency.
4
+
5
+ ## Pass values at runtime
6
+
7
+ ```js
8
+ import { createBus } from "nanoeffects";
9
+
10
+ // createBus({ context }) — every task gets these fields
11
+ const bus = createBus({ context: { store } });
12
+
13
+ bus.on("save", (draft, { store }) => {
14
+ store.put(draft);
15
+ });
16
+ ```
17
+
18
+ That is enough in JavaScript. The fields appear on handlers, `ctx.run`, `ctx.on`, commands, modules, and child tasks of effects and policies.
19
+
20
+ Library fields (`signal`, `emit`, `run`, …) win on name clashes. `context` may be a function `(task) => fields` when the value depends on the task.
21
+
22
+ To get `race`, `all`, `take` and the like on every task without passing `ctx`, see [`effectsPlugin`](effects.md#put-selected-effects-on-ctx).
23
+
24
+ ## Type the fields (optional)
25
+
26
+ In TypeScript, `contextPlugin` puts the fields on every task of **this** bus and takes their types from the value you pass. The event map holds only events. It does not wire anything across the app — another bus can have different fields or none.
4
27
 
5
28
  ```ts
6
- type Events = { save: Draft } & WithContext<{ store: Store }>;
29
+ import { contextPlugin, createBus } from "nanoeffects";
30
+
31
+ // Events — event name → payload
32
+ type Events = { save: Draft };
33
+
34
+ // contextPlugin — every task gets ctx.store; the type is the type of `store`
35
+ const bus = createBus<Events>().use(contextPlugin({ store }));
7
36
 
8
- const bus = createBus<Events>({ context: { store } });
9
37
  bus.on("save", (draft, { store }) => store.put(draft));
10
38
  ```
11
39
 
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.
40
+ - A function `(task) => fields` makes the fields per task. It gets `{ type, payload }`.
41
+ - Several `contextPlugin` calls, `effectsPlugin` and the `context` option of `createBus` all add their fields to the same context.
42
+ - A name that the bus already has is a type error. The fields of the library (`signal`, `emit`, `run`, …) win at runtime, and a field that the context already has is not replaced.
43
+ - The plugin returns the same bus, so keep the result of the chain: `const bus = createBus().use(contextPlugin({ store }))`.
13
44
 
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.
45
+ ### Name the fields in the event map instead
46
+
47
+ `createBus<Events>({ context: { store } })` also types the fields, but through the event map: `WithContext` is checker-only and says which fields the handlers of this bus have. Use it when you cannot call `use`, for example for the bus that `createEffectsMiddleware` makes.
48
+
49
+ ```ts
50
+ import { createBus, type WithContext } from "nanoeffects";
51
+
52
+ // WithContext — checker-only: handlers also have ctx.store
53
+ type Events = { save: Draft } & WithContext<{ store: Store }>;
54
+
55
+ const bus = createBus<Events>({ context: { store } });
56
+ ```