nanoeffects 0.3.0 → 0.5.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 +53 -55
- package/dist/{action-Bdfp-Wvd.d.ts → action-creator-DOWcbXpy.d.ts} +79 -73
- package/dist/actions.d.ts +75 -6
- package/dist/actions.js +1 -1
- package/dist/bus-BAs9x7IQ.js +1 -0
- package/dist/{context-DlA80GOA.js → context-CK0ZomWt.js} +1 -1
- package/dist/context-plugin-B0sLWOxR.d.ts +34 -0
- package/dist/dev.d.ts +2 -2
- package/dist/effects.d.ts +153 -56
- package/dist/effects.js +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -1
- package/dist/{module-I1ixyJzx.d.ts → module-DCJl-qfi.d.ts} +24 -9
- package/dist/notrace/{action-Bdfp-Wvd.d.ts → action-creator-DOWcbXpy.d.ts} +79 -73
- package/dist/notrace/actions.d.ts +97 -13
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/bus-Bdk0YyFQ.js +1 -0
- package/dist/notrace/{context-yAWj0BFZ.js → context-CctVmHLC.js} +1 -1
- package/dist/notrace/context-plugin-B0sLWOxR.d.ts +34 -0
- package/dist/notrace/{delay-Cgvn16oY.js → delay-B_W1tnkS.js} +1 -1
- package/dist/notrace/effects.d.ts +153 -56
- package/dist/notrace/effects.js +1 -1
- package/dist/notrace/index.d.ts +3 -2
- package/dist/notrace/index.js +1 -1
- package/dist/notrace/policies.d.ts +1 -1
- package/dist/notrace/policies.js +1 -1
- package/dist/notrace/redux.d.ts +18 -20
- package/dist/notrace/redux.js +1 -1
- package/dist/observable.d.ts +1 -1
- package/dist/policies.d.ts +1 -1
- package/dist/policies.js +1 -1
- package/dist/react.d.ts +50 -0
- package/dist/react.js +1 -0
- package/dist/redux.d.ts +18 -20
- package/dist/redux.js +1 -1
- package/dist/trace.d.ts +1 -1
- package/docs/README.md +30 -9
- package/docs/actions.md +117 -41
- package/docs/context.md +48 -7
- package/docs/effects.md +229 -73
- package/docs/errors.md +23 -10
- package/docs/guide.md +238 -129
- package/docs/hmr.md +25 -13
- package/docs/migrating-from-effector.md +293 -0
- package/docs/migrating-from-rxjs.md +127 -0
- package/docs/migrating-from-saga.md +93 -0
- package/docs/observable.md +11 -9
- package/docs/package.md +24 -0
- package/docs/policies.md +93 -30
- package/docs/react.md +157 -0
- package/docs/semantics.md +5 -3
- package/docs/stores.md +488 -25
- package/docs/tracing.md +69 -54
- package/docs/typescript.md +153 -0
- package/llms-full.txt +2645 -0
- package/llms.txt +48 -0
- package/package.json +39 -9
- package/dist/bus-CRnlwmQ4.js +0 -1
- package/dist/notrace/bus-Dx0l9X6h.js +0 -1
- package/dist/notrace/event-0RIBGYqS.js +0 -1
package/docs/tracing.md
CHANGED
|
@@ -1,111 +1,126 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Trace tasks
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Reference for long-lived apps that need to observe task phases, Redux DevTools, or a build without tracing. Not part of onboarding.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Attach a tracer
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Pass `trace` to `createBus`. Make it with `createTracer` from `nanoeffects/trace`, which takes a logger (a function) or an array of them. The tracing code lives in that entry, so a bus without `trace` does not carry it.
|
|
8
|
+
|
|
9
|
+
```js
|
|
8
10
|
import { createBus } from "nanoeffects";
|
|
9
11
|
import { createTracer } from "nanoeffects/trace";
|
|
10
12
|
|
|
11
|
-
const bus = createBus
|
|
13
|
+
const bus = createBus({
|
|
14
|
+
// createTracer — observe every task phase
|
|
12
15
|
trace: createTracer((event) => console.log(event)),
|
|
13
16
|
});
|
|
14
17
|
```
|
|
15
18
|
|
|
16
19
|
Each `TraceEvent` has:
|
|
17
20
|
|
|
18
|
-
| Field
|
|
19
|
-
|
|
|
20
|
-
| `id`
|
|
21
|
-
| `parent`
|
|
22
|
-
| `type`
|
|
23
|
-
| `effect`
|
|
24
|
-
| `phase`
|
|
25
|
-
| `payload` | Event payload
|
|
26
|
-
| `error`
|
|
21
|
+
| Field | Meaning |
|
|
22
|
+
| ----- | ------- |
|
|
23
|
+
| `id` | Unique task id within the bus |
|
|
24
|
+
| `parent` | Id of the task that started this one (absent for handler tasks) |
|
|
25
|
+
| `type` | Event type the task works on |
|
|
26
|
+
| `effect` | `latest`, `debounce`, `throttle`, `leading`, `run`, `all`, `race`, … |
|
|
27
|
+
| `phase` | `start`, `done`, `error`, `cancel`, or `drop` |
|
|
28
|
+
| `payload` | Event payload |
|
|
29
|
+
| `error` | Thrown value, only for `error` |
|
|
27
30
|
|
|
28
31
|
Phases:
|
|
29
32
|
|
|
30
|
-
- `start
|
|
31
|
-
- `done` / `error
|
|
32
|
-
- `cancel
|
|
33
|
-
- `drop
|
|
33
|
+
- `start` — the work began.
|
|
34
|
+
- `done` / `error` — it finished or threw.
|
|
35
|
+
- `cancel` — aborted after it started (superseded by `latest`, parent aborted, lost a `race`).
|
|
36
|
+
- `drop` — never started (`leading` while busy, a `throttle` call replaced by a newer one, a `debounce` call replaced before its delay ended).
|
|
34
37
|
|
|
35
|
-
`all` and `race` are traced when you pass them the handler `ctx`: the group is one task
|
|
38
|
+
`all` and `race` are traced when you pass them the handler `ctx`: the group is one task; every starter is a child `run` task.
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
import { createBus } from "nanoeffects";
|
|
42
|
+
import { all } from "nanoeffects/effects";
|
|
43
|
+
|
|
44
|
+
const bus = createBus();
|
|
36
45
|
|
|
37
|
-
```ts
|
|
38
46
|
bus.on("load", async (id, ctx) => {
|
|
39
|
-
await all(ctx, {
|
|
47
|
+
await all(ctx, {
|
|
48
|
+
user: () => fetchUser(id),
|
|
49
|
+
posts: () => fetchPosts(id),
|
|
50
|
+
});
|
|
40
51
|
});
|
|
41
52
|
```
|
|
42
53
|
|
|
43
|
-
##
|
|
54
|
+
## Use built-in loggers
|
|
44
55
|
|
|
45
|
-
A logger is
|
|
56
|
+
A logger is `(event) => void`. `createTracer` takes one logger or an array. A logger that throws is reported to `console.error` and never breaks the task or the other loggers.
|
|
46
57
|
|
|
47
|
-
```
|
|
58
|
+
```js
|
|
48
59
|
import { createBus } from "nanoeffects";
|
|
49
60
|
import { consoleLogger, createTracer, reduxDevtools } from "nanoeffects/trace";
|
|
50
61
|
|
|
51
|
-
const bus = createBus
|
|
62
|
+
const bus = createBus({
|
|
52
63
|
trace: createTracer([reduxDevtools({ name: "app" }), consoleLogger()]),
|
|
53
64
|
});
|
|
54
65
|
```
|
|
55
66
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
62
|
-
| `performanceLogger()` | Records User Timing measures for the Timings track of the browser Performance panel. |
|
|
63
|
-
| `createTraceRecorder()` | Keeps events in `recorder.events`, for tests. |
|
|
67
|
+
| Logger | What it does |
|
|
68
|
+
| ------ | ------------ |
|
|
69
|
+
| `reduxDevtools()` | Sends tasks to Redux DevTools as `type/effect/phase`; state is `{ running }` |
|
|
70
|
+
| `consoleLogger()` | Prints one line per phase, indented by task depth |
|
|
71
|
+
| `performanceLogger()` | User Timing measures for the browser Performance panel |
|
|
72
|
+
| `createTraceRecorder()` | Keeps events in `recorder.events`, for tests |
|
|
64
73
|
|
|
65
|
-
###
|
|
74
|
+
### Cut volume in production
|
|
66
75
|
|
|
67
|
-
|
|
76
|
+
```js
|
|
77
|
+
import { createTracer } from "nanoeffects/trace";
|
|
68
78
|
|
|
69
|
-
```ts
|
|
70
79
|
createTracer(logger, {
|
|
71
|
-
sample: 0.05, // trace 5% of
|
|
72
|
-
filter: (event) => event.phase === "error", //
|
|
80
|
+
sample: 0.05, // trace 5% of handler calls (share, or function of { type, payload })
|
|
81
|
+
filter: (event) => event.phase === "error", // keep only failures
|
|
73
82
|
});
|
|
74
83
|
```
|
|
75
84
|
|
|
76
|
-
- `sample` is decided once per root task
|
|
85
|
+
- `sample` is decided once per root task; every task below it follows, so a trace is never cut in the middle. Unsampled tasks cost almost nothing.
|
|
77
86
|
- `filter` runs on each event before any logger sees it.
|
|
78
87
|
|
|
79
|
-
|
|
88
|
+
### Configure Redux DevTools
|
|
80
89
|
|
|
81
|
-
|
|
90
|
+
Read-only (the bus owns no state, so there is no time travel). Does nothing when the extension is not installed. Has its own instance in the DevTools picker, apart from your store.
|
|
82
91
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
- `
|
|
92
|
+
Quiet by default:
|
|
93
|
+
|
|
94
|
+
- **A task is one action**, sent when it ends (`done`, `error`, `cancel`, `drop`). Running tasks are in `{ running }`. `starts: true` also sends every start.
|
|
95
|
+
- **Only the top is shown**: handler tasks and the effects they start. Attempts and starters under them are hidden. `depth: 0` — handlers only; `depth: 3` — more; `Infinity` — everything.
|
|
96
|
+
- `maxAge: 200` — how many actions DevTools keeps (its default is 50).
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
import { reduxDevtools } from "nanoeffects/trace";
|
|
86
100
|
|
|
87
|
-
```ts
|
|
88
101
|
reduxDevtools({ name: "app", depth: 0 }); // one line per handler call
|
|
89
|
-
reduxDevtools({ name: "app", starts: true, depth: Infinity }); // everything
|
|
102
|
+
reduxDevtools({ name: "app", starts: true, depth: Infinity }); // everything
|
|
90
103
|
```
|
|
91
104
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
To write your own, return a `TraceLogger`:
|
|
105
|
+
### Write your own logger
|
|
95
106
|
|
|
96
107
|
```ts
|
|
97
108
|
import type { TraceLogger } from "nanoeffects";
|
|
98
109
|
|
|
99
110
|
const errorsOnly = (): TraceLogger => (event) => {
|
|
100
|
-
if (event.phase === "error")
|
|
111
|
+
if (event.phase === "error") {
|
|
112
|
+
report(event.error);
|
|
113
|
+
}
|
|
101
114
|
};
|
|
102
115
|
```
|
|
103
116
|
|
|
104
|
-
##
|
|
117
|
+
## Drop tracing from a build
|
|
105
118
|
|
|
106
|
-
Without a tracer the hooks cost almost nothing
|
|
119
|
+
Without a tracer the hooks cost almost nothing. `nanoeffects/trace` is only bundled when imported.
|
|
107
120
|
|
|
108
|
-
|
|
121
|
+
To also drop tracing code from the core, use the `notrace` export condition. The package ships a second build of `index`, `effects`, `policies`, and `actions` with `__NANOEFFECTS_TRACE__` set to `false`.
|
|
122
|
+
|
|
123
|
+
```js
|
|
109
124
|
// vite / rolldown
|
|
110
125
|
resolve: { conditions: ["notrace"] }
|
|
111
126
|
// webpack
|
|
@@ -114,4 +129,4 @@ resolve: { conditionNames: ["notrace", "import", "..."] }
|
|
|
114
129
|
conditions: ["notrace"]
|
|
115
130
|
```
|
|
116
131
|
|
|
117
|
-
In a build
|
|
132
|
+
In a build from the sources, `define: { __NANOEFFECTS_TRACE__: "false" }` does the same. With the condition set, that build’s types have no `trace` option: `createBus({ trace })` is a type error. `nanoeffects/trace` has no `notrace` build. The saving is about 0.4 kB gzip of the whole library.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Use nanoeffects in a TypeScript app with many files
|
|
2
|
+
|
|
3
|
+
Read this page if your app is in TypeScript and the commands and modules live in different files. It shows how to write a command so that TypeScript infers its payload and result, and what to add when the handlers need the fields of your bus (`ctx.store`, the effects of `effectsPlugin`). For one file or for JavaScript you do not need it.
|
|
4
|
+
|
|
5
|
+
## Write a command with its handler
|
|
6
|
+
|
|
7
|
+
Give `createCommand` the handler and write nothing else. The payload comes from the annotated parameter, the result from the body. You do not write a generic.
|
|
8
|
+
|
|
9
|
+
<!-- docs-check: skip - a file of examples/typescript, type-checked there -->
|
|
10
|
+
```ts
|
|
11
|
+
// user.ts
|
|
12
|
+
import { createCommand, createModule } from "nanoeffects/actions";
|
|
13
|
+
|
|
14
|
+
export type User = { id: string; name: string };
|
|
15
|
+
|
|
16
|
+
// payload: string, from `id: string`; result: Promise<User>, from the body
|
|
17
|
+
export const getUser = createCommand(
|
|
18
|
+
"user/get",
|
|
19
|
+
async (id: string, { signal }) => {
|
|
20
|
+
const response = await fetch(`/api/users/${id}`, { signal });
|
|
21
|
+
const user: User = await response.json();
|
|
22
|
+
return user;
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// createModule: m.use binds the handler the command carries, while the module is mounted
|
|
27
|
+
export const userModule = createModule("user", (m) => {
|
|
28
|
+
m.use(getUser);
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Mount the module where you create the bus. The result is typed from the handler:
|
|
33
|
+
|
|
34
|
+
<!-- docs-check: skip - a file of examples/typescript, type-checked there -->
|
|
35
|
+
```ts
|
|
36
|
+
// main.ts
|
|
37
|
+
import { createBus } from "nanoeffects";
|
|
38
|
+
import { modulePlugin } from "nanoeffects/actions";
|
|
39
|
+
import { getUser, userModule } from "./user";
|
|
40
|
+
|
|
41
|
+
const bus = createBus();
|
|
42
|
+
const app = bus.use(modulePlugin([userModule])); // mounts the modules
|
|
43
|
+
|
|
44
|
+
const user = bus.emit(getUser("1")); // Promise<User>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
What you annotate, and why:
|
|
48
|
+
|
|
49
|
+
| Part | Annotated? | Why |
|
|
50
|
+
| ---- | ---------- | --- |
|
|
51
|
+
| payload (`id: string`) | yes | a function parameter has no type to infer from |
|
|
52
|
+
| `ctx` | no, for `signal` and the fields every task has | `signal`, `emit`, `on` and `run` are on every `ctx` |
|
|
53
|
+
| result | no | inferred from the body |
|
|
54
|
+
|
|
55
|
+
A wrong payload at the call (`getUser(1)`) is an error. Annotate the parameters, not the variable: a type on the whole variable, such as `const handler: Task<string, Events> = …`, fixes the type of the function, and the result becomes `unknown`.
|
|
56
|
+
|
|
57
|
+
## Give `ctx` the fields of your bus
|
|
58
|
+
|
|
59
|
+
Types of a bus live on the bus. `contextPlugin({ store })` and `effectsPlugin({ race })` make `ctx.store` and `ctx.race` known to the handlers of **that bus**. A command made in another file does not see the bus, so its `ctx` has no `store`:
|
|
60
|
+
|
|
61
|
+
<!-- docs-check: skip - shows the error on purpose -->
|
|
62
|
+
```ts
|
|
63
|
+
export const getUser = createCommand("user/get", (id: string, ctx) => {
|
|
64
|
+
ctx.store; // error: Property 'store' does not exist
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
At runtime the bus still gives the task its fields. Only the checker does not know it. Name the types of the bus once, in the file that creates it. `contextPlugin` takes the type of each field from the value you pass, so the event map holds only events. `EventsOf` reads the event map from the bus, with the fields that the plugins added. The code of this section is in [`examples/typescript-context`](../examples/typescript-context).
|
|
69
|
+
|
|
70
|
+
<!-- docs-check: skip - a file of examples/typescript-context, type-checked there -->
|
|
71
|
+
```ts
|
|
72
|
+
// bus.ts
|
|
73
|
+
import { type Context, contextPlugin, createBus, type EventsOf } from "nanoeffects";
|
|
74
|
+
import { modulePlugin } from "nanoeffects/actions";
|
|
75
|
+
import { effectsPlugin, race } from "nanoeffects/effects";
|
|
76
|
+
|
|
77
|
+
// Events: event name → payload; nothing about ctx is written here
|
|
78
|
+
type Events = { "user/loaded": User };
|
|
79
|
+
|
|
80
|
+
export const bus = createBus<Events>()
|
|
81
|
+
.use(contextPlugin({ store })) // ctx.store, typed from the value `store`
|
|
82
|
+
.use(effectsPlugin({ race })); // ctx.race; each plugin returns the same bus, typed with its fields
|
|
83
|
+
|
|
84
|
+
// the app of the bus: mounts modules and creates them with the types of the bus
|
|
85
|
+
export const app = bus.use(modulePlugin()); // no modules yet: main.ts mounts them
|
|
86
|
+
export const { createModule } = app; // createModule with the types of the bus
|
|
87
|
+
|
|
88
|
+
export type AppEvents = EventsOf<typeof bus>; // the event map and all fields of ctx
|
|
89
|
+
export type AppContext = Context<AppEvents>; // ctx of a handler of this bus: has store and race
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
| Type | What it is | Why you name it |
|
|
93
|
+
| ---- | ---------- | --------------- |
|
|
94
|
+
| `EventsOf<typeof bus>` | the event map of the bus, with the context fields of the plugins (and of `WithContext`, if you use it) | the source of `AppContext` |
|
|
95
|
+
| `Context<AppEvents>` | the `ctx` that a handler of the bus gets | annotate the `ctx` of a handler given to `createCommand` |
|
|
96
|
+
|
|
97
|
+
These types change nothing at runtime. They only tell the checker what the bus already does.
|
|
98
|
+
|
|
99
|
+
Annotate `ctx` with `AppContext` where the handler reads a field of the bus, and create the module with the `createModule` that `bus.ts` exports, so its `m.on` handlers see the fields too:
|
|
100
|
+
|
|
101
|
+
<!-- docs-check: skip - a file of examples/typescript-context, type-checked there -->
|
|
102
|
+
```ts
|
|
103
|
+
// user.ts
|
|
104
|
+
import { createCommand } from "nanoeffects/actions";
|
|
105
|
+
import { type AppContext, createModule } from "./bus";
|
|
106
|
+
|
|
107
|
+
export const getUser = createCommand(
|
|
108
|
+
"user/get",
|
|
109
|
+
(id: string, { store, signal }: AppContext) => store.fetchUser(id, { signal }),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// createModule from bus.ts: handlers of the module see the context fields of the bus
|
|
113
|
+
export const userModule = createModule("user", (m) => {
|
|
114
|
+
m.use(getUser);
|
|
115
|
+
m.on("user/loaded", (user, { store }) => store.remember(user));
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
<!-- docs-check: skip - a file of examples/typescript-context, type-checked there -->
|
|
120
|
+
```ts
|
|
121
|
+
// main.ts: the modules are mounted here, because the module files need `app`
|
|
122
|
+
import { app, bus } from "./bus";
|
|
123
|
+
import { getUser, userModule } from "./user";
|
|
124
|
+
|
|
125
|
+
const unmount = app.mount(userModule); // starts the module; unmount() undoes it
|
|
126
|
+
const user = bus.emit(getUser("1")); // Promise<User>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`createModule` of the app only creates the module: it does not mount it, and it keeps no link to the bus. That is why the app in `bus.ts` starts without modules. `createModule` from `nanoeffects/actions` stays for a module made without a bus: there you write the event map yourself, `createModule<AppEvents>("user", …)`.
|
|
130
|
+
|
|
131
|
+
## Keep the contract apart from the code
|
|
132
|
+
|
|
133
|
+
When the file with the command must not import the code that implements it, make the command without a handler. Here you write the payload and the result once, as generics, and the handler goes to a module, where it needs no annotation:
|
|
134
|
+
|
|
135
|
+
<!-- docs-check: skip - fragment of the file above -->
|
|
136
|
+
```ts
|
|
137
|
+
export const getUser = createCommand<string, Promise<User>>("user/get"); // the contract
|
|
138
|
+
|
|
139
|
+
createModule("user", (m) => {
|
|
140
|
+
// payload and result come from the command, ctx from the app: no annotation
|
|
141
|
+
m.handle(getUser, (id, { store, signal }) => store.fetchUser(id, { signal }));
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
A wrong payload, a wrong field of `ctx` and a wrong result are errors.
|
|
146
|
+
|
|
147
|
+
## Check your setup
|
|
148
|
+
|
|
149
|
+
- **A field is missing on `ctx`.** The `ctx` of a handler given to `createCommand` is not annotated: add `: AppContext`. A handler written inline in `m.handle` or `m.on` of a module made with the `createModule` of `bus.ts` does not need it. The default `ctx` has no fields of your bus.
|
|
150
|
+
- **`EventsOf<typeof bus>` is `never`.** `bus` is not a bus. `bus.use(commandsPlugin())` and `bus.use(modulePlugin(...))` return the registry or the app, not the bus: keep the bus in its own constant (`bus`, then `app = bus.use(modulePlugin())`).
|
|
151
|
+
- **`effectsPlugin` is not in the type.** `use` must come before you read `typeof bus`. Chain `createBus(...).use(effectsPlugin(...))` in one binding.
|
|
152
|
+
- **A policy loses the types.** It does not: `latest(handler)` keeps the payload, the result and the context of `handler`.
|
|
153
|
+
- **Do not pass the context as a generic of `createCommand`.** It is the fourth generic, after the payload, the result and the type string, so you would have to write all four. Annotate `ctx` with `AppContext`, or put the handler in a module made with the `createModule` of `bus.ts`.
|