nanoeffects 0.4.0 → 0.6.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 +4 -1
  2. package/dist/{action-creator-D48fp13T.d.ts → action-creator-c292nK7v.d.ts} +130 -34
  3. package/dist/actions.d.ts +54 -5
  4. package/dist/actions.js +1 -1
  5. package/dist/{bus-Cho463Sq.js → bus-BAs9x7IQ.js} +1 -1
  6. package/dist/{context-DlA80GOA.js → context-CK0ZomWt.js} +1 -1
  7. package/dist/context-plugin-1pYmSQEG.d.ts +34 -0
  8. package/dist/dev.d.ts +2 -2
  9. package/dist/effects.d.ts +27 -129
  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-DVX8aYlq.d.ts → module-BlF2KSgy.d.ts} +34 -5
  14. package/dist/notrace/{action-creator-D48fp13T.d.ts → action-creator-DKG_sIia.d.ts} +130 -34
  15. package/dist/notrace/actions.d.ts +96 -7
  16. package/dist/notrace/actions.js +1 -1
  17. package/dist/notrace/{bus-B12SvxLv.js → bus-Bdk0YyFQ.js} +1 -1
  18. package/dist/notrace/{context-yAWj0BFZ.js → context-CctVmHLC.js} +1 -1
  19. package/dist/notrace/context-plugin-Dt1IgVo8.d.ts +34 -0
  20. package/dist/notrace/{delay-Cgvn16oY.js → delay-B_W1tnkS.js} +1 -1
  21. package/dist/notrace/effects.d.ts +27 -129
  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/plugin-B0AXKusc.d.ts +207 -0
  26. package/dist/notrace/policies.d.ts +1 -1
  27. package/dist/notrace/policies.js +1 -1
  28. package/dist/notrace/redux.d.ts +18 -20
  29. package/dist/notrace/redux.js +1 -1
  30. package/dist/observable.d.ts +1 -1
  31. package/dist/plugin-CCYlC0XS.d.ts +207 -0
  32. package/dist/policies.d.ts +1 -1
  33. package/dist/policies.js +1 -1
  34. package/dist/react.d.ts +50 -0
  35. package/dist/react.js +1 -0
  36. package/dist/redux.d.ts +18 -20
  37. package/dist/redux.js +1 -1
  38. package/dist/scope-key-B7l7mUS5.js +1 -0
  39. package/dist/scope.d.ts +58 -0
  40. package/dist/scope.js +1 -0
  41. package/dist/trace.d.ts +1 -1
  42. package/dist/types-mLaAx-EZ.d.ts +15 -0
  43. package/docs/README.md +7 -1
  44. package/docs/actions.md +25 -1
  45. package/docs/context.md +26 -2
  46. package/docs/effects.md +83 -5
  47. package/docs/guide.md +24 -0
  48. package/docs/migrating-from-effector.md +293 -0
  49. package/docs/migrating-from-rxjs.md +127 -0
  50. package/docs/migrating-from-saga.md +93 -0
  51. package/docs/observable.md +1 -0
  52. package/docs/package.md +13 -2
  53. package/docs/policies.md +1 -0
  54. package/docs/react.md +157 -0
  55. package/docs/scope.md +226 -0
  56. package/docs/stores.md +429 -40
  57. package/docs/typescript.md +153 -0
  58. package/llms-full.txt +2925 -0
  59. package/llms.txt +49 -0
  60. package/package.json +44 -6
@@ -0,0 +1,207 @@
1
+ import { B as EventMap, N as WithContext, V as Keys, b as Context, d as Effects, h as Bus, l as BoundForm } from "./action-creator-c292nK7v.js";
2
+ import { n as NoClash } from "./context-plugin-1pYmSQEG.js";
3
+ //#region src/utils/delay.d.ts
4
+ /**
5
+ * Resolves after `ms`, or rejects early if `ctx.signal` aborts. `ms` can be a
6
+ * function that returns the number of ms, called once when the delay starts:
7
+ * handy for a random pause (jitter).
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * await delay({}, 500);
12
+ *
13
+ * // 200 to 400 ms
14
+ * await delay({}, () => 200 + Math.random() * 200);
15
+ *
16
+ * // inside a task: stops waiting when the task is aborted
17
+ * bus.on("retry", async (_, ctx) => {
18
+ * await delay(ctx, 1000);
19
+ * await load();
20
+ * });
21
+ * ```
22
+ */
23
+ declare const delay: (ctx: {
24
+ signal?: AbortSignal | undefined;
25
+ }, ms: number | (() => number)) => Promise<void>;
26
+ //#endregion
27
+ //#region src/effects/run.d.ts
28
+ /** Context passed into `race` / `all` effect starters. */
29
+ type EffectContext = {
30
+ signal: AbortSignal;
31
+ abort: (reason?: unknown) => void;
32
+ };
33
+ type Starter<T> = (ctx: EffectContext) => T | PromiseLike<T>;
34
+ //#endregion
35
+ //#region src/effects/all.d.ts
36
+ /**
37
+ * Runs all starters; waits for every result. On failure or parent abort, cancels the rest.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * bus.on("load", async (id, ctx) => {
42
+ * const { user, posts } = await all(ctx, {
43
+ * user: ({ signal }) => fetchUser(id, { signal }),
44
+ * posts: ({ signal }) => fetchPosts(id, { signal }),
45
+ * });
46
+ *
47
+ * // arrays work too: const [user, posts] = await all(ctx, [getUser, getPosts]);
48
+ * });
49
+ * ```
50
+ */
51
+ declare function all<T extends Record<string, Starter<unknown>>>(ctx: {
52
+ signal?: AbortSignal;
53
+ }, effects: T): Promise<{ [K in keyof T]: Awaited<ReturnType<T[K]>>; }>;
54
+ declare function all<const T extends readonly Starter<unknown>[]>(ctx: {
55
+ signal?: AbortSignal;
56
+ }, effects: T): Promise<{ -readonly [K in keyof T]: Awaited<ReturnType<T[K]>>; }>;
57
+ //#endregion
58
+ //#region src/effects/allSettled.d.ts
59
+ /**
60
+ * Runs all starters and waits for every one to settle, whether it succeeds or
61
+ * fails: nothing is cancelled when a starter fails. The result has, for each
62
+ * starter, `{ status: "fulfilled", value }` or `{ status: "rejected", reason }`.
63
+ * Only an abort of `ctx` rejects it, with the abort reason.
64
+ *
65
+ * @example
66
+ * ```ts
67
+ * bus.on("load", async (id, ctx) => {
68
+ * const { user, posts } = await allSettled(ctx, {
69
+ * user: ({ signal }) => fetchUser(id, { signal }),
70
+ * posts: ({ signal }) => fetchPosts(id, { signal }),
71
+ * });
72
+ *
73
+ * if (user.status === "fulfilled") render(user.value);
74
+ * });
75
+ * ```
76
+ */
77
+ declare function allSettled<T extends Record<string, Starter<unknown>>>(ctx: {
78
+ signal?: AbortSignal;
79
+ }, effects: T): Promise<{ [K in keyof T]: PromiseSettledResult<Awaited<ReturnType<T[K]>>>; }>;
80
+ declare function allSettled<const T extends readonly Starter<unknown>[]>(ctx: {
81
+ signal?: AbortSignal;
82
+ }, effects: T): Promise<{ -readonly [K in keyof T]: PromiseSettledResult<Awaited<ReturnType<T[K]>>>; }>;
83
+ //#endregion
84
+ //#region src/effects/race.d.ts
85
+ type RaceStarter<T> = Starter<T>;
86
+ /**
87
+ * Runs all starters; first to settle wins, losers are cancelled via `ctx.abort`.
88
+ * The result holds only the winner's value, under its key or index.
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * bus.on("load", async (id, ctx) => {
93
+ * const result = await race(ctx, {
94
+ * data: ({ signal }) => fetchData(id, { signal }),
95
+ * timeout: ({ signal }) => delay({ signal }, 5000),
96
+ * });
97
+ *
98
+ * if ("timeout" in result) throw new Error("timed out");
99
+ * render(result.data);
100
+ * });
101
+ * ```
102
+ */
103
+ declare function race<T extends Record<string, RaceStarter<unknown>>>(ctx: {
104
+ signal?: AbortSignal;
105
+ }, effects: T): Promise<{ [K in keyof T]?: Awaited<ReturnType<T[K]>>; }>;
106
+ declare function race<const T extends readonly RaceStarter<unknown>[]>(ctx: {
107
+ signal?: AbortSignal;
108
+ }, effects: T): Promise<{ -readonly [K in keyof T]?: Awaited<ReturnType<T[K]>>; }>;
109
+ //#endregion
110
+ //#region src/effects/take.d.ts
111
+ type TakeOptions<P> = {
112
+ /**
113
+ * Wait for the first payload this accepts; others are skipped. A type guard
114
+ * (`(payload): payload is T`) narrows the type of the result to `T`.
115
+ */
116
+ predicate?: (payload: P) => boolean;
117
+ /** Stop waiting when this aborts, e.g. the signal of a `race` starter. */
118
+ signal?: AbortSignal | undefined;
119
+ };
120
+ /**
121
+ * Waits for the next `event` on the bus and resolves with its payload. It sees
122
+ * only events emitted after the call, takes only one, and unsubscribes when it
123
+ * is done. If `ctx` (or `options.signal`) aborts first, it unsubscribes and
124
+ * rejects with the abort reason.
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * bus.on("login", async (user, ctx) => {
129
+ * const reason = await take(ctx, "logout");
130
+ * cleanup(user, reason);
131
+ * });
132
+ *
133
+ * // wait for a matching event, but not longer than 5 s
134
+ * const done = await timeout(ctx, 5000, ({ signal }) =>
135
+ * take(ctx, "saved", { signal, predicate: (id) => id === draftId }),
136
+ * );
137
+ *
138
+ * // a type guard narrows the result: `failed` is `{ ok: false }`
139
+ * const failed = await take(ctx, "result", {
140
+ * predicate: (result): result is { ok: false } => !result.ok,
141
+ * });
142
+ * ```
143
+ */
144
+ declare function take<E extends EventMap, K extends Keys<E>, T extends E[K]>(ctx: Context<E>, event: K, options: TakeOptions<E[K]> & {
145
+ predicate: (payload: E[K]) => payload is T;
146
+ }): Promise<T>;
147
+ declare function take<E extends EventMap, K extends Keys<E>>(ctx: Context<E>, event: K, options?: TakeOptions<E[K]>): Promise<E[K]>;
148
+ //#endregion
149
+ //#region src/effects/plugin.d.ts
150
+ /** The effects that `effectsPlugin` can put on `ctx`. */
151
+ type Supported = {
152
+ race: typeof race;
153
+ all: typeof all;
154
+ allSettled: typeof allSettled;
155
+ take: typeof take;
156
+ delay: typeof delay;
157
+ };
158
+ type Results = Record<string, Starter<unknown>>;
159
+ type List = readonly Starter<unknown>[];
160
+ /** Each supported effect without its leading `ctx` argument. `take` knows the events of the bus. */
161
+ type Bound<E extends EventMap> = {
162
+ race: {
163
+ <T extends Results>(effects: T): Promise<{ [K in keyof T]?: Awaited<ReturnType<T[K]>>; }>;
164
+ <const T extends List>(effects: T): Promise<{ -readonly [K in keyof T]?: Awaited<ReturnType<T[K]>>; }>;
165
+ };
166
+ all: {
167
+ <T extends Results>(effects: T): Promise<{ [K in keyof T]: Awaited<ReturnType<T[K]>>; }>;
168
+ <const T extends List>(effects: T): Promise<{ -readonly [K in keyof T]: Awaited<ReturnType<T[K]>>; }>;
169
+ };
170
+ allSettled: {
171
+ <T extends Results>(effects: T): Promise<{ [K in keyof T]: PromiseSettledResult<Awaited<ReturnType<T[K]>>>; }>;
172
+ <const T extends List>(effects: T): Promise<{ -readonly [K in keyof T]: PromiseSettledResult<Awaited<ReturnType<T[K]>>>; }>;
173
+ };
174
+ take: <K extends Keys<E>>(event: K, options?: TakeOptions<E[K]>) => Promise<E[K]>;
175
+ delay: (ms: number | (() => number)) => Promise<void>;
176
+ };
177
+ /** Any function whose first parameter takes `{ signal }` can go on the context, not only the built-in ones. */
178
+ type Helpers = Effects;
179
+ /**
180
+ * The effects as a handler sees them: the built-in ones keep their hand-written
181
+ * types, any other loses its first parameter, or has the type `Effect<F>` says.
182
+ */
183
+ type BoundOf<H extends Helpers, E extends EventMap> = { [K in keyof H]: K extends keyof Bound<E> ? H[K] extends Supported[K] ? Bound<E>[K] : BoundForm<H[K]> : BoundForm<H[K]>; };
184
+ /**
185
+ * Puts the effects you pass on the context of every task of the bus, bound to
186
+ * that task, so a handler can write `({ race })` instead of `race(ctx, …)`.
187
+ * The key is the name of the effect: `race`, `all`, `allSettled`, `take`,
188
+ * `delay`. Returns the same bus, typed with the new fields.
189
+ *
190
+ * @example
191
+ * ```ts
192
+ * import { createBus } from "nanoeffects";
193
+ * import { all, delay, effectsPlugin, race } from "nanoeffects/effects";
194
+ *
195
+ * const bus = createBus().use(effectsPlugin({ race, delay }));
196
+ *
197
+ * bus.on("load", async (id, { race, delay }) => {
198
+ * const result = await race({
199
+ * data: ({ signal }) => fetchData(id, { signal }),
200
+ * timeout: () => delay(5000),
201
+ * });
202
+ * });
203
+ * ```
204
+ */
205
+ declare const effectsPlugin: <H extends Helpers>(helpers: H) => <E extends EventMap>(bus: Bus<E> & NoClash<BoundOf<H, E>, E>) => Bus<E & WithContext<BoundOf<H, E>>>;
206
+ //#endregion
207
+ export { RaceStarter as a, all as c, delay as d, take as i, EffectContext as l, effectsPlugin as n, race as o, TakeOptions as r, allSettled as s, BoundOf as t, Starter as u };
@@ -1,4 +1,4 @@
1
- import { o as EventMap, u as Task } from "./action-creator-D48fp13T.js";
1
+ import { B as EventMap, W as Task } from "./action-creator-c292nK7v.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 { B as EventMap, T as EventKey, h as Bus, r as FreeActionCreator, t as ActionCreator } from "./action-creator-c292nK7v.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 { _ as Bus, o as EventMap, v as BusOptions } from "./action-creator-D48fp13T.js";
1
+ import { B as EventMap, h as Bus } from "./action-creator-c292nK7v.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{n as t}from"./bus-Cho463Sq.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};
@@ -0,0 +1 @@
1
+ const e=Symbol.for(`nanoeffects.scope`);export{e as t};
@@ -0,0 +1,58 @@
1
+ import { B as EventMap, a as EmitScope, d as Effects, f as Signaled, h as Bus, o as Scope, u as Effect } from "./action-creator-c292nK7v.js";
2
+ import { n as Scoped } from "./types-mLaAx-EZ.js";
3
+ //#region src/scope/effect.d.ts
4
+ /**
5
+ * Marks a function whose first parameter is `{ signal }` as an effect, so that
6
+ * as a value of a scope it is bound to the task: `ctx.first(items)`
7
+ * calls `first(ctx, items)`. The type is the function without that parameter,
8
+ * generics included, which `effects` cannot do: no annotation is needed, and
9
+ * JavaScript editors show the exact hint.
10
+ *
11
+ * Only `scopePlugin` binds it; the function itself is returned unchanged.
12
+ * Call it where the function is declared, not inline in the scope: TypeScript
13
+ * does not infer the fields of a scope from a generic call written there.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const first = effect(<T>(ctx: { signal: AbortSignal }, items: T[]) => items[0]);
18
+ *
19
+ * const getUser = createCommand(
20
+ * "user/get",
21
+ * (id: string, { first }) => first([id]), // first: <T>(items: T[]) => T
22
+ * { first },
23
+ * );
24
+ * ```
25
+ */
26
+ export declare const effect: <A extends unknown[], R>(fn: (ctx: Signaled, ...args: A) => R) => ((...args: A) => R);
27
+ //#endregion
28
+ //#region src/scope/plugin.d.ts
29
+ /**
30
+ * Switches scopes on for a bus: the scope given to a command, a job, an
31
+ * action or a module (an object whose keys go on `ctx`, with `effects` bound to
32
+ * the task), and the second argument of `emit`, put fields on
33
+ * the context of the tasks they are for. A stronger layer replaces a field of
34
+ * the same name: `emit`, then action / command / job, then module, then the bus.
35
+ * Fields of the library (`signal`, `emit`, `run`, …) are never replaced. Returns
36
+ * the same bus.
37
+ *
38
+ * Use it after `effectsPlugin`, which assigns its effects over those of a scope
39
+ * when it comes later, and before `modulePlugin`, which mounts its modules when
40
+ * it is used. Without this plugin a scope is ignored.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * import { createBus } from "nanoeffects";
45
+ * import { createCommand } from "nanoeffects/actions";
46
+ * import { scopePlugin } from "nanoeffects/scope";
47
+ *
48
+ * const bus = createBus().use(scopePlugin()); // scopes work on this bus
49
+ *
50
+ * const getUser = createCommand("user/get", (id: string, { http }) => http.get(id), {
51
+ * http, // a field of this command only
52
+ * });
53
+ * bus.emit(getUser("1"), { http: fakeHttp }); // this call only
54
+ * ```
55
+ */
56
+ export declare const scopePlugin: () => <E extends EventMap>(bus: Bus<E>) => Bus<E>;
57
+ //#endregion
58
+ export type { Effect, Effects, EmitScope, Scope, Scoped, Signaled };
package/dist/scope.js ADDED
@@ -0,0 +1 @@
1
+ import{t as e}from"./scope-key-B7l7mUS5.js";const t=/* @__PURE__ */ new WeakSet,n=e=>(t.add(e),e),r=/* @__PURE__ */ new Set([`signal`,`abort`,`emit`,`on`,`off`,`run`,`type`,`payload`]),i=Symbol(`nanoeffects.scope.entry`),a=[],o=/* @__PURE__ */ new WeakSet,s=(e,n)=>{if(!n)return;for(let i of Object.keys(n)){let a=n[i];if(i!==`effects`&&!r.has(i)){let n=a;e[i]=typeof a==`function`&&t.has(a)?c(e,n):a}}let i=n.effects??{};for(let t of Object.keys(i)){let n=i[t];r.has(t)||(e[t]=c(e,n))}},c=(e,t)=>(...n)=>t(e,...n),l=(e,t)=>{s(e,t.module);for(let n of t.call)s(e,n)},u=t=>{if(typeof t==`object`&&t||typeof t==`function`)return Reflect.get(t,e)},d=e=>typeof e==`object`&&!!e,f=(e,t)=>{let n=u(e),r=d(e)?t[0]:void 0;if(!(n||r))return;let i=n&&r?[n,r]:[n??r];return{type:typeof e==`string`?e:e.type,payload:d(e)?e.payload:t[0],layers:i}},p=()=>e=>{let t,n=e=>{let n=e.run;if(o.has(n))return;let a=(a,...o)=>{let s=t;t=void 0;try{return n((t,n)=>{let o=e[i];return o&&r(n,o.call,o.module),a(t,n)},...o)}finally{t=s}};o.add(a),e.run=a},r=(e,t,r)=>{let a={call:t,module:r};e[i]=a,l(e,a),n(e)},s=e.addContext;e.addContext=e=>{s?.(e);let n=e;t&&n.type===t.type&&Object.is(n.payload,t.payload)&&r(n,t.layers,void 0)},e.applyScope=(e,t)=>{let n=e,o=n[i]?.call??a;r(n,o,t)},e.fitScope=(t,n)=>(r,i)=>(e.applyScope?.(i,t),Reflect.apply(n,null,[r,i]));let c=e.emit;return e.emit=(e,...n)=>{let r=f(e,n);if(!(r||t))return c(e,...n);let i=t;t=r;try{return d(e)?c(e):c(e,...n)}finally{t=i}},e};export{n as effect,p as scopePlugin};
package/dist/trace.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { R as Tracer, d as Trace, f as TraceEvent, p as TraceLogger } from "./action-creator-D48fp13T.js";
1
+ import { G as Trace, K as TraceEvent, L as Tracer, q as TraceLogger } from "./action-creator-c292nK7v.js";
2
2
  //#region src/trace/console.d.ts
3
3
  type ConsoleLoggerOptions = {
4
4
  /** Defaults to `globalThis.console`. */
@@ -0,0 +1,15 @@
1
+ import { B as EventMap, d as Effects, o as Scope, s as WithFields } from "./action-creator-c292nK7v.js";
2
+ import { t as BoundOf } from "./plugin-CCYlC0XS.js";
3
+ //#region src/scope/types.d.ts
4
+ type BoundEffects<E extends EventMap, S extends Scope> = S extends {
5
+ effects: infer H extends Effects;
6
+ } ? BoundOf<H, E> : unknown;
7
+ /** The fields a scope gives a task: its values as they are and its `effects` bound; `unknown` for an empty scope. */
8
+ type FieldsOfScope<E extends EventMap, S extends Scope> = [keyof S] extends [never] ? unknown : Omit<S, "effects"> & BoundEffects<E, S>;
9
+ /**
10
+ * The event map `E` with the fields of a scope on top of its context fields.
11
+ * A field the scope sets replaces the one of the bus in the type too.
12
+ */
13
+ type Scoped<E extends EventMap, S extends Scope> = WithFields<E, FieldsOfScope<E, S>>;
14
+ //#endregion
15
+ export { Scoped as n, FieldsOfScope as t };
package/docs/README.md CHANGED
@@ -14,14 +14,20 @@ Code for every guide step: [`examples/guide.ts`](../examples/guide.ts) (type-che
14
14
  | SPA with async UI | [Policies](policies.md), [Guide § latest](guide.md#3-abort-the-previous-run-with-latest) |
15
15
  | Unreliable network / long flows | [Effects](effects.md) — one helper per job, then the rest as reference |
16
16
  | Domain logic off the UI | [Actions, commands, modules](actions.md) |
17
- | Already on Redux / Zustand / MobX | [Stores](stores.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) |
18
22
 
19
23
  ## Reference
20
24
 
21
25
  - [Policies](policies.md) — `latest`, `leading`, `debounce`, `throttle`, `queue`, `key`
22
26
  - [Effects and cancellation](effects.md) — `retry`, `timeout`, `poll`, `take`, `all`, …
23
27
  - [Actions, commands, jobs and modules](actions.md)
28
+ - [Sign-up form in React](react.md) — one form: races, double submit, retry
24
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
25
31
  - [Errors and unheard events](errors.md)
26
32
  - [Package entries](package.md) — which import holds what
27
33
  - [Tracing](tracing.md) — loggers, Redux DevTools, `notrace`
package/docs/actions.md CHANGED
@@ -109,7 +109,7 @@ The registry lives in `nanoeffects/actions`, so a bus without one carries none o
109
109
 
110
110
  **When.** You want one mount / one unmount for that domain. Skip modules while a few top-level `bus.on` calls are enough.
111
111
 
112
- `bus.use(modulePlugin(modules))` assembles an app from modules and returns `mount`, `unmount`, `isMounted`, `dispose`, and `commands`.
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)).
113
113
 
114
114
  ```js
115
115
  import { createBus } from "nanoeffects";
@@ -152,3 +152,27 @@ app.dispose(); // unmounts every module
152
152
  - `use` — bring sub-modules and commands that carry a handler; the returned function undoes that call.
153
153
  - `spawn` — background task, aborted on unmount.
154
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.
155
+
156
+ ## Give a command, an action or a module its own fields
157
+
158
+ A command can need its own client, a module its own logger. Pass a scope, a plain object whose keys go on `ctx` (`effects` binds functions to the task), as the last argument of `createCommand`, `createAction`, `createModule` and `createJob`, and turn it on with `scopePlugin`:
159
+
160
+ ```js
161
+ import { createBus } from "nanoeffects";
162
+ import { createCommand } from "nanoeffects/actions";
163
+ import { scopePlugin } from "nanoeffects/scope";
164
+
165
+ // scopePlugin — makes the scope of a command work on this bus
166
+ const bus = createBus().use(scopePlugin());
167
+
168
+ // the third argument — fields of this command only
169
+ const getUser = createCommand("user/get", (id, { api }) => api.getUser(id), {
170
+ api,
171
+ });
172
+
173
+ // a test replaces them for one call
174
+ const fakeApi = { getUser: async (id) => ({ id, name: "test" }) };
175
+ bus.emit(getUser("1"), { api: fakeApi });
176
+ ```
177
+
178
+ The handler gets the fields typed. Without `scopePlugin` the scope is ignored. The full picture, with jobs, actions, modules and which value wins, is in [scopes](scope.md).
package/docs/context.md CHANGED
@@ -19,14 +19,38 @@ That is enough in JavaScript. The fields appear on handlers, `ctx.run`, `ctx.on`
19
19
 
20
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
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
+ To give fields to **one** command, action, job or module, or to replace them for one `emit`, see [scopes](scope.md).
25
+
22
26
  ## Type the fields (optional)
23
27
 
24
- `WithContext` on the event map only tells TypeScript that handlers of **this** bus have those fields. It does not wire anything at runtime and is not app-wide — another bus can have different fields or none.
28
+ 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.
25
29
 
26
30
  ```ts
27
- import { createBus, type WithContext } from "nanoeffects";
31
+ import { contextPlugin, createBus } from "nanoeffects";
28
32
 
29
33
  // Events — event name → payload
34
+ type Events = { save: Draft };
35
+
36
+ // contextPlugin — every task gets ctx.store; the type is the type of `store`
37
+ const bus = createBus<Events>().use(contextPlugin({ store }));
38
+
39
+ bus.on("save", (draft, { store }) => store.put(draft));
40
+ ```
41
+
42
+ - A function `(task) => fields` makes the fields per task. It gets `{ type, payload }`.
43
+ - Several `contextPlugin` calls, `effectsPlugin` and the `context` option of `createBus` all add their fields to the same context.
44
+ - 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.
45
+ - The plugin returns the same bus, so keep the result of the chain: `const bus = createBus().use(contextPlugin({ store }))`.
46
+
47
+ ### Name the fields in the event map instead
48
+
49
+ `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.
50
+
51
+ ```ts
52
+ import { createBus, type WithContext } from "nanoeffects";
53
+
30
54
  // WithContext — checker-only: handlers also have ctx.store
31
55
  type Events = { save: Draft } & WithContext<{ store: Store }>;
32
56