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.
Files changed (60) hide show
  1. package/README.md +53 -55
  2. package/dist/{action-Bdfp-Wvd.d.ts → action-creator-DOWcbXpy.d.ts} +79 -73
  3. package/dist/actions.d.ts +75 -6
  4. package/dist/actions.js +1 -1
  5. package/dist/bus-BAs9x7IQ.js +1 -0
  6. package/dist/{context-DlA80GOA.js → context-CK0ZomWt.js} +1 -1
  7. package/dist/context-plugin-B0sLWOxR.d.ts +34 -0
  8. package/dist/dev.d.ts +2 -2
  9. package/dist/effects.d.ts +153 -56
  10. package/dist/effects.js +1 -1
  11. package/dist/index.d.ts +3 -2
  12. package/dist/index.js +1 -1
  13. package/dist/{module-I1ixyJzx.d.ts → module-DCJl-qfi.d.ts} +24 -9
  14. package/dist/notrace/{action-Bdfp-Wvd.d.ts → action-creator-DOWcbXpy.d.ts} +79 -73
  15. package/dist/notrace/actions.d.ts +97 -13
  16. package/dist/notrace/actions.js +1 -1
  17. package/dist/notrace/bus-Bdk0YyFQ.js +1 -0
  18. package/dist/notrace/{context-yAWj0BFZ.js → context-CctVmHLC.js} +1 -1
  19. package/dist/notrace/context-plugin-B0sLWOxR.d.ts +34 -0
  20. package/dist/notrace/{delay-Cgvn16oY.js → delay-B_W1tnkS.js} +1 -1
  21. package/dist/notrace/effects.d.ts +153 -56
  22. package/dist/notrace/effects.js +1 -1
  23. package/dist/notrace/index.d.ts +3 -2
  24. package/dist/notrace/index.js +1 -1
  25. package/dist/notrace/policies.d.ts +1 -1
  26. package/dist/notrace/policies.js +1 -1
  27. package/dist/notrace/redux.d.ts +18 -20
  28. package/dist/notrace/redux.js +1 -1
  29. package/dist/observable.d.ts +1 -1
  30. package/dist/policies.d.ts +1 -1
  31. package/dist/policies.js +1 -1
  32. package/dist/react.d.ts +50 -0
  33. package/dist/react.js +1 -0
  34. package/dist/redux.d.ts +18 -20
  35. package/dist/redux.js +1 -1
  36. package/dist/trace.d.ts +1 -1
  37. package/docs/README.md +30 -9
  38. package/docs/actions.md +117 -41
  39. package/docs/context.md +48 -7
  40. package/docs/effects.md +229 -73
  41. package/docs/errors.md +23 -10
  42. package/docs/guide.md +238 -129
  43. package/docs/hmr.md +25 -13
  44. package/docs/migrating-from-effector.md +293 -0
  45. package/docs/migrating-from-rxjs.md +127 -0
  46. package/docs/migrating-from-saga.md +93 -0
  47. package/docs/observable.md +11 -9
  48. package/docs/package.md +24 -0
  49. package/docs/policies.md +93 -30
  50. package/docs/react.md +157 -0
  51. package/docs/semantics.md +5 -3
  52. package/docs/stores.md +488 -25
  53. package/docs/tracing.md +69 -54
  54. package/docs/typescript.md +153 -0
  55. package/llms-full.txt +2645 -0
  56. package/llms.txt +48 -0
  57. package/package.json +39 -9
  58. package/dist/bus-CRnlwmQ4.js +0 -1
  59. package/dist/notrace/bus-Dx0l9X6h.js +0 -1
  60. package/dist/notrace/event-0RIBGYqS.js +0 -1
package/docs/tracing.md CHANGED
@@ -1,111 +1,126 @@
1
- # Tracing
1
+ # Trace tasks
2
2
 
3
- ## Tracing
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
- Pass `trace` to `createBus` to observe every task phase. 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.
5
+ ## Attach a tracer
6
6
 
7
- ```ts
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<Events>({
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 | Meaning |
19
- | --------- | -------------------------------------------------------------------- |
20
- | `id` | Unique task id within the bus. |
21
- | `parent` | Id of the task that started this one (absent for handler tasks). |
22
- | `type` | Event type the task works on. |
23
- | `effect` | `latest`, `debounce`, `throttle`, `leading`, `run`, `all` or `race`. |
24
- | `phase` | `start`, `done`, `error`, `cancel` or `drop`. |
25
- | `payload` | Event payload. |
26
- | `error` | Thrown value, only for `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`: the work began.
31
- - `done` / `error`: it finished or threw.
32
- - `cancel`: it was aborted after it started (superseded by `latest`, parent aborted, lost a `race`).
33
- - `drop`: it never started (`leading` while busy, a `throttle` call replaced by a newer one, a `debounce` call replaced before its delay ended).
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, every starter is a child `run` 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, { user: () => fetchUser(id), posts: () => fetchPosts(id) });
47
+ await all(ctx, {
48
+ user: () => fetchUser(id),
49
+ posts: () => fetchPosts(id),
50
+ });
40
51
  });
41
52
  ```
42
53
 
43
- ## Loggers
54
+ ## Use built-in loggers
44
55
 
45
- A logger is a function `(event: TraceEvent) => void` (the `TraceLogger` type). `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.
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
- ```ts
58
+ ```js
48
59
  import { createBus } from "nanoeffects";
49
60
  import { consoleLogger, createTracer, reduxDevtools } from "nanoeffects/trace";
50
61
 
51
- const bus = createBus<Events>({
62
+ const bus = createBus({
52
63
  trace: createTracer([reduxDevtools({ name: "app" }), consoleLogger()]),
53
64
  });
54
65
  ```
55
66
 
56
- `nanoeffects/trace` ships `createTracer` and these loggers, each a factory that returns a `TraceLogger`:
57
-
58
- | Logger | What it does |
59
- | ----------------------- | ------------------------------------------------------------------------------------ |
60
- | `reduxDevtools()` | Sends tasks to Redux DevTools as `type/effect/phase`; state is `{ running }`. |
61
- | `consoleLogger()` | Prints one line per phase, indented by task depth. |
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
- ### Production: filter and sampling
74
+ ### Cut volume in production
66
75
 
67
- `createTracer(loggers, options)` can cut the volume:
76
+ ```js
77
+ import { createTracer } from "nanoeffects/trace";
68
78
 
69
- ```ts
70
79
  createTracer(logger, {
71
- sample: 0.05, // trace 5% of the handler calls (a share, or a function of { type, payload })
72
- filter: (event) => event.phase === "error", // and keep only failures
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 (one handler call) and every task below it follows, so a trace is never cut in the middle. Tasks that are not sampled cost almost nothing: their effects run without tracing.
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
- Notes on `reduxDevtools`: it is read-only (the bus owns no state, so there is no time travel) and does nothing when the extension is not installed. It has its own instance in the DevTools picker, apart from your store.
88
+ ### Configure Redux DevTools
80
89
 
81
- It is quiet by default, so the log stays readable:
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
- - **A task is one action**, sent when it ends (`done`, `error`, `cancel`, `drop`). The tasks that are running are in the state, `{ running }`. `starts: true` also sends every start.
84
- - **Only the top is shown**: the handler tasks and the effects they start (`retry`, `timeout`, ...). The attempts and starters under them are hidden. `depth: 0` shows the handler tasks only, `depth: 3` more, `Infinity` everything.
85
- - `maxAge: 200` sets how many actions DevTools keeps (its own default is 50).
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, for a hunt for a bug
102
+ reduxDevtools({ name: "app", starts: true, depth: Infinity }); // everything
90
103
  ```
91
104
 
92
- For more control, `createTracer(logger, { filter, sample })` cuts events before any logger sees them.
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") report(event.error);
111
+ if (event.phase === "error") {
112
+ report(event.error);
113
+ }
101
114
  };
102
115
  ```
103
116
 
104
- ## Removing tracing from a build
117
+ ## Drop tracing from a build
105
118
 
106
- Without a tracer the hooks cost almost nothing (a lookup that finds no tracer), and `nanoeffects/trace` is a separate entry that is only bundled when imported. To also drop the tracing code from the core, use the `notrace` export condition: the package ships a second build of `index`, `effects`, `policies` and `actions` made with `__NANOEFFECTS_TRACE__` set to `false`, so the bundler removes the tracer calls.
119
+ Without a tracer the hooks cost almost nothing. `nanoeffects/trace` is only bundled when imported.
107
120
 
108
- ```ts
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 of your own from the sources, `define: { __NANOEFFECTS_TRACE__: "false" }` does the same. With the condition set, the types of that build have no `trace` option: `createBus({ trace })` is a type error, so tracing cannot be left on by mistake (`scripts/notrace-types.mjs` makes `BusOptions.trace` `never` there). `nanoeffects/trace` has no `notrace` build. The saving is about 0.4 kB gzip of the whole library.
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`.