nanoeffects 0.28.0 → 0.30.1

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 (139) hide show
  1. package/README.md +6 -5
  2. package/dist/{action-creator-CcAEvUnw.d.ts → action-creator-6rlBTmH9.d.ts} +60 -33
  3. package/dist/actions.d.ts +18 -18
  4. package/dist/actions.js +1 -1
  5. package/dist/bus-DFlmNJ6h.js +1 -0
  6. package/dist/context-B5Z2RpJt.js +1 -0
  7. package/dist/context-types-DKNc5mkS.d.ts +31 -0
  8. package/dist/context.d.ts +8 -3
  9. package/dist/context.js +1 -1
  10. package/dist/{delay-18LJ8jSk.js → delay-5OhJZZQ0.js} +1 -1
  11. package/dist/dev.d.ts +44 -3
  12. package/dist/dev.js +2 -1
  13. package/dist/effects.d.ts +2 -2
  14. package/dist/effects.js +1 -1
  15. package/dist/emit-key-C6FL7Ewn.js +1 -0
  16. package/dist/guard.d.ts +59 -0
  17. package/dist/guard.js +1 -0
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.js +1 -1
  20. package/dist/match.d.ts +3 -3
  21. package/dist/match.js +1 -1
  22. package/dist/{module-C_oEXw1n.d.ts → module-G39MaVOt.d.ts} +38 -38
  23. package/dist/notrace/{action-creator-DxOvVo8I.d.ts → action-creator-BrMSO3RP.d.ts} +35 -32
  24. package/dist/notrace/actions.d.ts +52 -52
  25. package/dist/notrace/actions.js +1 -1
  26. package/dist/notrace/{bus-DmKn4SDD.js → bus-37nlVljc.js} +1 -1
  27. package/dist/notrace/effects.d.ts +2 -2
  28. package/dist/notrace/index.d.ts +2 -2
  29. package/dist/notrace/index.js +1 -1
  30. package/dist/notrace/{plugin-lJjjd-SN.d.ts → plugin-7GUL2EW5.d.ts} +1 -1
  31. package/dist/notrace/policies.d.ts +1 -1
  32. package/dist/notrace/redux.d.ts +1 -1
  33. package/dist/observable.d.ts +43 -1
  34. package/dist/observable.js +1 -1
  35. package/dist/onAbort-BgMfspK4.js +1 -0
  36. package/dist/oop.d.ts +3 -3
  37. package/dist/oop.js +1 -1
  38. package/dist/{plugin-CuvIgjnX.d.ts → plugin-BWNP23hr.d.ts} +2 -2
  39. package/dist/policies.d.ts +2 -2
  40. package/dist/policies.js +1 -1
  41. package/dist/production/dev.js +1 -1
  42. package/dist/react.d.ts +2 -2
  43. package/dist/react.js +1 -1
  44. package/dist/redux.d.ts +1 -1
  45. package/dist/scope.d.ts +9 -9
  46. package/dist/signal-field-key-CWT-3OXC.js +1 -0
  47. package/dist/signal.d.ts +34 -6
  48. package/dist/signal.js +1 -1
  49. package/dist/site-DLvUoXs3.js +1 -0
  50. package/dist/svelte.d.ts +1 -1
  51. package/dist/trace.d.ts +170 -6
  52. package/dist/trace.js +3 -1
  53. package/dist/{types-FV8fuvI9.d.ts → types-N_cdRr1r.d.ts} +2 -2
  54. package/dist/vue.d.ts +1 -1
  55. package/dist/{with-policy-D-SQRkPh.js → with-policy-CeM1rtES.js} +1 -1
  56. package/dist/{with-policy-3bhFLEXs.d.ts → with-policy-CthiBp_z.d.ts} +1 -1
  57. package/docs/README.md +65 -22
  58. package/docs/actions.md +56 -13
  59. package/docs/cancel-stale-requests.md +2 -0
  60. package/docs/chat-example.md +15 -1
  61. package/docs/chat-widget.md +9 -7
  62. package/docs/context.md +352 -8
  63. package/docs/debounce-and-abort.md +3 -1
  64. package/docs/debugging-loops.md +283 -0
  65. package/docs/effects.md +259 -131
  66. package/docs/errors.md +4 -2
  67. package/docs/examples.md +60 -0
  68. package/docs/guide.md +57 -22
  69. package/docs/hmr.md +35 -1
  70. package/docs/loop-guard.md +109 -0
  71. package/docs/migrating-from-effector.md +15 -2
  72. package/docs/migrating-from-mitt.md +6 -4
  73. package/docs/migrating-from-rxjs.md +2 -0
  74. package/docs/migrating-from-saga.md +2 -0
  75. package/docs/motivation.md +14 -21
  76. package/docs/observable.md +45 -0
  77. package/docs/oop.md +20 -1
  78. package/docs/package.md +25 -13
  79. package/docs/pass-the-signal.md +281 -0
  80. package/docs/plugins.md +90 -15
  81. package/docs/policies.md +73 -46
  82. package/docs/prevent-double-submit.md +3 -1
  83. package/docs/query-libraries.md +276 -6
  84. package/docs/react.md +50 -11
  85. package/docs/recipes.md +528 -0
  86. package/docs/ru/README.md +62 -0
  87. package/docs/ru/actions.md +412 -0
  88. package/docs/ru/cancel-stale-requests.md +106 -0
  89. package/docs/ru/chat-example.md +259 -0
  90. package/docs/ru/chat-widget.md +131 -0
  91. package/docs/ru/context.md +401 -0
  92. package/docs/ru/debounce-and-abort.md +105 -0
  93. package/docs/ru/debugging-loops.md +283 -0
  94. package/docs/ru/effects.md +538 -0
  95. package/docs/ru/errors.md +36 -0
  96. package/docs/ru/examples.md +60 -0
  97. package/docs/ru/guide.md +530 -0
  98. package/docs/ru/hmr.md +83 -0
  99. package/docs/ru/loop-guard.md +109 -0
  100. package/docs/ru/migrating-from-effector.md +339 -0
  101. package/docs/ru/migrating-from-mitt.md +99 -0
  102. package/docs/ru/migrating-from-rxjs.md +197 -0
  103. package/docs/ru/migrating-from-saga.md +97 -0
  104. package/docs/ru/motivation.md +47 -0
  105. package/docs/ru/observable.md +233 -0
  106. package/docs/ru/oop.md +431 -0
  107. package/docs/ru/package.md +42 -0
  108. package/docs/ru/pass-the-signal.md +281 -0
  109. package/docs/ru/plugins.md +398 -0
  110. package/docs/ru/policies.md +201 -0
  111. package/docs/ru/prevent-double-submit.md +122 -0
  112. package/docs/ru/query-libraries.md +392 -0
  113. package/docs/ru/react.md +309 -0
  114. package/docs/ru/recipes.md +528 -0
  115. package/docs/ru/semantics.md +19 -0
  116. package/docs/ru/server.md +204 -0
  117. package/docs/ru/starter.md +118 -0
  118. package/docs/ru/stores.md +673 -0
  119. package/docs/ru/svelte.md +78 -0
  120. package/docs/ru/tracing.md +466 -0
  121. package/docs/ru/typescript.md +170 -0
  122. package/docs/ru/vue.md +92 -0
  123. package/docs/semantics.md +12 -0
  124. package/docs/server.md +5 -3
  125. package/docs/starter.md +2 -0
  126. package/docs/stores.md +2 -0
  127. package/docs/svelte.md +3 -1
  128. package/docs/tracing.md +132 -7
  129. package/docs/typescript.md +3 -1
  130. package/docs/vue.md +3 -1
  131. package/llms-full.txt +2718 -545
  132. package/llms.txt +17 -14
  133. package/package.json +25 -8
  134. package/skills/nanoeffects/SKILL.md +2 -2
  135. package/dist/bus-DlpEluzd.js +0 -1
  136. package/dist/context-UcmrAIhP.js +0 -1
  137. package/dist/context-types-hCKgU8sC.d.ts +0 -14
  138. package/docs/scope.md +0 -266
  139. package/docs/stability.md +0 -90
package/llms.txt CHANGED
@@ -9,56 +9,59 @@ 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` and `bus.useEventState`; `usePolicy`; needs React), `nanoeffects/svelte` (`sveltePlugin`, adds `bus.eventStore`; needs Svelte), `nanoeffects/vue` (`vuePlugin`, adds `bus.watchEmitter` and `bus.useEventRef`; needs Vue), `nanoeffects/trace` (`createTracer`, loggers, `tracePlugin`), `nanoeffects/dev`.
13
- - `bus.emit(command(payload))` runs the command's handler and returns its result. Plain events return nothing.
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` and `bus.useEventState`; `usePolicy`; needs React), `nanoeffects/svelte` (`sveltePlugin`, adds `bus.eventStore`; needs Svelte), `nanoeffects/vue` (`vuePlugin`, adds `bus.watchEmitter` and `bus.useEventRef`; needs Vue), `nanoeffects/trace` (`createTracer`, loggers, `graphLogger`, `tracePlugin`), `nanoeffects/guard` (`loopGuardPlugin`), `nanoeffects/dev`.
13
+ - `bus.emit(action(payload))` runs the action's handler and returns its result. Plain events return nothing.
14
14
 
15
15
  ## Start here
16
16
 
17
- - [Guide](docs/guide.md): bus, handler context, policies, effects, commands and modules, step by step
17
+ - [Guide](docs/guide.md): bus, handler context, policies, effects, actions and modules, step by step
18
+ - [Common tasks](docs/recipes.md): the problem, a short solution and a link for each common task (a race, a double submit, a flaky network, a store, a framework), simplest first
18
19
  - [Start a project from a template](docs/starter.md): `npm create nanoeffects` makes a Vite project (`vanilla`, `react`, `vue-ts`, `svelte-ts` and others, in JavaScript or TypeScript) with a bus, `debounce` and `latest`
19
- - [Why nanoeffects exists](docs/motivation.md): logic and side effects outside UI components as plain async functions, no dependencies and no global state for SDKs and Module Federation, and why not redux-saga, RxJS, Redux or Effector
20
+ - [Why nanoeffects exists](docs/motivation.md): logic and side effects outside UI components as plain async functions, DI by context, and no dependencies and no global state for micro-frontends and Module Federation
20
21
  - [Package entries](docs/package.md): which import holds what
21
- - [Stability](docs/stability.md): which entries are stable or unstable, what a breaking change is, the supported Node, TypeScript and framework versions, the call forms that stay
22
22
  - [Plugins](docs/plugins.md): every plugin of a bus, what `bus.use` returns, and the order to use them in
23
23
 
24
24
  ## Async UI and unreliable networks
25
25
 
26
26
  - [Policies](docs/policies.md): `latest`, `leading`, `debounce`, `throttle`, `queue`, and `key`; `withPolicy` makes one a plain function without a bus
27
27
  - [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
28
- - [Debounce a request and abort the previous one](docs/debounce-and-abort.md): `debounce` outside, `latest` inside, and when to use `throttle`
28
+ - [Pause between requests](docs/debounce-and-abort.md): `debounce` outside, `latest` inside, and when to use `throttle`
29
29
  - [Prevent a double submit](docs/prevent-double-submit.md): `leading`, one run per item with `key`, retry only on network errors
30
30
  - [Effects and cancellation](docs/effects.md): `retry`, `timeout`, `poll`, `take`, `all`, `race`, `backoff`
31
31
  - [Errors and unheard events](docs/errors.md): `onError`, `onUnhandled`
32
32
 
33
33
  ## Domain logic and stores
34
34
 
35
- - [Actions, jobs and modules](docs/actions.md): `createAction`, `createAction`, `createJob`, `createModule`
35
+ - [Actions, jobs and modules](docs/actions.md): `createAction` (with or without a handler), `createJob`, `createModule`
36
36
  - [Connect Redux, Zustand, MobX, or Pinia](docs/stores.md): the Redux middleware and callbacks of other stores
37
- - [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
37
+ - [Use nanoeffects with React Query, SWR and RTK Query](docs/query-libraries.md): call an action from `queryFn` or the SWR fetcher; `signalPlugin` passes the abort of the library to the task; `fromSignal` clients, a live stream into the cache with `events`, refetch on a bus event
38
38
  - [Stop work on a server when the client leaves](docs/server.md): one `AbortController` for each request or socket, `signalPlugin`, a clean shutdown, and a queue worker; Fastify examples
39
39
  - [Build a sign-up form in React](docs/react.md): a form with a username check, a submit that cannot fire twice, and retry; `useEventState` shows an event as state, `usePolicy` keeps a debounced handler between renders
40
40
  - [Build a chat with entities, features, modules and a small server](docs/chat-example.md): rooms, a history under `latest`, a send under `queue` and `retry`, a search with `allSettled` and `timeout`, new messages polled with `poll`
41
41
  - [Build a chat widget that listens only while it is open](docs/chat-widget.md): a bot widget for a site; opening mounts a module (handlers, polling, Escape), closing unmounts it and aborts running requests
42
+ - [Examples](docs/examples.md): playgrounds, bigger projects, backend examples, recipes and TypeScript projects that live in `examples/`
42
43
  - [Use nanoeffects in a Vue app](docs/vue.md): `vuePlugin` adds `watchEmitter` (a value of a component to the bus) and `useEventRef` (an event as a ref)
43
44
  - [Use nanoeffects in a Svelte app](docs/svelte.md): `sveltePlugin` adds `eventStore` (an event as a Svelte store)
44
- - [Context fields](docs/context.md): put a store or client on every task
45
- - [Scopes](docs/scope.md): fields for one command, action, job, module or `emit`, and a way to replace them in a test
45
+ - [Working with context](docs/context.md): put a store or client on every task, and give fields to one action, job, module or `emit` with a scope
46
46
  - [Use the bus from a class](docs/oop.md): `Emitter`, `extendBus`, `@WithBus`, `@On`, `@Once`, and `dispose()` for the subscriptions of an instance
47
47
 
48
48
  ## TypeScript
49
49
 
50
- - [TypeScript in many files](docs/typescript.md): keep the types of the bus in commands and modules
50
+ - [TypeScript in many files](docs/typescript.md): keep the types of the bus in actions and modules
51
51
 
52
52
  ## Coming from another library
53
53
 
54
54
  - [Coming from redux-saga](docs/migrating-from-saga.md)
55
55
  - [Coming from Effector](docs/migrating-from-effector.md)
56
- - [Coming from mitt, nanoevents or EventEmitter](docs/migrating-from-mitt.md): the same calls, plus cancellation for async handlers
56
+ - [Coming from mitt and nanoevents](docs/migrating-from-mitt.md): the same calls, plus cancellation for async handlers
57
57
  - [Use nanoeffects next to RxJS](docs/migrating-from-rxjs.md)
58
58
 
59
59
  ## Reference
60
60
 
61
61
  - [Semantics](docs/semantics.md): exact rules of emit, abort and ordering
62
- - [Trace tasks](docs/tracing.md): loggers, Redux DevTools, the `notrace` build
63
- - [Turn events into Observables](docs/observable.md): `createObserve`, `Subject`
62
+ - [Keep the signal through a handler](docs/pass-the-signal.md): every way a policy handler loses `ctx.signal`, and the lint rules and `devPlugin` check that find it
63
+ - [Trace tasks](docs/tracing.md): loggers, Redux DevTools, `graphLogger`, the `notrace` build
64
+ - [Find and cut event loops](docs/debugging-loops.md): symptoms, `graphLogger`, the graph as Mermaid, Graphviz or JSON, Redux DevTools
65
+ - [Cut a loop with loopGuardPlugin](docs/loop-guard.md): limits on repeats, depth and emits from outside
66
+ - [Turn events into Observables](docs/observable.md): `createObserve`, `Subject`, `events` (an event as a `for await` loop)
64
67
  - [Hot reload and development diagnostics](docs/hmr.md): `devPlugin`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nanoeffects",
3
- "version": "0.28.0",
3
+ "version": "0.30.1",
4
4
  "description": "Business logic and side effects outside UI components, as plain async functions. Tiny, no dependencies.",
5
5
  "keywords": [
6
6
  "event-bus",
@@ -35,7 +35,7 @@
35
35
  "bugs": {
36
36
  "url": "https://github.com/nanoeffects/nanoeffects/issues"
37
37
  },
38
- "homepage": "https://github.com/nanoeffects/nanoeffects#readme",
38
+ "homepage": "https://nanoeffects.github.io/nanoeffects/",
39
39
  "engines": {
40
40
  "node": ">=18"
41
41
  },
@@ -106,6 +106,10 @@
106
106
  "types": "./dist/oop.d.ts",
107
107
  "import": "./dist/oop.js"
108
108
  },
109
+ "./guard": {
110
+ "types": "./dist/guard.d.ts",
111
+ "import": "./dist/guard.js"
112
+ },
109
113
  "./trace": {
110
114
  "types": "./dist/trace.d.ts",
111
115
  "import": "./dist/trace.js"
@@ -160,6 +164,7 @@
160
164
  "templates:check": "node scripts/templates-check.mjs",
161
165
  "docs:check": "node scripts/docs-check.mjs",
162
166
  "llms:check": "node scripts/llms-full.mjs --check",
167
+ "docs:site": "pnpm --filter nanoeffects-docs-site build",
163
168
  "smoke": "node scripts/smoke.mjs && node scripts/smoke.mjs --notrace",
164
169
  "consumer-check": "node scripts/consumer-check.mjs"
165
170
  },
@@ -177,6 +182,7 @@
177
182
  "@types/react-dom": "^19.3.0",
178
183
  "@types/ws": "^8.18.2",
179
184
  "@vitejs/plugin-react": "^6.1.2",
185
+ "acorn": "^8.19.0",
180
186
  "archunit": "^2.5.4",
181
187
  "fastify": "^5.12.7",
182
188
  "happy-dom": "^21.0.4",
@@ -216,7 +222,7 @@
216
222
  {
217
223
  "name": "policies (all)",
218
224
  "path": "dist/policies.js",
219
- "limit": "2.7 kB"
225
+ "limit": "2.71 kB"
220
226
  },
221
227
  {
222
228
  "name": "effects (all)",
@@ -236,12 +242,23 @@
236
242
  {
237
243
  "name": "signal (plugin)",
238
244
  "path": "dist/signal.js",
239
- "limit": "0.6 kB"
245
+ "limit": "0.7 kB"
240
246
  },
241
247
  {
242
248
  "name": "context (plugin)",
243
249
  "path": "dist/context.js",
244
- "limit": "0.2 kB"
250
+ "limit": "0.4 kB"
251
+ },
252
+ {
253
+ "name": "guard (plugin)",
254
+ "path": "dist/guard.js",
255
+ "limit": "1 kB"
256
+ },
257
+ {
258
+ "name": "trace: graphLogger",
259
+ "path": "dist/trace.js",
260
+ "import": "{ graphLogger }",
261
+ "limit": "5 kB"
245
262
  },
246
263
  {
247
264
  "name": "oop (class entry)",
@@ -261,7 +278,7 @@
261
278
  {
262
279
  "name": "observable",
263
280
  "path": "dist/observable.js",
264
- "limit": "0.5 kB"
281
+ "limit": "0.85 kB"
265
282
  },
266
283
  {
267
284
  "name": "react",
@@ -269,7 +286,7 @@
269
286
  "ignore": [
270
287
  "react"
271
288
  ],
272
- "limit": "1.24 kB"
289
+ "limit": "1.25 kB"
273
290
  },
274
291
  {
275
292
  "name": "svelte",
@@ -290,7 +307,7 @@
290
307
  {
291
308
  "name": "dev",
292
309
  "path": "dist/dev.js",
293
- "limit": "1 kB"
310
+ "limit": "1.6 kB"
294
311
  },
295
312
  {
296
313
  "name": "dev (production)",
@@ -26,7 +26,7 @@ Do not write the setup by hand. `npm create nanoeffects@latest my-app -- --templ
26
26
 
27
27
  - `vanilla`, `react`: JavaScript, one search handler with `debounce` and `latest`.
28
28
  - `vanilla-ts`, `react-ts`: the same apps in TypeScript.
29
- - `domains-ts`: React and TypeScript with the logic split by domain (commands, a job, modules). Pick it for an app with several features.
29
+ - `domains-ts`: React and TypeScript with the logic split by domain (actions, a job, modules). Pick it for an app with several features.
30
30
 
31
31
  In an existing project, start from `docs/cancel-stale-requests.md` (races) or `docs/guide.md`.
32
32
 
@@ -38,7 +38,7 @@ In an existing project, start from `docs/cancel-stale-requests.md` (races) or `d
38
38
  - Effects take `ctx` first: `retry(ctx, fn, options)`, `timeout(ctx, ms, fn)`, `take(ctx, "event")`. The wrapper form is `retry(fn, options)`.
39
39
  - Policies wrap a handler: `bus.on("search", latest(async (query, ctx) => { ... }))`.
40
40
  - Import each part from its path: `nanoeffects`, `nanoeffects/policies`, `nanoeffects/effects`, `nanoeffects/actions`, `nanoeffects/redux`, `nanoeffects/react`.
41
- - `bus.emit(command(payload))` returns the command's result. Plain events return nothing.
41
+ - `bus.emit(action(payload))` returns the action's result. Plain events return nothing.
42
42
  - The bus keeps no values: an event emitted before a listener exists is lost. `useEventState(event, initial)` (`nanoeffects/react`) is state of one component, not a store. Share a value through a Context provider at the root or a state manager.
43
43
 
44
44
  ## Where to read more
@@ -1 +0,0 @@
1
- import{d as e,n as t,o as n,r}from"./context-UcmrAIhP.js";import{n as i,t as a}from"./event-DVcW0mME.js";const o=()=>{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()},i=()=>{e={},t={}},a=n=>n===void 0?Object.keys(e).flatMap(a):(t[n]??=[...e[n]??[]].flatMap(([e,t])=>Array(t).fill(e)),t[n]);return{get:a,add:(e,t)=>{let i=n(e);return i.set(t,(i.get(t)??0)+1),()=>r(e,t)},remove:r,clear:i}},s=Symbol(),c=(e,t,n)=>{let r={type:t,payload:n};return Object.defineProperty(r,s,{value:e,enumerable:!1}),r},l=e=>{if(typeof e==`function`)return e;if(typeof e==`object`&&e&&s in e)return e[s]},u=(e,t,n)=>Reflect.apply(e,null,[t,n]),d=(s={})=>{let c=o(),d=(e,t)=>c.add(a(e),t),f=(e,t)=>{let n=d(e,(e,r)=>(n(),t(e,r)));return n},p=(e,t)=>{e===void 0?c.clear():c.remove(a(e),t)},m=e=>t({event:e,bus:g,tracer:s.trace,parent:{ref:r()}}).ctx,h=(t,n)=>{let{onError:r}=s,i=m(n);if(!r){t(n.payload,i);return}let a=t=>{try{r(t,n)}catch(t){e(`onError failed`,t)}};try{let e=t(n.payload,i);typeof e?.then==`function`&&Promise.resolve(e).catch(a)}catch(e){a(e)}},g={get:c.get,on:d,off:p,once:f,emit:(e,t)=>{let r=a(e),o=i(e,t),d={type:r,payload:o},f=l(e);f||=g.actions?.bound?.(r);let p=[...c.get(r),...c.get(`*`)];p.length===0&&!f&&s.onUnhandled?.(d);for(let e of p)h(e,d);if(f){let e=m(d),t=g.actions?.run??u;return n(e,`command`,()=>t(f,o,e))}},use:e=>e(g),ctx:s.context,trace:s.trace};return g};export{d as n,c 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))}},o=Symbol(),s=typeof __NANOEFFECTS_TRACE__>`u`||__NANOEFFECTS_TRACE__,c=/* @__PURE__ */ new WeakMap;let l;const u=()=>l,d=()=>{},f=(e,t,n,r)=>{let i=s&&c.get(e);i&&i.tracer.report(i.ref,t,n,r)},p=(e,t)=>{let n=s&&c.get(e);n&&n.tracer.rename?.(n.ref,t)},m=(e,t)=>{let n=s&&c.get(e);return n?n.tracer.settler(n.ref,t):d},h=(e,t,n)=>{let r=s&&c.get(e);return r?r.tracer.run(r.ref,t,e.signal,n):n()},g=Symbol(`nanoeffects.addContext`),_=(e,t)=>typeof e==`function`?e(t):e,v=({event:e,bus:t,tracer:n,parent:r})=>{let{controller:u,unlink:f}=a(r?.signal),{signal:p}=u,m=s&&n?{tracer:n,ref:n.task(e,r?.ref)}:void 0,y=(...e)=>{if(p.aborted&&!S[o])return;let n=t?.emit,r=l;l=m?.ref;try{return n?.(...e)}finally{l=r}},b=(...e)=>{if(p.aborted)return d;let n=t?.on,r=n?.(...e);if(!r)return d;let a=i({signal:p},r);return()=>{a(),r()}},x=async(r,...i)=>{p.throwIfAborted();let a=i[0],o=v({event:{type:e.type,payload:a},bus:t,tracer:n,parent:{signal:p,ref:m?.ref}});try{return await h(o.ctx,`run`,()=>r(a,o.ctx))}finally{o.unlink()}},S={..._(t?.ctx,e),type:e.type,payload:e.payload,signal:p,abort:e=>u.abort(e),emit:y,on:b,off:t?.off??d,run:x,[g]:t?.addContext};return t?.addContext?.(S),m&&c.set(S,m),{ctx:S,unlink:f}},y=(e,t)=>{let n=s?c.get(e):void 0;return v({event:{type:t??e.type??``,payload:e.payload},bus:{emit:e.emit,on:e.on,off:e.off,ctx:e,addContext:Reflect.get(e,g)},tracer:n?.tracer,parent:{signal:e.signal,ref:n?.ref}})};export{p as a,s as c,e as d,y as i,o as l,v as n,h as o,u as r,f as s,m as t,i as u};
@@ -1,14 +0,0 @@
1
- import { C as ContextOf, H as EventMap } from "./action-creator-CcAEvUnw.js";
2
- //#region src/core/context-types.d.ts
3
- /** What a function that makes the fields of a task gets to see. */
4
- type ContextTask = {
5
- type: string;
6
- payload?: unknown;
7
- };
8
- /**
9
- * `unknown` when none of the `fields` is already a context field of the bus,
10
- * else `never`: the call does not compile. Used as `bus: Bus<E> & NoClash<…>`.
11
- */
12
- type NoClash<Fields extends object, E extends EventMap> = [keyof Fields & keyof ContextOf<E>] extends [never] ? unknown : never;
13
- //#endregion
14
- export { NoClash as n, ContextTask as t };
package/docs/scope.md DELETED
@@ -1,266 +0,0 @@
1
- # Pin context fields to an action, a module or a call
2
-
3
- Optional, for domain logic. Use it when one action needs its own client, one module its own logger, or one test its own fake, and a field on the whole bus ([context fields](context.md)) is too wide.
4
-
5
- A **scope** is a plain object. Every key goes on `ctx` as it is (`cache`, `api`, `logger`), except one: `$effects`. Its functions take `{ signal }` as the first parameter, like `retry(ctx, fn)`, and go on `ctx` bound to the task, so you call `ctx.fetchJson(url)` instead of `fetchJson(ctx, url)`.
6
-
7
- You give a scope to the thing that uses it. A call can replace it. Nothing else changes: handlers still get `(payload, ctx)`.
8
-
9
- ## Turn scopes on
10
-
11
- Scopes are a plugin, so a bus that does not use them pays nothing.
12
-
13
- ```js
14
- // bus.js
15
- import { createBus } from "nanoeffects";
16
- import { scopePlugin } from "nanoeffects/scope";
17
-
18
- // scopePlugin — makes the scopes below work on this bus; returns the same bus
19
- export const bus = createBus().use(scopePlugin());
20
- ```
21
-
22
- The bus is in one file and the actions in files named after their domain, so the next example imports `bus` from here. Each section after it keeps its own `bus.js` and domain file, as if it were an app of its own.
23
-
24
- - Without `scopePlugin` a scope is **ignored**: no error and no warning. If a field is missing from `ctx`, check this first.
25
- - Use it in any order with the other plugins: a scope wins over `effectsPlugin`, `contextPlugin` and `createBus({ context })`.
26
-
27
- ## Give an action its own fields
28
-
29
- <!-- docs-check: continue -->
30
-
31
- ```js
32
- // user.js
33
- import { createAction } from "nanoeffects/actions";
34
-
35
- // createAction — third argument is the scope of this action
36
- export const getUser = createAction(
37
- "user/get",
38
- (id, { api, cache }) => {
39
- // api and cache are on ctx for this command only
40
- return cache.get(id) ?? api.getUser(id);
41
- },
42
- { api, cache: new Map() },
43
- );
44
- ```
45
-
46
- ```js
47
- import { bus } from "./bus";
48
- import { getUser } from "./user";
49
-
50
- const user = await bus.emit(getUser("1")); // the handler has api and cache
51
- ```
52
-
53
- The listeners of `"user/get"` get the same fields when the action is emitted, because the fields belong to the call.
54
-
55
- ## Bind an effect to the task
56
-
57
- An effect is a function with `{ signal }` as its first parameter. In `$effects` it is bound to the task, so it stops with the task.
58
-
59
- ```js
60
- // feed.js
61
- import { createAction } from "nanoeffects/actions";
62
- import { delay, race } from "nanoeffects/effects";
63
-
64
- // your own effect: first parameter is the task (only its signal is used)
65
- const fetchJson = (ctx, url) =>
66
- fetch(url, { signal: ctx.signal }).then((response) => response.json());
67
-
68
- export const loadFeed = createAction(
69
- "feed/load",
70
- (url, { fetchJson, race, delay }) =>
71
- // race, delay, fetchJson — bound to this task: no ctx argument
72
- race({
73
- feed: () => fetchJson(url),
74
- timeout: () => delay(5000),
75
- }),
76
- // $effects — built-in and your own functions share one object
77
- { $effects: { fetchJson, race, delay } },
78
- );
79
- ```
80
-
81
- ```js
82
- // bus.js
83
- import { createBus } from "nanoeffects";
84
- import { scopePlugin } from "nanoeffects/scope";
85
-
86
- export const bus = createBus().use(scopePlugin());
87
- ```
88
-
89
- ```js
90
- import { bus } from "./bus";
91
- import { loadFeed } from "./feed";
92
-
93
- const result = await bus.emit(loadFeed("/feed"));
94
- ```
95
-
96
- - Put in `$effects` only functions that take `ctx` first. A plain function (a formatter, a logger) belongs in `context`.
97
- - A function under any other key is copied as it is; a function in `$effects` is bound.
98
- - `$effects` is the one name with a meaning. A field called `effects` is an ordinary field: `{ effects: { retries: 3 } }` puts that object on `ctx`, and nothing in it is bound.
99
-
100
- ## Replace a field for one call
101
-
102
- The second argument of `emit` is a scope for the tasks of **that** call. It is the usual way to give a test a fake.
103
-
104
- ```js
105
- // bus.js
106
- import { createBus } from "nanoeffects";
107
- import { scopePlugin } from "nanoeffects/scope";
108
-
109
- export const bus = createBus().use(scopePlugin());
110
- ```
111
-
112
- ```js
113
- // user.js
114
- import { createAction } from "nanoeffects/actions";
115
-
116
- export const getUser = createAction(
117
- "user/get",
118
- (id, { api }) => api.getUser(id),
119
- { api },
120
- );
121
- ```
122
-
123
- ```js
124
- import { bus } from "./bus";
125
- import { getUser } from "./user";
126
-
127
- // the real api for this call
128
- await bus.emit(getUser("1"));
129
-
130
- // the second argument — a fake api, for this call only
131
- const fakeApi = { getUser: async (id) => ({ id, name: "test" }) };
132
- await bus.emit(getUser("1"), { api: fakeApi });
133
- ```
134
-
135
- Rules for the second argument:
136
-
137
- - It works with the call of an action (`emit(getUser("1"), scope)`, `ctx.emit(loaded(user), scope)`).
138
- - It does not work with the string form: in `emit("user/get", payload)` the second argument is the payload.
139
- - It can replace `$effects` too: `{ $effects: { delay: instantDelay } }`.
140
- - Fields of the library (`signal`, `emit`, …) in it are ignored.
141
-
142
- ## Give an action, a job or a module its own fields
143
-
144
- ```js
145
- // user.js
146
- import { createAction, createJob } from "nanoeffects/actions";
147
-
148
- // createAction — the listeners of this action's calls get the fields
149
- export const userLoaded = createAction("user/loaded", { store });
150
-
151
- // createJob — the scope is the fourth argument, after the outcomes;
152
- // the handler and the listeners of the outcome get its fields
153
- export const loadUser = createJob(
154
- "user/load",
155
- fetchUser,
156
- { success: userLoaded, fail: "user/failed" },
157
- { logger },
158
- );
159
- ```
160
-
161
- ```js
162
- // audit-module.js
163
- import { createModule } from "nanoeffects/actions";
164
- import { userLoaded } from "./user";
165
-
166
- // createModule — third argument; everything the module owns gets the fields:
167
- // m.on listeners, m.handle handlers, m.spawn tasks
168
- export const auditModule = createModule(
169
- "audit",
170
- (m) => {
171
- m.on(userLoaded, (user, { logger }) => logger.info(user.id));
172
- },
173
- { logger },
174
- );
175
- ```
176
-
177
- If the outcome action (`success` or `fail`) has a scope of its own, its listeners see the job's fields with the action's own fields over them; the handler of the job sees only the job's.
178
-
179
- A module scope does not leak: a listener registered outside the module does not see it.
180
-
181
- ## Which value wins
182
-
183
- When several places set the same field, the strongest one wins:
184
-
185
- | Strength | Where it is set |
186
- | -------- | --------------- |
187
- | 1 (strongest) | the second argument of `emit` |
188
- | 2 | the scope of the action, command or job |
189
- | 3 | the scope of the module that owns the handler |
190
- | 4 (weakest) | the bus: `createBus({ context })`, `contextPlugin`, `effectsPlugin` |
191
-
192
- The fields of the library (`signal`, `abort`, `emit`, `on`, `off`, `run`, `type`, `payload`) are never replaced. The name `traceGroupId` is reserved: the tracer reads it, and it is not put on `ctx` ([Follow one flow](tracing.md#follow-one-flow-with-a-tracegroupid)). A child task (`ctx.run`, an attempt of `retry`) has the fields of its parent.
193
-
194
- ## Keep a generic effect generic
195
-
196
- In `$effects` a function loses its first parameter, and TypeScript cannot keep a generic through that step: `<T>(items: T[]) => T` becomes `(items: unknown[]) => unknown`. JavaScript code is not affected. If you want the exact type, use one of two ways.
197
-
198
- `effect(fn)`: mark the function where you declare it, then put it in the scope like any other value. No annotation, so a JavaScript editor shows the exact hint too.
199
-
200
- ```js
201
- // pick.js
202
- import { createAction } from "nanoeffects/actions";
203
- import { effect } from "nanoeffects/scope";
204
-
205
- // effect — marks the function, so a scope binds it to the task; the editor keeps its generic
206
- const first = effect((ctx, items) => items[0]);
207
-
208
- export const pick = createAction("pick", (items, { first }) => first(items), {
209
- first, // a plain key of the scope, not an entry of `$effects`
210
- });
211
- ```
212
-
213
- Call `effect` where you declare the function, as above, and not inline in the scope: TypeScript does not infer the scope from a generic call written there.
214
-
215
- `Effect<F>` (TypeScript): declare how the effect looks **after** it is bound, and keep it in `$effects`.
216
-
217
- ```ts
218
- import { createAction } from "nanoeffects/actions";
219
- import type { Effect } from "nanoeffects/effects";
220
-
221
- // First — checker-only: how a handler calls the effect (no ctx)
222
- type First = <T>(items: T[]) => T;
223
-
224
- // Effect<First> — the function takes the task first and is typed as First once bound
225
- const first: Effect<First> = (ctx, items) => items[0];
226
-
227
- const pick = createAction(
228
- "pick",
229
- (id: string, { first }) => first([id]), // string
230
- { $effects: { first } },
231
- );
232
- ```
233
-
234
- `Effect<F>` and `effect` change what the checker sees. At run time the function is the same.
235
-
236
- ## Type the fields (optional)
237
-
238
- A handler of an action, job or module with a scope sees the fields of the scope in `ctx`, typed from the values you gave. You write no `WithContext`.
239
-
240
- ```ts
241
- import { createAction } from "nanoeffects/actions";
242
-
243
- type User = { id: string; name: string };
244
-
245
- const getUser = createAction(
246
- "user/get",
247
- async (id: string, ctx) => {
248
- ctx.cache; // Map<string, User>, from the scope below
249
- return ctx.cache.get(id);
250
- },
251
- { cache: new Map<string, User>() },
252
- );
253
- ```
254
-
255
- - A field of a scope replaces a field of the same name from the bus in the type too.
256
- - An override in `emit` must match the type of the field it replaces: `emit(getUser("1"), { cache: 1 })` is an error.
257
- - A field that no scope and no bus declares has no type to check against, so `emit(call, { unknown: 1 })` is an error in TypeScript.
258
- - `bus.on(creator, fn)` for an action with a scope types `ctx` with its fields.
259
-
260
- ## Limits
261
-
262
- - No plugin, no scope: see [Turn scopes on](#turn-scopes-on).
263
- - The fields of the bus are not in the handler of an action made with `createAction(…, scope)` unless you type `ctx` yourself; the fields of the scope are.
264
- - The string form of `emit` takes no scope.
265
- - `effect(fn)` works only as a value of a scope, not as an entry of `$effects`.
266
- - A scope on an action, job or module is typed without the events of a bus, so `take` in its `$effects` takes any event name.
package/docs/stability.md DELETED
@@ -1,90 +0,0 @@
1
- # Know what is stable
2
-
3
- Use this page to decide what you can rely on. It says which imports are stable, what counts as a breaking change, which versions are supported, and which call forms stay.
4
-
5
- ## See which entries are stable
6
-
7
- A stable entry follows the rules below. An unstable entry can change in any minor release; every change is written in the [CHANGELOG](../CHANGELOG.md), and nothing promises a deprecation period first.
8
-
9
- | Import | Status | Note |
10
- | ------ | ------ | ---- |
11
- | `nanoeffects` | Stable | `createBus` and the types of a bus |
12
- | `nanoeffects/effects` | Stable | `retry`, `timeout`, `poll`, `take`, `delay`, `all`, `allSettled`, `race`, `backoff`, `cancellable`, `effectsPlugin` |
13
- | `nanoeffects/policies` | Stable | `latest`, `leading`, `debounce`, `throttle`, `queue`, `withPolicy` |
14
- | `nanoeffects/actions` | Stable | `createAction`, `createJob`, `createModule` and their plugins |
15
- | `nanoeffects/trace` | Stable: the contract | `Tracer`, `Trace`, `TraceEvent`, `TraceLogger`, `Phase`, `EffectName` and `tracePlugin` |
16
- | `nanoeffects/trace` | Unstable: the tools | `createTracer`, `consoleLogger`, `performanceLogger`, `createTraceRecorder`, `reduxDevtools` and their option types |
17
- | `nanoeffects/scope` | Unstable | `scopePlugin`, `effect` |
18
- | `nanoeffects/match` | Unstable | `matchPlugin` |
19
- | `nanoeffects/signal` | Unstable | `signalPlugin` |
20
- | `nanoeffects/context` | Unstable | `contextPlugin` |
21
- | `nanoeffects/oop` | Unstable | `Emitter`, `extendBus`, `On`, `Once`, `WithBus`, `disposeBus` |
22
- | `nanoeffects/observable` | Unstable | `createObserve`, `Subject` |
23
- | `nanoeffects/redux` | Unstable | `createEffectsMiddleware` |
24
- | `nanoeffects/react` | Unstable | `reactPlugin`, `usePolicy` |
25
- | `nanoeffects/svelte` | Unstable | `sveltePlugin` |
26
- | `nanoeffects/vue` | Unstable | `vuePlugin` |
27
- | `nanoeffects/dev` | Unstable | `devPlugin` |
28
-
29
- The adapters (`react`, `vue`, `svelte`, `redux`) are unstable because they follow the APIs of other libraries and have had little use. They can become stable after the library has been in use for a while.
30
-
31
- ## Know what is a breaking change
32
-
33
- For a stable entry, these changes are breaking:
34
-
35
- - A change in what a call does at run time.
36
- - A change in a type that stops code that compiled before from compiling.
37
- - A removed or renamed export.
38
-
39
- A stricter type that only finds mistakes in code that was already wrong is not breaking. It ships in a minor release with a note in the CHANGELOG.
40
-
41
- These are not covered by the promise:
42
-
43
- - Any unstable entry.
44
- - The text of the messages of `nanoeffects/dev`.
45
- - The payload details of trace events. The shape of an event (`id`, `parentId`, `rootId`, `type`, `effect`, `phase`) is covered; the contents of `payload` are not.
46
-
47
- Before version 1.0.0, a breaking change ships in a minor release under `### Breaking` in the CHANGELOG. From 1.0.0 it ships only in a major release.
48
-
49
- A deprecated export keeps working and is removed only in a major release. `RaceStarter` is deprecated: use `StarterOf` instead.
50
-
51
- ## Check the supported versions
52
-
53
- | What | Supported |
54
- | ---- | --------- |
55
- | Node.js, the library | 18, 20, 22 and 24 (a smoke test runs the built package on each) |
56
- | Node.js, the dev tools and the trace in a browser | 22 and newer |
57
- | TypeScript | 5.9 and 7. The built declaration files are type-checked with TypeScript 5.9 in CI, and the source with TypeScript 7. Older versions are not tested. |
58
- | React | 18 or newer (`nanoeffects/react`) |
59
- | Vue | 3.3 or newer (`nanoeffects/vue`) |
60
- | Svelte | 5 or newer (`nanoeffects/svelte`) |
61
-
62
- The library has no dependencies. React, Vue and Svelte are optional peer dependencies of their own entries only.
63
-
64
- ## Rely on these call forms
65
-
66
- `retry`, `timeout` and `poll` have three call forms, and all three stay:
67
-
68
- ```js
69
- import { createBus } from "nanoeffects";
70
- import { race, retry } from "nanoeffects/effects";
71
-
72
- const bus = createBus();
73
-
74
- bus.on("open", async (id, ctx) => {
75
- // with ctx: you pass the context, the function gets the signal
76
- const user = await retry(ctx, ({ signal }) => fetchUser(id, { signal }), { times: 3 });
77
-
78
- // as a starter: a group (race, all, allSettled, timeout) calls it with its child
79
- const result = await race(ctx, {
80
- user: retry((_, { signal }) => fetchUser(id, { signal }), { times: 3 }),
81
- });
82
- });
83
-
84
- // as a Task: a function of (payload, ctx), which bus.on and ctx.run call
85
- bus.on("reload", retry((id, { signal }) => fetchUser(id, { signal }), { times: 3 }));
86
- ```
87
-
88
- The forms are told apart by the arguments: a context first is the form with `ctx`; a function called with a payload and a context is the Task; the same function called by a group with one argument, its child, is the starter. The rule is part of the contract and does not depend on how many parameters your function declares.
89
-
90
- `all` and `allSettled` take options only in the form with `ctx`, `all(ctx, effects, { concurrency })`. The form without `ctx` has one argument, so a second one would look like the `ctx` form. `race` takes no `concurrency`: it ends at the first result, so a starter that waited for a free slot would never run. See [Effects](effects.md#run-several-starters-together).