nanoeffects 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/dist/{action-creator-jeEqIKgK.d.ts → action-creator-6rlBTmH9.d.ts} +27 -2
- package/dist/actions.d.ts +3 -3
- package/dist/actions.js +1 -1
- package/dist/{bus-0uYn_kRj.js → bus-DFlmNJ6h.js} +1 -1
- package/dist/context-B5Z2RpJt.js +1 -0
- package/dist/{context-types-Bme1mzkn.d.ts → context-types-DKNc5mkS.d.ts} +1 -1
- package/dist/context.d.ts +2 -2
- package/dist/dev.d.ts +49 -3
- package/dist/dev.js +2 -1
- package/dist/effects.d.ts +2 -2
- package/dist/effects.js +1 -1
- package/dist/emit-key-C6FL7Ewn.js +1 -0
- package/dist/guard.d.ts +59 -0
- package/dist/guard.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/match.d.ts +1 -1
- package/dist/{module-BWMW83qk.d.ts → module-G39MaVOt.d.ts} +2 -2
- package/dist/notrace/{action-creator-xpQ_I2h8.d.ts → action-creator-BrMSO3RP.d.ts} +1 -0
- package/dist/notrace/actions.d.ts +2 -2
- package/dist/notrace/effects.d.ts +2 -2
- package/dist/notrace/index.d.ts +1 -1
- package/dist/notrace/{plugin-6LhNx0th.d.ts → plugin-7GUL2EW5.d.ts} +1 -1
- package/dist/notrace/policies.d.ts +1 -1
- package/dist/notrace/redux.d.ts +1 -1
- package/dist/observable.d.ts +1 -1
- package/dist/oop.d.ts +1 -1
- package/dist/oop.js +1 -1
- package/dist/{plugin-BaErevLy.d.ts → plugin-BWNP23hr.d.ts} +2 -2
- package/dist/policies.d.ts +2 -2
- package/dist/policies.js +1 -1
- package/dist/production/dev.js +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/redux.d.ts +1 -1
- package/dist/scope.d.ts +2 -2
- package/dist/signal.d.ts +22 -2
- package/dist/signal.js +1 -1
- package/dist/site-DLvUoXs3.js +1 -0
- package/dist/svelte.d.ts +1 -1
- package/dist/trace.d.ts +151 -2
- package/dist/trace.js +3 -1
- package/dist/{types-D5xsR37j.d.ts → types-N_cdRr1r.d.ts} +2 -2
- package/dist/vue.d.ts +1 -1
- package/dist/{with-policy-C6BIiPIi.js → with-policy-CeM1rtES.js} +1 -1
- package/dist/{with-policy-BZVRBUOm.d.ts → with-policy-CthiBp_z.d.ts} +1 -1
- package/docs/README.md +62 -31
- package/docs/actions.md +44 -27
- package/docs/cancel-stale-requests.md +2 -0
- package/docs/chat-example.md +3 -1
- package/docs/chat-widget.md +2 -0
- package/docs/context.md +322 -9
- package/docs/debounce-and-abort.md +3 -1
- package/docs/debugging-loops.md +318 -0
- package/docs/effects.md +255 -138
- package/docs/errors.md +4 -2
- package/docs/examples.md +60 -0
- package/docs/guide.md +39 -22
- package/docs/hmr.md +35 -1
- package/docs/loop-guard.md +109 -0
- package/docs/migrating-from-effector.md +4 -2
- package/docs/migrating-from-mitt.md +6 -4
- package/docs/migrating-from-rxjs.md +2 -0
- package/docs/migrating-from-saga.md +2 -0
- package/docs/motivation.md +14 -21
- package/docs/observable.md +2 -0
- package/docs/oop.md +3 -1
- package/docs/package.md +4 -1
- package/docs/pass-the-signal.md +346 -0
- package/docs/plugins.md +22 -5
- package/docs/policies.md +73 -46
- package/docs/prevent-double-submit.md +3 -1
- package/docs/query-libraries.md +3 -1
- package/docs/react.md +42 -13
- package/docs/recipes.md +528 -0
- package/docs/ru/README.md +62 -0
- package/docs/ru/actions.md +412 -0
- package/docs/ru/cancel-stale-requests.md +106 -0
- package/docs/ru/chat-example.md +259 -0
- package/docs/ru/chat-widget.md +131 -0
- package/docs/ru/context.md +402 -0
- package/docs/ru/debounce-and-abort.md +105 -0
- package/docs/ru/debugging-loops.md +318 -0
- package/docs/ru/effects.md +538 -0
- package/docs/ru/errors.md +36 -0
- package/docs/ru/examples.md +60 -0
- package/docs/ru/guide.md +530 -0
- package/docs/ru/hmr.md +83 -0
- package/docs/ru/loop-guard.md +109 -0
- package/docs/ru/migrating-from-effector.md +339 -0
- package/docs/ru/migrating-from-mitt.md +99 -0
- package/docs/ru/migrating-from-rxjs.md +197 -0
- package/docs/ru/migrating-from-saga.md +97 -0
- package/docs/ru/motivation.md +47 -0
- package/docs/ru/observable.md +233 -0
- package/docs/ru/oop.md +431 -0
- package/docs/ru/package.md +42 -0
- package/docs/ru/pass-the-signal.md +346 -0
- package/docs/ru/plugins.md +398 -0
- package/docs/ru/policies.md +201 -0
- package/docs/ru/prevent-double-submit.md +122 -0
- package/docs/ru/query-libraries.md +392 -0
- package/docs/ru/react.md +309 -0
- package/docs/ru/recipes.md +528 -0
- package/docs/ru/semantics.md +19 -0
- package/docs/ru/server.md +204 -0
- package/docs/ru/starter.md +118 -0
- package/docs/ru/stores.md +673 -0
- package/docs/ru/svelte.md +78 -0
- package/docs/ru/tracing.md +466 -0
- package/docs/ru/typescript.md +170 -0
- package/docs/ru/vue.md +92 -0
- package/docs/semantics.md +2 -0
- package/docs/server.md +2 -0
- package/docs/starter.md +2 -0
- package/docs/stores.md +2 -0
- package/docs/svelte.md +3 -1
- package/docs/tracing.md +58 -1
- package/docs/typescript.md +2 -0
- package/docs/vue.md +3 -1
- package/llms-full.txt +2087 -418
- package/llms.txt +11 -8
- package/package.json +23 -6
- package/dist/context-DtUnzC-1.js +0 -1
- package/docs/scope.md +0 -278
- package/docs/stability.md +0 -90
package/llms.txt
CHANGED
|
@@ -9,23 +9,23 @@ 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`.
|
|
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
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
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
|
|
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
|
-
- [
|
|
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`
|
|
@@ -39,10 +39,10 @@ Rules that generated code often gets wrong:
|
|
|
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
|
-
- [
|
|
45
|
-
- [Scopes](docs/scope.md): fields for one 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
|
|
@@ -53,12 +53,15 @@ Rules that generated code often gets wrong:
|
|
|
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
|
|
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
|
-
- [
|
|
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
|
|
63
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.
|
|
3
|
+
"version": "0.31.0",
|
|
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.
|
|
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.
|
|
225
|
+
"limit": "2.71 kB"
|
|
220
226
|
},
|
|
221
227
|
{
|
|
222
228
|
"name": "effects (all)",
|
|
@@ -236,13 +242,24 @@
|
|
|
236
242
|
{
|
|
237
243
|
"name": "signal (plugin)",
|
|
238
244
|
"path": "dist/signal.js",
|
|
239
|
-
"limit": "0.
|
|
245
|
+
"limit": "0.8 kB"
|
|
240
246
|
},
|
|
241
247
|
{
|
|
242
248
|
"name": "context (plugin)",
|
|
243
249
|
"path": "dist/context.js",
|
|
244
250
|
"limit": "0.4 kB"
|
|
245
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"
|
|
262
|
+
},
|
|
246
263
|
{
|
|
247
264
|
"name": "oop (class entry)",
|
|
248
265
|
"path": "dist/oop.js",
|
|
@@ -269,7 +286,7 @@
|
|
|
269
286
|
"ignore": [
|
|
270
287
|
"react"
|
|
271
288
|
],
|
|
272
|
-
"limit": "1.
|
|
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": "
|
|
310
|
+
"limit": "2.2 kB"
|
|
294
311
|
},
|
|
295
312
|
{
|
|
296
313
|
"name": "dev (production)",
|
package/dist/context-DtUnzC-1.js
DELETED
|
@@ -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=Symbol(),r=typeof __NANOEFFECTS_TRACE__>`u`||__NANOEFFECTS_TRACE__,i=/* @__PURE__ */ new WeakMap;let a;const o=()=>a,s=()=>{},c=(e,t,n,a)=>{let o=r&&i.get(e);o&&o.tracer.report(o.ref,t,n,a)},l=(e,t)=>{let n=r&&i.get(e);n&&n.tracer.rename?.(n.ref,t)},u=(e,t)=>{let n=r&&i.get(e);return n?n.tracer.settler(n.ref,t):s},d=(e,t,n)=>{let a=r&&i.get(e);return a?a.tracer.run(a.ref,t,e.signal,n):n()},f=Symbol(`nanoeffects.addContext`),p=(e,t)=>typeof e==`function`?e(t):e,m=({event:o,bus:c,tracer:l,parent:u})=>{let{controller:h,unlink:g}=t(u?.signal),{signal:_}=h,v=r&&l?{tracer:l,ref:l.task(o,u?.ref)}:void 0,y=(...e)=>{if(_.aborted&&!S[n])return;let t=c?.emit,r=a;a=v?.ref;try{return t?.(...e)}finally{a=r}},b=(...t)=>{if(_.aborted)return s;let n=c?.on,r=n?.(...t);if(!r)return s;let i=e({signal:_},r);return()=>{i(),r()}},x=async(e,...t)=>{_.throwIfAborted();let n=t[0],r=m({event:{type:o.type,payload:n},bus:c,tracer:l,parent:{signal:_,ref:v?.ref}});try{return await d(r.ctx,`run`,()=>e(n,r.ctx))}finally{r.unlink()}},S={...p(c?.ctx,o),type:o.type,payload:o.payload,signal:_,abort:e=>h.abort(e),emit:y,on:b,off:c?.off??s,run:x,[f]:c?.addContext};return c?.addContext?.(S),v&&i.set(S,v),{ctx:S,unlink:g}},h=(e,t)=>{let n=r?i.get(e):void 0;return m({event:{type:t??e.type??``,payload:e.payload},bus:{emit:e.emit,on:e.on,off:e.off,ctx:e,addContext:Reflect.get(e,f)},tracer:n?.tracer,parent:{signal:e.signal,ref:n?.ref}})};export{l as a,r as c,h as i,n as l,m as n,d as o,o as r,c as s,u as t};
|
package/docs/scope.md
DELETED
|
@@ -1,278 +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
|
-
## On this page
|
|
10
|
-
|
|
11
|
-
- [Turn scopes on](#turn-scopes-on)
|
|
12
|
-
- [Give an action its own fields](#give-an-action-its-own-fields)
|
|
13
|
-
- [Bind an effect to the task](#bind-an-effect-to-the-task)
|
|
14
|
-
- [Replace a field for one call](#replace-a-field-for-one-call)
|
|
15
|
-
- [Give an action, a job or a module its own fields](#give-an-action-a-job-or-a-module-its-own-fields)
|
|
16
|
-
- [Which value wins](#which-value-wins)
|
|
17
|
-
- [Keep a generic effect generic](#keep-a-generic-effect-generic)
|
|
18
|
-
- [Type the fields (optional)](#type-the-fields-optional)
|
|
19
|
-
- [Limits](#limits)
|
|
20
|
-
|
|
21
|
-
## Turn scopes on
|
|
22
|
-
|
|
23
|
-
Scopes are a plugin, so a bus that does not use them pays nothing.
|
|
24
|
-
|
|
25
|
-
```js
|
|
26
|
-
// bus.js
|
|
27
|
-
import { createBus } from "nanoeffects";
|
|
28
|
-
import { scopePlugin } from "nanoeffects/scope";
|
|
29
|
-
|
|
30
|
-
// scopePlugin — makes the scopes below work on this bus; returns the same bus
|
|
31
|
-
export const bus = createBus().use(scopePlugin());
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
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.
|
|
35
|
-
|
|
36
|
-
- Without `scopePlugin` a scope is **ignored**: no error and no warning. If a field is missing from `ctx`, check this first.
|
|
37
|
-
- Use it in any order with the other plugins: a scope wins over `effectsPlugin`, `contextPlugin` and `createBus({ context })`.
|
|
38
|
-
|
|
39
|
-
## Give an action its own fields
|
|
40
|
-
|
|
41
|
-
<!-- docs-check: continue -->
|
|
42
|
-
|
|
43
|
-
```js
|
|
44
|
-
// user.js
|
|
45
|
-
import { createAction } from "nanoeffects/actions";
|
|
46
|
-
|
|
47
|
-
// createAction — third argument is the scope of this action
|
|
48
|
-
export const getUser = createAction(
|
|
49
|
-
"user/get",
|
|
50
|
-
(id, { api, cache }) => {
|
|
51
|
-
// api and cache are on ctx for this action only
|
|
52
|
-
return cache.get(id) ?? api.getUser(id);
|
|
53
|
-
},
|
|
54
|
-
{ api, cache: new Map() },
|
|
55
|
-
);
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
```js
|
|
59
|
-
import { bus } from "./bus";
|
|
60
|
-
import { getUser } from "./user";
|
|
61
|
-
|
|
62
|
-
const user = await bus.emit(getUser("1")); // the handler has api and cache
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
The listeners of `"user/get"` get the same fields when the action is emitted, because the fields belong to the call.
|
|
66
|
-
|
|
67
|
-
## Bind an effect to the task
|
|
68
|
-
|
|
69
|
-
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.
|
|
70
|
-
|
|
71
|
-
```js
|
|
72
|
-
// feed.js
|
|
73
|
-
import { createAction } from "nanoeffects/actions";
|
|
74
|
-
import { delay, race } from "nanoeffects/effects";
|
|
75
|
-
|
|
76
|
-
// your own effect: first parameter is the task (only its signal is used)
|
|
77
|
-
const fetchJson = (ctx, url) =>
|
|
78
|
-
fetch(url, { signal: ctx.signal }).then((response) => response.json());
|
|
79
|
-
|
|
80
|
-
export const loadFeed = createAction(
|
|
81
|
-
"feed/load",
|
|
82
|
-
(url, { fetchJson, race, delay }) =>
|
|
83
|
-
// race, delay, fetchJson — bound to this task: no ctx argument
|
|
84
|
-
race({
|
|
85
|
-
feed: () => fetchJson(url),
|
|
86
|
-
timeout: () => delay(5000),
|
|
87
|
-
}),
|
|
88
|
-
// $effects — built-in and your own functions share one object
|
|
89
|
-
{ $effects: { fetchJson, race, delay } },
|
|
90
|
-
);
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
```js
|
|
94
|
-
// bus.js
|
|
95
|
-
import { createBus } from "nanoeffects";
|
|
96
|
-
import { scopePlugin } from "nanoeffects/scope";
|
|
97
|
-
|
|
98
|
-
export const bus = createBus().use(scopePlugin());
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
```js
|
|
102
|
-
import { bus } from "./bus";
|
|
103
|
-
import { loadFeed } from "./feed";
|
|
104
|
-
|
|
105
|
-
const result = await bus.emit(loadFeed("/feed"));
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
- Put in `$effects` only functions that take `ctx` first. A plain function (a formatter, a logger) belongs in `context`.
|
|
109
|
-
- A function under any other key is copied as it is; a function in `$effects` is bound.
|
|
110
|
-
- `$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.
|
|
111
|
-
|
|
112
|
-
## Replace a field for one call
|
|
113
|
-
|
|
114
|
-
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.
|
|
115
|
-
|
|
116
|
-
```js
|
|
117
|
-
// bus.js
|
|
118
|
-
import { createBus } from "nanoeffects";
|
|
119
|
-
import { scopePlugin } from "nanoeffects/scope";
|
|
120
|
-
|
|
121
|
-
export const bus = createBus().use(scopePlugin());
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
```js
|
|
125
|
-
// user.js
|
|
126
|
-
import { createAction } from "nanoeffects/actions";
|
|
127
|
-
|
|
128
|
-
export const getUser = createAction(
|
|
129
|
-
"user/get",
|
|
130
|
-
(id, { api }) => api.getUser(id),
|
|
131
|
-
{ api },
|
|
132
|
-
);
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
```js
|
|
136
|
-
import { bus } from "./bus";
|
|
137
|
-
import { getUser } from "./user";
|
|
138
|
-
|
|
139
|
-
// the real api for this call
|
|
140
|
-
await bus.emit(getUser("1"));
|
|
141
|
-
|
|
142
|
-
// the second argument — a fake api, for this call only
|
|
143
|
-
const fakeApi = { getUser: async (id) => ({ id, name: "test" }) };
|
|
144
|
-
await bus.emit(getUser("1"), { api: fakeApi });
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
Rules for the second argument:
|
|
148
|
-
|
|
149
|
-
- It works with the call of an action (`emit(getUser("1"), scope)`, `ctx.emit(loaded(user), scope)`).
|
|
150
|
-
- It does not work with the string form: in `emit("user/get", payload)` the second argument is the payload.
|
|
151
|
-
- It can replace `$effects` too: `{ $effects: { delay: instantDelay } }`.
|
|
152
|
-
- Fields of the library (`signal`, `emit`, …) in it are ignored.
|
|
153
|
-
|
|
154
|
-
## Give an action, a job or a module its own fields
|
|
155
|
-
|
|
156
|
-
```js
|
|
157
|
-
// user.js
|
|
158
|
-
import { createAction, createJob } from "nanoeffects/actions";
|
|
159
|
-
|
|
160
|
-
// createAction — the listeners of this action's calls get the fields
|
|
161
|
-
export const userLoaded = createAction("user/loaded", { store });
|
|
162
|
-
|
|
163
|
-
// createJob — the scope is the fourth argument, after the outcomes;
|
|
164
|
-
// the handler and the listeners of the outcome get its fields
|
|
165
|
-
export const loadUser = createJob(
|
|
166
|
-
"user/load",
|
|
167
|
-
fetchUser,
|
|
168
|
-
{ success: userLoaded, fail: "user/failed" },
|
|
169
|
-
{ logger },
|
|
170
|
-
);
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
```js
|
|
174
|
-
// audit-module.js
|
|
175
|
-
import { createModule } from "nanoeffects/actions";
|
|
176
|
-
import { userLoaded } from "./user";
|
|
177
|
-
|
|
178
|
-
// createModule — third argument; everything the module owns gets the fields:
|
|
179
|
-
// m.on listeners, m.handle handlers, m.spawn tasks
|
|
180
|
-
export const auditModule = createModule(
|
|
181
|
-
"audit",
|
|
182
|
-
(m) => {
|
|
183
|
-
m.on(userLoaded, (user, { logger }) => logger.info(user.id));
|
|
184
|
-
},
|
|
185
|
-
{ logger },
|
|
186
|
-
);
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
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.
|
|
190
|
-
|
|
191
|
-
A module scope does not leak: a listener registered outside the module does not see it.
|
|
192
|
-
|
|
193
|
-
## Which value wins
|
|
194
|
-
|
|
195
|
-
When several places set the same field, the strongest one wins:
|
|
196
|
-
|
|
197
|
-
| Strength | Where it is set |
|
|
198
|
-
| -------- | --------------- |
|
|
199
|
-
| 1 (strongest) | the second argument of `emit` |
|
|
200
|
-
| 2 | the scope of the action or job |
|
|
201
|
-
| 3 | the scope of the module that owns the handler |
|
|
202
|
-
| 4 (weakest) | the bus: `createBus({ context })`, `contextPlugin`, `effectsPlugin` |
|
|
203
|
-
|
|
204
|
-
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.
|
|
205
|
-
|
|
206
|
-
## Keep a generic effect generic
|
|
207
|
-
|
|
208
|
-
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.
|
|
209
|
-
|
|
210
|
-
`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.
|
|
211
|
-
|
|
212
|
-
```js
|
|
213
|
-
// pick.js
|
|
214
|
-
import { createAction } from "nanoeffects/actions";
|
|
215
|
-
import { effect } from "nanoeffects/scope";
|
|
216
|
-
|
|
217
|
-
// effect — marks the function, so a scope binds it to the task; the editor keeps its generic
|
|
218
|
-
const first = effect((ctx, items) => items[0]);
|
|
219
|
-
|
|
220
|
-
export const pick = createAction("pick", (items, { first }) => first(items), {
|
|
221
|
-
first, // a plain key of the scope, not an entry of `$effects`
|
|
222
|
-
});
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
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.
|
|
226
|
-
|
|
227
|
-
`Effect<F>` (TypeScript): declare how the effect looks **after** it is bound, and keep it in `$effects`.
|
|
228
|
-
|
|
229
|
-
```ts
|
|
230
|
-
import { createAction } from "nanoeffects/actions";
|
|
231
|
-
import type { Effect } from "nanoeffects/effects";
|
|
232
|
-
|
|
233
|
-
// First — checker-only: how a handler calls the effect (no ctx)
|
|
234
|
-
type First = <T>(items: T[]) => T;
|
|
235
|
-
|
|
236
|
-
// Effect<First> — the function takes the task first and is typed as First once bound
|
|
237
|
-
const first: Effect<First> = (ctx, items) => items[0];
|
|
238
|
-
|
|
239
|
-
const pick = createAction(
|
|
240
|
-
"pick",
|
|
241
|
-
(id: string, { first }) => first([id]), // string
|
|
242
|
-
{ $effects: { first } },
|
|
243
|
-
);
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
`Effect<F>` and `effect` change what the checker sees. At run time the function is the same.
|
|
247
|
-
|
|
248
|
-
## Type the fields (optional)
|
|
249
|
-
|
|
250
|
-
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`.
|
|
251
|
-
|
|
252
|
-
```ts
|
|
253
|
-
import { createAction } from "nanoeffects/actions";
|
|
254
|
-
|
|
255
|
-
type User = { id: string; name: string };
|
|
256
|
-
|
|
257
|
-
const getUser = createAction(
|
|
258
|
-
"user/get",
|
|
259
|
-
async (id: string, ctx) => {
|
|
260
|
-
ctx.cache; // Map<string, User>, from the scope below
|
|
261
|
-
return ctx.cache.get(id);
|
|
262
|
-
},
|
|
263
|
-
{ cache: new Map<string, User>() },
|
|
264
|
-
);
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
- A field of a scope replaces a field of the same name from the bus in the type too.
|
|
268
|
-
- An override in `emit` must match the type of the field it replaces: `emit(getUser("1"), { cache: 1 })` is an error.
|
|
269
|
-
- 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.
|
|
270
|
-
- `bus.on(creator, fn)` for an action with a scope types `ctx` with its fields.
|
|
271
|
-
|
|
272
|
-
## Limits
|
|
273
|
-
|
|
274
|
-
- No plugin, no scope: see [Turn scopes on](#turn-scopes-on).
|
|
275
|
-
- 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.
|
|
276
|
-
- The string form of `emit` takes no scope.
|
|
277
|
-
- `effect(fn)` works only as a value of a scope, not as an entry of `$effects`.
|
|
278
|
-
- 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, and the function takes the signal from it
|
|
76
|
-
const user = await retry(ctx, () => fetchUser(id, { signal: ctx.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).
|