nanoeffects 0.8.0 → 0.9.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 (49) hide show
  1. package/README.md +2 -2
  2. package/dist/actions.d.ts +38 -22
  3. package/dist/actions.js +1 -1
  4. package/dist/bus-CkTHUAvq.js +1 -0
  5. package/dist/context-COgyNlrC.js +1 -0
  6. package/dist/effects.js +1 -1
  7. package/dist/index.js +1 -1
  8. package/dist/notrace/actions.d.ts +38 -22
  9. package/dist/notrace/actions.js +1 -1
  10. package/dist/notrace/bus-BmCTo8BG.js +1 -0
  11. package/dist/notrace/context-yYU3_9lq.js +1 -0
  12. package/dist/notrace/{delay-B_W1tnkS.js → delay-BosJKFjg.js} +1 -1
  13. package/dist/notrace/effects.js +1 -1
  14. package/dist/notrace/index.js +1 -1
  15. package/dist/notrace/policies.d.ts +30 -2
  16. package/dist/notrace/policies.js +1 -1
  17. package/dist/oop.js +1 -1
  18. package/dist/policies.d.ts +2 -1
  19. package/dist/policies.js +1 -1
  20. package/dist/react.d.ts +33 -1
  21. package/dist/react.js +1 -1
  22. package/dist/scope.js +1 -1
  23. package/dist/signal.d.ts +51 -0
  24. package/dist/signal.js +1 -0
  25. package/dist/with-policy-CqUS-VWW.d.ts +30 -0
  26. package/dist/with-policy-DBV9dh2F.js +1 -0
  27. package/docs/README.md +6 -0
  28. package/docs/actions.md +73 -4
  29. package/docs/cancel-stale-requests.md +111 -0
  30. package/docs/debounce-and-abort.md +106 -0
  31. package/docs/guide.md +1 -1
  32. package/docs/migrating-from-effector.md +2 -2
  33. package/docs/migrating-from-mitt.md +97 -0
  34. package/docs/motivation.md +41 -0
  35. package/docs/observable.md +124 -10
  36. package/docs/package.md +4 -2
  37. package/docs/plugins.md +20 -0
  38. package/docs/policies.md +33 -0
  39. package/docs/prevent-double-submit.md +128 -0
  40. package/docs/query-libraries.md +124 -0
  41. package/docs/react.md +44 -0
  42. package/llms-full.txt +939 -26
  43. package/llms.txt +9 -3
  44. package/package.json +25 -4
  45. package/skills/nanoeffects/SKILL.md +52 -0
  46. package/dist/bus-BAs9x7IQ.js +0 -1
  47. package/dist/context-CK0ZomWt.js +0 -1
  48. package/dist/notrace/bus-Bdk0YyFQ.js +0 -1
  49. package/dist/notrace/context-CctVmHLC.js +0 -1
package/llms.txt CHANGED
@@ -9,18 +9,22 @@ Rules that generated code often gets wrong:
9
9
  - A handler gets the payload and a task context: `(payload, ctx)`. The context has `signal`, `emit`, `on`, `run`. Pass `ctx.signal` to `fetch`.
10
10
  - Effects take `ctx` as the first argument: `retry(ctx, fn, options)`, `timeout(ctx, ms, fn)`, `take(ctx, "event")`.
11
11
  - Policies wrap a handler: `bus.on("search", latest(async (query, ctx) => { ... }))`.
12
- - Each part is imported from its own path: `nanoeffects` (`createBus`), `nanoeffects/policies`, `nanoeffects/effects`, `nanoeffects/actions`, `nanoeffects/redux`, `nanoeffects/observable`, `nanoeffects/react` (`reactPlugin`, adds `bus.useEmitter`; needs React), `nanoeffects/trace`, `nanoeffects/dev`.
12
+ - Each part is imported from its own path: `nanoeffects` (`createBus`), `nanoeffects/policies`, `nanoeffects/effects`, `nanoeffects/actions`, `nanoeffects/redux`, `nanoeffects/observable`, `nanoeffects/react` (`reactPlugin`, adds `bus.useEmitter`; `usePolicy`; needs React), `nanoeffects/trace`, `nanoeffects/dev`.
13
13
  - `bus.emit(command(payload))` runs the command's handler and returns its result. Plain events return nothing.
14
14
 
15
15
  ## Start here
16
16
 
17
17
  - [Guide](docs/guide.md): bus, handler context, policies, effects, commands and modules, step by step
18
+ - [Why nanoeffects exists](docs/motivation.md): no dependencies and no global state for SDKs and Module Federation, and why not Redux, Effector or RxJS
18
19
  - [Package entries](docs/package.md): which import holds what
19
20
  - [Plugins](docs/plugins.md): every plugin of a bus, what `bus.use` returns, and the order to use them in
20
21
 
21
22
  ## Async UI and unreliable networks
22
23
 
23
- - [Policies](docs/policies.md): `latest`, `leading`, `debounce`, `throttle`, `queue`, and `key`
24
+ - [Policies](docs/policies.md): `latest`, `leading`, `debounce`, `throttle`, `queue`, and `key`; `withPolicy` makes one a plain function without a bus
25
+ - [Cancel stale requests and fix race conditions](docs/cancel-stale-requests.md): an older response overwrites a newer one; `latest`, `debounce`, `retry`, `timeout` in one recipe
26
+ - [Debounce a request and abort the previous one](docs/debounce-and-abort.md): `debounce` outside, `latest` inside, and when to use `throttle`
27
+ - [Prevent a double submit](docs/prevent-double-submit.md): `leading`, one run per item with `key`, retry only on network errors
24
28
  - [Effects and cancellation](docs/effects.md): `retry`, `timeout`, `poll`, `take`, `all`, `race`, `backoff`
25
29
  - [Errors and unheard events](docs/errors.md): `onError`, `onUnhandled`
26
30
 
@@ -28,7 +32,8 @@ Rules that generated code often gets wrong:
28
32
 
29
33
  - [Actions, commands, jobs and modules](docs/actions.md): `createAction`, `createCommand`, `createJob`, `createModule`
30
34
  - [Connect Redux, Zustand, MobX, or Pinia](docs/stores.md): the Redux middleware and callbacks of other stores
31
- - [Build a sign-up form in React](docs/react.md): a form with a username check, a submit that cannot fire twice, and retry
35
+ - [Use nanoeffects with React Query and SWR](docs/query-libraries.md): call a command from `queryFn` or the SWR fetcher; `signalPlugin` passes the abort of the library to the task
36
+ - [Build a sign-up form in React](docs/react.md): a form with a username check, a submit that cannot fire twice, and retry; `usePolicy` keeps a debounced handler between renders
32
37
  - [Context fields](docs/context.md): put a store or client on every task
33
38
  - [Scopes](docs/scope.md): fields for one command, action, job, module or `emit`, and a way to replace them in a test
34
39
  - [Use the bus from a class](docs/oop.md): `extendBus`, `@WithBus`, `@On`, `@Once`, and `dispose()` for the subscriptions of an instance
@@ -41,6 +46,7 @@ Rules that generated code often gets wrong:
41
46
 
42
47
  - [Coming from redux-saga](docs/migrating-from-saga.md)
43
48
  - [Coming from Effector](docs/migrating-from-effector.md)
49
+ - [Coming from mitt, nanoevents or EventEmitter](docs/migrating-from-mitt.md): the same calls, plus cancellation for async handlers
44
50
  - [Use nanoeffects next to RxJS](docs/migrating-from-rxjs.md)
45
51
 
46
52
  ## Reference
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nanoeffects",
3
- "version": "0.8.0",
4
- "description": "Typed event bus where every handler is a task with an AbortSignal, plus policies (latest, debounce, throttle, queue), effects (retry, timeout, poll) and tracing. About 1.3 kB gzip.",
3
+ "version": "0.9.0",
4
+ "description": "Cancel stale async work and fix race conditions in event handlers: a typed event bus where every handler is a task with an AbortSignal, plus latest, debounce, throttle, queue, retry, timeout and poll. About 1.4 kB gzip.",
5
5
  "keywords": [
6
6
  "event-bus",
7
7
  "event-emitter",
@@ -13,6 +13,17 @@
13
13
  "queue",
14
14
  "retry",
15
15
  "side-effects",
16
+ "race-condition",
17
+ "cancel",
18
+ "cancellation",
19
+ "async",
20
+ "takelatest",
21
+ "switchmap",
22
+ "redux-middleware",
23
+ "saga-alternative",
24
+ "rxjs-alternative",
25
+ "mitt-alternative",
26
+ "typed-events",
16
27
  "typescript"
17
28
  ],
18
29
  "license": "MIT",
@@ -35,6 +46,7 @@
35
46
  "dist",
36
47
  "llms.txt",
37
48
  "llms-full.txt",
49
+ "skills",
38
50
  "README.md",
39
51
  "LICENSE",
40
52
  "docs"
@@ -82,6 +94,10 @@
82
94
  "types": "./dist/match.d.ts",
83
95
  "import": "./dist/match.js"
84
96
  },
97
+ "./signal": {
98
+ "types": "./dist/signal.d.ts",
99
+ "import": "./dist/signal.js"
100
+ },
85
101
  "./oop": {
86
102
  "types": "./dist/oop.d.ts",
87
103
  "import": "./dist/oop.js"
@@ -185,13 +201,18 @@
185
201
  {
186
202
  "name": "actions (all)",
187
203
  "path": "dist/actions.js",
188
- "limit": "2.65 kB"
204
+ "limit": "2.7 kB"
189
205
  },
190
206
  {
191
207
  "name": "match (plugin)",
192
208
  "path": "dist/match.js",
193
209
  "limit": "0.5 kB"
194
210
  },
211
+ {
212
+ "name": "signal (plugin)",
213
+ "path": "dist/signal.js",
214
+ "limit": "0.6 kB"
215
+ },
195
216
  {
196
217
  "name": "oop (class entry)",
197
218
  "path": "dist/oop.js",
@@ -218,7 +239,7 @@
218
239
  "ignore": [
219
240
  "react"
220
241
  ],
221
- "limit": "320 B"
242
+ "limit": "1.15 kB"
222
243
  },
223
244
  {
224
245
  "name": "dev",
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: nanoeffects
3
+ description: Use nanoeffects when async event handlers race or need cancelling. Triggers - an older response overwrites a newer one (search box, filters, tab switch), a submit can fire twice, debounce or throttle plus AbortController written by hand, retry with backoff, timeout, polling, replacing redux-saga takeLatest or RxJS switchMap, or replacing mitt / EventEmitter when handlers become async.
4
+ ---
5
+
6
+ # nanoeffects
7
+
8
+ A typed event bus where each handler is a task with an `AbortSignal`. Policies (`latest`, `leading`, `debounce`, `throttle`, `queue`) wrap a handler; effects (`retry`, `timeout`, `poll`, `take`, `all`, `race`) run inside one.
9
+
10
+ ## When to suggest it
11
+
12
+ - Overlapping requests: a newer one should cancel the older one (`latest`).
13
+ - A button or action must not run twice at once (`leading`) or must run in order (`queue`).
14
+ - Handlers need debounce, throttle, retry, timeout or polling with cancellation.
15
+ - The project moves off redux-saga, Effector, or RxJS `switchMap` chains, or wants more than `mitt` or `EventEmitter` gives.
16
+
17
+ ## When not to suggest it
18
+
19
+ - Handlers are synchronous and nothing overlaps: a plain emitter is enough.
20
+ - The job is combining or transforming streams of values: keep RxJS.
21
+ - The job is server state caching (stale-while-revalidate, pagination): use a query library.
22
+
23
+ ## Rules that generated code often gets wrong
24
+
25
+ - ESM only. No default export, no `require`.
26
+ - `const unbind = bus.on("event", handler)` subscribes and returns the unsubscribe function. There is no `subscribe` or `addListener`.
27
+ - A handler gets `(payload, ctx)`. `ctx` has `signal`, `emit`, `on`, `run`. Pass `ctx.signal` to `fetch`, or `latest` drops the old result but the request keeps running.
28
+ - Effects take `ctx` first: `retry(ctx, fn, options)`, `timeout(ctx, ms, fn)`, `take(ctx, "event")`. The wrapper form is `retry(fn, options)`.
29
+ - Policies wrap a handler: `bus.on("search", latest(async (query, ctx) => { ... }))`.
30
+ - Import each part from its path: `nanoeffects`, `nanoeffects/policies`, `nanoeffects/effects`, `nanoeffects/actions`, `nanoeffects/redux`, `nanoeffects/react`.
31
+ - `bus.emit(command(payload))` returns the command's result. Plain events return nothing.
32
+
33
+ ## Minimal example
34
+
35
+ ```js
36
+ import { createBus } from "nanoeffects";
37
+ import { latest } from "nanoeffects/policies";
38
+
39
+ const bus = createBus();
40
+
41
+ bus.on(
42
+ "search",
43
+ latest(async (query, { signal }) => {
44
+ const items = await api.search(query, { signal });
45
+ bus.emit("results", items);
46
+ }),
47
+ );
48
+ ```
49
+
50
+ ## Where to read more
51
+
52
+ The package ships `llms.txt` (index of the docs) and `llms-full.txt` (all docs in one file). Start with `docs/cancel-stale-requests.md` for races and `docs/guide.md` for the step-by-step guide.
@@ -1 +0,0 @@
1
- import{n as e}from"./onAbort-BgMfspK4.js";import{a as t,n,r}from"./context-CK0ZomWt.js";import{n as i}from"./promise-CSLpzwSR.js";import{n as a,t as o}from"./event-0RIBGYqS.js";const s=`*`,c=()=>{let e={},t={},n=n=>(t[n]=void 0,e[n]??=/* @__PURE__ */ new Map,e[n]),r=(e,t)=>{let r=n(e),i=r.get(t)??0;i>1?r.set(t,i-1):t?r.delete(t):r.clear()};return{get:n=>(t[n]??=[...e[n]??[]].flatMap(([e,t])=>Array(t).fill(e)),t[n]),add:(e,t)=>{let i=n(e);return i.set(t,(i.get(t)??0)+1),()=>{r(e,t)}},remove:r,clear:()=>{for(let t of Reflect.ownKeys(e))delete e[t];for(let e of Reflect.ownKeys(t))delete t[e]}}},l=Symbol(`nanoeffects.commandCall`),u=(e,t,n)=>{let r={type:t,payload:n};return Object.defineProperty(r,l,{value:e,enumerable:!1}),r},d=e=>{if(typeof e==`function`)return e;if(typeof e==`object`&&e&&l in e)return e[l]},f=(e,t,n)=>Reflect.apply(e,null,[t,n]),p=(s={})=>{let l=c(),u=(e,t)=>l.add(o(e),t),p=(e,t)=>{let n=u(e,(e,r)=>(n(),t(e,r)));return n},m=(e,t)=>{e===void 0?l.clear():l.remove(o(e),t)},h=(t,a)=>{let{onError:o}=s,{ctx:c}=n({event:a,bus:g,tracer:s.trace,parent:{ref:r()}});if(!o){t(a.payload,c);return}let l=t=>{try{o(t,a)}catch(t){e(`onError failed`,t)}};try{let e=t(a.payload,c);i(e)&&Promise.resolve(e).catch(l)}catch(e){l(e)}},g={on:u,once:p,off:m,emit:(e,i)=>{let c=o(e),u=a(e,i),p={type:c,payload:u},m=d(e),_=[...l.get(c),...l.get(`*`)];_.length===0&&!m&&s.onUnhandled?.(p);for(let e of _)h(e,p);if(m){let{ctx:e}=n({event:p,bus:g,tracer:s.trace,parent:{ref:r()}}),i=g.commands?.run??f;return t(e,`command`,()=>i(m,u,e))}},use:e=>e(g),ctx:s.context,trace:s.trace};return g};export{s as i,p as n,u as r,l as t};
@@ -1 +0,0 @@
1
- import{t as e}from"./onAbort-BgMfspK4.js";const t=t=>{let n=new AbortController;return{controller:n,unlink:e({signal:t},e=>n.abort(e))}},n=typeof __NANOEFFECTS_TRACE__>`u`||__NANOEFFECTS_TRACE__,r=/* @__PURE__ */ new WeakMap;let i;const a=()=>i,o=()=>{},s=(e,t,i,a)=>{let o=n&&r.get(e);o&&o.tracer.report(o.ref,t,i,a)},c=(e,t)=>{let i=n&&r.get(e);return i?i.tracer.settler(i.ref,t):o},l=(e,t,i)=>{let a=n&&r.get(e);return a?a.tracer.run(a.ref,t,e.signal,i):i()},u=(e,t)=>typeof e==`function`?e(t):e,d=e=>e,f=({event:a,bus:s,tracer:c,parent:p})=>{let{controller:m,unlink:h}=t(p?.signal),{signal:g}=m,_=n&&c?{tracer:c,ref:c.task(a,p?.ref)}:void 0,v=(...e)=>{if(g.aborted)return;let t=s?.emit,n=i;i=_?.ref;try{return t?.(...e)}finally{i=n}},y=(...t)=>{if(g.aborted)return o;let n=s?.on,r=n?.(...t);if(!r)return o;let i=e({signal:g},r);return()=>{i(),r()}},b=async(e,...t)=>{g.throwIfAborted();let n=t[0],r=f({event:{type:a.type,payload:n},bus:s,tracer:c,parent:{signal:g,ref:_?.ref}});try{return await l(r.ctx,`run`,()=>e(n,r.ctx))}finally{r.unlink()}},x=d({...u(s?.ctx,a),type:a.type,payload:a.payload,signal:g,abort:e=>m.abort(e),emit:v,on:y,off:s?.off??o,run:b});return s?.addContext?.(x),_&&r.set(x,_),{ctx:x,unlink:h}},p=(e,t)=>{let i=n?r.get(e):void 0;return f({event:{type:t??e.type??``,payload:e.payload},bus:{emit:e.emit,on:e.on,off:e.off,ctx:e},tracer:i?.tracer,parent:{signal:e.signal,ref:i?.ref}})};export{l as a,p as i,f as n,s as o,a as r,c as t};
@@ -1 +0,0 @@
1
- import{a as e,c as t,n,r}from"./context-CctVmHLC.js";import{n as i}from"./promise-CSLpzwSR.js";const a=e=>typeof e==`string`?e:e.type,o=(e,t)=>typeof e==`object`?e.payload:t,s=`*`,c=()=>{let e={},t={},n=n=>(t[n]=void 0,e[n]??=/* @__PURE__ */ new Map,e[n]),r=(e,t)=>{let r=n(e),i=r.get(t)??0;i>1?r.set(t,i-1):t?r.delete(t):r.clear()};return{get:n=>(t[n]??=[...e[n]??[]].flatMap(([e,t])=>Array(t).fill(e)),t[n]),add:(e,t)=>{let i=n(e);return i.set(t,(i.get(t)??0)+1),()=>{r(e,t)}},remove:r,clear:()=>{for(let t of Reflect.ownKeys(e))delete e[t];for(let e of Reflect.ownKeys(t))delete t[e]}}},l=Symbol(`nanoeffects.commandCall`),u=(e,t,n)=>{let r={type:t,payload:n};return Object.defineProperty(r,l,{value:e,enumerable:!1}),r},d=e=>{if(typeof e==`function`)return e;if(typeof e==`object`&&e&&l in e)return e[l]},f=(e,t,n)=>Reflect.apply(e,null,[t,n]),p=(s={})=>{let l=c(),u=(e,t)=>l.add(a(e),t),p=(e,t)=>{let n=u(e,(e,r)=>(n(),t(e,r)));return n},m=(e,t)=>{e===void 0?l.clear():l.remove(a(e),t)},h=(e,a)=>{let{onError:o}=s,{ctx:c}=n({event:a,bus:g,tracer:s.trace,parent:{ref:r()}});if(!o){e(a.payload,c);return}let l=e=>{try{o(e,a)}catch(e){t(`onError failed`,e)}};try{let t=e(a.payload,c);i(t)&&Promise.resolve(t).catch(l)}catch(e){l(e)}},g={on:u,once:p,off:m,emit:(t,i)=>{let c=a(t),u=o(t,i),p={type:c,payload:u},m=d(t),_=[...l.get(c),...l.get(`*`)];_.length===0&&!m&&s.onUnhandled?.(p);for(let e of _)h(e,p);if(m){let{ctx:t}=n({event:p,bus:g,tracer:s.trace,parent:{ref:r()}}),i=g.commands?.run??f;return e(t,`command`,()=>i(m,u,t))}},use:e=>e(g),ctx:s.context,trace:s.trace};return g};export{a,s as i,p as n,u as r,l as t};
@@ -1 +0,0 @@
1
- const e=(e,t)=>{globalThis.console?.error(`[nanoeffects] ${e}`,t)},t=()=>{},n=/* @__PURE__ */ new WeakMap,r=t=>{let r=n.get(t);if(r)return r;let i=/* @__PURE__ */ new Set;return n.set(t,i),t.addEventListener(`abort`,()=>{let n=[...i];i.clear();for(let{fn:r}of n)try{r(t.reason)}catch(t){e(`abort callback failed`,t)}},{once:!0}),i},i=(e,n)=>{let{signal:i}=e;if(!i)return t;if(i.aborted)return n(i.reason),t;let a=r(i),o={fn:n};return a.add(o),()=>{a.delete(o)}},a=e=>{let t=new AbortController;return{controller:t,unlink:i({signal:e},e=>t.abort(e))}};let o;const s=()=>o,c=()=>{},l=(e,t,n,r)=>{},u=(e,t)=>c,d=(e,t,n)=>n(),f=(e,t)=>typeof e==`function`?e(t):e,p=e=>e,m=({event:e,bus:t,tracer:n,parent:r})=>{let{controller:s,unlink:l}=a(r?.signal),{signal:u}=s,h=(...e)=>{if(u.aborted)return;let n=t?.emit,r=o;o=void 0;try{return n?.(...e)}finally{o=r}},g=(...e)=>{if(u.aborted)return c;let n=t?.on,r=n?.(...e);if(!r)return c;let a=i({signal:u},r);return()=>{a(),r()}},_=async(r,...i)=>{u.throwIfAborted();let a=i[0],o=m({event:{type:e.type,payload:a},bus:t,tracer:n,parent:{signal:u,ref:void 0}});try{return await d(o.ctx,`run`,()=>r(a,o.ctx))}finally{o.unlink()}},v=p({...f(t?.ctx,e),type:e.type,payload:e.payload,signal:u,abort:e=>s.abort(e),emit:h,on:g,off:t?.off??c,run:_});return t?.addContext?.(v),{ctx:v,unlink:l}},h=(e,t)=>m({event:{type:t??e.type??``,payload:e.payload},bus:{emit:e.emit,on:e.on,off:e.off,ctx:e},tracer:void 0,parent:{signal:e.signal,ref:void 0}});export{d as a,e as c,h as i,m as n,l as o,s as r,i as s,u as t};