nanoeffects 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{action-creator-DOWcbXpy.d.ts → action-creator-c292nK7v.d.ts} +115 -33
- package/dist/actions.d.ts +18 -4
- package/dist/actions.js +1 -1
- package/dist/{notrace/context-plugin-B0sLWOxR.d.ts → context-plugin-1pYmSQEG.d.ts} +1 -1
- package/dist/dev.d.ts +2 -2
- package/dist/effects.d.ts +3 -202
- package/dist/index.d.ts +2 -2
- package/dist/{module-DCJl-qfi.d.ts → module-BlF2KSgy.d.ts} +19 -4
- package/dist/notrace/{action-creator-DOWcbXpy.d.ts → action-creator-DKG_sIia.d.ts} +115 -33
- package/dist/notrace/actions.d.ts +45 -5
- package/dist/notrace/actions.js +1 -1
- package/dist/{context-plugin-B0sLWOxR.d.ts → notrace/context-plugin-Dt1IgVo8.d.ts} +1 -1
- package/dist/notrace/effects.d.ts +3 -202
- package/dist/notrace/index.d.ts +2 -2
- package/dist/notrace/plugin-B0AXKusc.d.ts +207 -0
- 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/plugin-CCYlC0XS.d.ts +207 -0
- package/dist/policies.d.ts +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/redux.d.ts +1 -1
- package/dist/scope-key-B7l7mUS5.js +1 -0
- package/dist/scope.d.ts +58 -0
- package/dist/scope.js +1 -0
- package/dist/trace.d.ts +1 -1
- package/dist/types-mLaAx-EZ.d.ts +15 -0
- package/docs/actions.md +24 -0
- package/docs/context.md +2 -0
- package/docs/effects.md +3 -2
- package/docs/guide.md +22 -0
- package/docs/package.md +1 -0
- package/docs/scope.md +226 -0
- package/llms-full.txt +282 -2
- package/llms.txt +1 -0
- package/package.json +11 -2
package/docs/scope.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# Pin context fields to a command, an action, a module or a call
|
|
2
|
+
|
|
3
|
+
Optional, for domain logic. Use it when one command 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
|
+
import { createBus } from "nanoeffects";
|
|
15
|
+
import { scopePlugin } from "nanoeffects/scope";
|
|
16
|
+
|
|
17
|
+
// scopePlugin — makes the scopes below work on this bus; returns the same bus
|
|
18
|
+
const bus = createBus().use(scopePlugin());
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- Without `scopePlugin` a scope is **ignored**: no error and no warning. If a field is missing from `ctx`, check this first.
|
|
22
|
+
- Use it **after** `effectsPlugin` (an effect it adds later would be assigned over an effect of a scope with the same name) and **before** `modulePlugin` (it mounts the modules when it is used). `contextPlugin` and `createBus({ context })` can come in any order: a scope wins over them.
|
|
23
|
+
|
|
24
|
+
## Give a command its own fields
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
import { createBus } from "nanoeffects";
|
|
28
|
+
import { createCommand } from "nanoeffects/actions";
|
|
29
|
+
import { scopePlugin } from "nanoeffects/scope";
|
|
30
|
+
|
|
31
|
+
const bus = createBus().use(scopePlugin());
|
|
32
|
+
|
|
33
|
+
// createCommand — third argument is the scope of this command
|
|
34
|
+
const getUser = createCommand(
|
|
35
|
+
"user/get",
|
|
36
|
+
(id, { api, cache }) => {
|
|
37
|
+
// api and cache are on ctx for this command only
|
|
38
|
+
return cache.get(id) ?? api.getUser(id);
|
|
39
|
+
},
|
|
40
|
+
{ api, cache: new Map() },
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const user = await bus.emit(getUser("1")); // the handler has api and cache
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The listeners of `"user/get"` get the same fields when the command is emitted, because the fields belong to the call.
|
|
47
|
+
|
|
48
|
+
## Bind an effect to the task
|
|
49
|
+
|
|
50
|
+
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.
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
import { createBus } from "nanoeffects";
|
|
54
|
+
import { createCommand } from "nanoeffects/actions";
|
|
55
|
+
import { delay, race } from "nanoeffects/effects";
|
|
56
|
+
import { scopePlugin } from "nanoeffects/scope";
|
|
57
|
+
|
|
58
|
+
const bus = createBus().use(scopePlugin());
|
|
59
|
+
|
|
60
|
+
// your own effect: first parameter is the task (only its signal is used)
|
|
61
|
+
const fetchJson = (ctx, url) =>
|
|
62
|
+
fetch(url, { signal: ctx.signal }).then((response) => response.json());
|
|
63
|
+
|
|
64
|
+
const loadFeed = createCommand(
|
|
65
|
+
"feed/load",
|
|
66
|
+
(url, { fetchJson, race, delay }) =>
|
|
67
|
+
// race, delay, fetchJson — bound to this task: no ctx argument
|
|
68
|
+
race({
|
|
69
|
+
feed: () => fetchJson(url),
|
|
70
|
+
timeout: () => delay(5000),
|
|
71
|
+
}),
|
|
72
|
+
// effects — built-in and your own functions share one object
|
|
73
|
+
{ effects: { fetchJson, race, delay } },
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const result = await bus.emit(loadFeed("/feed"));
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- Put in `effects` only functions that take `ctx` first. A plain function (a formatter, a logger) belongs in `context`.
|
|
80
|
+
- A function under any other key is copied as it is; a function in `effects` is bound.
|
|
81
|
+
- `effects` is the one name a field of the scope cannot have.
|
|
82
|
+
|
|
83
|
+
## Replace a field for one call
|
|
84
|
+
|
|
85
|
+
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.
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
import { createBus } from "nanoeffects";
|
|
89
|
+
import { createCommand } from "nanoeffects/actions";
|
|
90
|
+
import { scopePlugin } from "nanoeffects/scope";
|
|
91
|
+
|
|
92
|
+
const bus = createBus().use(scopePlugin());
|
|
93
|
+
const getUser = createCommand("user/get", (id, { api }) => api.getUser(id), {
|
|
94
|
+
api,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// the real api for this call
|
|
98
|
+
await bus.emit(getUser("1"));
|
|
99
|
+
|
|
100
|
+
// the second argument — a fake api, for this call only
|
|
101
|
+
const fakeApi = { getUser: async (id) => ({ id, name: "test" }) };
|
|
102
|
+
await bus.emit(getUser("1"), { api: fakeApi });
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Rules for the second argument:
|
|
106
|
+
|
|
107
|
+
- It works with an action or a command call (`emit(getUser("1"), scope)`, `ctx.emit(loaded(user), scope)`).
|
|
108
|
+
- It does not work with the string form: in `emit("user/get", payload)` the second argument is the payload.
|
|
109
|
+
- It can replace `effects` too: `{ effects: { delay: instantDelay } }`.
|
|
110
|
+
- Fields of the library (`signal`, `emit`, …) in it are ignored.
|
|
111
|
+
|
|
112
|
+
## Give an action, a job or a module its own fields
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
import { createAction, createJob, createModule } from "nanoeffects/actions";
|
|
116
|
+
|
|
117
|
+
// createAction — the listeners of this action's calls get the fields
|
|
118
|
+
const userLoaded = createAction("user/loaded", { store });
|
|
119
|
+
|
|
120
|
+
// createJob — the scope is the fourth argument, after the outcomes;
|
|
121
|
+
// the handler and the listeners of the outcome get its fields
|
|
122
|
+
const loadUser = createJob(
|
|
123
|
+
"user/load",
|
|
124
|
+
fetchUser,
|
|
125
|
+
{ success: userLoaded, fail: "user/failed" },
|
|
126
|
+
{ logger },
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// createModule — third argument; everything the module owns gets the fields:
|
|
130
|
+
// m.on listeners, m.handle handlers, m.spawn tasks
|
|
131
|
+
const auditModule = createModule(
|
|
132
|
+
"audit",
|
|
133
|
+
(m) => {
|
|
134
|
+
m.on(userLoaded, (user, { logger }) => logger.info(user.id));
|
|
135
|
+
},
|
|
136
|
+
{ logger },
|
|
137
|
+
);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
A module scope does not leak: a listener registered outside the module does not see it.
|
|
141
|
+
|
|
142
|
+
## Which value wins
|
|
143
|
+
|
|
144
|
+
When several places set the same field, the strongest one wins:
|
|
145
|
+
|
|
146
|
+
| Strength | Where it is set |
|
|
147
|
+
| -------- | --------------- |
|
|
148
|
+
| 1 (strongest) | the second argument of `emit` |
|
|
149
|
+
| 2 | the scope of the action, command or job |
|
|
150
|
+
| 3 | the scope of the module that owns the handler |
|
|
151
|
+
| 4 (weakest) | the bus: `createBus({ context })`, `contextPlugin`, `effectsPlugin` |
|
|
152
|
+
|
|
153
|
+
The fields of the library (`signal`, `abort`, `emit`, `on`, `off`, `run`, `type`, `payload`) are never replaced. A child task (`ctx.run`, an attempt of `retry`) has the fields of its parent.
|
|
154
|
+
|
|
155
|
+
## Keep a generic effect generic
|
|
156
|
+
|
|
157
|
+
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.
|
|
158
|
+
|
|
159
|
+
`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.
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
import { createCommand } from "nanoeffects/actions";
|
|
163
|
+
import { effect } from "nanoeffects/scope";
|
|
164
|
+
|
|
165
|
+
// effect — marks the function, so a scope binds it to the task; the editor keeps its generic
|
|
166
|
+
const first = effect((ctx, items) => items[0]);
|
|
167
|
+
|
|
168
|
+
const pick = createCommand("pick", (items, { first }) => first(items), {
|
|
169
|
+
first, // a plain key of the scope, not an entry of effects
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
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.
|
|
174
|
+
|
|
175
|
+
`Effect<F>` (TypeScript): declare how the effect looks **after** it is bound, and keep it in `effects`.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
import { createCommand } from "nanoeffects/actions";
|
|
179
|
+
import type { Effect } from "nanoeffects/effects";
|
|
180
|
+
|
|
181
|
+
// First — checker-only: how a handler calls the effect (no ctx)
|
|
182
|
+
type First = <T>(items: T[]) => T;
|
|
183
|
+
|
|
184
|
+
// Effect<First> — the function takes the task first and is typed as First once bound
|
|
185
|
+
const first: Effect<First> = (ctx, items) => items[0];
|
|
186
|
+
|
|
187
|
+
const pick = createCommand(
|
|
188
|
+
"pick",
|
|
189
|
+
(id: string, { first }) => first([id]), // string
|
|
190
|
+
{ effects: { first } },
|
|
191
|
+
);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`Effect<F>` and `effect` change what the checker sees. At run time the function is the same.
|
|
195
|
+
|
|
196
|
+
## Type the fields (optional)
|
|
197
|
+
|
|
198
|
+
A handler of a command, job or module with a scope sees the fields of the scope in `ctx`, typed from the values you gave. You write no `WithContext`.
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { createCommand } from "nanoeffects/actions";
|
|
202
|
+
|
|
203
|
+
type User = { id: string; name: string };
|
|
204
|
+
|
|
205
|
+
const getUser = createCommand(
|
|
206
|
+
"user/get",
|
|
207
|
+
async (id: string, ctx) => {
|
|
208
|
+
ctx.cache; // Map<string, User>, from the scope below
|
|
209
|
+
return ctx.cache.get(id);
|
|
210
|
+
},
|
|
211
|
+
{ cache: new Map<string, User>() },
|
|
212
|
+
);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
- A field of a scope replaces a field of the same name from the bus in the type too.
|
|
216
|
+
- An override in `emit` must match the type of the field it replaces: `emit(getUser("1"), { cache: 1 })` is an error.
|
|
217
|
+
- 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.
|
|
218
|
+
- `bus.on(creator, fn)` for an action with a scope types `ctx` with its fields.
|
|
219
|
+
|
|
220
|
+
## Limits
|
|
221
|
+
|
|
222
|
+
- No plugin, no scope: see [Turn scopes on](#turn-scopes-on).
|
|
223
|
+
- The fields of the bus are not in the handler of a command made with `createCommand(…, scope)` unless you type `ctx` yourself; the fields of the scope are.
|
|
224
|
+
- The string form of `emit` takes no scope.
|
|
225
|
+
- `effect(fn)` works only as a value of a scope, not as an entry of `effects`.
|
|
226
|
+
- A scope on a command, action, job or module is typed without the events of a bus, so `take` in its `effects` takes any event name.
|
package/llms-full.txt
CHANGED
|
@@ -64,6 +64,28 @@ bus.on("tick", () => {});
|
|
|
64
64
|
bus.off(); // clear every subscription
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
### Listen to every event
|
|
68
|
+
|
|
69
|
+
`bus.on("*", fn)` runs for every event of the bus. It is for logging, debugging, or forwarding events elsewhere. The handler gets the payload and the task context as usual, and `ctx.type` is the name of the event.
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
import { createBus } from "nanoeffects";
|
|
73
|
+
|
|
74
|
+
const bus = createBus();
|
|
75
|
+
|
|
76
|
+
// "*" — runs for every event, after the listeners of that event
|
|
77
|
+
const unbind = bus.on("*", (payload, ctx) => {
|
|
78
|
+
console.log(ctx.type, payload);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
bus.emit("greet", "Ada"); // greet Ada
|
|
82
|
+
bus.emit("moved", { x: 1, y: 2 }); // moved { x: 1, y: 2 }
|
|
83
|
+
unbind();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- An event that only `"*"` listens to counts as heard, so `onUnhandled` does not fire for it.
|
|
87
|
+
- `bus.off("*")` removes every `"*"` listener; `bus.off()` clears the whole bus, `"*"` included.
|
|
88
|
+
|
|
67
89
|
## 2. Use the handler context
|
|
68
90
|
|
|
69
91
|
The second argument is the **task** context for this call. Emit further events through it. Pass `ctx.signal` to `fetch` when you cancel work.
|
|
@@ -367,6 +389,7 @@ Imports are split so you only load what you use. `createBus` stays small; the re
|
|
|
367
389
|
| `nanoeffects/policies` | `latest`, `leading`, `debounce`, `throttle`, `queue` |
|
|
368
390
|
| `nanoeffects/effects` | `retry`, `timeout`, `poll`, `delay`, `take`, `all`, `allSettled`, `race`, `effectsPlugin` |
|
|
369
391
|
| `nanoeffects/actions` | `createAction`, `createCommand`, `createJob`, `createModule`, `modulePlugin`, `commandsPlugin` |
|
|
392
|
+
| `nanoeffects/scope` | `scopePlugin`, `effect`: fields for one command, action, job, module or `emit` (the types `Scope`, `Effect`) |
|
|
370
393
|
| `nanoeffects/redux` | `createEffectsMiddleware` |
|
|
371
394
|
| `nanoeffects/observable` | `createObserve` |
|
|
372
395
|
| `nanoeffects/react` | `reactPlugin`: adds the `useEmitter` hook to a bus (needs React 18 or newer) |
|
|
@@ -673,8 +696,9 @@ bus.on("load", async (id, { race, all, delay }) => {
|
|
|
673
696
|
});
|
|
674
697
|
```
|
|
675
698
|
|
|
676
|
-
-
|
|
677
|
-
- `
|
|
699
|
+
- The key is the name the effect gets on `ctx`. `race`, `all`, `allSettled`, `take` and `delay` are typed by hand.
|
|
700
|
+
- Your own function works too if its first parameter is `{ signal }` (an *effect*): `effectsPlugin({ fetchJson })` gives `ctx.fetchJson(url)`. TypeScript removes the first parameter from its type; for a generic function see [keep a generic effect generic](scope.md#keep-a-generic-effect-generic).
|
|
701
|
+
- Leave `retry`, `timeout` and `poll` out: bound to the task they would look like their handler wrappers.
|
|
678
702
|
- Fields from `createBus({ context })` stay. TypeScript rejects an effect whose name is already a context field of the bus, including one added by an earlier `effectsPlugin`.
|
|
679
703
|
- Free forms keep working: `race(ctx, …)`.
|
|
680
704
|
|
|
@@ -997,6 +1021,30 @@ app.dispose(); // unmounts every module
|
|
|
997
1021
|
- `spawn` — background task, aborted on unmount.
|
|
998
1022
|
- A command that carries a handler keeps answering after unmount (`use` only tracks registration). Use `handle` for a handler that must live only as long as the module.
|
|
999
1023
|
|
|
1024
|
+
## Give a command, an action or a module its own fields
|
|
1025
|
+
|
|
1026
|
+
A command can need its own client, a module its own logger. Pass a scope, a plain object whose keys go on `ctx` (`effects` binds functions to the task), as the last argument of `createCommand`, `createAction`, `createModule` and `createJob`, and turn it on with `scopePlugin`:
|
|
1027
|
+
|
|
1028
|
+
```js
|
|
1029
|
+
import { createBus } from "nanoeffects";
|
|
1030
|
+
import { createCommand } from "nanoeffects/actions";
|
|
1031
|
+
import { scopePlugin } from "nanoeffects/scope";
|
|
1032
|
+
|
|
1033
|
+
// scopePlugin — makes the scope of a command work on this bus
|
|
1034
|
+
const bus = createBus().use(scopePlugin());
|
|
1035
|
+
|
|
1036
|
+
// the third argument — fields of this command only
|
|
1037
|
+
const getUser = createCommand("user/get", (id, { api }) => api.getUser(id), {
|
|
1038
|
+
api,
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
// a test replaces them for one call
|
|
1042
|
+
const fakeApi = { getUser: async (id) => ({ id, name: "test" }) };
|
|
1043
|
+
bus.emit(getUser("1"), { api: fakeApi });
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
The handler gets the fields typed. Without `scopePlugin` the scope is ignored. The full picture, with jobs, actions, modules and which value wins, is in [scopes](scope.md).
|
|
1047
|
+
|
|
1000
1048
|
---
|
|
1001
1049
|
Source: docs/stores.md
|
|
1002
1050
|
|
|
@@ -1699,6 +1747,8 @@ Library fields (`signal`, `emit`, `run`, …) win on name clashes. `context` may
|
|
|
1699
1747
|
|
|
1700
1748
|
To get `race`, `all`, `take` and the like on every task without passing `ctx`, see [`effectsPlugin`](effects.md#put-selected-effects-on-ctx).
|
|
1701
1749
|
|
|
1750
|
+
To give fields to **one** command, action, job or module, or to replace them for one `emit`, see [scopes](scope.md).
|
|
1751
|
+
|
|
1702
1752
|
## Type the fields (optional)
|
|
1703
1753
|
|
|
1704
1754
|
In TypeScript, `contextPlugin` puts the fields on every task of **this** bus and takes their types from the value you pass. The event map holds only events. It does not wire anything across the app — another bus can have different fields or none.
|
|
@@ -1733,6 +1783,236 @@ type Events = { save: Draft } & WithContext<{ store: Store }>;
|
|
|
1733
1783
|
const bus = createBus<Events>({ context: { store } });
|
|
1734
1784
|
```
|
|
1735
1785
|
|
|
1786
|
+
---
|
|
1787
|
+
Source: docs/scope.md
|
|
1788
|
+
|
|
1789
|
+
# Pin context fields to a command, an action, a module or a call
|
|
1790
|
+
|
|
1791
|
+
Optional, for domain logic. Use it when one command 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.
|
|
1792
|
+
|
|
1793
|
+
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)`.
|
|
1794
|
+
|
|
1795
|
+
You give a scope to the thing that uses it. A call can replace it. Nothing else changes: handlers still get `(payload, ctx)`.
|
|
1796
|
+
|
|
1797
|
+
## Turn scopes on
|
|
1798
|
+
|
|
1799
|
+
Scopes are a plugin, so a bus that does not use them pays nothing.
|
|
1800
|
+
|
|
1801
|
+
```js
|
|
1802
|
+
import { createBus } from "nanoeffects";
|
|
1803
|
+
import { scopePlugin } from "nanoeffects/scope";
|
|
1804
|
+
|
|
1805
|
+
// scopePlugin — makes the scopes below work on this bus; returns the same bus
|
|
1806
|
+
const bus = createBus().use(scopePlugin());
|
|
1807
|
+
```
|
|
1808
|
+
|
|
1809
|
+
- Without `scopePlugin` a scope is **ignored**: no error and no warning. If a field is missing from `ctx`, check this first.
|
|
1810
|
+
- Use it **after** `effectsPlugin` (an effect it adds later would be assigned over an effect of a scope with the same name) and **before** `modulePlugin` (it mounts the modules when it is used). `contextPlugin` and `createBus({ context })` can come in any order: a scope wins over them.
|
|
1811
|
+
|
|
1812
|
+
## Give a command its own fields
|
|
1813
|
+
|
|
1814
|
+
```js
|
|
1815
|
+
import { createBus } from "nanoeffects";
|
|
1816
|
+
import { createCommand } from "nanoeffects/actions";
|
|
1817
|
+
import { scopePlugin } from "nanoeffects/scope";
|
|
1818
|
+
|
|
1819
|
+
const bus = createBus().use(scopePlugin());
|
|
1820
|
+
|
|
1821
|
+
// createCommand — third argument is the scope of this command
|
|
1822
|
+
const getUser = createCommand(
|
|
1823
|
+
"user/get",
|
|
1824
|
+
(id, { api, cache }) => {
|
|
1825
|
+
// api and cache are on ctx for this command only
|
|
1826
|
+
return cache.get(id) ?? api.getUser(id);
|
|
1827
|
+
},
|
|
1828
|
+
{ api, cache: new Map() },
|
|
1829
|
+
);
|
|
1830
|
+
|
|
1831
|
+
const user = await bus.emit(getUser("1")); // the handler has api and cache
|
|
1832
|
+
```
|
|
1833
|
+
|
|
1834
|
+
The listeners of `"user/get"` get the same fields when the command is emitted, because the fields belong to the call.
|
|
1835
|
+
|
|
1836
|
+
## Bind an effect to the task
|
|
1837
|
+
|
|
1838
|
+
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.
|
|
1839
|
+
|
|
1840
|
+
```js
|
|
1841
|
+
import { createBus } from "nanoeffects";
|
|
1842
|
+
import { createCommand } from "nanoeffects/actions";
|
|
1843
|
+
import { delay, race } from "nanoeffects/effects";
|
|
1844
|
+
import { scopePlugin } from "nanoeffects/scope";
|
|
1845
|
+
|
|
1846
|
+
const bus = createBus().use(scopePlugin());
|
|
1847
|
+
|
|
1848
|
+
// your own effect: first parameter is the task (only its signal is used)
|
|
1849
|
+
const fetchJson = (ctx, url) =>
|
|
1850
|
+
fetch(url, { signal: ctx.signal }).then((response) => response.json());
|
|
1851
|
+
|
|
1852
|
+
const loadFeed = createCommand(
|
|
1853
|
+
"feed/load",
|
|
1854
|
+
(url, { fetchJson, race, delay }) =>
|
|
1855
|
+
// race, delay, fetchJson — bound to this task: no ctx argument
|
|
1856
|
+
race({
|
|
1857
|
+
feed: () => fetchJson(url),
|
|
1858
|
+
timeout: () => delay(5000),
|
|
1859
|
+
}),
|
|
1860
|
+
// effects — built-in and your own functions share one object
|
|
1861
|
+
{ effects: { fetchJson, race, delay } },
|
|
1862
|
+
);
|
|
1863
|
+
|
|
1864
|
+
const result = await bus.emit(loadFeed("/feed"));
|
|
1865
|
+
```
|
|
1866
|
+
|
|
1867
|
+
- Put in `effects` only functions that take `ctx` first. A plain function (a formatter, a logger) belongs in `context`.
|
|
1868
|
+
- A function under any other key is copied as it is; a function in `effects` is bound.
|
|
1869
|
+
- `effects` is the one name a field of the scope cannot have.
|
|
1870
|
+
|
|
1871
|
+
## Replace a field for one call
|
|
1872
|
+
|
|
1873
|
+
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.
|
|
1874
|
+
|
|
1875
|
+
```js
|
|
1876
|
+
import { createBus } from "nanoeffects";
|
|
1877
|
+
import { createCommand } from "nanoeffects/actions";
|
|
1878
|
+
import { scopePlugin } from "nanoeffects/scope";
|
|
1879
|
+
|
|
1880
|
+
const bus = createBus().use(scopePlugin());
|
|
1881
|
+
const getUser = createCommand("user/get", (id, { api }) => api.getUser(id), {
|
|
1882
|
+
api,
|
|
1883
|
+
});
|
|
1884
|
+
|
|
1885
|
+
// the real api for this call
|
|
1886
|
+
await bus.emit(getUser("1"));
|
|
1887
|
+
|
|
1888
|
+
// the second argument — a fake api, for this call only
|
|
1889
|
+
const fakeApi = { getUser: async (id) => ({ id, name: "test" }) };
|
|
1890
|
+
await bus.emit(getUser("1"), { api: fakeApi });
|
|
1891
|
+
```
|
|
1892
|
+
|
|
1893
|
+
Rules for the second argument:
|
|
1894
|
+
|
|
1895
|
+
- It works with an action or a command call (`emit(getUser("1"), scope)`, `ctx.emit(loaded(user), scope)`).
|
|
1896
|
+
- It does not work with the string form: in `emit("user/get", payload)` the second argument is the payload.
|
|
1897
|
+
- It can replace `effects` too: `{ effects: { delay: instantDelay } }`.
|
|
1898
|
+
- Fields of the library (`signal`, `emit`, …) in it are ignored.
|
|
1899
|
+
|
|
1900
|
+
## Give an action, a job or a module its own fields
|
|
1901
|
+
|
|
1902
|
+
```js
|
|
1903
|
+
import { createAction, createJob, createModule } from "nanoeffects/actions";
|
|
1904
|
+
|
|
1905
|
+
// createAction — the listeners of this action's calls get the fields
|
|
1906
|
+
const userLoaded = createAction("user/loaded", { store });
|
|
1907
|
+
|
|
1908
|
+
// createJob — the scope is the fourth argument, after the outcomes;
|
|
1909
|
+
// the handler and the listeners of the outcome get its fields
|
|
1910
|
+
const loadUser = createJob(
|
|
1911
|
+
"user/load",
|
|
1912
|
+
fetchUser,
|
|
1913
|
+
{ success: userLoaded, fail: "user/failed" },
|
|
1914
|
+
{ logger },
|
|
1915
|
+
);
|
|
1916
|
+
|
|
1917
|
+
// createModule — third argument; everything the module owns gets the fields:
|
|
1918
|
+
// m.on listeners, m.handle handlers, m.spawn tasks
|
|
1919
|
+
const auditModule = createModule(
|
|
1920
|
+
"audit",
|
|
1921
|
+
(m) => {
|
|
1922
|
+
m.on(userLoaded, (user, { logger }) => logger.info(user.id));
|
|
1923
|
+
},
|
|
1924
|
+
{ logger },
|
|
1925
|
+
);
|
|
1926
|
+
```
|
|
1927
|
+
|
|
1928
|
+
A module scope does not leak: a listener registered outside the module does not see it.
|
|
1929
|
+
|
|
1930
|
+
## Which value wins
|
|
1931
|
+
|
|
1932
|
+
When several places set the same field, the strongest one wins:
|
|
1933
|
+
|
|
1934
|
+
| Strength | Where it is set |
|
|
1935
|
+
| -------- | --------------- |
|
|
1936
|
+
| 1 (strongest) | the second argument of `emit` |
|
|
1937
|
+
| 2 | the scope of the action, command or job |
|
|
1938
|
+
| 3 | the scope of the module that owns the handler |
|
|
1939
|
+
| 4 (weakest) | the bus: `createBus({ context })`, `contextPlugin`, `effectsPlugin` |
|
|
1940
|
+
|
|
1941
|
+
The fields of the library (`signal`, `abort`, `emit`, `on`, `off`, `run`, `type`, `payload`) are never replaced. A child task (`ctx.run`, an attempt of `retry`) has the fields of its parent.
|
|
1942
|
+
|
|
1943
|
+
## Keep a generic effect generic
|
|
1944
|
+
|
|
1945
|
+
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.
|
|
1946
|
+
|
|
1947
|
+
`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.
|
|
1948
|
+
|
|
1949
|
+
```js
|
|
1950
|
+
import { createCommand } from "nanoeffects/actions";
|
|
1951
|
+
import { effect } from "nanoeffects/scope";
|
|
1952
|
+
|
|
1953
|
+
// effect — marks the function, so a scope binds it to the task; the editor keeps its generic
|
|
1954
|
+
const first = effect((ctx, items) => items[0]);
|
|
1955
|
+
|
|
1956
|
+
const pick = createCommand("pick", (items, { first }) => first(items), {
|
|
1957
|
+
first, // a plain key of the scope, not an entry of effects
|
|
1958
|
+
});
|
|
1959
|
+
```
|
|
1960
|
+
|
|
1961
|
+
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.
|
|
1962
|
+
|
|
1963
|
+
`Effect<F>` (TypeScript): declare how the effect looks **after** it is bound, and keep it in `effects`.
|
|
1964
|
+
|
|
1965
|
+
```ts
|
|
1966
|
+
import { createCommand } from "nanoeffects/actions";
|
|
1967
|
+
import type { Effect } from "nanoeffects/effects";
|
|
1968
|
+
|
|
1969
|
+
// First — checker-only: how a handler calls the effect (no ctx)
|
|
1970
|
+
type First = <T>(items: T[]) => T;
|
|
1971
|
+
|
|
1972
|
+
// Effect<First> — the function takes the task first and is typed as First once bound
|
|
1973
|
+
const first: Effect<First> = (ctx, items) => items[0];
|
|
1974
|
+
|
|
1975
|
+
const pick = createCommand(
|
|
1976
|
+
"pick",
|
|
1977
|
+
(id: string, { first }) => first([id]), // string
|
|
1978
|
+
{ effects: { first } },
|
|
1979
|
+
);
|
|
1980
|
+
```
|
|
1981
|
+
|
|
1982
|
+
`Effect<F>` and `effect` change what the checker sees. At run time the function is the same.
|
|
1983
|
+
|
|
1984
|
+
## Type the fields (optional)
|
|
1985
|
+
|
|
1986
|
+
A handler of a command, job or module with a scope sees the fields of the scope in `ctx`, typed from the values you gave. You write no `WithContext`.
|
|
1987
|
+
|
|
1988
|
+
```ts
|
|
1989
|
+
import { createCommand } from "nanoeffects/actions";
|
|
1990
|
+
|
|
1991
|
+
type User = { id: string; name: string };
|
|
1992
|
+
|
|
1993
|
+
const getUser = createCommand(
|
|
1994
|
+
"user/get",
|
|
1995
|
+
async (id: string, ctx) => {
|
|
1996
|
+
ctx.cache; // Map<string, User>, from the scope below
|
|
1997
|
+
return ctx.cache.get(id);
|
|
1998
|
+
},
|
|
1999
|
+
{ cache: new Map<string, User>() },
|
|
2000
|
+
);
|
|
2001
|
+
```
|
|
2002
|
+
|
|
2003
|
+
- A field of a scope replaces a field of the same name from the bus in the type too.
|
|
2004
|
+
- An override in `emit` must match the type of the field it replaces: `emit(getUser("1"), { cache: 1 })` is an error.
|
|
2005
|
+
- 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.
|
|
2006
|
+
- `bus.on(creator, fn)` for an action with a scope types `ctx` with its fields.
|
|
2007
|
+
|
|
2008
|
+
## Limits
|
|
2009
|
+
|
|
2010
|
+
- No plugin, no scope: see [Turn scopes on](#turn-scopes-on).
|
|
2011
|
+
- The fields of the bus are not in the handler of a command made with `createCommand(…, scope)` unless you type `ctx` yourself; the fields of the scope are.
|
|
2012
|
+
- The string form of `emit` takes no scope.
|
|
2013
|
+
- `effect(fn)` works only as a value of a scope, not as an entry of `effects`.
|
|
2014
|
+
- A scope on a command, action, job or module is typed without the events of a bus, so `take` in its `effects` takes any event name.
|
|
2015
|
+
|
|
1736
2016
|
---
|
|
1737
2017
|
Source: docs/typescript.md
|
|
1738
2018
|
|
package/llms.txt
CHANGED
|
@@ -29,6 +29,7 @@ Rules that generated code often gets wrong:
|
|
|
29
29
|
- [Connect Redux, Zustand, MobX, or Pinia](docs/stores.md): the Redux middleware and callbacks of other stores
|
|
30
30
|
- [Build a sign-up form in React](docs/react.md): a form with a username check, a submit that cannot fire twice, and retry
|
|
31
31
|
- [Context fields](docs/context.md): put a store or client on every task
|
|
32
|
+
- [Scopes](docs/scope.md): fields for one command, action, job, module or `emit`, and a way to replace them in a test
|
|
32
33
|
|
|
33
34
|
## TypeScript
|
|
34
35
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nanoeffects",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
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.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"event-bus",
|
|
@@ -74,6 +74,10 @@
|
|
|
74
74
|
"types": "./dist/actions.d.ts",
|
|
75
75
|
"import": "./dist/actions.js"
|
|
76
76
|
},
|
|
77
|
+
"./scope": {
|
|
78
|
+
"types": "./dist/scope.d.ts",
|
|
79
|
+
"import": "./dist/scope.js"
|
|
80
|
+
},
|
|
77
81
|
"./trace": {
|
|
78
82
|
"types": "./dist/trace.d.ts",
|
|
79
83
|
"import": "./dist/trace.js"
|
|
@@ -173,7 +177,12 @@
|
|
|
173
177
|
{
|
|
174
178
|
"name": "actions (all)",
|
|
175
179
|
"path": "dist/actions.js",
|
|
176
|
-
"limit": "
|
|
180
|
+
"limit": "2.1 kB"
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"name": "scope (plugin and effect)",
|
|
184
|
+
"path": "dist/scope.js",
|
|
185
|
+
"limit": "0.75 kB"
|
|
177
186
|
},
|
|
178
187
|
{
|
|
179
188
|
"name": "redux",
|