nanoeffects 0.13.0 → 0.15.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 +2 -1
- package/dist/actions.d.ts +2 -2
- package/dist/context-types-qXxr_Grf.d.ts +14 -0
- package/dist/context.d.ts +25 -0
- package/dist/context.js +1 -0
- package/dist/dev.d.ts +1 -1
- package/dist/effects.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/{module-woawvR28.d.ts → module-COv9ZSzS.d.ts} +1 -1
- package/dist/notrace/actions.d.ts +1 -1
- package/dist/notrace/context-types-DQYBFJG4.d.ts +14 -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/{plugin-Cw45_-Bo.d.ts → plugin-W4pZY7mM.d.ts} +1 -1
- package/dist/observable.d.ts +29 -0
- package/dist/observable.js +1 -1
- package/dist/oop.d.ts +48 -0
- package/dist/oop.js +1 -1
- package/dist/{plugin-BrTsPg2Y.d.ts → plugin-15-kkZp0.d.ts} +1 -1
- package/dist/scope.d.ts +1 -1
- package/dist/{types-bzocBVYc.d.ts → types-C7ukMwE-.d.ts} +1 -1
- package/docs/cancel-stale-requests.md +6 -2
- package/docs/context.md +3 -2
- package/docs/debounce-and-abort.md +1 -3
- package/docs/effects.md +10 -3
- package/docs/observable.md +50 -0
- package/docs/oop.md +126 -4
- package/docs/package.md +1 -0
- package/docs/plugins.md +12 -4
- package/docs/stores.md +4 -2
- package/docs/tracing.md +34 -8
- package/docs/typescript.md +2 -1
- package/llms-full.txt +249 -29
- package/llms.txt +2 -2
- package/package.json +12 -3
- package/dist/context-plugin-Cphk8jfq.d.ts +0 -34
- package/dist/notrace/context-plugin-BQa-wtbj.d.ts +0 -34
package/docs/plugins.md
CHANGED
|
@@ -24,7 +24,8 @@ These four cover what most apps need: shared dependencies, effects, commands and
|
|
|
24
24
|
Every task gets the same fields on `ctx`: a store, an API client, a logger. The types come from the values. More in [Context fields](context.md).
|
|
25
25
|
|
|
26
26
|
```js
|
|
27
|
-
import {
|
|
27
|
+
import { createBus } from "nanoeffects";
|
|
28
|
+
import { contextPlugin } from "nanoeffects/context";
|
|
28
29
|
|
|
29
30
|
// contextPlugin — every handler gets ctx.store
|
|
30
31
|
const bus = createBus().use(contextPlugin({ store }));
|
|
@@ -41,7 +42,9 @@ import { createBus } from "nanoeffects";
|
|
|
41
42
|
import { delay, effectsPlugin, race } from "nanoeffects/effects";
|
|
42
43
|
|
|
43
44
|
// effectsPlugin — race and delay are bound to the task of each handler
|
|
44
|
-
const
|
|
45
|
+
const effects = effectsPlugin({ race, delay });
|
|
46
|
+
|
|
47
|
+
const bus = createBus().use(effects);
|
|
45
48
|
|
|
46
49
|
bus.on("load", async (id, { race, delay }) => {
|
|
47
50
|
const result = await race({
|
|
@@ -147,8 +150,12 @@ With a tracer on the bus, the plugin lets a trace say which subscription began a
|
|
|
147
150
|
import { createBus } from "nanoeffects";
|
|
148
151
|
import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
149
152
|
|
|
153
|
+
// createTracer: sends each trace event to the logger; consoleLogger: prints one line each
|
|
154
|
+
const trace = createTracer(consoleLogger());
|
|
155
|
+
|
|
150
156
|
// tracePlugin — names the roots, groups and listeners of the trace
|
|
151
|
-
const bus = createBus({ trace
|
|
157
|
+
const bus = createBus({ trace })
|
|
158
|
+
.use(tracePlugin());
|
|
152
159
|
|
|
153
160
|
bus.on("name/changed", saveName, { traceId: "saveNotifier" }); // a listener you can find in the log
|
|
154
161
|
```
|
|
@@ -232,7 +239,8 @@ One bus with every plugin that does not need React, in a file of its own. The ev
|
|
|
232
239
|
|
|
233
240
|
```js
|
|
234
241
|
// bus.js
|
|
235
|
-
import {
|
|
242
|
+
import { createBus } from "nanoeffects";
|
|
243
|
+
import { contextPlugin } from "nanoeffects/context";
|
|
236
244
|
import {
|
|
237
245
|
commandsPlugin,
|
|
238
246
|
emitterPlugin,
|
package/docs/stores.md
CHANGED
|
@@ -131,8 +131,9 @@ The bus, the events, the handlers and the link from the store to the bus are the
|
|
|
131
131
|
|
|
132
132
|
```js
|
|
133
133
|
// bus.js
|
|
134
|
-
import {
|
|
134
|
+
import { createBus } from "nanoeffects";
|
|
135
135
|
import { emitterPlugin } from "nanoeffects/actions";
|
|
136
|
+
import { contextPlugin } from "nanoeffects/context";
|
|
136
137
|
import { store } from "./store";
|
|
137
138
|
|
|
138
139
|
export const bus = createBus()
|
|
@@ -513,8 +514,9 @@ export const query = createValue("");
|
|
|
513
514
|
|
|
514
515
|
```js
|
|
515
516
|
// bus.js
|
|
516
|
-
import {
|
|
517
|
+
import { createBus } from "nanoeffects";
|
|
517
518
|
import { emitterPlugin } from "nanoeffects/actions";
|
|
519
|
+
import { contextPlugin } from "nanoeffects/context";
|
|
518
520
|
import { query } from "./query";
|
|
519
521
|
|
|
520
522
|
// emitterPlugin adds bus.emitter; contextPlugin puts `query` on every task as ctx.query
|
package/docs/tracing.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Trace tasks
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Find out what your app did and why: which events were emitted, which calls a policy cancelled or dropped, which request failed. You see it in the browser console, in Redux DevTools, or in the Performance panel.
|
|
4
|
+
|
|
5
|
+
Tracing is optional. A bus works the same without it, and it costs almost nothing until you turn it on. If you only want to see what happens while you develop, jump to [Show every emit](#show-every-emit) and [Configure Redux DevTools](#configure-redux-devtools); the rest of the page is for apps that need to follow a flow or turn tracing off in a build.
|
|
4
6
|
|
|
5
7
|
## Attach a tracer
|
|
6
8
|
|
|
@@ -64,7 +66,10 @@ bus.on("load", async (id, ctx) => {
|
|
|
64
66
|
import { createBus } from "nanoeffects";
|
|
65
67
|
import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
66
68
|
|
|
67
|
-
|
|
69
|
+
// createTracer: sends each trace event to the logger; consoleLogger: prints one line each
|
|
70
|
+
const trace = createTracer(consoleLogger());
|
|
71
|
+
|
|
72
|
+
export const bus = createBus({ trace })
|
|
68
73
|
.use(tracePlugin()); // tracePlugin: reports each emit as an event
|
|
69
74
|
```
|
|
70
75
|
|
|
@@ -93,7 +98,9 @@ import { createAction, emitterPlugin } from "nanoeffects/actions";
|
|
|
93
98
|
import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
94
99
|
|
|
95
100
|
// createTracer: reports every task phase to the console
|
|
96
|
-
const
|
|
101
|
+
const trace = createTracer(consoleLogger());
|
|
102
|
+
|
|
103
|
+
const bus = createBus({ trace })
|
|
97
104
|
.use(tracePlugin()) // tracePlugin: makes the root task that traceId names
|
|
98
105
|
.use(emitterPlugin());
|
|
99
106
|
|
|
@@ -134,8 +141,12 @@ import { createBus } from "nanoeffects";
|
|
|
134
141
|
import { createCommand } from "nanoeffects/actions";
|
|
135
142
|
import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
136
143
|
|
|
144
|
+
// createTracer: sends each trace event to the logger; consoleLogger: prints one line each
|
|
145
|
+
const trace = createTracer(consoleLogger());
|
|
146
|
+
|
|
137
147
|
// tracePlugin: reads traceGroupId from the scope of a call
|
|
138
|
-
const bus = createBus({ trace
|
|
148
|
+
const bus = createBus({ trace })
|
|
149
|
+
.use(tracePlugin());
|
|
139
150
|
|
|
140
151
|
// the scope names the flow; traceGroupId is for the tracer, not a field of ctx
|
|
141
152
|
const pay = createCommand("checkout/pay", async () => {}, {
|
|
@@ -161,7 +172,11 @@ Give the listener a `traceId` when you register it. The plugin makes each call o
|
|
|
161
172
|
import { createBus } from "nanoeffects";
|
|
162
173
|
import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
163
174
|
|
|
164
|
-
|
|
175
|
+
// createTracer: sends each trace event to the logger; consoleLogger: prints one line each
|
|
176
|
+
const trace = createTracer(consoleLogger());
|
|
177
|
+
|
|
178
|
+
const bus = createBus({ trace })
|
|
179
|
+
.use(tracePlugin());
|
|
165
180
|
|
|
166
181
|
// traceId: the name of this listener in the trace
|
|
167
182
|
bus.on("name/changed", saveName, { traceId: "saveNotifier" });
|
|
@@ -268,7 +283,10 @@ export const bus = createBus({ trace: createTracer(logger) })
|
|
|
268
283
|
import { reduxDevtools } from "nanoeffects/trace";
|
|
269
284
|
|
|
270
285
|
// debugging a race: which calls did a policy cancel or drop?
|
|
271
|
-
reduxDevtools({
|
|
286
|
+
reduxDevtools({
|
|
287
|
+
name: "app",
|
|
288
|
+
policies: ["cancel", "drop"],
|
|
289
|
+
});
|
|
272
290
|
|
|
273
291
|
// every stage of every policy, one function for the choice
|
|
274
292
|
reduxDevtools({
|
|
@@ -277,10 +295,18 @@ reduxDevtools({
|
|
|
277
295
|
});
|
|
278
296
|
|
|
279
297
|
// the old log: every stage, and the effects below the handlers
|
|
280
|
-
reduxDevtools({
|
|
298
|
+
reduxDevtools({
|
|
299
|
+
name: "app",
|
|
300
|
+
policies: true,
|
|
301
|
+
depth: 1,
|
|
302
|
+
payload: "all",
|
|
303
|
+
});
|
|
281
304
|
|
|
282
305
|
// keep the payloads out of the extension
|
|
283
|
-
reduxDevtools({
|
|
306
|
+
reduxDevtools({
|
|
307
|
+
name: "app",
|
|
308
|
+
payload: "none",
|
|
309
|
+
});
|
|
284
310
|
```
|
|
285
311
|
|
|
286
312
|
With two quick `search` emits and a `latest` handler, the default list is `search`, `search`. With `policies: true` it adds the `latest` stages of each call, for example `search/latest/cancel` for the first one.
|
package/docs/typescript.md
CHANGED
|
@@ -70,8 +70,9 @@ At runtime the bus still gives the task its fields. Only the checker does not kn
|
|
|
70
70
|
<!-- docs-check: skip - a file of examples/typescript-context, type-checked there -->
|
|
71
71
|
```ts
|
|
72
72
|
// bus.ts
|
|
73
|
-
import { type Context,
|
|
73
|
+
import { type Context, createBus, type EventsOf } from "nanoeffects";
|
|
74
74
|
import { modulePlugin } from "nanoeffects/actions";
|
|
75
|
+
import { contextPlugin } from "nanoeffects/context";
|
|
75
76
|
import { effectsPlugin, race } from "nanoeffects/effects";
|
|
76
77
|
|
|
77
78
|
// Events: event name → payload; nothing about ctx is written here
|
package/llms-full.txt
CHANGED
|
@@ -559,6 +559,7 @@ Imports are split so you only load what you use. `createBus` stays small; the re
|
|
|
559
559
|
| `nanoeffects/effects` | `retry`, `timeout`, `poll`, `delay`, `take`, `all`, `allSettled`, `race`, `effectsPlugin` |
|
|
560
560
|
| `nanoeffects/actions` | `createAction`, `createCommand`, `createJob`, `createModule`, `modulePlugin`, `commandsPlugin` |
|
|
561
561
|
| `nanoeffects/match` | `matchPlugin`: `on`, `once` and `off` that take a function which picks the events |
|
|
562
|
+
| `nanoeffects/context` | `contextPlugin`: your own fields (a store, a client) on `ctx` of every task, typed from the value |
|
|
562
563
|
| `nanoeffects/signal` | `signalPlugin`: `emit(call, { signal })` ties the tasks of an emit to an outside `AbortSignal` |
|
|
563
564
|
| `nanoeffects/oop` | `extendBus`, `WithBus`, `On`, `Once`, `disposeBus`: the methods of a bus on a class, with the subscriptions of each instance ended by `dispose()` |
|
|
564
565
|
| `nanoeffects/scope` | `scopePlugin`, `effect`: fields for one command, action, job, module or `emit` (the types `Scope`, `Effect`) |
|
|
@@ -609,7 +610,8 @@ These four cover what most apps need: shared dependencies, effects, commands and
|
|
|
609
610
|
Every task gets the same fields on `ctx`: a store, an API client, a logger. The types come from the values. More in [Context fields](context.md).
|
|
610
611
|
|
|
611
612
|
```js
|
|
612
|
-
import {
|
|
613
|
+
import { createBus } from "nanoeffects";
|
|
614
|
+
import { contextPlugin } from "nanoeffects/context";
|
|
613
615
|
|
|
614
616
|
// contextPlugin — every handler gets ctx.store
|
|
615
617
|
const bus = createBus().use(contextPlugin({ store }));
|
|
@@ -626,7 +628,9 @@ import { createBus } from "nanoeffects";
|
|
|
626
628
|
import { delay, effectsPlugin, race } from "nanoeffects/effects";
|
|
627
629
|
|
|
628
630
|
// effectsPlugin — race and delay are bound to the task of each handler
|
|
629
|
-
const
|
|
631
|
+
const effects = effectsPlugin({ race, delay });
|
|
632
|
+
|
|
633
|
+
const bus = createBus().use(effects);
|
|
630
634
|
|
|
631
635
|
bus.on("load", async (id, { race, delay }) => {
|
|
632
636
|
const result = await race({
|
|
@@ -732,8 +736,12 @@ With a tracer on the bus, the plugin lets a trace say which subscription began a
|
|
|
732
736
|
import { createBus } from "nanoeffects";
|
|
733
737
|
import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
734
738
|
|
|
739
|
+
// createTracer: sends each trace event to the logger; consoleLogger: prints one line each
|
|
740
|
+
const trace = createTracer(consoleLogger());
|
|
741
|
+
|
|
735
742
|
// tracePlugin — names the roots, groups and listeners of the trace
|
|
736
|
-
const bus = createBus({ trace
|
|
743
|
+
const bus = createBus({ trace })
|
|
744
|
+
.use(tracePlugin());
|
|
737
745
|
|
|
738
746
|
bus.on("name/changed", saveName, { traceId: "saveNotifier" }); // a listener you can find in the log
|
|
739
747
|
```
|
|
@@ -817,7 +825,8 @@ One bus with every plugin that does not need React, in a file of its own. The ev
|
|
|
817
825
|
|
|
818
826
|
```js
|
|
819
827
|
// bus.js
|
|
820
|
-
import {
|
|
828
|
+
import { createBus } from "nanoeffects";
|
|
829
|
+
import { contextPlugin } from "nanoeffects/context";
|
|
821
830
|
import {
|
|
822
831
|
commandsPlugin,
|
|
823
832
|
emitterPlugin,
|
|
@@ -1121,7 +1130,7 @@ const bus = createBus(); // the bus: events and handlers
|
|
|
1121
1130
|
// latest: a newer search aborts the request in flight
|
|
1122
1131
|
const searchLatest = latest(async (query, { signal }) => {
|
|
1123
1132
|
const items = await api.search(query, { signal });
|
|
1124
|
-
|
|
1133
|
+
render(items);
|
|
1125
1134
|
});
|
|
1126
1135
|
|
|
1127
1136
|
// debounce: run after 300 ms without a new "typed"
|
|
@@ -1145,7 +1154,11 @@ const searchOnce = timeout(5000, (query, { signal }) => api.search(query, { sign
|
|
|
1145
1154
|
// retry: up to 4 attempts, backoff makes the pause longer each time
|
|
1146
1155
|
const searchWithRetry = retry(searchOnce, {
|
|
1147
1156
|
times: 4,
|
|
1148
|
-
delay: backoff({
|
|
1157
|
+
delay: backoff({
|
|
1158
|
+
base: 200,
|
|
1159
|
+
max: 5000,
|
|
1160
|
+
jitter: true,
|
|
1161
|
+
}),
|
|
1149
1162
|
});
|
|
1150
1163
|
|
|
1151
1164
|
// latest: a newer search aborts the whole retry chain
|
|
@@ -1201,13 +1214,11 @@ const bus = createBus(); // the bus: events and handlers
|
|
|
1201
1214
|
const searchLatest = latest(async (query, { signal }) => {
|
|
1202
1215
|
// signal: pass it on so the browser cancels the request itself
|
|
1203
1216
|
const items = await api.search(query, { signal });
|
|
1204
|
-
|
|
1217
|
+
render(items);
|
|
1205
1218
|
});
|
|
1206
1219
|
|
|
1207
1220
|
// debounce: run searchLatest after 300 ms without a new "typed"
|
|
1208
1221
|
bus.on("typed", debounce(300, searchLatest));
|
|
1209
|
-
|
|
1210
|
-
bus.on("results", (items) => render(items));
|
|
1211
1222
|
```
|
|
1212
1223
|
|
|
1213
1224
|
What to replace: `api.search` is your own request function. It must accept `{ signal }` and give it to `fetch`. `render` shows the items.
|
|
@@ -1542,7 +1553,11 @@ import { backoff, retry } from "nanoeffects/effects";
|
|
|
1542
1553
|
const loadWithRetry = retry(loadUser, {
|
|
1543
1554
|
times: 5,
|
|
1544
1555
|
// backoff — pause 200, 400, 800, … ms, never over 5 s, a random part of it
|
|
1545
|
-
delay: backoff({
|
|
1556
|
+
delay: backoff({
|
|
1557
|
+
base: 200,
|
|
1558
|
+
max: 5000,
|
|
1559
|
+
jitter: true,
|
|
1560
|
+
}),
|
|
1546
1561
|
});
|
|
1547
1562
|
```
|
|
1548
1563
|
|
|
@@ -1565,8 +1580,11 @@ If handlers call the same helpers often, put them on the task once. A handler th
|
|
|
1565
1580
|
import { createBus } from "nanoeffects";
|
|
1566
1581
|
import { all, delay, effectsPlugin, race } from "nanoeffects/effects";
|
|
1567
1582
|
|
|
1568
|
-
// effectsPlugin — add race, all and delay to the context of every task
|
|
1569
|
-
const
|
|
1583
|
+
// effectsPlugin — add race, all and delay to the context of every task
|
|
1584
|
+
const effects = effectsPlugin({ race, all, delay });
|
|
1585
|
+
|
|
1586
|
+
// use returns the same bus, typed with the new fields
|
|
1587
|
+
const bus = createBus().use(effects);
|
|
1570
1588
|
|
|
1571
1589
|
bus.on("load", async (id, { race, all, delay }) => {
|
|
1572
1590
|
// race — same as race(ctx, …)
|
|
@@ -2189,8 +2207,9 @@ The bus, the events, the handlers and the link from the store to the bus are the
|
|
|
2189
2207
|
|
|
2190
2208
|
```js
|
|
2191
2209
|
// bus.js
|
|
2192
|
-
import {
|
|
2210
|
+
import { createBus } from "nanoeffects";
|
|
2193
2211
|
import { emitterPlugin } from "nanoeffects/actions";
|
|
2212
|
+
import { contextPlugin } from "nanoeffects/context";
|
|
2194
2213
|
import { store } from "./store";
|
|
2195
2214
|
|
|
2196
2215
|
export const bus = createBus()
|
|
@@ -2571,8 +2590,9 @@ export const query = createValue("");
|
|
|
2571
2590
|
|
|
2572
2591
|
```js
|
|
2573
2592
|
// bus.js
|
|
2574
|
-
import {
|
|
2593
|
+
import { createBus } from "nanoeffects";
|
|
2575
2594
|
import { emitterPlugin } from "nanoeffects/actions";
|
|
2595
|
+
import { contextPlugin } from "nanoeffects/context";
|
|
2576
2596
|
import { query } from "./query";
|
|
2577
2597
|
|
|
2578
2598
|
// emitterPlugin adds bus.emitter; contextPlugin puts `query` on every task as ctx.query
|
|
@@ -3101,10 +3121,11 @@ To give fields to **one** command, action, job or module, or to replace them for
|
|
|
3101
3121
|
|
|
3102
3122
|
## Type the fields (optional)
|
|
3103
3123
|
|
|
3104
|
-
In TypeScript, `contextPlugin` puts the fields on every task of **this** bus and takes their types from the value you pass. The event map holds only events. It does not wire anything across the app — another bus can have different fields or none.
|
|
3124
|
+
In TypeScript, `contextPlugin` (from `nanoeffects/context`) puts the fields on every task of **this** bus and takes their types from the value you pass. The event map holds only events. It does not wire anything across the app — another bus can have different fields or none.
|
|
3105
3125
|
|
|
3106
3126
|
```ts
|
|
3107
|
-
import {
|
|
3127
|
+
import { createBus } from "nanoeffects";
|
|
3128
|
+
import { contextPlugin } from "nanoeffects/context";
|
|
3108
3129
|
|
|
3109
3130
|
// Events — event name → payload
|
|
3110
3131
|
type Events = { save: Draft };
|
|
@@ -3408,7 +3429,12 @@ Source: docs/oop.md
|
|
|
3408
3429
|
|
|
3409
3430
|
# Use the bus from a class
|
|
3410
3431
|
|
|
3411
|
-
Use this page when **your domain logic lives in classes**: a service, a store or a controller that needs the bus inside its methods.
|
|
3432
|
+
Use this page when **your domain logic lives in classes**: a service, a store or a controller that needs the bus inside its methods. There are two ways to give a class the methods of a bus, and both remember what each instance subscribed to, so `dispose()` ends it all:
|
|
3433
|
+
|
|
3434
|
+
| You want | Use |
|
|
3435
|
+
| -------- | --- |
|
|
3436
|
+
| every `new Service()` to be an independent emitter | `extends Emitter` |
|
|
3437
|
+
| the instances to share a bus, or to use plugins | `extends extendBus(bus)` |
|
|
3412
3438
|
|
|
3413
3439
|
If your logic is plain functions, stay with [modules](actions.md#mount-domains-with-modules): a module is the same idea without a class.
|
|
3414
3440
|
|
|
@@ -3416,7 +3442,68 @@ If your logic is plain functions, stay with [modules](actions.md#mount-domains-w
|
|
|
3416
3442
|
|
|
3417
3443
|
A class that listens to the bus keeps a reference to it, and keeps every unsubscribe function in a list to call when the object goes away. Forget one and the old object keeps handling events. `extendBus` does both for you.
|
|
3418
3444
|
|
|
3419
|
-
##
|
|
3445
|
+
## Make an independent emitter with `Emitter`
|
|
3446
|
+
|
|
3447
|
+
`Emitter` is a base class. Every instance of a class that extends it has a bus of its own: the events of one instance never reach another. It is the shape of Node's `EventEmitter`.
|
|
3448
|
+
|
|
3449
|
+
```js
|
|
3450
|
+
// video-service.js
|
|
3451
|
+
import { Emitter } from "nanoeffects/oop";
|
|
3452
|
+
|
|
3453
|
+
// Emitter — a base class: each new VideoService() gets on, once, off, emit, dispose and its own bus
|
|
3454
|
+
export class VideoService extends Emitter {
|
|
3455
|
+
constructor() {
|
|
3456
|
+
super();
|
|
3457
|
+
// this.on — a subscription of this instance; dispose() removes it
|
|
3458
|
+
this.on("played", () => console.log("playing"));
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
```
|
|
3462
|
+
|
|
3463
|
+
```js
|
|
3464
|
+
import { VideoService } from "./video-service";
|
|
3465
|
+
|
|
3466
|
+
const first = new VideoService();
|
|
3467
|
+
const second = new VideoService();
|
|
3468
|
+
first.on("paused", () => console.log("first paused"));
|
|
3469
|
+
second.emit("paused"); // nothing: the instances do not share a bus
|
|
3470
|
+
first.emit("paused"); // logs "first paused"
|
|
3471
|
+
first.dispose(); // its subscriptions are gone
|
|
3472
|
+
```
|
|
3473
|
+
|
|
3474
|
+
What you get:
|
|
3475
|
+
|
|
3476
|
+
- `on`, `once`, `off`, `emit`, `dispose` and `bus` (the bus of that instance). `instance[Symbol.dispose]()` does the same as `dispose()`, so `using video = new VideoService()` works where the runtime supports it.
|
|
3477
|
+
- `@On` and `@Once` work on its methods (see below). They are subscribed when the base constructor runs, so before the fields of your subclass.
|
|
3478
|
+
- `on`, `once`, `off` and `emit` are bound to the instance, so `const { emit } = video` works. A subclass replaces one with an arrow property (`emit = (…) => …`); TypeScript does not let it redeclare one as a method. `dispose` is a method and can be overridden as usual.
|
|
3479
|
+
- No methods of plugins: the bus is made inside, so no plugin is on it. For plugins, or for a bus that several instances or classes share, use `extendBus` (next sections).
|
|
3480
|
+
- For a stream of one event, see [`Subject`](observable.md#read-and-write-one-event-with-a-subject) and [Stream an event with a `Subject`](#stream-an-event-with-a-subject).
|
|
3481
|
+
|
|
3482
|
+
### Type the events (optional)
|
|
3483
|
+
|
|
3484
|
+
`Emitter` takes the events of the class as a type parameter.
|
|
3485
|
+
|
|
3486
|
+
```ts
|
|
3487
|
+
// video-service.ts
|
|
3488
|
+
import { Emitter } from "nanoeffects/oop";
|
|
3489
|
+
|
|
3490
|
+
// VideoEvents — checker only: the payload of each event of this class
|
|
3491
|
+
type VideoEvents = { "video/time": number; "video/paused": void };
|
|
3492
|
+
|
|
3493
|
+
export class VideoService extends Emitter<VideoEvents> {
|
|
3494
|
+
constructor() {
|
|
3495
|
+
super();
|
|
3496
|
+
// seconds is a number because "video/time" carries one
|
|
3497
|
+
this.on("video/time", (seconds) => console.log(seconds));
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
|
|
3501
|
+
const video = new VideoService();
|
|
3502
|
+
video.emit("video/time", 12); // checked against VideoEvents
|
|
3503
|
+
video.emit("video/paused"); // no payload
|
|
3504
|
+
```
|
|
3505
|
+
|
|
3506
|
+
## Give a class the methods of a shared bus
|
|
3420
3507
|
|
|
3421
3508
|
`extendBus(bus, Base)` returns a subclass of `Base`. Its instances have the methods of the bus, and the arguments of `new` go to the constructor of `Base`.
|
|
3422
3509
|
|
|
@@ -3477,9 +3564,33 @@ What you get:
|
|
|
3477
3564
|
- `bus` is the bus, and `Class.bus` is the same one.
|
|
3478
3565
|
- `this.on` works in the constructor of `Base`. The methods exist on the prototype of the subclass, which is the prototype of the instance already.
|
|
3479
3566
|
|
|
3567
|
+
### Extend the result with no class of your own
|
|
3568
|
+
|
|
3569
|
+
`extendBus(bus)` without a class returns a class with the methods of that bus and nothing else. Extend it as it is.
|
|
3570
|
+
|
|
3571
|
+
```js
|
|
3572
|
+
// service.js
|
|
3573
|
+
import { createBus } from "nanoeffects";
|
|
3574
|
+
import { extendBus } from "nanoeffects/oop";
|
|
3575
|
+
|
|
3576
|
+
const bus = createBus(); // the bus that the services share
|
|
3577
|
+
|
|
3578
|
+
// extendBus(bus) — a class with on, once, off, emit and dispose over this bus
|
|
3579
|
+
export class AudioService extends extendBus(bus) {
|
|
3580
|
+
constructor() {
|
|
3581
|
+
super();
|
|
3582
|
+
this.on("muted", () => console.log("muted")); // ends with dispose()
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
const audio = new AudioService();
|
|
3587
|
+
bus.emit("muted"); // the instances share this bus: logs "muted"
|
|
3588
|
+
audio.dispose();
|
|
3589
|
+
```
|
|
3590
|
+
|
|
3480
3591
|
## Let the class make its bus
|
|
3481
3592
|
|
|
3482
|
-
Leave the bus out and the class gets its own, shared by all of its instances.
|
|
3593
|
+
Leave the bus out and the class gets its own, shared by all of its instances. That is one bus for each class; `Emitter` makes one for each instance.
|
|
3483
3594
|
|
|
3484
3595
|
```js
|
|
3485
3596
|
// notifier.js
|
|
@@ -3521,6 +3632,37 @@ stop(); // the function on returned still works, and does nothing now
|
|
|
3521
3632
|
|
|
3522
3633
|
A subscription that you removed yourself (the function `on` returned, `this.off`, or a `once` that fired) is forgotten, so the instance holds nothing for it.
|
|
3523
3634
|
|
|
3635
|
+
## Stream an event with a `Subject`
|
|
3636
|
+
|
|
3637
|
+
Problem: a service hands one of its events to code written for streams, and the stream subscription should end with the service. `createObserve(this.bus)` would not: it subscribes through the bus, so `dispose()` does not know it.
|
|
3638
|
+
|
|
3639
|
+
Pass `this` to `Subject` as the source. The subject subscribes through `this.on`, so the subscription belongs to the instance ([Read and write one event with a Subject](observable.md#read-and-write-one-event-with-a-subject)).
|
|
3640
|
+
|
|
3641
|
+
```js
|
|
3642
|
+
// typing.js
|
|
3643
|
+
import { createBus } from "nanoeffects";
|
|
3644
|
+
import { Subject } from "nanoeffects/observable";
|
|
3645
|
+
import { extendBus } from "nanoeffects/oop";
|
|
3646
|
+
|
|
3647
|
+
const bus = createBus(); // the bus: events and handlers
|
|
3648
|
+
|
|
3649
|
+
class TypingBase {
|
|
3650
|
+
constructor() {
|
|
3651
|
+
this.typed = new Subject(this, "typed"); // Subject — the event "typed" as a stream; this is the source
|
|
3652
|
+
this.typed.subscribe((text) => console.log(text));
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
const Typing = extendBus(bus, TypingBase); // extendBus — a class with on, emit and dispose
|
|
3657
|
+
|
|
3658
|
+
const typing = new Typing();
|
|
3659
|
+
typing.typed.next("a"); // logs "a"
|
|
3660
|
+
typing.dispose(); // ends the subscription of the Subject too
|
|
3661
|
+
typing.typed.next("b"); // nothing is logged
|
|
3662
|
+
```
|
|
3663
|
+
|
|
3664
|
+
In TypeScript, declare `on` and `emit` in the class (`declare on: BusMethods<typeof bus>["on"]`, see [Type the instances](#type-the-instances-optional)) so that `this` fits as the source.
|
|
3665
|
+
|
|
3524
3666
|
## Keep a method of your own
|
|
3525
3667
|
|
|
3526
3668
|
The class wins any clash of names. If `Base` has `emit`, `dispose`, `on` or `bus`, yours stays and the bus method is not added. A `Base` with its own `dispose` would never end the subscriptions, so call `disposeBus(this)` from there.
|
|
@@ -3693,7 +3835,8 @@ service.emit("order/placed", { id: "o1" }); // checked against Events
|
|
|
3693
3835
|
|
|
3694
3836
|
- `this` is not bound in a handler that you pass by hand (see above).
|
|
3695
3837
|
- The methods of a plugin used after `extendBus` are not on the class. Use the plugins first, then `extendBus`.
|
|
3696
|
-
-
|
|
3838
|
+
- `Emitter` has no methods of plugins, because its bus is made inside. Use `extendBus(bus)` for them.
|
|
3839
|
+
- Mixing the methods into an object that exists, and decorators for fields or parameters are not part of this entry.
|
|
3697
3840
|
|
|
3698
3841
|
---
|
|
3699
3842
|
Source: docs/typescript.md
|
|
@@ -3770,8 +3913,9 @@ At runtime the bus still gives the task its fields. Only the checker does not kn
|
|
|
3770
3913
|
<!-- docs-check: skip - a file of examples/typescript-context, type-checked there -->
|
|
3771
3914
|
```ts
|
|
3772
3915
|
// bus.ts
|
|
3773
|
-
import { type Context,
|
|
3916
|
+
import { type Context, createBus, type EventsOf } from "nanoeffects";
|
|
3774
3917
|
import { modulePlugin } from "nanoeffects/actions";
|
|
3918
|
+
import { contextPlugin } from "nanoeffects/context";
|
|
3775
3919
|
import { effectsPlugin, race } from "nanoeffects/effects";
|
|
3776
3920
|
|
|
3777
3921
|
// Events: event name → payload; nothing about ctx is written here
|
|
@@ -4540,7 +4684,9 @@ Source: docs/tracing.md
|
|
|
4540
4684
|
|
|
4541
4685
|
# Trace tasks
|
|
4542
4686
|
|
|
4543
|
-
|
|
4687
|
+
Find out what your app did and why: which events were emitted, which calls a policy cancelled or dropped, which request failed. You see it in the browser console, in Redux DevTools, or in the Performance panel.
|
|
4688
|
+
|
|
4689
|
+
Tracing is optional. A bus works the same without it, and it costs almost nothing until you turn it on. If you only want to see what happens while you develop, jump to [Show every emit](#show-every-emit) and [Configure Redux DevTools](#configure-redux-devtools); the rest of the page is for apps that need to follow a flow or turn tracing off in a build.
|
|
4544
4690
|
|
|
4545
4691
|
## Attach a tracer
|
|
4546
4692
|
|
|
@@ -4604,7 +4750,10 @@ bus.on("load", async (id, ctx) => {
|
|
|
4604
4750
|
import { createBus } from "nanoeffects";
|
|
4605
4751
|
import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
4606
4752
|
|
|
4607
|
-
|
|
4753
|
+
// createTracer: sends each trace event to the logger; consoleLogger: prints one line each
|
|
4754
|
+
const trace = createTracer(consoleLogger());
|
|
4755
|
+
|
|
4756
|
+
export const bus = createBus({ trace })
|
|
4608
4757
|
.use(tracePlugin()); // tracePlugin: reports each emit as an event
|
|
4609
4758
|
```
|
|
4610
4759
|
|
|
@@ -4633,7 +4782,9 @@ import { createAction, emitterPlugin } from "nanoeffects/actions";
|
|
|
4633
4782
|
import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
4634
4783
|
|
|
4635
4784
|
// createTracer: reports every task phase to the console
|
|
4636
|
-
const
|
|
4785
|
+
const trace = createTracer(consoleLogger());
|
|
4786
|
+
|
|
4787
|
+
const bus = createBus({ trace })
|
|
4637
4788
|
.use(tracePlugin()) // tracePlugin: makes the root task that traceId names
|
|
4638
4789
|
.use(emitterPlugin());
|
|
4639
4790
|
|
|
@@ -4674,8 +4825,12 @@ import { createBus } from "nanoeffects";
|
|
|
4674
4825
|
import { createCommand } from "nanoeffects/actions";
|
|
4675
4826
|
import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
4676
4827
|
|
|
4828
|
+
// createTracer: sends each trace event to the logger; consoleLogger: prints one line each
|
|
4829
|
+
const trace = createTracer(consoleLogger());
|
|
4830
|
+
|
|
4677
4831
|
// tracePlugin: reads traceGroupId from the scope of a call
|
|
4678
|
-
const bus = createBus({ trace
|
|
4832
|
+
const bus = createBus({ trace })
|
|
4833
|
+
.use(tracePlugin());
|
|
4679
4834
|
|
|
4680
4835
|
// the scope names the flow; traceGroupId is for the tracer, not a field of ctx
|
|
4681
4836
|
const pay = createCommand("checkout/pay", async () => {}, {
|
|
@@ -4701,7 +4856,11 @@ Give the listener a `traceId` when you register it. The plugin makes each call o
|
|
|
4701
4856
|
import { createBus } from "nanoeffects";
|
|
4702
4857
|
import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
4703
4858
|
|
|
4704
|
-
|
|
4859
|
+
// createTracer: sends each trace event to the logger; consoleLogger: prints one line each
|
|
4860
|
+
const trace = createTracer(consoleLogger());
|
|
4861
|
+
|
|
4862
|
+
const bus = createBus({ trace })
|
|
4863
|
+
.use(tracePlugin());
|
|
4705
4864
|
|
|
4706
4865
|
// traceId: the name of this listener in the trace
|
|
4707
4866
|
bus.on("name/changed", saveName, { traceId: "saveNotifier" });
|
|
@@ -4808,7 +4967,10 @@ export const bus = createBus({ trace: createTracer(logger) })
|
|
|
4808
4967
|
import { reduxDevtools } from "nanoeffects/trace";
|
|
4809
4968
|
|
|
4810
4969
|
// debugging a race: which calls did a policy cancel or drop?
|
|
4811
|
-
reduxDevtools({
|
|
4970
|
+
reduxDevtools({
|
|
4971
|
+
name: "app",
|
|
4972
|
+
policies: ["cancel", "drop"],
|
|
4973
|
+
});
|
|
4812
4974
|
|
|
4813
4975
|
// every stage of every policy, one function for the choice
|
|
4814
4976
|
reduxDevtools({
|
|
@@ -4817,10 +4979,18 @@ reduxDevtools({
|
|
|
4817
4979
|
});
|
|
4818
4980
|
|
|
4819
4981
|
// the old log: every stage, and the effects below the handlers
|
|
4820
|
-
reduxDevtools({
|
|
4982
|
+
reduxDevtools({
|
|
4983
|
+
name: "app",
|
|
4984
|
+
policies: true,
|
|
4985
|
+
depth: 1,
|
|
4986
|
+
payload: "all",
|
|
4987
|
+
});
|
|
4821
4988
|
|
|
4822
4989
|
// keep the payloads out of the extension
|
|
4823
|
-
reduxDevtools({
|
|
4990
|
+
reduxDevtools({
|
|
4991
|
+
name: "app",
|
|
4992
|
+
payload: "none",
|
|
4993
|
+
});
|
|
4824
4994
|
```
|
|
4825
4995
|
|
|
4826
4996
|
With two quick `search` emits and a `latest` handler, the default list is `search`, `search`. With `policies: true` it adds the `latest` stages of each call, for example `search/latest/cancel` for the first one.
|
|
@@ -4929,6 +5099,56 @@ bus.on(queryChanged, (query) => console.log("search for", query));
|
|
|
4929
5099
|
|
|
4930
5100
|
The Observable can come from anywhere, not only from `createObserve`. `subscription.unsubscribe()` stops it; the bus does not know about it. Pass the callback to `subscribe` as it is, and do not call `bus.emitter(...)` inside the operators on every value.
|
|
4931
5101
|
|
|
5102
|
+
## Read and write one event with a Subject
|
|
5103
|
+
|
|
5104
|
+
Problem: `createObserve` only reads. Code written for streams often also needs to push a value into the same event, and passing a bus and an event name around for that is clumsy.
|
|
5105
|
+
|
|
5106
|
+
`Subject` is one event of a bus as one object. `subscribe` listens, `next` emits. It has no `complete`, no `error` and no replay: an event of a bus does not end, and the bus keeps no values.
|
|
5107
|
+
|
|
5108
|
+
```js
|
|
5109
|
+
import { createBus } from "nanoeffects";
|
|
5110
|
+
import { Subject } from "nanoeffects/observable";
|
|
5111
|
+
|
|
5112
|
+
const bus = createBus(); // the bus: events and handlers
|
|
5113
|
+
|
|
5114
|
+
const typed = new Subject(bus, "typed"); // Subject — the event "typed" as a stream you can also write to
|
|
5115
|
+
|
|
5116
|
+
const subscription = typed.subscribe((text) => console.log(text)); // like bus.on("typed", …)
|
|
5117
|
+
typed.next("hello"); // like bus.emit("typed", "hello"): bus.on handlers and subscribers both run
|
|
5118
|
+
subscription.unsubscribe(); // removes this subscription only
|
|
5119
|
+
```
|
|
5120
|
+
|
|
5121
|
+
- `next` works when you take it off the subject, so `input.addEventListener("input", (event) => typed.next(event.target.value))` and `store.subscribe(typed.next)` need no wrapper.
|
|
5122
|
+
- A subscriber that is added later does not get a value emitted before. Add `startWith` or `shareReplay` in RxJS when it should.
|
|
5123
|
+
- RxJS takes a `Subject` as it takes `createObserve`: `from(typed)`. It is not an RxJS `Subject` (no `error`, `complete` or `pipe`), so it does not fit where that type is wanted.
|
|
5124
|
+
|
|
5125
|
+
### Extend it for a named stream
|
|
5126
|
+
|
|
5127
|
+
Problem: the event name is repeated wherever the stream is made. A subclass fixes it once.
|
|
5128
|
+
|
|
5129
|
+
```js
|
|
5130
|
+
import { createBus } from "nanoeffects";
|
|
5131
|
+
import { Subject } from "nanoeffects/observable";
|
|
5132
|
+
|
|
5133
|
+
class TypedText extends Subject {
|
|
5134
|
+
constructor(bus) {
|
|
5135
|
+
super(bus, "typed"); // the event is fixed by the class
|
|
5136
|
+
}
|
|
5137
|
+
|
|
5138
|
+
next(text) {
|
|
5139
|
+
super.next(text.trim()); // a method of the subclass replaces the one of Subject
|
|
5140
|
+
}
|
|
5141
|
+
}
|
|
5142
|
+
|
|
5143
|
+
const bus = createBus(); // the bus: events and handlers
|
|
5144
|
+
const typed = new TypedText(bus);
|
|
5145
|
+
typed.next(" hello "); // emits "typed" with "hello"
|
|
5146
|
+
```
|
|
5147
|
+
|
|
5148
|
+
### Use it inside a class with a bus
|
|
5149
|
+
|
|
5150
|
+
A class made with `extendBus` has its own `on` and `emit`. Pass `this` as the source and the subscription belongs to the instance, so `dispose()` ends it ([Classes with a bus](oop.md)).
|
|
5151
|
+
|
|
4932
5152
|
## Do the work in a handler, not in `switchMap`
|
|
4933
5153
|
|
|
4934
5154
|
An Observable is good for shaping values. A request is better in a handler with a policy, because the handler has a `signal` and the trace shows it. Split it: RxJS ends at the value, the bus starts the work.
|
package/llms.txt
CHANGED
|
@@ -36,7 +36,7 @@ Rules that generated code often gets wrong:
|
|
|
36
36
|
- [Build a sign-up form in React](docs/react.md): a form with a username check, a submit that cannot fire twice, and retry; `usePolicy` keeps a debounced handler between renders
|
|
37
37
|
- [Context fields](docs/context.md): put a store or client on every task
|
|
38
38
|
- [Scopes](docs/scope.md): fields for one command, action, job, module or `emit`, and a way to replace them in a test
|
|
39
|
-
- [Use the bus from a class](docs/oop.md): `extendBus`, `@WithBus`, `@On`, `@Once`, and `dispose()` for the subscriptions of an instance
|
|
39
|
+
- [Use the bus from a class](docs/oop.md): `Emitter`, `extendBus`, `@WithBus`, `@On`, `@Once`, and `dispose()` for the subscriptions of an instance
|
|
40
40
|
|
|
41
41
|
## TypeScript
|
|
42
42
|
|
|
@@ -53,5 +53,5 @@ Rules that generated code often gets wrong:
|
|
|
53
53
|
|
|
54
54
|
- [Semantics](docs/semantics.md): exact rules of emit, abort and ordering
|
|
55
55
|
- [Trace tasks](docs/tracing.md): loggers, Redux DevTools, the `notrace` build
|
|
56
|
-
- [Turn events into Observables](docs/observable.md): `createObserve`
|
|
56
|
+
- [Turn events into Observables](docs/observable.md): `createObserve`, `Subject`
|
|
57
57
|
- [Hot reload and development diagnostics](docs/hmr.md): `devPlugin`
|