nanoeffects 0.7.0 → 0.9.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 -2
- package/dist/{action-creator-Bd4TBZgl.d.ts → action-creator-BrZF6hXc.d.ts} +0 -4
- package/dist/actions.d.ts +41 -25
- package/dist/actions.js +1 -1
- package/dist/bus-CkTHUAvq.js +1 -0
- package/dist/context-COgyNlrC.js +1 -0
- package/dist/{context-plugin-BVNKqkIx.d.ts → context-plugin--HkNN1ha.d.ts} +1 -1
- package/dist/dev.d.ts +9 -8
- package/dist/dev.js +1 -1
- package/dist/effects.d.ts +2 -2
- package/dist/effects.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/match.d.ts +50 -0
- package/dist/match.js +1 -0
- package/dist/{module-DZ5ngUrM.d.ts → module-DqKegXHt.d.ts} +43 -18
- package/dist/notrace/{action-creator-D-y_-xRS.d.ts → action-creator-DHAEgwuh.d.ts} +0 -4
- package/dist/notrace/actions.d.ts +90 -39
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/bus-BmCTo8BG.js +1 -0
- package/dist/notrace/{context-plugin-DDfsaErA.d.ts → context-plugin-BapQkGwQ.d.ts} +1 -1
- package/dist/notrace/context-yYU3_9lq.js +1 -0
- package/dist/notrace/{delay-B_W1tnkS.js → delay-BosJKFjg.js} +1 -1
- package/dist/notrace/effects.d.ts +2 -2
- package/dist/notrace/effects.js +1 -1
- package/dist/notrace/index.d.ts +2 -2
- package/dist/notrace/index.js +1 -1
- package/dist/notrace/{plugin-CDAngHwg.d.ts → plugin-Cxadz6uU.d.ts} +2 -2
- package/dist/notrace/policies.d.ts +30 -2
- package/dist/notrace/policies.js +1 -1
- package/dist/notrace/redux.d.ts +1 -1
- package/dist/observable.d.ts +1 -1
- package/dist/oop.d.ts +131 -0
- package/dist/oop.js +1 -0
- package/dist/{plugin-DQNPBcxx.d.ts → plugin-DkXCvsmB.d.ts} +2 -2
- package/dist/policies.d.ts +3 -2
- package/dist/policies.js +1 -1
- package/dist/react.d.ts +33 -1
- package/dist/react.js +1 -1
- package/dist/redux.d.ts +1 -1
- package/dist/scope.d.ts +5 -6
- package/dist/scope.js +1 -1
- package/dist/signal.d.ts +51 -0
- package/dist/signal.js +1 -0
- package/dist/trace.d.ts +1 -1
- package/dist/{types-BJ0fzV1p.d.ts → types-KavbvHix.d.ts} +13 -3
- package/dist/with-policy-CqUS-VWW.d.ts +30 -0
- package/dist/with-policy-DBV9dh2F.js +1 -0
- package/docs/README.md +7 -0
- package/docs/actions.md +152 -39
- package/docs/cancel-stale-requests.md +111 -0
- package/docs/debounce-and-abort.md +106 -0
- package/docs/guide.md +143 -19
- package/docs/hmr.md +6 -5
- package/docs/migrating-from-effector.md +51 -18
- package/docs/migrating-from-mitt.md +97 -0
- package/docs/migrating-from-rxjs.md +17 -6
- package/docs/motivation.md +41 -0
- package/docs/observable.md +124 -10
- package/docs/oop.md +286 -0
- package/docs/package.md +6 -2
- package/docs/plugins.md +247 -51
- package/docs/policies.md +33 -0
- package/docs/prevent-double-submit.md +128 -0
- package/docs/query-libraries.md +124 -0
- package/docs/react.md +61 -5
- package/docs/scope.md +60 -22
- package/docs/stores.md +263 -109
- package/llms-full.txt +2153 -355
- package/llms.txt +10 -3
- package/package.json +43 -4
- package/skills/nanoeffects/SKILL.md +52 -0
- package/dist/bus-BAs9x7IQ.js +0 -1
- package/dist/context-CK0ZomWt.js +0 -1
- package/dist/notrace/bus-Bdk0YyFQ.js +0 -1
- package/dist/notrace/context-CctVmHLC.js +0 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Why nanoeffects exists
|
|
2
|
+
|
|
3
|
+
Read this page if you build an SDK, a core library or a micro-frontend shell and wonder why you would pick a new event bus. It explains the constraints the library was made for. It does not teach the API: start with the [Guide](guide.md).
|
|
4
|
+
|
|
5
|
+
## The problem: a shared dependency is a shared risk
|
|
6
|
+
|
|
7
|
+
A shared library (an SDK, a core package, a platform shell) is loaded by many apps. Whatever it depends on, the apps depend on too. State and event libraries are the worst case, because they keep **internal state and identity** (a store, a context, a class). Two copies or two versions of them do not merge: they silently stop seeing each other.
|
|
8
|
+
|
|
9
|
+
Module Federation makes this visible. Each remote can bring its own copy unless every team agrees on a `singleton` and a version range. The [Nx guide on library versions](https://nx.dev/docs/kb/manage-library-versions-with-module-federation) lists the result: libraries with internal state conflict, and a missing method in one version breaks callers written for another.
|
|
10
|
+
|
|
11
|
+
Real cases from the libraries nanoeffects was compared with:
|
|
12
|
+
|
|
13
|
+
| Library | What goes wrong | Source |
|
|
14
|
+
| ------- | --------------- | ------ |
|
|
15
|
+
| Redux, React Redux | A shared component library uses `react-redux`. Every micro-frontend must use the same version of that library, or `react-redux` "breaks weirdly". | [react-redux#1949](https://github.com/reduxjs/react-redux/issues/1949) |
|
|
16
|
+
| Redux | A store is reachable only under a `Provider`. Each sub-app needs its own provider, and a second one must not be created when a module is injected. Reducers duplicated between the host and a module add to it. | [Redux on Module Federation](https://dev.to/manomano-tech-team/how-to-work-with-redux-on-module-federation-13gm) |
|
|
17
|
+
| Effector | A library that uses `effector-react/scope` and is bundled into another project gets two instances. Its `Provider` is not seen by `useStore` of the app: "no scope found". | [effector#727](https://github.com/effector/effector/issues/727) |
|
|
18
|
+
| RxJS | Two copies give two `Observable` classes. Types of one version do not fit the other, and operators added to one copy are missing on the other. The advice is "one version in `node_modules`". | [RxJS#2984](https://github.com/ReactiveX/rxjs/issues/2984), [neo4j-javascript-driver#978](https://github.com/neo4j/neo4j-javascript-driver/issues/978) |
|
|
19
|
+
|
|
20
|
+
A library that is **not** a shared singleton cannot break this way. nanoeffects has no dependencies and no module-level state: a bus is an object you create. Two copies, or two versions, of the package in one page are two independent buses. Nothing is registered globally, no `Provider` is needed, and the host app does not have to pick a version for you.
|
|
21
|
+
|
|
22
|
+
## Why not the alternatives
|
|
23
|
+
|
|
24
|
+
| Option | Why it did not fit |
|
|
25
|
+
| ------ | ------------------ |
|
|
26
|
+
| Redux (with sagas) | A store and a provider shared by every consumer, which is the risk above. `redux-saga` also has no cheap way to get an answer back from an event: effects talk through the store. |
|
|
27
|
+
| Effector | A graph with global identity. A version or scope mismatch between the SDK and the app reaches the whole infrastructure. |
|
|
28
|
+
| Zustand | Good state, but it is a store, not a way to run and cancel work. It is the natural place to hold state next to nanoeffects: see [Stores](stores.md). |
|
|
29
|
+
| RxJS | Flexible, but learning its ecosystem (operators, subjects, schedulers) is a high entry cost for every developer who touches the SDK. The package is also not small: [Bundlephobia](https://bundlephobia.com/package/rxjs) reports 17.3 kB minified and gzipped for 7.8.2. |
|
|
30
|
+
|
|
31
|
+
## What the library does instead
|
|
32
|
+
|
|
33
|
+
- **No dependencies, no global state.** It is safe to ship inside an SDK next to whatever the host app runs. It does not need a `singleton` entry in the Module Federation config.
|
|
34
|
+
- **Small enough for a strict budget.** SDKs often get a few kilobytes for all third-party code. `createBus` is about 1.4 kB gzip (1.3 kB brotli); policies, effects and commands are separate entries you import only when you need them. See [Package entries](package.md).
|
|
35
|
+
- **Simple first.** `on` and `emit` are the whole model. Cancellation, retry and the rest are plain functions on top. The [Guide](guide.md) and [Coming from redux-saga](migrating-from-saga.md) map the familiar names.
|
|
36
|
+
- **`emit` can return an answer.** A [command](actions.md) is an event whose handler returns a result, so one module can ask another and wait for the reply. Events alone cannot do that, and it was the main thing missing from the Redux-based options.
|
|
37
|
+
- **Easy to debug.** Event-driven code is hard to follow: you see an effect and not the event that caused it. Every task knows its parent, so [tracing](tracing.md) shows the chain of events, with loggers for the console, the Performance panel and Redux DevTools.
|
|
38
|
+
|
|
39
|
+
## When this is not the right tool
|
|
40
|
+
|
|
41
|
+
nanoeffects holds no state. If you need a store, keep Redux, Zustand or Effector and let the bus run the async work: [Stores](stores.md). If you already have streams everywhere, keep RxJS: [Use nanoeffects next to RxJS](migrating-from-rxjs.md).
|
package/docs/observable.md
CHANGED
|
@@ -1,24 +1,138 @@
|
|
|
1
1
|
# Turn events into Observables
|
|
2
2
|
|
|
3
|
-
Use this page
|
|
3
|
+
Use this page when your app already works with Observables or RxJS and you want a bus event as a stream. If you start from scratch, prefer `bus.on` with policies: they cancel work and show up in traces.
|
|
4
4
|
|
|
5
5
|
`createObserve(bus)` from `nanoeffects/observable` turns a bus event into an Observable that follows the TC39 proposal (`subscribe`, `unsubscribe`, `Symbol.observable`). RxJS and other stream libraries take it as it is. Nothing here depends on them.
|
|
6
6
|
|
|
7
|
+
## Turn an event into a stream
|
|
8
|
+
|
|
9
|
+
Problem: a search box emits `typed` on every key. You want to wait for a pause and skip repeats with operators you already know.
|
|
10
|
+
|
|
7
11
|
<!-- docs-check: skip - needs rxjs, which the project does not depend on -->
|
|
8
12
|
```js
|
|
9
|
-
import {
|
|
10
|
-
import { debounceTime, distinctUntilChanged, switchMap } from "rxjs/operators";
|
|
13
|
+
import { createBus } from "nanoeffects";
|
|
11
14
|
import { createObserve } from "nanoeffects/observable";
|
|
15
|
+
import { from } from "rxjs";
|
|
16
|
+
import { debounceTime, distinctUntilChanged } from "rxjs/operators";
|
|
12
17
|
|
|
13
|
-
const
|
|
18
|
+
const bus = createBus(); // the bus: events and handlers
|
|
19
|
+
const observe = createObserve(bus); // observe(event) gives an Observable per event name
|
|
14
20
|
|
|
15
|
-
from(observe("typed"))
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
const typed$ = from(observe("typed")); // RxJS wraps it with from()
|
|
22
|
+
|
|
23
|
+
const subscription = typed$
|
|
24
|
+
.pipe(debounceTime(300), distinctUntilChanged())
|
|
25
|
+
.subscribe((query) => bus.emit("search", query)); // back into the bus
|
|
18
26
|
```
|
|
19
27
|
|
|
20
28
|
- `observe(event)` returns an Observable of the payloads of that event. Each `subscribe` is a `bus.on` of its own; `unsubscribe()` removes it.
|
|
21
29
|
- `subscribe` takes a function or an object with `next`.
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
- Types follow the event map: `observe("typed")` is an `Observable<string>` when `typed` carries a string.
|
|
31
|
+
|
|
32
|
+
## Send a stream into the bus
|
|
33
|
+
|
|
34
|
+
Problem: `.subscribe((value) => bus.emit(action(value)))` repeats the bus and the action in every subscription. `bus.emitter(action)` from `emitterPlugin` returns that callback, so it is the argument of `subscribe`. It works with any source that takes a `next` function: an RxJS stream, a store callback, a DOM listener.
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
// bus.js
|
|
38
|
+
import { createBus } from "nanoeffects";
|
|
39
|
+
import { emitterPlugin } from "nanoeffects/actions";
|
|
40
|
+
|
|
41
|
+
export const bus = createBus().use(emitterPlugin()); // adds bus.emitter
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
// search.js
|
|
46
|
+
import { createAction } from "nanoeffects/actions";
|
|
47
|
+
|
|
48
|
+
export const queryChanged = createAction("search/queryChanged"); // a plain action creator
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
<!-- docs-check: skip - needs rxjs, which the project does not depend on -->
|
|
52
|
+
```js
|
|
53
|
+
import { fromEvent } from "rxjs";
|
|
54
|
+
import { debounceTime, distinctUntilChanged, map } from "rxjs/operators";
|
|
55
|
+
import { bus } from "./bus";
|
|
56
|
+
import { queryChanged } from "./search";
|
|
57
|
+
|
|
58
|
+
const input = document.querySelector("#search"); // the search box
|
|
59
|
+
|
|
60
|
+
const subscription = fromEvent(input, "input")
|
|
61
|
+
.pipe(
|
|
62
|
+
map((event) => event.target.value),
|
|
63
|
+
debounceTime(300),
|
|
64
|
+
distinctUntilChanged(),
|
|
65
|
+
)
|
|
66
|
+
.subscribe(bus.emitter(queryChanged)); // calls bus.emit(queryChanged(value))
|
|
67
|
+
|
|
68
|
+
bus.on(queryChanged, (query) => console.log("search for", query));
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
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.
|
|
72
|
+
|
|
73
|
+
## Do the work in a handler, not in `switchMap`
|
|
74
|
+
|
|
75
|
+
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.
|
|
76
|
+
|
|
77
|
+
<!-- docs-check: skip - needs rxjs, which the project does not depend on -->
|
|
78
|
+
```js
|
|
79
|
+
import { latest } from "nanoeffects/policies";
|
|
80
|
+
|
|
81
|
+
// RxJS: shape the values (debounce, skip repeats), then hand them to the bus
|
|
82
|
+
from(observe("typed"))
|
|
83
|
+
.pipe(debounceTime(300), distinctUntilChanged())
|
|
84
|
+
.subscribe((query) => bus.emit("search", query));
|
|
85
|
+
|
|
86
|
+
// nanoeffects: one cancellable request; a newer "search" aborts the old one
|
|
87
|
+
bus.on(
|
|
88
|
+
"search",
|
|
89
|
+
latest(async (query, { signal }) => {
|
|
90
|
+
const items = await api.search(query, { signal });
|
|
91
|
+
bus.emit("results", items);
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`switchMap` drops the old inner stream. `latest` aborts the old task through `signal`, so a `fetch` that got the signal really stops.
|
|
97
|
+
|
|
98
|
+
## Convert back to a stream or a promise
|
|
99
|
+
|
|
100
|
+
| You need | Write |
|
|
101
|
+
| -------- | ----- |
|
|
102
|
+
| A stream of events, with operators | `from(observe("event"))` |
|
|
103
|
+
| The next event as a promise | `firstValueFrom(from(observe("event")))`; inside a task `take(ctx, "event")` does the same and respects abort |
|
|
104
|
+
| The events of a bus as an RxJS `Observable` class (for `instanceof` or `pipe` as a method) | `new Observable((subscriber) => observe("event").subscribe(subscriber))` |
|
|
105
|
+
| A task's abort as a stream | `fromEvent(ctx.signal, "abort")`, for `takeUntil` |
|
|
106
|
+
| The latest value to new subscribers | `from(observe("event")).pipe(shareReplay(1))`; the Observable replays nothing by itself |
|
|
107
|
+
|
|
108
|
+
Events that fired before `subscribe` are lost: the Observable is hot, like `bus.on`. Add `startWith(initial)` or `shareReplay` when a late subscriber needs a value.
|
|
109
|
+
|
|
110
|
+
## End the subscription
|
|
111
|
+
|
|
112
|
+
A subscription from `observe` is not tied to a task. Nothing ends it for you. Pick one:
|
|
113
|
+
|
|
114
|
+
<!-- docs-check: skip - needs rxjs, which the project does not depend on -->
|
|
115
|
+
```js
|
|
116
|
+
import { fromEvent, from } from "rxjs";
|
|
117
|
+
import { takeUntil } from "rxjs/operators";
|
|
118
|
+
import { onAbort } from "nanoeffects/effects";
|
|
119
|
+
|
|
120
|
+
bus.on("screen-opened", (_, ctx) => {
|
|
121
|
+
// takeUntil: stop the stream when the task aborts
|
|
122
|
+
from(observe("typed"))
|
|
123
|
+
.pipe(takeUntil(fromEvent(ctx.signal, "abort")))
|
|
124
|
+
.subscribe((query) => bus.emit("search", query));
|
|
125
|
+
|
|
126
|
+
// or: onAbort calls unsubscribe for you
|
|
127
|
+
const subscription = observe("scrolled").subscribe(updatePosition);
|
|
128
|
+
onAbort(ctx, subscription.unsubscribe);
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Outside a task, call `subscription.unsubscribe()` yourself when the screen goes away, or use the signal of a module.
|
|
133
|
+
|
|
134
|
+
## Know the limits
|
|
135
|
+
|
|
136
|
+
- The stream never completes and never fails: events are the only thing that arrives. An error thrown by your subscriber is yours; the bus handles it only if `onError` is set ([Errors](errors.md)).
|
|
137
|
+
- `observe` reads the bus when you call `subscribe`, not when you create it. Create it once, call it as often as needed.
|
|
138
|
+
- Operators that keep state (`scan`, `buffer`, `window`) live in RxJS. The bus keeps no values. For the full split of work see [Use nanoeffects next to RxJS](migrating-from-rxjs.md).
|
package/docs/oop.md
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# Use the bus from a class
|
|
2
|
+
|
|
3
|
+
Use this page when **your domain logic lives in classes**: a service, a store or a controller that needs the bus inside its methods. A class made with `extendBus` has `on`, `emit` and the other methods of the bus, and remembers what each instance subscribed to, so `dispose()` ends it all.
|
|
4
|
+
|
|
5
|
+
If your logic is plain functions, stay with [modules](actions.md#mount-domains-with-modules): a module is the same idea without a class.
|
|
6
|
+
|
|
7
|
+
## The problem without it
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
## Give a class the methods of a bus
|
|
12
|
+
|
|
13
|
+
`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`.
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
// bus.js
|
|
17
|
+
import { createBus } from "nanoeffects";
|
|
18
|
+
|
|
19
|
+
// createBus — the bus that the services of the app share
|
|
20
|
+
export const bus = createBus();
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
// search.js
|
|
25
|
+
import { createAction } from "nanoeffects/actions";
|
|
26
|
+
|
|
27
|
+
// createAction — the events of the search domain
|
|
28
|
+
export const queryChanged = createAction("search/query-changed");
|
|
29
|
+
export const resultsLoaded = createAction("search/results-loaded");
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
// search-service.js
|
|
34
|
+
import { extendBus } from "nanoeffects/oop";
|
|
35
|
+
import { bus } from "./bus";
|
|
36
|
+
import { queryChanged, resultsLoaded } from "./search";
|
|
37
|
+
|
|
38
|
+
class SearchServiceBase {
|
|
39
|
+
constructor(api) {
|
|
40
|
+
this.api = api;
|
|
41
|
+
// this.on — a subscription of this instance; dispose() removes it
|
|
42
|
+
this.on(queryChanged, (query, ctx) => this.search(query, ctx)); // an arrow keeps this
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async search(query, { signal, emit }) {
|
|
46
|
+
const items = await this.api.search(query, { signal });
|
|
47
|
+
emit(resultsLoaded(items));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// extendBus — a subclass whose instances have on, once, off, emit and dispose
|
|
52
|
+
export const SearchService = extendBus(bus, SearchServiceBase);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
import { queryChanged } from "./search";
|
|
57
|
+
import { SearchService } from "./search-service";
|
|
58
|
+
|
|
59
|
+
const service = new SearchService(api); // the arguments go to SearchServiceBase
|
|
60
|
+
service.emit(queryChanged("nano")); // the same bus as bus.emit
|
|
61
|
+
service.dispose(); // its subscriptions are gone
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
What you get:
|
|
65
|
+
|
|
66
|
+
- The class keeps the name of `Base`, and its instances are instances of `Base`.
|
|
67
|
+
- `on`, `once`, `off` and `emit` act on the bus. They can be destructured: `const { emit } = service`.
|
|
68
|
+
- The other methods of the bus, such as `emitter` from [`emitterPlugin`](plugins.md), are there too. Only the plugins used **before** `extendBus` count: the names are taken when it runs.
|
|
69
|
+
- `bus` is the bus, and `Class.bus` is the same one.
|
|
70
|
+
- `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.
|
|
71
|
+
|
|
72
|
+
## Let the class make its bus
|
|
73
|
+
|
|
74
|
+
Leave the bus out and the class gets its own, shared by all of its instances.
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
// notifier.js
|
|
78
|
+
import { extendBus } from "nanoeffects/oop";
|
|
79
|
+
|
|
80
|
+
class NotifierBase {}
|
|
81
|
+
|
|
82
|
+
// extendBus without a bus — a new bus for this class; two classes have two buses
|
|
83
|
+
export const Notifier = extendBus(NotifierBase);
|
|
84
|
+
|
|
85
|
+
const first = new Notifier();
|
|
86
|
+
const second = new Notifier();
|
|
87
|
+
second.on("notified", (message) => console.log(message));
|
|
88
|
+
first.emit("notified", "hello"); // the instances share Notifier.bus
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Pass a bus when you need plugins (`contextPlugin`, `scopePlugin`) or when several classes talk to each other.
|
|
92
|
+
|
|
93
|
+
## End the subscriptions with `dispose`
|
|
94
|
+
|
|
95
|
+
`dispose()` removes every subscription made through `this.on` and `this.once` of that instance, and nothing else. It is safe to call twice. `instance[Symbol.dispose]()` does the same, so `using service = new SearchService(api)` works where the runtime supports it.
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
// dispose.js
|
|
99
|
+
import { createBus } from "nanoeffects";
|
|
100
|
+
import { extendBus } from "nanoeffects/oop";
|
|
101
|
+
|
|
102
|
+
const bus = createBus();
|
|
103
|
+
const Service = extendBus(bus, class {});
|
|
104
|
+
|
|
105
|
+
const service = new Service();
|
|
106
|
+
const stop = service.on("ping", () => console.log("service heard it"));
|
|
107
|
+
bus.on("ping", () => console.log("the bus heard it")); // not a subscription of the service
|
|
108
|
+
|
|
109
|
+
service.dispose();
|
|
110
|
+
bus.emit("ping"); // only "the bus heard it"
|
|
111
|
+
stop(); // the function on returned still works, and does nothing now
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
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.
|
|
115
|
+
|
|
116
|
+
## Keep a method of your own
|
|
117
|
+
|
|
118
|
+
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.
|
|
119
|
+
|
|
120
|
+
```js
|
|
121
|
+
// cache.js
|
|
122
|
+
import { createBus } from "nanoeffects";
|
|
123
|
+
import { disposeBus, extendBus } from "nanoeffects/oop";
|
|
124
|
+
|
|
125
|
+
const bus = createBus();
|
|
126
|
+
|
|
127
|
+
class CacheBase {
|
|
128
|
+
dispose() {
|
|
129
|
+
this.items.clear(); // your own cleanup
|
|
130
|
+
disposeBus(this); // disposeBus — ends the subscriptions of this instance
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
items = new Map();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const Cache = extendBus(bus, CacheBase);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`disposeBus` does nothing for a value that is not an instance of such a class.
|
|
140
|
+
|
|
141
|
+
## Subscribe a method with `@On` and `@Once`
|
|
142
|
+
|
|
143
|
+
A decorator turns a method into a handler of an event for each instance, with `this` bound. You write no `this.on` in the constructor, and `dispose()` ends the subscription too.
|
|
144
|
+
|
|
145
|
+
The bus and the events are the same as above, in TypeScript:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
// bus.ts
|
|
149
|
+
import { createBus } from "nanoeffects";
|
|
150
|
+
|
|
151
|
+
export const bus = createBus();
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
// search.ts
|
|
156
|
+
import { createAction } from "nanoeffects/actions";
|
|
157
|
+
|
|
158
|
+
export const queryChanged = createAction<string>("search/query-changed");
|
|
159
|
+
export const resultsLoaded = createAction<string[]>("search/results-loaded");
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
// search-service.ts
|
|
164
|
+
import { latest } from "nanoeffects/policies";
|
|
165
|
+
import { extendBus, On, Once } from "nanoeffects/oop";
|
|
166
|
+
import { bus } from "./bus";
|
|
167
|
+
import { queryChanged, resultsLoaded } from "./search";
|
|
168
|
+
|
|
169
|
+
class SearchServiceBase {
|
|
170
|
+
// On — this method handles queryChanged for each instance, this is the instance;
|
|
171
|
+
// latest — each new query aborts the one before it
|
|
172
|
+
@On(queryChanged, latest)
|
|
173
|
+
async search(query: string, { signal, emit }: { signal: AbortSignal; emit: (event: unknown) => void }) {
|
|
174
|
+
const items = await api.search(query, { signal });
|
|
175
|
+
emit(resultsLoaded(items));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Once — runs for the first event only
|
|
179
|
+
@Once("search/ready")
|
|
180
|
+
ready() {
|
|
181
|
+
console.log("first ready");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export const SearchService = extendBus(bus, SearchServiceBase);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Things to know:
|
|
189
|
+
|
|
190
|
+
- The event is a name, an action creator or a command, as for `bus.on`. For a creator or a command the payload of the method is checked against it.
|
|
191
|
+
- The second argument, `wrap`, takes the bound handler and returns the one to subscribe: `latest`, or `(handler) => debounce(300, handler)`. A policy needs it: `latest(this.search)` wraps the function without `this`, and the bound handler is what `wrap` gets.
|
|
192
|
+
- The subscriptions are made right after the constructor of `Base`. An event that is emitted inside that constructor does not reach them.
|
|
193
|
+
- A subclass inherits them. A method that a subclass overrides is subscribed only if the override is decorated.
|
|
194
|
+
- On a class that `extendBus` did not make, a decorator does nothing.
|
|
195
|
+
- Both the decorators of the language and `experimentalDecorators` work. Your compiler has to support the one you use: TypeScript 5 or newer for the first, and the `experimentalDecorators` option for the second.
|
|
196
|
+
|
|
197
|
+
## Decorate the class with `@WithBus`
|
|
198
|
+
|
|
199
|
+
`@WithBus(bus)` does what `extendBus(bus, Base)` does, as a class decorator. `@WithBus()` makes a bus for the class.
|
|
200
|
+
|
|
201
|
+
<!-- docs-check: continue -->
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
// search-service.ts
|
|
205
|
+
import { type BusMethods, On, WithBus } from "nanoeffects/oop";
|
|
206
|
+
import { bus } from "./bus";
|
|
207
|
+
import { queryChanged } from "./search";
|
|
208
|
+
|
|
209
|
+
// TypeScript does not change the type of a decorated class, so say what it gets;
|
|
210
|
+
// checker only: at run time WithBus already adds the methods
|
|
211
|
+
interface SearchService extends BusMethods<typeof bus> {}
|
|
212
|
+
|
|
213
|
+
// WithBus — replaces the class by extendBus(bus, SearchService)
|
|
214
|
+
@WithBus(bus)
|
|
215
|
+
class SearchService {
|
|
216
|
+
@On(queryChanged)
|
|
217
|
+
search(query: string) {
|
|
218
|
+
console.log(query);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export { SearchService };
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Prefer `extendBus(bus, Base)` when you can: its types come from the call and need no `interface`.
|
|
226
|
+
|
|
227
|
+
## Pass a method by hand
|
|
228
|
+
|
|
229
|
+
A handler given to `this.on` is called as the bus calls it, with no `this`. A method passed bare loses `this`, as in plain JavaScript:
|
|
230
|
+
|
|
231
|
+
```js
|
|
232
|
+
// handlers.js
|
|
233
|
+
import { createBus } from "nanoeffects";
|
|
234
|
+
import { extendBus } from "nanoeffects/oop";
|
|
235
|
+
|
|
236
|
+
class BaseWithHandler {
|
|
237
|
+
name = "handler";
|
|
238
|
+
|
|
239
|
+
handle() {
|
|
240
|
+
console.log(this?.name);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
listen() {
|
|
244
|
+
this.on("ping", this.handle); // this is lost: prints undefined
|
|
245
|
+
this.on("ping", () => this.handle()); // an arrow keeps it
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export const Listener = extendBus(createBus(), BaseWithHandler);
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Use an arrow or a decorated method. The library does not bind `this` for you: a policy such as `latest(this.handle)` wraps the function, so binding would work in half of the cases.
|
|
253
|
+
|
|
254
|
+
## Type the instances (optional)
|
|
255
|
+
|
|
256
|
+
`extendBus` returns a class that takes the arguments of `Base`, and its instances are `Base` plus `BusMethods<typeof bus>`: the function members of the bus (`on`, `emit`, and those of plugins used before) with `dispose` and `bus`. A member of `Base` with the same name keeps its own type.
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
// service.ts
|
|
260
|
+
import { createBus } from "nanoeffects";
|
|
261
|
+
import { extendBus } from "nanoeffects/oop";
|
|
262
|
+
|
|
263
|
+
type Events = { "order/placed": { id: string } };
|
|
264
|
+
|
|
265
|
+
// Events — checker only: the payloads of the bus
|
|
266
|
+
const bus = createBus<Events>();
|
|
267
|
+
|
|
268
|
+
class OrderServiceBase {
|
|
269
|
+
name: string;
|
|
270
|
+
|
|
271
|
+
constructor(name: string) {
|
|
272
|
+
this.name = name;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export const OrderService = extendBus(bus, OrderServiceBase);
|
|
277
|
+
|
|
278
|
+
const service = new OrderService("main"); // the constructor of OrderServiceBase
|
|
279
|
+
service.emit("order/placed", { id: "o1" }); // checked against Events
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## What is not done
|
|
283
|
+
|
|
284
|
+
- `this` is not bound in a handler that you pass by hand (see above).
|
|
285
|
+
- The methods of a plugin used after `extendBus` are not on the class. Use the plugins first, then `extendBus`.
|
|
286
|
+
- One bus for each instance, mixing the methods into an object that exists, and decorators for fields or parameters are not part of this entry.
|
package/docs/package.md
CHANGED
|
@@ -8,6 +8,9 @@ Imports are split so you only load what you use. `createBus` stays small; the re
|
|
|
8
8
|
| `nanoeffects/policies` | `latest`, `leading`, `debounce`, `throttle`, `queue` |
|
|
9
9
|
| `nanoeffects/effects` | `retry`, `timeout`, `poll`, `delay`, `take`, `all`, `allSettled`, `race`, `effectsPlugin` |
|
|
10
10
|
| `nanoeffects/actions` | `createAction`, `createCommand`, `createJob`, `createModule`, `modulePlugin`, `commandsPlugin` |
|
|
11
|
+
| `nanoeffects/match` | `matchPlugin`: `on`, `once` and `off` that take a function which picks the events |
|
|
12
|
+
| `nanoeffects/signal` | `signalPlugin`: `emit(call, { signal })` ties the tasks of an emit to an outside `AbortSignal` |
|
|
13
|
+
| `nanoeffects/oop` | `extendBus`, `WithBus`, `On`, `Once`, `disposeBus`: the methods of a bus on a class, with the subscriptions of each instance ended by `dispose()` |
|
|
11
14
|
| `nanoeffects/scope` | `scopePlugin`, `effect`: fields for one command, action, job, module or `emit` (the types `Scope`, `Effect`) |
|
|
12
15
|
| `nanoeffects/redux` | `createEffectsMiddleware` |
|
|
13
16
|
| `nanoeffects/observable` | `createObserve` |
|
|
@@ -19,9 +22,10 @@ The `...Plugin` names above are used with `bus.use`. [Plugins](plugins.md) lists
|
|
|
19
22
|
|
|
20
23
|
## Give the docs to an AI assistant
|
|
21
24
|
|
|
22
|
-
The package ships
|
|
25
|
+
The package ships plain-text files for assistants that write code with it:
|
|
23
26
|
|
|
24
27
|
- `llms.txt`: a short index. It states the rules that generated code often gets wrong and links to every page of these docs.
|
|
25
28
|
- `llms-full.txt`: the same summary followed by the text of every linked page, in one file.
|
|
29
|
+
- `skills/nanoeffects/SKILL.md`: a skill for assistants that support skills (for example Claude Code). It says when to suggest nanoeffects, when not to, and the same rules. Copy the folder to `.claude/skills/` in your project or to `~/.claude/skills/`.
|
|
26
30
|
|
|
27
|
-
Find them in `node_modules/nanoeffects/`, or load `llms-full.txt` from a CDN such as `https://unpkg.com/nanoeffects/llms-full.txt`.
|
|
31
|
+
Find them in `node_modules/nanoeffects/`, or load `llms-full.txt` from a CDN such as `https://unpkg.com/nanoeffects/llms-full.txt`. All match the installed version.
|