nanoeffects 0.3.0 → 0.4.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 (42) hide show
  1. package/README.md +50 -55
  2. package/dist/{action-Bdfp-Wvd.d.ts → action-creator-D48fp13T.d.ts} +64 -72
  3. package/dist/actions.d.ts +40 -6
  4. package/dist/actions.js +1 -1
  5. package/dist/bus-Cho463Sq.js +1 -0
  6. package/dist/dev.d.ts +2 -2
  7. package/dist/effects.d.ts +1 -1
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.js +1 -1
  10. package/dist/{module-I1ixyJzx.d.ts → module-DVX8aYlq.d.ts} +9 -8
  11. package/dist/notrace/{action-Bdfp-Wvd.d.ts → action-creator-D48fp13T.d.ts} +64 -72
  12. package/dist/notrace/actions.d.ts +47 -12
  13. package/dist/notrace/actions.js +1 -1
  14. package/dist/notrace/bus-B12SvxLv.js +1 -0
  15. package/dist/notrace/effects.d.ts +1 -1
  16. package/dist/notrace/index.d.ts +2 -2
  17. package/dist/notrace/index.js +1 -1
  18. package/dist/notrace/policies.d.ts +1 -1
  19. package/dist/notrace/redux.d.ts +1 -1
  20. package/dist/notrace/redux.js +1 -1
  21. package/dist/observable.d.ts +1 -1
  22. package/dist/policies.d.ts +1 -1
  23. package/dist/redux.d.ts +1 -1
  24. package/dist/redux.js +1 -1
  25. package/dist/trace.d.ts +1 -1
  26. package/docs/README.md +24 -9
  27. package/docs/actions.md +117 -41
  28. package/docs/context.md +27 -8
  29. package/docs/effects.md +152 -73
  30. package/docs/errors.md +23 -10
  31. package/docs/guide.md +236 -129
  32. package/docs/hmr.md +25 -13
  33. package/docs/observable.md +10 -9
  34. package/docs/package.md +14 -0
  35. package/docs/policies.md +92 -30
  36. package/docs/semantics.md +5 -3
  37. package/docs/stores.md +96 -22
  38. package/docs/tracing.md +69 -54
  39. package/package.json +5 -4
  40. package/dist/bus-CRnlwmQ4.js +0 -1
  41. package/dist/notrace/bus-Dx0l9X6h.js +0 -1
  42. package/dist/notrace/event-0RIBGYqS.js +0 -1
package/docs/policies.md CHANGED
@@ -1,41 +1,92 @@
1
1
  # Policies
2
2
 
3
- A policy wraps a handler and decides what happens when calls overlap. Every policy takes `key` in its options (see below), so each key has its own state.
3
+ Use this page when handlers overlap in a SPA (search-as-you-type, double submit, leave page). A **policy** wraps a handler and decides what happens then.
4
4
 
5
- | Policy | Overlapping calls |
6
- | ------------------------------ | -------------------------------------------------------------------------- |
7
- | `latest(fn)` | the previous run is aborted and its result dropped |
8
- | `leading(fn)` | new calls are dropped while a run is going |
9
- | `debounce(ms, fn, options?)` | run after `ms` of quiet |
10
- | `throttle(ms, fn, options?)` | at most one run per `ms` |
11
- | `queue(fn, options?)` | one after another, in order |
5
+ Start with `latest`. The table and sections below are the rest of the reference.
12
6
 
13
- `debounce` and `throttle` take `leading` and `trailing`:
7
+ ## Abort the previous run
14
8
 
15
- - `throttle(ms, fn)` works like lodash `throttle` (and patronum): a call made when the throttle is idle, at least `ms` after the previous call and the previous run, runs at once (`leading`, default `true`); calls made while it waits are held, and the latest of them runs when the window closes (`trailing`, default `true`). Under a steady stream of calls it runs every `ms`. `leading: false` makes even the first call wait for the end of its window; `trailing: false` drops the calls made during a window.
16
- - `debounce(ms, fn)` runs the last call after `ms` of quiet (`trailing`, default `true`). `leading: true` also runs the first call of a burst at once, and then the trailing run happens only if more calls follow it. `leading: true, trailing: false` runs only the first call of a burst.
17
- - With both options `false` nothing runs.
18
- - `throttle` and `debounce` are checked against lodash itself: the tests feed random sequences of calls to both and compare the moments `fn` runs.
9
+ ```js
10
+ import { createBus } from "nanoeffects";
11
+ import { latest } from "nanoeffects/policies";
19
12
 
20
- `key` gives each key its own state, so calls with different keys do not affect each other:
13
+ const bus = createBus();
21
14
 
22
- ```ts
23
- // one latest run per user instead of one for all of them
24
- bus.on("load", latest(loadUser, { key: (user) => user.id }));
25
-
26
- // uploads of the same file go one after another, different files in parallel
27
- bus.on("upload", queue(upload, { key: (file) => file.name }));
15
+ // latest — a newer run aborts the older one
16
+ bus.on(
17
+ "search",
18
+ latest(async (query, { signal }) => {
19
+ await api.search(query, { signal });
20
+ }),
21
+ );
28
22
  ```
29
23
 
30
- The state of a key is dropped when that key goes idle, so keys can be unbounded (ids, urls).
24
+ Pass `signal` to `fetch` (and the like) so the request cancels.
25
+
26
+ ## Choose a policy
27
+
28
+ Every policy accepts `key` in its options (see [Isolate state by key](#isolate-state-by-key)).
29
+
30
+ | Policy | Overlapping calls |
31
+ | ------ | ----------------- |
32
+ | `latest(fn)` | previous run aborted, result dropped |
33
+ | `leading(fn)` | new calls dropped while a run is going |
34
+ | `debounce(ms, fn, options?)` | run after `ms` of quiet |
35
+ | `throttle(ms, fn, options?)` | at most one run per `ms` |
36
+ | `queue(fn, options?)` | one after another, in order |
37
+
38
+ ## Configure debounce and throttle
39
+
40
+ `debounce` and `throttle` take `leading` and `trailing`. Behavior matches lodash (and patronum); tests compare random call sequences to lodash itself.
41
+
42
+ **Throttle** (`throttle(ms, fn)`):
43
+
44
+ - Default `leading: true`, `trailing: true`.
45
+ - A call when the throttle is idle (at least `ms` after the previous call and run) runs at once.
46
+ - Calls made while it waits are held; the latest runs when the window closes.
47
+ - Under a steady stream it runs every `ms`.
48
+ - `leading: false` — even the first call waits for the end of its window.
49
+ - `trailing: false` — drop calls made during a window.
50
+
51
+ **Debounce** (`debounce(ms, fn)`):
52
+
53
+ - Default `trailing: true` — run the last call after `ms` of quiet.
54
+ - `leading: true` — also run the first call of a burst at once; the trailing run happens only if more calls follow.
55
+ - `leading: true, trailing: false` — only the first call of a burst.
56
+ - Both `false` — nothing runs.
57
+
58
+ ## Isolate state by key
31
59
 
32
- ## Queue
60
+ `key` gives each key its own state. Calls with different keys do not affect each other. Idle keys are dropped, so keys can be unbounded (ids, urls).
33
61
 
34
- `queue(fn, options?)` runs the calls of a handler one after another, in the order they arrived, instead of dropping or replacing them like `leading` and `latest` do:
62
+ ```js
63
+ import { createBus } from "nanoeffects";
64
+ import { latest, queue } from "nanoeffects/policies";
35
65
 
36
- ```ts
66
+ const bus = createBus();
67
+
68
+ // one latest run per user id
69
+ bus.on(
70
+ "load",
71
+ latest(loadUser, { key: (user) => user.id }),
72
+ );
73
+
74
+ // uploads of the same file go one after another
75
+ bus.on(
76
+ "upload",
77
+ queue(upload, { key: (file) => file.name }),
78
+ );
79
+ ```
80
+
81
+ ## Run calls one after another with `queue`
82
+
83
+ ```js
84
+ import { createBus } from "nanoeffects";
37
85
  import { queue } from "nanoeffects/policies";
38
86
 
87
+ const bus = createBus();
88
+
89
+ // queue — one after another, in order
39
90
  bus.on(
40
91
  "save",
41
92
  queue(async (draft, { signal }) => {
@@ -43,11 +94,22 @@ bus.on(
43
94
  }),
44
95
  );
45
96
 
46
- bus.emit("save", a); // runs now
47
- bus.emit("save", b); // waits for a, then runs
97
+ bus.emit("save", draftA); // runs now
98
+ bus.emit("save", draftB); // waits for draftA, then runs
48
99
  ```
49
100
 
50
- - `concurrency` (default 1) is how many calls may run at the same time.
51
- - `limit` caps how many calls may wait, and `overflow` says what to drop when the line is full: the new call (`"drop-new"`, the default) or the oldest waiting one (`"drop-old"`). A dropped call resolves with `undefined`. `queue(fn, { limit: 1, overflow: "drop-old" })` keeps only the latest call while one is running and runs it when the current one is done; `limit: 0` lets nothing wait.
52
- - Every call returns a promise with the result of its own run. An error rejects only that call and the queue goes on.
53
- - A waiting call whose task aborts is skipped and resolves with `undefined`. When the task of a running call aborts, its slot is freed at once and its result is dropped, so pass `signal` to what `fn` does: a `fn` that ignores it can overlap with the next run.
101
+ | Option | Meaning |
102
+ | ------ | ------- |
103
+ | `concurrency` | how many calls may run at once (default `1`) |
104
+ | `limit` | how many calls may wait |
105
+ | `overflow` | when the line is full: `"drop-new"` (default) or `"drop-old"` |
106
+
107
+ A dropped call resolves with `undefined`. `queue(fn, { limit: 1, overflow: "drop-old" })` keeps only the latest waiting call. `limit: 0` lets nothing wait.
108
+
109
+ Every call returns a promise with the result of its own run. An error rejects only that call; the queue continues.
110
+
111
+ **Abort:**
112
+
113
+ - A waiting call whose task aborts is skipped and resolves with `undefined`.
114
+ - When a running call aborts, its slot is freed at once and its result is dropped.
115
+ - Pass `signal` into what `fn` does. A `fn` that ignores it can overlap with the next run.
package/docs/semantics.md CHANGED
@@ -1,7 +1,9 @@
1
1
  # Semantics
2
2
 
3
- - **An aborted task delivers no result.** If the task aborts before a promise settles, the promise rejects with the abort reason, even if the value was just produced in the same tick. `latest`, `debounce`, `all`, `race`, `take` and the rest follow this rule.
4
- - **`emit` is a plain, synchronous loop** (see "Errors in handlers"). A `ctx.emit` inside a handler is the same call: without `onError` an error of a nested handler is thrown into the handler that emitted; with `onError` it goes there and `ctx.emit` does not throw.
3
+ Guarantees and sharp edges. Read when something surprises you; not required to start.
4
+
5
+ - **An aborted task delivers no result.** If the task aborts before a promise settles, the promise rejects with the abort reason, even if the value was just produced in the same tick. `latest`, `debounce`, `all`, `race`, `take`, and the rest follow this rule.
6
+ - **`emit` is a plain, synchronous loop** (see [Errors](errors.md)). A `ctx.emit` inside a handler is the same call: without `onError`, an error of a nested handler is thrown into the handler that emitted; with `onError` it goes there and `ctx.emit` does not throw.
5
7
  - **Rejections of async handlers** are not handled for you. Without `onError` they are unhandled rejections, which end a Node process by default. Use `onError` for async handlers.
6
- - **`leading` releases its lock when the task aborts**, not only when `fn` settles. A `fn` that ignores the abort signal can then overlap with the next run, so pass `ctx.signal` to what `fn` does.
8
+ - **`leading` releases its lock when the task aborts**, not only when `fn` settles. A `fn` that ignores the abort signal can then overlap with the next run — pass `ctx.signal` to what `fn` does.
7
9
  - **Effects start their function synchronously**, so an event emitted right after the call reaches a `take` inside it.
package/docs/stores.md CHANGED
@@ -1,48 +1,122 @@
1
- # Redux, Zustand, MobX
1
+ # Connect Redux, Zustand, or MobX
2
2
 
3
- ## Redux
3
+ Use this page when you **already have a store** and want async work with cancellation and policies next to it. It is not a reason to pick Redux (or any store).
4
4
 
5
- `createEffectsMiddleware()` from `nanoeffects/redux` gives you a bus and the Redux middleware that feeds it: the effects of an app in one place, without sagas. Every action goes to the bus after the reducers have handled it, so a handler that reads the store sees the new state, and every task gets `ctx.store` (`dispatch` and `getState`).
5
+ If you do not have a store yet, use `createBus` from the [Guide](guide.md). Recipe: [`examples/redux.ts`](../examples/redux.ts).
6
6
 
7
- ```ts
8
- type Events = { userRequested: string } & WithContext<{ store: ReduxStoreApi<RootState> }>;
7
+ ## Wire Redux Toolkit to a bus
9
8
 
10
- const effects = createEffectsMiddleware<Events>();
9
+ Redux’s `dispatch` → reducers updates **state**. It does not give you first-class **tasks** (abort in-flight work, `latest` / `debounce` / `queue`, `retry` / `timeout`). People usually reach for thunks, RTK listener middleware, or sagas for that.
10
+
11
+ `createEffectsMiddleware` does the same job: after the reducer runs, the action also hits a nanoeffects bus. Keep RTK for state; use policies and effects for async.
12
+
13
+ Skip this section if you are not already on Redux.
14
+
15
+ ```js
16
+ import { configureStore, createAction } from "@reduxjs/toolkit";
17
+ import { createEffectsMiddleware } from "nanoeffects/redux";
18
+ import { latest } from "nanoeffects/policies";
19
+
20
+ const userRequested = createAction("user/requested"); // RTK
21
+ const userLoaded = createAction("user/loaded");
22
+
23
+ // bus + middleware: actions reach the bus after reducers; tasks get ctx.store
24
+ const effects = createEffectsMiddleware();
11
25
 
12
26
  const store = configureStore({
13
27
  reducer,
14
28
  middleware: (getDefault) => getDefault().concat(effects.middleware),
15
29
  });
16
30
 
17
- effects.bus.on(userRequested, latest(async (id, { store, signal }) => {
18
- store.dispatch(userLoaded(await fetchUser(id, { signal })));
19
- }));
31
+ // latest — a newer run aborts the older one
32
+ effects.bus.on(
33
+ userRequested,
34
+ latest(async (id, { store, signal }) => {
35
+ const user = await fetchUser(id, { signal });
36
+ store.dispatch(userLoaded(user));
37
+ }),
38
+ );
20
39
  ```
21
40
 
22
- It looks after what is easy to get wrong:
41
+ String event names (`"user/requested"`) work the same. Use your usual RTK actions (`createSlice` / `createAction`). nanoeffects adds the middleware and policies; it does not replace RTK actions.
42
+
43
+ ### What the middleware guarantees
23
44
 
24
- - **A handler does not loop on its own action.** An action that a handler dispatches through `ctx.store` and that has the type the handler works on does not go back to the bus. The action is not marked or changed: the middleware remembers which task dispatched it. Other handlers of that type do not get it either. Longer cycles, `a` to `b` to `a`, are yours to avoid.
25
- - **A failing handler does not break `dispatch`.** Errors go to `onError` (default `console.error`), as in Redux Toolkit listeners. Reducers still throw to the caller.
26
- - **The store gets to the tasks by itself**, though the bus is made before the store: `ctx.store` appears when the middleware is added.
45
+ - **No self-loop.** An action that a handler dispatches through `ctx.store` and that has the type the handler works on does not go back to the bus. Other handlers of that type do not get it either. Longer cycles (`a` → `b` → `a`) are yours to avoid.
46
+ - **A failing handler does not break `dispatch`.** Errors go to `onError` (default `console.error`), as in RTK listeners. Reducers still throw to the caller.
47
+ - **`store` reaches tasks when the middleware is installed**, even though the bus is created before `configureStore`.
27
48
 
28
- Options are those of `createBus` (except `context`, which adds fields next to `store`) and `filter`, which keeps the actions it rejects away from the bus. Values that are not plain actions (thunks) go through untouched. Nothing here depends on Redux.
49
+ Options are those of `createBus` (except `context`, which adds fields next to `store`) and `filter`, which keeps rejected actions away from the bus. Values that are not plain actions (thunks) go through untouched. Nothing here depends on the `redux` package at runtime.
29
50
 
30
- ## Zustand, MobX and other stores
51
+ ### Type `ctx.store` (optional)
31
52
 
32
- No adapter is needed: a store tells about its changes, and `bus.emit` is a call.
53
+ 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.
33
54
 
34
55
  ```ts
35
- // Zustand
56
+ import type { WithContext } from "nanoeffects";
57
+ import {
58
+ createEffectsMiddleware,
59
+ type ReduxStoreApi,
60
+ } from "nanoeffects/redux";
61
+
62
+ // RootState — your Redux state shape (for getState)
63
+ type RootState = { user: string | null };
64
+
65
+ // Events — event name → payload for this bus
66
+ // WithContext — checker-only: handlers also have ctx.store
67
+ type Events = {
68
+ "user/requested": string;
69
+ } & WithContext<{ store: ReduxStoreApi<RootState> }>;
70
+
71
+ const effects = createEffectsMiddleware<Events>();
72
+ ```
73
+
74
+ ## Emit from Zustand
75
+
76
+ No adapter: the store notifies, you `emit` onto a normal bus.
77
+
78
+ ```js
79
+ import { createBus } from "nanoeffects";
80
+ import { latest } from "nanoeffects/policies";
81
+
82
+ const bus = createBus(); // plain bus; no Redux middleware
83
+
36
84
  const stop = store.subscribe((state, previous) => {
37
- if (state.user !== previous.user) bus.emit(userChanged, state.user);
85
+ if (state.user === previous.user) return;
86
+ bus.emit("user/changed", state.user);
38
87
  });
39
88
 
40
- // MobX
41
- const stop = reaction(() => store.user, (user) => bus.emit(userChanged, user));
89
+ // latest — drop in-flight sync when the user changes again
90
+ bus.on(
91
+ "user/changed",
92
+ latest(async (user, { signal }) => {
93
+ await sync(user, { signal });
94
+ }),
95
+ );
96
+ ```
97
+
98
+ ## Emit from MobX
99
+
100
+ ```js
101
+ import { createBus } from "nanoeffects";
102
+ import { latest } from "nanoeffects/policies";
103
+ import { reaction } from "mobx";
104
+
105
+ const bus = createBus();
106
+
107
+ const stop = reaction(
108
+ () => store.user,
109
+ (user) => bus.emit("user/changed", user),
110
+ );
42
111
 
43
- bus.on(userChanged, latest((user, { signal }) => sync(user, { signal })));
112
+ bus.on(
113
+ "user/changed",
114
+ latest(async (user, { signal }) => {
115
+ await sync(user, { signal });
116
+ }),
117
+ );
44
118
  ```
45
119
 
46
120
  Stop watching with what the store returned: `onAbort(ctx, stop)` inside a task, or `m.signal.addEventListener("abort", stop)` in a module.
47
121
 
48
- The other way, give the store to the tasks, `createBus({ context: { store } })`, and change it from a handler: `setState` for Zustand, an action of the MobX store (it wraps `runInAction`). A handler that changes the value it was started by emits the event again, so watch the smallest part of the state. For a MobX `flow`, `onAbort(ctx, () => promise.cancel())` cancels it with the task.
122
+ The other way: give the store to tasks with `createBus({ context: { store } })`, and change it from a handler (`setState`, MobX action). A handler that changes the value it was started by emits again, so watch the smallest slice. For a MobX `flow`, `onAbort(ctx, () => promise.cancel())` cancels it with the task.
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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "nanoeffects",
3
- "version": "0.3.0",
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.2 kB gzip.",
3
+ "version": "0.4.0",
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",
7
7
  "event-emitter",
@@ -121,6 +121,7 @@
121
121
  "@reduxjs/toolkit": "^2.13.0",
122
122
  "@size-limit/esbuild": "^14.2.0",
123
123
  "@size-limit/file": "^14.2.0",
124
+ "archunit": "^2.5.4",
124
125
  "jotai": "^3.0.1",
125
126
  "lefthook": "2.2.1",
126
127
  "lodash": "4.17.21",
@@ -144,7 +145,7 @@
144
145
  "name": "createBus",
145
146
  "path": "dist/index.js",
146
147
  "import": "{ createBus }",
147
- "limit": "1.2 kB"
148
+ "limit": "1.3 kB"
148
149
  },
149
150
  {
150
151
  "name": "policies (all)",
@@ -164,7 +165,7 @@
164
165
  {
165
166
  "name": "redux",
166
167
  "path": "dist/redux.js",
167
- "limit": "1.5 kB"
168
+ "limit": "1.55 kB"
168
169
  },
169
170
  {
170
171
  "name": "observable",
@@ -1 +0,0 @@
1
- import{n as e}from"./onAbort-BgMfspK4.js";import{a as t,n,r}from"./context-DlA80GOA.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=/* @__PURE__ */ new WeakSet,n=n=>{let r=e[n]??[];if(!t.has(r))return e[n]=r,r;let i=[...r];return e[n]=i,i},r=(t,r)=>{if(!r){e[t]=[];return}let i=(e[t]??[]).indexOf(r);i>=0&&n(t).splice(i,1)};return{get:n=>{let r=e[n]??[];return t.add(r),r},add:(e,t)=>(n(e).push(t),()=>{r(e,t)}),remove:r}},l=(e,t,n)=>Reflect.apply(e,null,[t,n]),u=(s={})=>{let u=c(),d=(e,t)=>u.add(o(e),t),f=(e,t)=>u.remove(o(e),t),p=(t,a)=>{let{onError:o}=s,{ctx:c}=n({event:a,bus:m,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)}},m={on:d,off:f,emit:(e,i)=>{let c=o(e),d=a(e,i),f={type:c,payload:d},h=[...u.get(c),...u.get(`*`)];h.length===0&&typeof e!=`function`&&s.onUnhandled?.(f);for(let e of h)p(e,f);if(typeof e==`function`){let{ctx:i}=n({event:f,bus:m,tracer:s.trace,parent:{ref:r()}}),a=m.commands?.run??l;return t(i,`command`,()=>a(e,d,i))}},use:e=>e(m),context:s.context,trace:s.trace};return m};export{s as n,u as t};
@@ -1 +0,0 @@
1
- import{a as e,c as t,n,r}from"./context-yAWj0BFZ.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=/* @__PURE__ */ new WeakSet,n=n=>{let r=e[n]??[];if(!t.has(r))return e[n]=r,r;let i=[...r];return e[n]=i,i},r=(t,r)=>{if(!r){e[t]=[];return}let i=(e[t]??[]).indexOf(r);i>=0&&n(t).splice(i,1)};return{get:n=>{let r=e[n]??[];return t.add(r),r},add:(e,t)=>(n(e).push(t),()=>{r(e,t)}),remove:r}},l=(e,t,n)=>Reflect.apply(e,null,[t,n]),u=(s={})=>{let u=c(),d=(e,t)=>u.add(o(e),t),f=(e,t)=>u.remove(o(e),t),p=(e,a)=>{let{onError:o}=s,{ctx:c}=n({event:a,bus:m,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)}},m={on:d,off:f,emit:(t,i)=>{let c=o(t),d=a(t,i),f={type:c,payload:d},h=[...u.get(c),...u.get(`*`)];h.length===0&&typeof t!=`function`&&s.onUnhandled?.(f);for(let e of h)p(e,f);if(typeof t==`function`){let{ctx:i}=n({event:f,bus:m,tracer:s.trace,parent:{ref:r()}}),a=m.commands?.run??l;return e(i,`command`,()=>a(t,d,i))}},use:e=>e(m),context:s.context,trace:s.trace};return m};export{s as n,u as t};
@@ -1 +0,0 @@
1
- const e=e=>typeof e==`string`?e:e.type,t=(e,t)=>typeof e==`object`?e.payload:t;export{t as n,e as t};