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.
- package/README.md +50 -55
- package/dist/{action-Bdfp-Wvd.d.ts → action-creator-D48fp13T.d.ts} +64 -72
- package/dist/actions.d.ts +40 -6
- package/dist/actions.js +1 -1
- package/dist/bus-Cho463Sq.js +1 -0
- package/dist/dev.d.ts +2 -2
- package/dist/effects.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/{module-I1ixyJzx.d.ts → module-DVX8aYlq.d.ts} +9 -8
- package/dist/notrace/{action-Bdfp-Wvd.d.ts → action-creator-D48fp13T.d.ts} +64 -72
- package/dist/notrace/actions.d.ts +47 -12
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/bus-B12SvxLv.js +1 -0
- package/dist/notrace/effects.d.ts +1 -1
- package/dist/notrace/index.d.ts +2 -2
- package/dist/notrace/index.js +1 -1
- package/dist/notrace/policies.d.ts +1 -1
- package/dist/notrace/redux.d.ts +1 -1
- package/dist/notrace/redux.js +1 -1
- package/dist/observable.d.ts +1 -1
- package/dist/policies.d.ts +1 -1
- package/dist/redux.d.ts +1 -1
- package/dist/redux.js +1 -1
- package/dist/trace.d.ts +1 -1
- package/docs/README.md +24 -9
- package/docs/actions.md +117 -41
- package/docs/context.md +27 -8
- package/docs/effects.md +152 -73
- package/docs/errors.md +23 -10
- package/docs/guide.md +236 -129
- package/docs/hmr.md +25 -13
- package/docs/observable.md +10 -9
- package/docs/package.md +14 -0
- package/docs/policies.md +92 -30
- package/docs/semantics.md +5 -3
- package/docs/stores.md +96 -22
- package/docs/tracing.md +69 -54
- package/package.json +5 -4
- package/dist/bus-CRnlwmQ4.js +0 -1
- package/dist/notrace/bus-Dx0l9X6h.js +0 -1
- package/dist/notrace/event-0RIBGYqS.js +0 -1
package/docs/policies.md
CHANGED
|
@@ -1,41 +1,92 @@
|
|
|
1
1
|
# Policies
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
+
## Abort the previous run
|
|
14
8
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
13
|
+
const bus = createBus();
|
|
21
14
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
62
|
+
```js
|
|
63
|
+
import { createBus } from "nanoeffects";
|
|
64
|
+
import { latest, queue } from "nanoeffects/policies";
|
|
35
65
|
|
|
36
|
-
|
|
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",
|
|
47
|
-
bus.emit("save",
|
|
97
|
+
bus.emit("save", draftA); // runs now
|
|
98
|
+
bus.emit("save", draftB); // waits for draftA, then runs
|
|
48
99
|
```
|
|
49
100
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
4
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
type Events = { userRequested: string } & WithContext<{ store: ReduxStoreApi<RootState> }>;
|
|
7
|
+
## Wire Redux Toolkit to a bus
|
|
9
8
|
|
|
10
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
- **
|
|
25
|
-
- **A failing handler does not break `dispatch`.** Errors go to `onError` (default `console.error`), as in
|
|
26
|
-
-
|
|
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
|
|
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
|
-
|
|
51
|
+
### Type `ctx.store` (optional)
|
|
31
52
|
|
|
32
|
-
|
|
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
|
-
|
|
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
|
|
85
|
+
if (state.user === previous.user) return;
|
|
86
|
+
bus.emit("user/changed", state.user);
|
|
38
87
|
});
|
|
39
88
|
|
|
40
|
-
//
|
|
41
|
-
|
|
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(
|
|
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
|
|
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
|
-
#
|
|
1
|
+
# Trace tasks
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Reference for long-lived apps that need to observe task phases, Redux DevTools, or a build without tracing. Not part of onboarding.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Attach a tracer
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Pass `trace` to `createBus`. Make it with `createTracer` from `nanoeffects/trace`, which takes a logger (a function) or an array of them. The tracing code lives in that entry, so a bus without `trace` does not carry it.
|
|
8
|
+
|
|
9
|
+
```js
|
|
8
10
|
import { createBus } from "nanoeffects";
|
|
9
11
|
import { createTracer } from "nanoeffects/trace";
|
|
10
12
|
|
|
11
|
-
const bus = createBus
|
|
13
|
+
const bus = createBus({
|
|
14
|
+
// createTracer — observe every task phase
|
|
12
15
|
trace: createTracer((event) => console.log(event)),
|
|
13
16
|
});
|
|
14
17
|
```
|
|
15
18
|
|
|
16
19
|
Each `TraceEvent` has:
|
|
17
20
|
|
|
18
|
-
| Field
|
|
19
|
-
|
|
|
20
|
-
| `id`
|
|
21
|
-
| `parent`
|
|
22
|
-
| `type`
|
|
23
|
-
| `effect`
|
|
24
|
-
| `phase`
|
|
25
|
-
| `payload` | Event payload
|
|
26
|
-
| `error`
|
|
21
|
+
| Field | Meaning |
|
|
22
|
+
| ----- | ------- |
|
|
23
|
+
| `id` | Unique task id within the bus |
|
|
24
|
+
| `parent` | Id of the task that started this one (absent for handler tasks) |
|
|
25
|
+
| `type` | Event type the task works on |
|
|
26
|
+
| `effect` | `latest`, `debounce`, `throttle`, `leading`, `run`, `all`, `race`, … |
|
|
27
|
+
| `phase` | `start`, `done`, `error`, `cancel`, or `drop` |
|
|
28
|
+
| `payload` | Event payload |
|
|
29
|
+
| `error` | Thrown value, only for `error` |
|
|
27
30
|
|
|
28
31
|
Phases:
|
|
29
32
|
|
|
30
|
-
- `start
|
|
31
|
-
- `done` / `error
|
|
32
|
-
- `cancel
|
|
33
|
-
- `drop
|
|
33
|
+
- `start` — the work began.
|
|
34
|
+
- `done` / `error` — it finished or threw.
|
|
35
|
+
- `cancel` — aborted after it started (superseded by `latest`, parent aborted, lost a `race`).
|
|
36
|
+
- `drop` — never started (`leading` while busy, a `throttle` call replaced by a newer one, a `debounce` call replaced before its delay ended).
|
|
34
37
|
|
|
35
|
-
`all` and `race` are traced when you pass them the handler `ctx`: the group is one task
|
|
38
|
+
`all` and `race` are traced when you pass them the handler `ctx`: the group is one task; every starter is a child `run` task.
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
import { createBus } from "nanoeffects";
|
|
42
|
+
import { all } from "nanoeffects/effects";
|
|
43
|
+
|
|
44
|
+
const bus = createBus();
|
|
36
45
|
|
|
37
|
-
```ts
|
|
38
46
|
bus.on("load", async (id, ctx) => {
|
|
39
|
-
await all(ctx, {
|
|
47
|
+
await all(ctx, {
|
|
48
|
+
user: () => fetchUser(id),
|
|
49
|
+
posts: () => fetchPosts(id),
|
|
50
|
+
});
|
|
40
51
|
});
|
|
41
52
|
```
|
|
42
53
|
|
|
43
|
-
##
|
|
54
|
+
## Use built-in loggers
|
|
44
55
|
|
|
45
|
-
A logger is
|
|
56
|
+
A logger is `(event) => void`. `createTracer` takes one logger or an array. A logger that throws is reported to `console.error` and never breaks the task or the other loggers.
|
|
46
57
|
|
|
47
|
-
```
|
|
58
|
+
```js
|
|
48
59
|
import { createBus } from "nanoeffects";
|
|
49
60
|
import { consoleLogger, createTracer, reduxDevtools } from "nanoeffects/trace";
|
|
50
61
|
|
|
51
|
-
const bus = createBus
|
|
62
|
+
const bus = createBus({
|
|
52
63
|
trace: createTracer([reduxDevtools({ name: "app" }), consoleLogger()]),
|
|
53
64
|
});
|
|
54
65
|
```
|
|
55
66
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
62
|
-
| `performanceLogger()` | Records User Timing measures for the Timings track of the browser Performance panel. |
|
|
63
|
-
| `createTraceRecorder()` | Keeps events in `recorder.events`, for tests. |
|
|
67
|
+
| Logger | What it does |
|
|
68
|
+
| ------ | ------------ |
|
|
69
|
+
| `reduxDevtools()` | Sends tasks to Redux DevTools as `type/effect/phase`; state is `{ running }` |
|
|
70
|
+
| `consoleLogger()` | Prints one line per phase, indented by task depth |
|
|
71
|
+
| `performanceLogger()` | User Timing measures for the browser Performance panel |
|
|
72
|
+
| `createTraceRecorder()` | Keeps events in `recorder.events`, for tests |
|
|
64
73
|
|
|
65
|
-
###
|
|
74
|
+
### Cut volume in production
|
|
66
75
|
|
|
67
|
-
|
|
76
|
+
```js
|
|
77
|
+
import { createTracer } from "nanoeffects/trace";
|
|
68
78
|
|
|
69
|
-
```ts
|
|
70
79
|
createTracer(logger, {
|
|
71
|
-
sample: 0.05, // trace 5% of
|
|
72
|
-
filter: (event) => event.phase === "error", //
|
|
80
|
+
sample: 0.05, // trace 5% of handler calls (share, or function of { type, payload })
|
|
81
|
+
filter: (event) => event.phase === "error", // keep only failures
|
|
73
82
|
});
|
|
74
83
|
```
|
|
75
84
|
|
|
76
|
-
- `sample` is decided once per root task
|
|
85
|
+
- `sample` is decided once per root task; every task below it follows, so a trace is never cut in the middle. Unsampled tasks cost almost nothing.
|
|
77
86
|
- `filter` runs on each event before any logger sees it.
|
|
78
87
|
|
|
79
|
-
|
|
88
|
+
### Configure Redux DevTools
|
|
80
89
|
|
|
81
|
-
|
|
90
|
+
Read-only (the bus owns no state, so there is no time travel). Does nothing when the extension is not installed. Has its own instance in the DevTools picker, apart from your store.
|
|
82
91
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
- `
|
|
92
|
+
Quiet by default:
|
|
93
|
+
|
|
94
|
+
- **A task is one action**, sent when it ends (`done`, `error`, `cancel`, `drop`). Running tasks are in `{ running }`. `starts: true` also sends every start.
|
|
95
|
+
- **Only the top is shown**: handler tasks and the effects they start. Attempts and starters under them are hidden. `depth: 0` — handlers only; `depth: 3` — more; `Infinity` — everything.
|
|
96
|
+
- `maxAge: 200` — how many actions DevTools keeps (its default is 50).
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
import { reduxDevtools } from "nanoeffects/trace";
|
|
86
100
|
|
|
87
|
-
```ts
|
|
88
101
|
reduxDevtools({ name: "app", depth: 0 }); // one line per handler call
|
|
89
|
-
reduxDevtools({ name: "app", starts: true, depth: Infinity }); // everything
|
|
102
|
+
reduxDevtools({ name: "app", starts: true, depth: Infinity }); // everything
|
|
90
103
|
```
|
|
91
104
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
To write your own, return a `TraceLogger`:
|
|
105
|
+
### Write your own logger
|
|
95
106
|
|
|
96
107
|
```ts
|
|
97
108
|
import type { TraceLogger } from "nanoeffects";
|
|
98
109
|
|
|
99
110
|
const errorsOnly = (): TraceLogger => (event) => {
|
|
100
|
-
if (event.phase === "error")
|
|
111
|
+
if (event.phase === "error") {
|
|
112
|
+
report(event.error);
|
|
113
|
+
}
|
|
101
114
|
};
|
|
102
115
|
```
|
|
103
116
|
|
|
104
|
-
##
|
|
117
|
+
## Drop tracing from a build
|
|
105
118
|
|
|
106
|
-
Without a tracer the hooks cost almost nothing
|
|
119
|
+
Without a tracer the hooks cost almost nothing. `nanoeffects/trace` is only bundled when imported.
|
|
107
120
|
|
|
108
|
-
|
|
121
|
+
To also drop tracing code from the core, use the `notrace` export condition. The package ships a second build of `index`, `effects`, `policies`, and `actions` with `__NANOEFFECTS_TRACE__` set to `false`.
|
|
122
|
+
|
|
123
|
+
```js
|
|
109
124
|
// vite / rolldown
|
|
110
125
|
resolve: { conditions: ["notrace"] }
|
|
111
126
|
// webpack
|
|
@@ -114,4 +129,4 @@ resolve: { conditionNames: ["notrace", "import", "..."] }
|
|
|
114
129
|
conditions: ["notrace"]
|
|
115
130
|
```
|
|
116
131
|
|
|
117
|
-
In a build
|
|
132
|
+
In a build from the sources, `define: { __NANOEFFECTS_TRACE__: "false" }` does the same. With the condition set, that build’s types have no `trace` option: `createBus({ trace })` is a type error. `nanoeffects/trace` has no `notrace` build. The saving is about 0.4 kB gzip of the whole library.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nanoeffects",
|
|
3
|
-
"version": "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
|
+
"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.
|
|
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.
|
|
168
|
+
"limit": "1.55 kB"
|
|
168
169
|
},
|
|
169
170
|
{
|
|
170
171
|
"name": "observable",
|
package/dist/bus-CRnlwmQ4.js
DELETED
|
@@ -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};
|