nanoeffects 0.9.0 → 0.11.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 (68) hide show
  1. package/dist/{action-creator-BrZF6hXc.d.ts → action-creator-9e8jesRa.d.ts} +28 -12
  2. package/dist/actions.d.ts +12 -5
  3. package/dist/actions.js +1 -1
  4. package/dist/bus-Cdw5ajKF.js +1 -0
  5. package/dist/context-CJGW6OWj.js +1 -0
  6. package/dist/{context-plugin--HkNN1ha.d.ts → context-plugin-Xibukkbj.d.ts} +1 -1
  7. package/dist/{delay-CIHnOh_L.js → delay-BnBdGPh7.js} +1 -1
  8. package/dist/dev.d.ts +2 -2
  9. package/dist/effects.d.ts +2 -2
  10. package/dist/effects.js +1 -1
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.js +1 -1
  13. package/dist/lifecycle-key-8iX8RUVD.js +1 -0
  14. package/dist/match.d.ts +1 -1
  15. package/dist/match.js +1 -1
  16. package/dist/{module-DqKegXHt.d.ts → module-BlkU3i6B.d.ts} +3 -3
  17. package/dist/notrace/{action-creator-DHAEgwuh.d.ts → action-creator-DQTwMBwP.d.ts} +27 -11
  18. package/dist/notrace/actions.d.ts +12 -5
  19. package/dist/notrace/actions.js +1 -1
  20. package/dist/notrace/bus-G2AdGObY.js +1 -0
  21. package/dist/notrace/{context-plugin-BapQkGwQ.d.ts → context-plugin-DlZCkcX9.d.ts} +1 -1
  22. package/dist/notrace/effects.d.ts +2 -2
  23. package/dist/notrace/index.d.ts +2 -2
  24. package/dist/notrace/index.js +1 -1
  25. package/dist/notrace/lifecycle-key-8iX8RUVD.js +1 -0
  26. package/dist/notrace/origin-key-BbWN3gmN.js +1 -0
  27. package/dist/notrace/origin-key-CxK0SkeG.d.ts +10 -0
  28. package/dist/notrace/{plugin-Cxadz6uU.d.ts → plugin-DOYHG78z.d.ts} +2 -2
  29. package/dist/notrace/policies.d.ts +1 -1
  30. package/dist/notrace/policies.js +1 -1
  31. package/dist/notrace/redux.d.ts +4 -3
  32. package/dist/notrace/redux.js +1 -1
  33. package/dist/observable.d.ts +1 -1
  34. package/dist/oop.d.ts +13 -7
  35. package/dist/oop.js +1 -1
  36. package/dist/origin-key-BbWN3gmN.js +1 -0
  37. package/dist/origin-key-CxK0SkeG.d.ts +10 -0
  38. package/dist/{plugin-DkXCvsmB.d.ts → plugin-DHkamZdH.d.ts} +2 -2
  39. package/dist/policies.d.ts +2 -2
  40. package/dist/policies.js +1 -1
  41. package/dist/react.d.ts +15 -8
  42. package/dist/react.js +1 -1
  43. package/dist/redux.d.ts +4 -3
  44. package/dist/redux.js +1 -1
  45. package/dist/scope.d.ts +2 -2
  46. package/dist/scope.js +1 -1
  47. package/dist/signal.d.ts +1 -1
  48. package/dist/signal.js +1 -1
  49. package/dist/trace.d.ts +30 -1
  50. package/dist/trace.js +1 -1
  51. package/dist/{types-KavbvHix.d.ts → types-ByPzv7tw.d.ts} +2 -2
  52. package/dist/{with-policy-DBV9dh2F.js → with-policy-CW50Edgk.js} +1 -1
  53. package/dist/{with-policy-CqUS-VWW.d.ts → with-policy-DpCvSZRk.d.ts} +1 -1
  54. package/docs/actions.md +10 -3
  55. package/docs/migrating-from-effector.md +1 -1
  56. package/docs/oop.md +2 -0
  57. package/docs/package.md +1 -1
  58. package/docs/plugins.md +16 -1
  59. package/docs/scope.md +1 -1
  60. package/docs/stores.md +5 -1
  61. package/docs/tracing.md +121 -1
  62. package/llms-full.txt +158 -10
  63. package/llms.txt +1 -1
  64. package/package.json +3 -3
  65. package/dist/bus-CkTHUAvq.js +0 -1
  66. package/dist/context-COgyNlrC.js +0 -1
  67. package/dist/notrace/bus-BmCTo8BG.js +0 -1
  68. package/dist/onAbort-BgMfspK4.js +0 -1
package/docs/tracing.md CHANGED
@@ -21,12 +21,14 @@ Each `TraceEvent` has:
21
21
  | Field | Meaning |
22
22
  | ----- | ------- |
23
23
  | `id` | Unique task id within the bus |
24
- | `parent` | Id of the task that started this one (absent for handler tasks) |
24
+ | `parentId` | Id of the task that started this one (absent for handler tasks) |
25
+ | `rootId` | Id of the root task of the chain; absent on the root itself, and it is the same through a listener that reports nothing |
25
26
  | `type` | Event type the task works on |
26
27
  | `effect` | `latest`, `debounce`, `throttle`, `leading`, `run`, `all`, `race`, … |
27
28
  | `phase` | `start`, `done`, `error`, `cancel`, or `drop` |
28
29
  | `payload` | Event payload |
29
30
  | `error` | Thrown value, only for `error` |
31
+ | `traceGroupId` | The flow of the task, when it or a task above has one |
30
32
 
31
33
  Phases:
32
34
 
@@ -51,6 +53,124 @@ bus.on("load", async (id, ctx) => {
51
53
  });
52
54
  ```
53
55
 
56
+ ## Show which subscription began a chain
57
+
58
+ **Problem.** `useEmitter` and `bus.emitter` put an event on the bus from outside any task, such as a render or the callback of a store. In the trace that event has no sign of where it came from. With many such subscriptions it is hard to tell which one started a chain of commands and retries.
59
+
60
+ **When.** The bus has a tracer and the project has several `useEmitter` or `bus.emitter` calls. The names come from `tracePlugin`: use it on the bus, or the `traceId` is ignored.
61
+
62
+ Give each one a `traceId`. When the bus has a tracer, the emit is made from a root task with that `traceId` as its `type` and `run` as its effect. What the event starts is below it. Without a tracer nothing changes.
63
+
64
+ ```js
65
+ import { createBus } from "nanoeffects";
66
+ import { createAction, emitterPlugin } from "nanoeffects/actions";
67
+ import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
68
+
69
+ // createTracer: reports every task phase to the console
70
+ const bus = createBus({ trace: createTracer(consoleLogger()) })
71
+ .use(tracePlugin()) // tracePlugin: makes the root task that traceId names
72
+ .use(emitterPlugin());
73
+
74
+ const cartChanged = createAction("cart/changed");
75
+
76
+ // emitter: a callback for a store; traceId names it in the trace
77
+ const emitCartChanged = bus.emitter(cartChanged, { traceId: "cartStore" });
78
+ ```
79
+
80
+ In a component, the same option goes in the options of the hook: `useEmitter(nameChanged, name, { traceId: "Profile" })`. In the console:
81
+
82
+ ```
83
+ [nanoeffects] Profile · run start
84
+ [nanoeffects] user/save · command start
85
+ [nanoeffects] user/save · command done
86
+ [nanoeffects] Profile · run done
87
+ ```
88
+
89
+ A `traceId` is a string you choose and keep stable: the name of the component or of the store. Without one the root has the type of the event.
90
+
91
+ Two things to know:
92
+
93
+ - **`sample` sees the `traceId`.** The root of an emitter is a task like any other, so a `sample` function gets `{ type, payload }` with the `traceId` as the `type` and the emitted action as the `payload`. A function that matches on the type of the event, such as `event.type === "search"`, does not pick a root that has another `traceId`; match the `traceId`, or the `payload.type`.
94
+ - **Inside a handler, emit with `ctx.emit`.** `ctx.emit` keeps the chain: what it starts is below the handler. A plain `bus.emit` inside a handler starts a new root with no parent and no `rootId`, so the trace loses the link.
95
+
96
+ Every event of a task below another one has `rootId`, the id of the root of the chain, also when a listener, which reports nothing, stands in between. `parentId` is the task directly above, which can be such a listener.
97
+
98
+ ## Follow one flow with a traceGroupId
99
+
100
+ **Problem.** A flow such as a checkout has several roots (a click, a timer, a store) and many branches. Parent ids give a tree, but you want to filter the log by the flow.
101
+
102
+ **When.** One business flow runs through several commands, and you read the log by flow.
103
+
104
+ Give the flow a `traceGroupId` in the scope of the command, the job or the action that starts it. Every task that it starts through `ctx.emit` or `ctx.run` has the same `traceGroupId`, and so does every trace event of those tasks. A task that declares its own `traceGroupId` uses it for itself and for what it starts.
105
+
106
+ ```js
107
+ import { createBus } from "nanoeffects";
108
+ import { createCommand } from "nanoeffects/actions";
109
+ import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
110
+
111
+ // tracePlugin: reads traceGroupId from the scope of a call
112
+ const bus = createBus({ trace: createTracer(consoleLogger()) }).use(tracePlugin());
113
+
114
+ // the scope names the flow; traceGroupId is for the tracer, not a field of ctx
115
+ const pay = createCommand("checkout/pay", async () => {}, {
116
+ traceGroupId: "checkout",
117
+ });
118
+ ```
119
+
120
+ ```
121
+ [nanoeffects] [checkout] checkout/pay · command start
122
+ ```
123
+
124
+ The emitters take it too: `useEmitter(cartChanged, cart, { traceId: "Cart", traceGroupId: "checkout" })`. `traceGroupId` works without `scopePlugin`, and needs `tracePlugin`.
125
+
126
+ ## Name a listener
127
+
128
+ **Problem.** The log shows what started a chain and what it started, but a listener of an event reports nothing, so you cannot tell which of several listeners ran.
129
+
130
+ **When.** An event has more than one listener and you read the trace to see which one did the work.
131
+
132
+ Give the listener a `traceId` when you register it. The plugin makes each call of it a task with that name, and what it emits with `ctx.emit` is below it. `off` works as before. It also works in a module (`m.on(event, handler, { traceId })`) and on an instance made by `extendBus`.
133
+
134
+ ```js
135
+ import { createBus } from "nanoeffects";
136
+ import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
137
+
138
+ const bus = createBus({ trace: createTracer(consoleLogger()) }).use(tracePlugin());
139
+
140
+ // traceId: the name of this listener in the trace
141
+ bus.on("name/changed", saveName, { traceId: "saveNotifier" });
142
+ bus.once("name/changed", (name) => console.log(name), { traceId: "auditLog" });
143
+ ```
144
+
145
+ ```
146
+ [nanoeffects] saveNotifier · run start
147
+ [nanoeffects] user/save · command start
148
+ [nanoeffects] saveNotifier · run done
149
+ ```
150
+
151
+ ## Keep the chain: `ctx.emit`, not `bus.emit`
152
+
153
+ Inside a handler, emit with `ctx.emit`. It keeps the chain: what it starts is below the handler, and its events have the `rootId` of the chain. A plain `bus.emit` inside a handler starts a new root with no `parentId` and no `rootId`, so the trace loses the link. The bus cannot warn about it, because it does not know which handler is running.
154
+
155
+ The names work wherever an event or a listener is made:
156
+
157
+ | Where | How to name it |
158
+ | ----- | -------------- |
159
+ | `useEmitter` and `bus.emitter` | `{ traceId, traceGroupId }` in the options |
160
+ | A Redux dispatch | `createEffectsMiddleware(bus, { traceId, traceGroupId })` |
161
+ | A listener, `bus.on` and `bus.once` | `{ traceId }` as the third argument |
162
+ | A listener of a module or of an instance of `extendBus` | the same third argument of its `on` and `once` |
163
+ | A method with `@On` or `@Once` | `{ traceId }` in the decorator |
164
+ | A command, a job or an action | `traceGroupId` in its scope |
165
+
166
+ ## Turn the names off
167
+
168
+ `tracePlugin` is optional, and none of it changes how your code runs:
169
+
170
+ - **Without the plugin**, or on a bus without a tracer, `traceId` and `traceGroupId` are ignored. Handlers run as they are, with the same results, errors and rejections, and `onError` sees the same errors.
171
+ - **The types are the same** with and without the plugin, so you can use it only in development: `if (isDev) bus.use(tracePlugin())` compiles in both builds.
172
+ - **A `traceGroupId` in a scope** is never put on `ctx`.
173
+
54
174
  ## Use built-in loggers
55
175
 
56
176
  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.
package/llms-full.txt CHANGED
@@ -9,7 +9,7 @@ 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`; `usePolicy`; needs React), `nanoeffects/trace`, `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`; `usePolicy`; needs React), `nanoeffects/trace` (`createTracer`, loggers, `tracePlugin`), `nanoeffects/dev`.
13
13
  - `bus.emit(command(payload))` runs the command's handler and returns its result. Plain events return nothing.
14
14
 
15
15
  ---
@@ -566,7 +566,7 @@ Imports are split so you only load what you use. `createBus` stays small; the re
566
566
  | `nanoeffects/observable` | `createObserve` |
567
567
  | `nanoeffects/react` | `reactPlugin`: adds the `useEmitter` hook to a bus (needs React 18 or newer) |
568
568
  | `nanoeffects/dev` | `devPlugin` (no-op in production) |
569
- | `nanoeffects/trace` | `createTracer` and the loggers |
569
+ | `nanoeffects/trace` | `createTracer`, the loggers and `tracePlugin` (names for the trace) |
570
570
 
571
571
  The `...Plugin` names above are used with `bus.use`. [Plugins](plugins.md) lists all of them, what each returns, and the order to use them in.
572
572
 
@@ -680,6 +680,7 @@ Add one of these when you meet its need. None of them is required.
680
680
  | [`signalPlugin`](#tie-an-emit-to-an-outside-signal-signalplugin) | `emit(call, { signal })` that aborts its tasks when an outside signal aborts. | `nanoeffects/signal` | The same bus, typed with the new `emit` |
681
681
  | [`matchPlugin`](#listen-to-a-group-of-events-matchplugin) | `on` that listens to a group of events which a function picks. | `nanoeffects/match` | The same bus, typed with the new overloads |
682
682
  | [`emitterPlugin`](#hand-the-bus-to-a-state-manager-emitterplugin) | A callback that emits an action, to give to a state manager. | `nanoeffects/actions` | The same bus, with `emitter` |
683
+ | [`tracePlugin`](#name-the-origin-and-the-flow-of-a-trace-traceplugin) | `traceId` and `traceGroupId` for a trace: the root of an emitter, a flow, a named listener. | `nanoeffects/trace` | The same bus |
683
684
  | [`reactPlugin`](#tell-the-bus-when-a-react-value-changes-reactplugin) | A React hook that emits when a value of a component changes. | `nanoeffects/react` | The same bus, with `useEmitter` |
684
685
  | [`devPlugin`](#find-leaks-while-you-develop-devplugin) | Warnings about listeners that pile up, and messages about modules, while you develop. | `nanoeffects/dev` | A function that disconnects it |
685
686
 
@@ -723,6 +724,20 @@ bus.emit(getUser("1"), { signal: controller.signal }); // runs the command
723
724
  controller.abort(); // ctx.signal of the handler aborts with it
724
725
  ```
725
726
 
727
+ ### Name the origin and the flow of a trace: `tracePlugin`
728
+
729
+ With a tracer on the bus, the plugin lets a trace say which subscription began a chain (`traceId` of `useEmitter` and `bus.emitter`), follow one flow by a `traceGroupId`, and show which listener ran (`bus.on(event, handler, { traceId })`). Without the plugin the options are ignored. More in [Tracing](tracing.md#show-which-subscription-began-a-chain).
730
+
731
+ ```js
732
+ import { createBus } from "nanoeffects";
733
+ import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
734
+
735
+ // tracePlugin — names the roots, groups and listeners of the trace
736
+ const bus = createBus({ trace: createTracer(consoleLogger()) }).use(tracePlugin());
737
+
738
+ bus.on("name/changed", saveName, { traceId: "saveNotifier" }); // a listener you can find in the log
739
+ ```
740
+
726
741
  ### Listen to a group of events: `matchPlugin`
727
742
 
728
743
  `on`, `once` and `off` take a function that picks the events, such as every `user/*` event. A type guard narrows the payload. The subscription listens to every event, so for a few known events `on(creator, handler)` is cheaper. More in the [Guide](guide.md#pick-events-with-a-function).
@@ -759,7 +774,7 @@ $name.listen(bus.emitter(nameChanged)); // every change of $name emits nameChang
759
774
 
760
775
  ### Tell the bus when a React value changes: `reactPlugin`
761
776
 
762
- `useEmitter(event, value)` emits after a render commits with a new value. It does not emit on mount, and not twice under StrictMode. It needs React 18 or newer. More in [Stores](stores.md#react-sharing-a-value-across-components).
777
+ `useEmitter(event, value, options?)` emits after a render commits with a new value. It does not emit on mount, and not twice under StrictMode. The options are `{ equals, traceId, traceGroupId }`; the last two name the emit in the [trace](tracing.md#show-which-subscription-began-a-chain). It needs React 18 or newer. More in [Stores](stores.md#react-sharing-a-value-across-components).
763
778
 
764
779
  ```jsx
765
780
  import { useState } from "react";
@@ -1859,7 +1874,7 @@ The registry lives in `nanoeffects/actions`, so a bus without one carries none o
1859
1874
  - When the task is aborted before the handler settles, emits `loadUser.cancel(reason)` instead of `success` or `fail`. After `pending`, exactly one of the three follows.
1860
1875
  - `pending` / `success` / `fail` / `cancel` are a string (job makes an action creator) or an existing action creator. Missing ones become `<type>/pending`, `<type>/success`, `<type>/fail` or `<type>/cancel`.
1861
1876
  - `bus.emit(loadUser(id))` returns a promise of the result. Handler errors are always handled: they go to `fail` and the promise resolves with `undefined`. For a result that must reject, use a command.
1862
- - Outcome actions are emitted as the task that ran the job (also for a handler bound with `commands.handle`). `success` and `fail` are not sent after abort; `cancel` is, once the handler settles.
1877
+ - Outcome actions are emitted as the task that ran the job (also for a handler bound with `commands.handle`). `success` and `fail` are not sent after abort; `cancel` is, at the moment of the abort.
1863
1878
  - A missing handler is not an outcome: `emit` throws `No handler for command "..."` and nothing is sent.
1864
1879
  - Calling the job only builds `{ type, payload }`; the handler runs on `emit`.
1865
1880
  - The outcomes are events like any other: with `onUnhandled` set on the bus, it is called for a `pending` or a `cancel` that nobody listens to.
@@ -1926,9 +1941,16 @@ import { loadUser } from "./user";
1926
1941
  bus.emit(loadUser("1"));
1927
1942
  ```
1928
1943
 
1929
- `cancel` is sent when the handler settles, not at the moment of the abort. A handler that stops on `ctx.signal` (as `fetch` does) settles at once. A handler that ignores the signal sends `cancel` when it finishes, and one that never finishes sends nothing.
1944
+ `cancel` is sent at the moment of the abort, with the abort reason. A handler that ignores the signal and finishes later sends nothing more, so the flag is already `false`.
1945
+
1946
+ **With a policy.** A policy such as `latest`, `leading`, `debounce`, `throttle` or `queue` around the handler tells the job what it does with each call:
1930
1947
 
1931
- **Limit: policies.** A policy such as `latest`, `leading`, `throttle` or `queue` wraps the handler, inside the job. The job does not know what the policy decides, so `pending` is sent for every call. A call that the policy drops, and a run that `latest` replaces, then close with `success(undefined)`, not with `cancel`. A flag set by `pending` is exact for a job without a policy, and for an abort of the task or of its parent. With a policy, count the calls or use the result of `emit`.
1948
+ - `pending` is sent when the handler really starts: at once for `leading`, after the wait for `debounce`, when its turn comes for `queue`.
1949
+ - A call that the policy drops before it starts sends nothing, not even `pending`. A second click that `leading` ignores does not clear the flag of the first.
1950
+ - A run that `latest` replaces sends `cancel` before the next call sends `pending`.
1951
+ - Every `pending` is followed by exactly one of `success`, `fail` and `cancel`, and nothing is sent without `pending`.
1952
+
1953
+ A flag is exact when calls do not overlap, or when `latest` replaces them. If several runs can go at once (`queue` with `concurrency` above 1, or a job without a policy called many times), keep a counter: add one on `pending` and take one off on `success`, `fail` and `cancel`. A policy that you write yourself, or that you hide in your own function, does not report to the job; `pending` is then sent at the call, as for a job with no policy.
1932
1954
 
1933
1955
  ## Mount domains with modules
1934
1956
 
@@ -2119,6 +2141,10 @@ String event names (`"user/requested"`) work the same. Use your usual RTK action
2119
2141
 
2120
2142
  The only option is `filter`, which keeps rejected actions away from the bus. Options of the bus (`onError`, `context`, `trace`) go to `createBus`. Use one bus for one store. Values that are not plain actions (thunks) go through untouched. Nothing here depends on the `redux` package at runtime.
2121
2143
 
2144
+ ### Show which dispatch began a chain
2145
+
2146
+ With a tracer and `tracePlugin` on the bus, each dispatched action reaches the bus from a root task of the trace. Name it with `traceId`, and put the flow in a group with `traceGroupId`: `createEffectsMiddleware(bus, { traceId: "store", traceGroupId: "checkout" })`. Without a `traceId` the root has the type of the action. Without the plugin the options are ignored. More in [Tracing](tracing.md#show-which-subscription-began-a-chain).
2147
+
2122
2148
  ### Type `ctx.store` (optional)
2123
2149
 
2124
2150
  Only if you want TypeScript to know `store` on handlers. **Skip this in JavaScript** — the middleware already puts `store` on the task at runtime. These lines do not connect the store across the app.
@@ -2497,7 +2523,7 @@ export function Results() {
2497
2523
  Two limits to know:
2498
2524
 
2499
2525
  - **All consumers re-render** on any change of the context value. Split it into two contexts (`query` and `items`) if that is slow.
2500
- - **A new object counts as a change.** For a value that is rebuilt with the same content, pass a third argument that says when two values are the same: `useEmitter(profileChanged, profile, (previous, next) => previous.id === next.id)`.
2526
+ - **A new object counts as a change.** For a value that is rebuilt with the same content, pass `equals` in the options, the third argument, to say when two values are the same: `useEmitter(profileChanged, profile, { equals: (previous, next) => previous.id === next.id })`. The same object takes `traceId` and `traceGroupId`, which name the subscription and the flow in the [trace](tracing.md#show-which-subscription-began-a-chain).
2501
2527
 
2502
2528
  `useEmitter` emits after the render, not at the moment of `setQuery`. To emit on every call, even with the same value, call an emitter from the event handler (`bus.emitter(action)`, see [Zustand](#zustand)) instead.
2503
2529
 
@@ -3301,7 +3327,7 @@ When several places set the same field, the strongest one wins:
3301
3327
  | 3 | the scope of the module that owns the handler |
3302
3328
  | 4 (weakest) | the bus: `createBus({ context })`, `contextPlugin`, `effectsPlugin` |
3303
3329
 
3304
- 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.
3330
+ 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.
3305
3331
 
3306
3332
  ## Keep a generic effect generic
3307
3333
 
@@ -3576,6 +3602,8 @@ Things to know:
3576
3602
  - On a class that `extendBus` did not make, a decorator does nothing.
3577
3603
  - Both the decorators of the language and `experimentalDecorators` work. Your compiler has to support the one you use: TypeScript 5 or newer for the first, and the `experimentalDecorators` option for the second.
3578
3604
 
3605
+ Name a method for the [trace](tracing.md#name-a-listener) with `{ traceId }`, as the second argument or, after a wrapper, the third: `@On(queryChanged, { traceId: "searcher" })` or `@On(queryChanged, latest, { traceId: "searcher" })`. It needs `tracePlugin` on the bus and is ignored without it.
3606
+
3579
3607
  ## Decorate the class with `@WithBus`
3580
3608
 
3581
3609
  `@WithBus(bus)` does what `extendBus(bus, Base)` does, as a class decorator. `@WithBus()` makes a bus for the class.
@@ -4244,7 +4272,7 @@ bus.on(
4244
4272
  ## What is different
4245
4273
 
4246
4274
  - **No state.** There is no `createStore`, `combine`, `restore`, `$store.map`. The bus holds listeners only. Take a store such as Zustand for state ([Stores](stores.md)).
4247
- - **`pending` is an action, not a store.** `createJob` sends `pending` when the handler starts, and `success`, `fail` or `cancel` when it ends; you keep the flag in your store ([Show a loading state](actions.md#show-a-loading-state-with-a-job)). With a policy such as `latest` around the handler the flag is not exact yet, which that section explains. A running task is also visible through [Tracing](tracing.md).
4275
+ - **`pending` is an action, not a store.** `createJob` sends `pending` when the handler starts, and `success`, `fail` or `cancel` when it ends; you keep the flag in your store ([Show a loading state](actions.md#show-a-loading-state-with-a-job)). Policies report to the job, so the flag also follows `latest`, `leading`, `debounce`, `throttle` and `queue`; that section says when to count instead. A running task is also visible through [Tracing](tracing.md).
4248
4276
  - **Cancellation is the default tool.** An Effector effect runs every call to the end. Here `latest` and `ctx.signal` stop what is no longer needed.
4249
4277
  - **Events are matched by name, not by identity.** An event is a string or a `createAction` creator, not a unit that you import and call. Two modules agree on the name.
4250
4278
  - **No graph.** `sample` and `forward` build a static graph. A bus subscribes handlers at runtime and the call order is the order of `on`.
@@ -4533,12 +4561,14 @@ Each `TraceEvent` has:
4533
4561
  | Field | Meaning |
4534
4562
  | ----- | ------- |
4535
4563
  | `id` | Unique task id within the bus |
4536
- | `parent` | Id of the task that started this one (absent for handler tasks) |
4564
+ | `parentId` | Id of the task that started this one (absent for handler tasks) |
4565
+ | `rootId` | Id of the root task of the chain; absent on the root itself, and it is the same through a listener that reports nothing |
4537
4566
  | `type` | Event type the task works on |
4538
4567
  | `effect` | `latest`, `debounce`, `throttle`, `leading`, `run`, `all`, `race`, … |
4539
4568
  | `phase` | `start`, `done`, `error`, `cancel`, or `drop` |
4540
4569
  | `payload` | Event payload |
4541
4570
  | `error` | Thrown value, only for `error` |
4571
+ | `traceGroupId` | The flow of the task, when it or a task above has one |
4542
4572
 
4543
4573
  Phases:
4544
4574
 
@@ -4563,6 +4593,124 @@ bus.on("load", async (id, ctx) => {
4563
4593
  });
4564
4594
  ```
4565
4595
 
4596
+ ## Show which subscription began a chain
4597
+
4598
+ **Problem.** `useEmitter` and `bus.emitter` put an event on the bus from outside any task, such as a render or the callback of a store. In the trace that event has no sign of where it came from. With many such subscriptions it is hard to tell which one started a chain of commands and retries.
4599
+
4600
+ **When.** The bus has a tracer and the project has several `useEmitter` or `bus.emitter` calls. The names come from `tracePlugin`: use it on the bus, or the `traceId` is ignored.
4601
+
4602
+ Give each one a `traceId`. When the bus has a tracer, the emit is made from a root task with that `traceId` as its `type` and `run` as its effect. What the event starts is below it. Without a tracer nothing changes.
4603
+
4604
+ ```js
4605
+ import { createBus } from "nanoeffects";
4606
+ import { createAction, emitterPlugin } from "nanoeffects/actions";
4607
+ import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
4608
+
4609
+ // createTracer: reports every task phase to the console
4610
+ const bus = createBus({ trace: createTracer(consoleLogger()) })
4611
+ .use(tracePlugin()) // tracePlugin: makes the root task that traceId names
4612
+ .use(emitterPlugin());
4613
+
4614
+ const cartChanged = createAction("cart/changed");
4615
+
4616
+ // emitter: a callback for a store; traceId names it in the trace
4617
+ const emitCartChanged = bus.emitter(cartChanged, { traceId: "cartStore" });
4618
+ ```
4619
+
4620
+ In a component, the same option goes in the options of the hook: `useEmitter(nameChanged, name, { traceId: "Profile" })`. In the console:
4621
+
4622
+ ```
4623
+ [nanoeffects] Profile · run start
4624
+ [nanoeffects] user/save · command start
4625
+ [nanoeffects] user/save · command done
4626
+ [nanoeffects] Profile · run done
4627
+ ```
4628
+
4629
+ A `traceId` is a string you choose and keep stable: the name of the component or of the store. Without one the root has the type of the event.
4630
+
4631
+ Two things to know:
4632
+
4633
+ - **`sample` sees the `traceId`.** The root of an emitter is a task like any other, so a `sample` function gets `{ type, payload }` with the `traceId` as the `type` and the emitted action as the `payload`. A function that matches on the type of the event, such as `event.type === "search"`, does not pick a root that has another `traceId`; match the `traceId`, or the `payload.type`.
4634
+ - **Inside a handler, emit with `ctx.emit`.** `ctx.emit` keeps the chain: what it starts is below the handler. A plain `bus.emit` inside a handler starts a new root with no parent and no `rootId`, so the trace loses the link.
4635
+
4636
+ Every event of a task below another one has `rootId`, the id of the root of the chain, also when a listener, which reports nothing, stands in between. `parentId` is the task directly above, which can be such a listener.
4637
+
4638
+ ## Follow one flow with a traceGroupId
4639
+
4640
+ **Problem.** A flow such as a checkout has several roots (a click, a timer, a store) and many branches. Parent ids give a tree, but you want to filter the log by the flow.
4641
+
4642
+ **When.** One business flow runs through several commands, and you read the log by flow.
4643
+
4644
+ Give the flow a `traceGroupId` in the scope of the command, the job or the action that starts it. Every task that it starts through `ctx.emit` or `ctx.run` has the same `traceGroupId`, and so does every trace event of those tasks. A task that declares its own `traceGroupId` uses it for itself and for what it starts.
4645
+
4646
+ ```js
4647
+ import { createBus } from "nanoeffects";
4648
+ import { createCommand } from "nanoeffects/actions";
4649
+ import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
4650
+
4651
+ // tracePlugin: reads traceGroupId from the scope of a call
4652
+ const bus = createBus({ trace: createTracer(consoleLogger()) }).use(tracePlugin());
4653
+
4654
+ // the scope names the flow; traceGroupId is for the tracer, not a field of ctx
4655
+ const pay = createCommand("checkout/pay", async () => {}, {
4656
+ traceGroupId: "checkout",
4657
+ });
4658
+ ```
4659
+
4660
+ ```
4661
+ [nanoeffects] [checkout] checkout/pay · command start
4662
+ ```
4663
+
4664
+ The emitters take it too: `useEmitter(cartChanged, cart, { traceId: "Cart", traceGroupId: "checkout" })`. `traceGroupId` works without `scopePlugin`, and needs `tracePlugin`.
4665
+
4666
+ ## Name a listener
4667
+
4668
+ **Problem.** The log shows what started a chain and what it started, but a listener of an event reports nothing, so you cannot tell which of several listeners ran.
4669
+
4670
+ **When.** An event has more than one listener and you read the trace to see which one did the work.
4671
+
4672
+ Give the listener a `traceId` when you register it. The plugin makes each call of it a task with that name, and what it emits with `ctx.emit` is below it. `off` works as before. It also works in a module (`m.on(event, handler, { traceId })`) and on an instance made by `extendBus`.
4673
+
4674
+ ```js
4675
+ import { createBus } from "nanoeffects";
4676
+ import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
4677
+
4678
+ const bus = createBus({ trace: createTracer(consoleLogger()) }).use(tracePlugin());
4679
+
4680
+ // traceId: the name of this listener in the trace
4681
+ bus.on("name/changed", saveName, { traceId: "saveNotifier" });
4682
+ bus.once("name/changed", (name) => console.log(name), { traceId: "auditLog" });
4683
+ ```
4684
+
4685
+ ```
4686
+ [nanoeffects] saveNotifier · run start
4687
+ [nanoeffects] user/save · command start
4688
+ [nanoeffects] saveNotifier · run done
4689
+ ```
4690
+
4691
+ ## Keep the chain: `ctx.emit`, not `bus.emit`
4692
+
4693
+ Inside a handler, emit with `ctx.emit`. It keeps the chain: what it starts is below the handler, and its events have the `rootId` of the chain. A plain `bus.emit` inside a handler starts a new root with no `parentId` and no `rootId`, so the trace loses the link. The bus cannot warn about it, because it does not know which handler is running.
4694
+
4695
+ The names work wherever an event or a listener is made:
4696
+
4697
+ | Where | How to name it |
4698
+ | ----- | -------------- |
4699
+ | `useEmitter` and `bus.emitter` | `{ traceId, traceGroupId }` in the options |
4700
+ | A Redux dispatch | `createEffectsMiddleware(bus, { traceId, traceGroupId })` |
4701
+ | A listener, `bus.on` and `bus.once` | `{ traceId }` as the third argument |
4702
+ | A listener of a module or of an instance of `extendBus` | the same third argument of its `on` and `once` |
4703
+ | A method with `@On` or `@Once` | `{ traceId }` in the decorator |
4704
+ | A command, a job or an action | `traceGroupId` in its scope |
4705
+
4706
+ ## Turn the names off
4707
+
4708
+ `tracePlugin` is optional, and none of it changes how your code runs:
4709
+
4710
+ - **Without the plugin**, or on a bus without a tracer, `traceId` and `traceGroupId` are ignored. Handlers run as they are, with the same results, errors and rejections, and `onError` sees the same errors.
4711
+ - **The types are the same** with and without the plugin, so you can use it only in development: `if (isDev) bus.use(tracePlugin())` compiles in both builds.
4712
+ - **A `traceGroupId` in a scope** is never put on `ctx`.
4713
+
4566
4714
  ## Use built-in loggers
4567
4715
 
4568
4716
  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.
package/llms.txt CHANGED
@@ -9,7 +9,7 @@ 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`; `usePolicy`; needs React), `nanoeffects/trace`, `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`; `usePolicy`; needs React), `nanoeffects/trace` (`createTracer`, loggers, `tracePlugin`), `nanoeffects/dev`.
13
13
  - `bus.emit(command(payload))` runs the command's handler and returns its result. Plain events return nothing.
14
14
 
15
15
  ## Start here
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nanoeffects",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Cancel stale async work and fix race conditions in event handlers: a typed event bus where every handler is a task with an AbortSignal, plus latest, debounce, throttle, queue, retry, timeout and poll. About 1.4 kB gzip.",
5
5
  "keywords": [
6
6
  "event-bus",
@@ -201,7 +201,7 @@
201
201
  {
202
202
  "name": "actions (all)",
203
203
  "path": "dist/actions.js",
204
- "limit": "2.7 kB"
204
+ "limit": "2.87 kB"
205
205
  },
206
206
  {
207
207
  "name": "match (plugin)",
@@ -239,7 +239,7 @@
239
239
  "ignore": [
240
240
  "react"
241
241
  ],
242
- "limit": "1.15 kB"
242
+ "limit": "1.18 kB"
243
243
  },
244
244
  {
245
245
  "name": "dev",
@@ -1 +0,0 @@
1
- import{n as e}from"./onAbort-BgMfspK4.js";import{a as t,n,r}from"./context-COgyNlrC.js";import{n as i}from"./promise-CSLpzwSR.js";import{n as a,t as o}from"./event-0RIBGYqS.js";const s=`*`,c=()=>{let e={},t={},n=n=>(t[n]=void 0,e[n]??=/* @__PURE__ */ new Map,e[n]),r=(e,t)=>{let r=n(e),i=r.get(t)??0;i>1?r.set(t,i-1):t?r.delete(t):r.clear()};return{get:n=>(t[n]??=[...e[n]??[]].flatMap(([e,t])=>Array(t).fill(e)),t[n]),add:(e,t)=>{let i=n(e);return i.set(t,(i.get(t)??0)+1),()=>{r(e,t)}},remove:r,clear:()=>{for(let t of Reflect.ownKeys(e))delete e[t];for(let e of Reflect.ownKeys(t))delete t[e]}}},l=Symbol(),u=(e,t,n)=>{let r={type:t,payload:n};return Object.defineProperty(r,l,{value:e,enumerable:!1}),r},d=e=>{if(typeof e==`function`)return e;if(typeof e==`object`&&e&&l in e)return e[l]},f=(e,t,n)=>Reflect.apply(e,null,[t,n]),p=(s={})=>{let l=c(),u=(e,t)=>l.add(o(e),t),p=(e,t)=>{let n=u(e,(e,r)=>(n(),t(e,r)));return n},m=(e,t)=>{e===void 0?l.clear():l.remove(o(e),t)},h=(t,a)=>{let{onError:o}=s,{ctx:c}=n({event:a,bus:g,tracer:s.trace,parent:{ref:r()}});if(!o){t(a.payload,c);return}let l=t=>{try{o(t,a)}catch(t){e(`onError failed`,t)}};try{let e=t(a.payload,c);i(e)&&Promise.resolve(e).catch(l)}catch(e){l(e)}},g={on:u,once:p,off:m,emit:(e,i)=>{let c=o(e),u=a(e,i),p={type:c,payload:u},m=d(e),_=[...l.get(c),...l.get(`*`)];_.length===0&&!m&&s.onUnhandled?.(p);for(let e of _)h(e,p);if(m){let{ctx:e}=n({event:p,bus:g,tracer:s.trace,parent:{ref:r()}}),i=g.commands?.run??f;return t(e,`command`,()=>i(m,u,e))}},use:e=>e(g),ctx:s.context,trace:s.trace};return g};export{s as a,u as i,d as n,p as r,l as t};
@@ -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);return n?n.tracer.settler(n.ref,t):s},u=(e,t,n)=>{let a=r&&i.get(e);return a?a.tracer.run(a.ref,t,e.signal,n):n()},d=(e,t)=>typeof e==`function`?e(t):e,f=({event:o,bus:c,tracer:l,parent:p})=>{let{controller:m,unlink:h}=t(p?.signal),{signal:g}=m,_=r&&l?{tracer:l,ref:l.task(o,p?.ref)}:void 0,v=(...e)=>{if(g.aborted&&!x[n])return;let t=c?.emit,r=a;a=_?.ref;try{return t?.(...e)}finally{a=r}},y=(...t)=>{if(g.aborted)return s;let n=c?.on,r=n?.(...t);if(!r)return s;let i=e({signal:g},r);return()=>{i(),r()}},b=async(e,...t)=>{g.throwIfAborted();let n=t[0],r=f({event:{type:o.type,payload:n},bus:c,tracer:l,parent:{signal:g,ref:_?.ref}});try{return await u(r.ctx,`run`,()=>e(n,r.ctx))}finally{r.unlink()}},x={...d(c?.ctx,o),type:o.type,payload:o.payload,signal:g,abort:e=>m.abort(e),emit:v,on:y,off:c?.off??s,run:b};return c?.addContext?.(x),_&&i.set(x,_),{ctx:x,unlink:h}},p=(e,t)=>{let n=r?i.get(e):void 0;return f({event:{type:t??e.type??``,payload:e.payload},bus:{emit:e.emit,on:e.on,off:e.off,ctx:e},tracer:n?.tracer,parent:{signal:e.signal,ref:n?.ref}})};export{u as a,p as i,f as n,c as o,o as r,n as s,l as t};
@@ -1 +0,0 @@
1
- import{a as e,l as t,n,r}from"./context-yYU3_9lq.js";import{n as i}from"./promise-CSLpzwSR.js";const a=e=>typeof e==`string`?e:e.type,o=(e,t)=>typeof e==`object`?e.payload:t,s=`*`,c=()=>{let e={},t={},n=n=>(t[n]=void 0,e[n]??=/* @__PURE__ */ new Map,e[n]),r=(e,t)=>{let r=n(e),i=r.get(t)??0;i>1?r.set(t,i-1):t?r.delete(t):r.clear()};return{get:n=>(t[n]??=[...e[n]??[]].flatMap(([e,t])=>Array(t).fill(e)),t[n]),add:(e,t)=>{let i=n(e);return i.set(t,(i.get(t)??0)+1),()=>{r(e,t)}},remove:r,clear:()=>{for(let t of Reflect.ownKeys(e))delete e[t];for(let e of Reflect.ownKeys(t))delete t[e]}}},l=Symbol(),u=(e,t,n)=>{let r={type:t,payload:n};return Object.defineProperty(r,l,{value:e,enumerable:!1}),r},d=e=>{if(typeof e==`function`)return e;if(typeof e==`object`&&e&&l in e)return e[l]},f=(e,t,n)=>Reflect.apply(e,null,[t,n]),p=(s={})=>{let l=c(),u=(e,t)=>l.add(a(e),t),p=(e,t)=>{let n=u(e,(e,r)=>(n(),t(e,r)));return n},m=(e,t)=>{e===void 0?l.clear():l.remove(a(e),t)},h=(e,a)=>{let{onError:o}=s,{ctx:c}=n({event:a,bus:g,tracer:s.trace,parent:{ref:r()}});if(!o){e(a.payload,c);return}let l=e=>{try{o(e,a)}catch(e){t(`onError failed`,e)}};try{let t=e(a.payload,c);i(t)&&Promise.resolve(t).catch(l)}catch(e){l(e)}},g={on:u,once:p,off:m,emit:(t,i)=>{let c=a(t),u=o(t,i),p={type:c,payload:u},m=d(t),_=[...l.get(c),...l.get(`*`)];_.length===0&&!m&&s.onUnhandled?.(p);for(let e of _)h(e,p);if(m){let{ctx:t}=n({event:p,bus:g,tracer:s.trace,parent:{ref:r()}}),i=g.commands?.run??f;return e(t,`command`,()=>i(m,u,t))}},use:e=>e(g),ctx:s.context,trace:s.trace};return g};export{a,s as i,p as n,u as r,l as t};
@@ -1 +0,0 @@
1
- const e=(e,t)=>{globalThis.console?.error(`[nanoeffects] ${e}`,t)},t=()=>{},n=/* @__PURE__ */ new WeakMap,r=t=>{let r=n.get(t);if(r)return r;let i=/* @__PURE__ */ new Set;return n.set(t,i),t.addEventListener(`abort`,()=>{let n=[...i];i.clear();for(let{fn:r}of n)try{r(t.reason)}catch(t){e(`abort callback failed`,t)}},{once:!0}),i},i=(e,n)=>{let{signal:i}=e;if(!i)return t;if(i.aborted)return n(i.reason),t;let a=r(i),o={fn:n};return a.add(o),()=>{a.delete(o)}};export{e as n,i as t};