nanoeffects 0.14.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/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/docs/cancel-stale-requests.md +6 -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/plugins.md +8 -2
- package/docs/tracing.md +31 -7
- package/llms-full.txt +232 -21
- package/llms.txt +2 -2
- package/package.json +2 -2
package/dist/observable.d.ts
CHANGED
|
@@ -17,4 +17,33 @@ export interface Subscribable<T> {
|
|
|
17
17
|
export type Observable<T> = Subscribable<T> & {};
|
|
18
18
|
export type Observe<E extends EventMap> = <K extends Keys<E>>(event: K) => Observable<E[K]>;
|
|
19
19
|
export declare const createObserve: <E extends EventMap>(bus: Bus<E>) => Observe<E>;
|
|
20
|
+
/**
|
|
21
|
+
* What a `Subject` reads and writes: the `on` and `emit` of a bus. An instance
|
|
22
|
+
* of a class made by `extendBus` or `Emitter` fits as well, and its `on` keeps
|
|
23
|
+
* the subscription for `dispose()`.
|
|
24
|
+
*/
|
|
25
|
+
export type Source<E extends EventMap> = Pick<Bus<E>, "on" | "emit">;
|
|
26
|
+
/**
|
|
27
|
+
* One event of a source as an Observable that can also push a payload back:
|
|
28
|
+
* `subscribe` is `source.on(event, …)` and `next` is `source.emit(event, payload)`.
|
|
29
|
+
* It never completes, fails or replays, and it can be extended.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* const typed = new Subject(bus, "typed"); // Subject — the event "typed" of the bus as a stream
|
|
34
|
+
* const subscription = typed.subscribe((text) => console.log(text));
|
|
35
|
+
* typed.next("hello"); // the same as bus.emit("typed", "hello")
|
|
36
|
+
* subscription.unsubscribe();
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare class Subject<E extends EventMap, K extends Keys<E>> implements Observable<E[K]> {
|
|
40
|
+
[$$observable]: () => Subscribable<E[K]>;
|
|
41
|
+
readonly source: Source<E>;
|
|
42
|
+
readonly event: K;
|
|
43
|
+
constructor(source: Source<E>, event: K);
|
|
44
|
+
subscribe(observer: Observer<E[K]>): {
|
|
45
|
+
unsubscribe(): void;
|
|
46
|
+
};
|
|
47
|
+
next(payload: E[K]): void;
|
|
48
|
+
}
|
|
20
49
|
//#endregion
|
package/dist/observable.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const e=typeof Symbol==`function`&&Symbol.observable||`@@observable`,t=t=>n=>({subscribe:e=>({unsubscribe:
|
|
1
|
+
const e=typeof Symbol==`function`&&Symbol.observable||`@@observable`,t=e=>t=>{typeof e==`function`?e(t):e.next?.(t)},n=n=>r=>({subscribe:e=>({unsubscribe:n.on(r,t(e))}),[e](){return this}});var r=class{source;event;constructor(e,t){this.source=e,this.event=t,this.next=this.next.bind(this)}subscribe(e){return{unsubscribe:this.source.on(this.event,t(e))}}next(e){let t=this.source.emit;Reflect.apply(t,this.source,[this.event,e])}[e](){return this}};export{e as $$observable,r as Subject,n as createObserve};
|
package/dist/oop.d.ts
CHANGED
|
@@ -87,7 +87,51 @@ export declare function Once<P>(event: EventOf<P>, options: ListenerOptions): Me
|
|
|
87
87
|
export declare function Once(event: string, wrap?: Wrap, options?: ListenerOptions): MethodDecorator<never>;
|
|
88
88
|
export declare function Once(event: string, options: ListenerOptions): MethodDecorator<never>;
|
|
89
89
|
//#endregion
|
|
90
|
+
//#region src/oop/emitter.d.ts
|
|
91
|
+
/**
|
|
92
|
+
* A base class whose every instance is an independent emitter: it has `on`,
|
|
93
|
+
* `once`, `off`, `emit`, `dispose` and its own `bus`, typed by the event map
|
|
94
|
+
* `E`. Events of one instance never reach another instance. A subscription
|
|
95
|
+
* made through `this.on` or `this.once`, a method decorated with `@On` or
|
|
96
|
+
* `@Once`, and a `Subject` made over `this` all end with `dispose()`.
|
|
97
|
+
*
|
|
98
|
+
* `on`, `once`, `off` and `emit` are bound to the instance, so they can be
|
|
99
|
+
* taken off it (`const { emit } = video`). A subclass replaces one with an
|
|
100
|
+
* arrow property; TypeScript does not let it redeclare one as a method.
|
|
101
|
+
*
|
|
102
|
+
* It has no methods of plugins, because its bus is made inside. For a bus that
|
|
103
|
+
* instances share, or for plugins, use `extendBus(bus)`.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* type VideoEvents = { "video/time": number };
|
|
108
|
+
*
|
|
109
|
+
* class VideoService extends Emitter<VideoEvents> {
|
|
110
|
+
* constructor() {
|
|
111
|
+
* super();
|
|
112
|
+
* this.on("video/time", (seconds) => console.log(seconds)); // ends with dispose()
|
|
113
|
+
* }
|
|
114
|
+
* }
|
|
115
|
+
*
|
|
116
|
+
* const video = new VideoService();
|
|
117
|
+
* video.emit("video/time", 12);
|
|
118
|
+
* video.dispose(); // its subscriptions are gone
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export declare class Emitter<E extends EventMap = EventMap> {
|
|
122
|
+
on: Bus<E>["on"];
|
|
123
|
+
once: Bus<E>["once"];
|
|
124
|
+
off: Bus<E>["off"];
|
|
125
|
+
emit: Bus<E>["emit"];
|
|
126
|
+
[Symbol.dispose]: () => void;
|
|
127
|
+
constructor();
|
|
128
|
+
get bus(): Bus<E>;
|
|
129
|
+
/** Removes every subscription made through `on` and `once` of this instance. */
|
|
130
|
+
dispose(): void;
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
90
133
|
//#region src/oop/extend.d.ts
|
|
134
|
+
declare class Empty {}
|
|
91
135
|
/**
|
|
92
136
|
* Makes a class from `Base` whose instances have the methods of a bus, with the
|
|
93
137
|
* subscriptions made through an instance ended by `dispose()`. `new Extended(…)`
|
|
@@ -100,6 +144,9 @@ export declare function Once(event: string, options: ListenerOptions): MethodDec
|
|
|
100
144
|
* shared by its instances. The methods of the plugins used on `bus` before this
|
|
101
145
|
* call are on the instances too; the ones of plugins used later are not.
|
|
102
146
|
*
|
|
147
|
+
* `extendBus(bus)` with no class returns a class over an empty base, to extend
|
|
148
|
+
* as it is: `class Service extends extendBus(bus) {}`.
|
|
149
|
+
*
|
|
103
150
|
* @example
|
|
104
151
|
* ```ts
|
|
105
152
|
* class SearchServiceBase {
|
|
@@ -116,6 +163,7 @@ export declare function Once(event: string, options: ListenerOptions): MethodDec
|
|
|
116
163
|
*/
|
|
117
164
|
export declare function extendBus<C extends Constructor>(Base: C): Extended<C, Bus<EventMap>>;
|
|
118
165
|
export declare function extendBus<B extends object, C extends Constructor>(bus: B, Base: C): Extended<C, B>;
|
|
166
|
+
export declare function extendBus<B extends object>(bus: B): Extended<typeof Empty, B>;
|
|
119
167
|
//#endregion
|
|
120
168
|
//#region src/oop/subscriptions.d.ts
|
|
121
169
|
/**
|
package/dist/oop.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as e}from"./bus-Cdw5ajKF.js";const t=/* @__PURE__ */ new WeakMap,n=e=>typeof e==`function`,r=e=>Reflect.ownKeys(e).flatMap(t=>{let r=Reflect.getOwnPropertyDescriptor(e,t)?.value;return n(r)?[{key:t,value:r}]:[]}),i=/* @__PURE__ */ new WeakMap,a=e=>{let n=i.get(e);if(n)return n;let a=/* @__PURE__ */ new Set,o=[];for(let n=e;n&&n!==Object.prototype;n=Reflect.getPrototypeOf(n))for(let{key:e,value:i}of r(n)){if(e===`constructor`||a.has(e))continue;a.add(e);let n=t.get(i);n&&o.push({key:e,method:i,spec:n})}return i.set(e,o),o},o=/* @__PURE__ */ new WeakMap,s=e=>typeof e==`object`&&!!e||typeof e==`function`,c=e=>{let t=o.get(e);if(t)return t;let n=/* @__PURE__ */ new Set;return o.set(e,n),n},l=e=>{if(typeof e==`string`)return e;if(s(e)){let t=Reflect.get(e,`type`);return typeof t==`string`?t:e}return e},u=(e,t,n)=>Reflect.apply(Reflect.get(e,t),e,n),d=(e,t,n,r)=>{let[i,a,...o]=r,s=c(e),l={event:i,handler:a,off:()=>{}},d=u(t,n,[i,n===`once`?(e,t)=>(s.delete(l),typeof a==`function`?Reflect.apply(a,null,[e,t]):void 0):a,...o]),f=typeof d==`function`?()=>d():()=>{};return l.off=f,s.add(l),()=>{s.delete(l),f()}},f=(e,t,n)=>{let[r,i]=n,a=c(e);r===void 0&&a.clear();for(let e of[...a])Object.is(l(e.event),l(r))&&(i===void 0||e.handler===i)&&(a.delete(e),e.off());return u(t,`off`,n)},p=e=>{if(!s(e))return;let t=o.get(e);if(t){o.delete(e);for(let e of t)e.off()}},m=/* @__PURE__ */ new Set([`on`,`once`,`off`,`emit`,`ctx`,`commands`,`trace`,`addContext`,`applyScope`,`fitScope`,`app`,`use`]),h=Reflect.get(Symbol,`dispose`),g=e=>typeof e==`function
|
|
1
|
+
import{r as e}from"./bus-Cdw5ajKF.js";const t=/* @__PURE__ */ new WeakMap,n=e=>typeof e==`function`,r=e=>Reflect.ownKeys(e).flatMap(t=>{let r=Reflect.getOwnPropertyDescriptor(e,t)?.value;return n(r)?[{key:t,value:r}]:[]}),i=/* @__PURE__ */ new WeakMap,a=e=>{let n=i.get(e);if(n)return n;let a=/* @__PURE__ */ new Set,o=[];for(let n=e;n&&n!==Object.prototype;n=Reflect.getPrototypeOf(n))for(let{key:e,value:i}of r(n)){if(e===`constructor`||a.has(e))continue;a.add(e);let n=t.get(i);n&&o.push({key:e,method:i,spec:n})}return i.set(e,o),o},o=/* @__PURE__ */ new WeakMap,s=e=>typeof e==`object`&&!!e||typeof e==`function`,c=e=>{let t=o.get(e);if(t)return t;let n=/* @__PURE__ */ new Set;return o.set(e,n),n},l=e=>{if(typeof e==`string`)return e;if(s(e)){let t=Reflect.get(e,`type`);return typeof t==`string`?t:e}return e},u=(e,t,n)=>Reflect.apply(Reflect.get(e,t),e,n),d=(e,t,n,r)=>{let[i,a,...o]=r,s=c(e),l={event:i,handler:a,off:()=>{}},d=u(t,n,[i,n===`once`?(e,t)=>(s.delete(l),typeof a==`function`?Reflect.apply(a,null,[e,t]):void 0):a,...o]),f=typeof d==`function`?()=>d():()=>{};return l.off=f,s.add(l),()=>{s.delete(l),f()}},f=(e,t,n)=>{let[r,i]=n,a=c(e);r===void 0&&a.clear();for(let e of[...a])Object.is(l(e.event),l(r))&&(i===void 0||e.handler===i)&&(a.delete(e),e.off());return u(t,`off`,n)},p=e=>{if(!s(e))return;let t=o.get(e);if(t){o.delete(e);for(let e of t)e.off()}},m=/* @__PURE__ */ new Set([`on`,`once`,`off`,`emit`,`ctx`,`commands`,`trace`,`addContext`,`applyScope`,`fitScope`,`app`,`use`]),h=Reflect.get(Symbol,`dispose`),g=e=>typeof e==`function`;var _=class{};const v=(e,t,n,r)=>{n in t||Object.defineProperty(e,n,{configurable:!0,enumerable:!1,...r})},y=/* @__PURE__ */ new WeakMap,b=(e,t)=>{let n=y.get(e);if(n)return n;let r={on:(...n)=>d(e,t,`on`,n),once:(...n)=>d(e,t,`once`,n),off:(...n)=>f(e,t,n)};return y.set(e,r),r},x=(e,t)=>(...n)=>Reflect.apply(Reflect.get(e,t),e,n),S=(e,t)=>{for(let{method:n,spec:r}of a(Reflect.getPrototypeOf(e)??{})){let i=(t,r)=>Reflect.apply(n,e,[t,r]),a=r.wrap?r.wrap(i):i,o=r.once?`once`:`on`;d(e,t,o,[r.event,a,r.options])}},C=(e,t,n)=>{for(let r of[`on`,`once`,`off`])v(e,t,r,{get(){return b(this,n)[r]}});v(e,t,`emit`,{value:x(n,`emit`)});for(let[r,i]of Object.entries(n))typeof i==`function`&&!m.has(r)&&v(e,t,r,{value:x(n,r)});let r=function(){p(this)};v(e,t,`dispose`,{value:r}),h&&v(e,t,h,{value:r}),v(e,t,`bus`,{get:()=>n})},w=(e,t)=>{let n=function(...r){let i=Reflect.construct(t,r,new.target??n);return S(i,e),i};return Object.setPrototypeOf(n,t),n.prototype=Object.create(t.prototype,{constructor:{value:n,writable:!0,configurable:!0}}),Object.defineProperty(n,"name",{value:t.name,configurable:!0}),`bus`in t||Object.defineProperty(n,"bus",{value:e,configurable:!0}),C(n.prototype,t.prototype,e),n};function T(t,n){return g(t)?w(e(),t):w(t,n??_)}function E(e){return t=>e?T(e,t):T(t)}const D=e=>{let[t,n,r]=e;return typeof n==`string`||typeof n==`symbol`?Reflect.get(Object(r),`value`):t},O=(e,n,r,i)=>((...a)=>{let o=D(a);if(typeof o==`function`){let a=typeof n==`function`?n:void 0,s=typeof n==`function`?r:n??r;t.set(o,{event:e,wrap:a,once:i,options:s})}});function k(e,t,n){return O(e,t,n,!1)}function A(e,t,n){return O(e,t,n,!0)}const j=/* @__PURE__ */ new WeakMap,M=t=>{let n=j.get(t);if(n)return n;let r=e();return j.set(t,r),r},N=[`on`,`once`,`off`,`emit`],P=e=>{for(let t of N){let n=Reflect.get(e,t);Reflect.set(e,t,n.bind(e))}};var F=class{constructor(){P(this),S(this,M(this))}get bus(){return M(this)}dispose(){p(this)}};const I={on(...e){return d(this,M(this),`on`,e)},once(...e){return d(this,M(this),`once`,e)},off(...e){return f(this,M(this),e)},emit(...e){let t=M(this);return Reflect.apply(Reflect.get(t,`emit`),t,e)}};for(let e of N)Object.defineProperty(F.prototype,e,{configurable:!0,writable:!0,value:I[e]});h&&Object.defineProperty(F.prototype,h,{configurable:!0,value:function(){this.dispose()}});export{F as Emitter,k as On,A as Once,E as WithBus,p as disposeBus,T as extendBus};
|
|
@@ -60,7 +60,7 @@ const bus = createBus(); // the bus: events and handlers
|
|
|
60
60
|
// latest: a newer search aborts the request in flight
|
|
61
61
|
const searchLatest = latest(async (query, { signal }) => {
|
|
62
62
|
const items = await api.search(query, { signal });
|
|
63
|
-
|
|
63
|
+
render(items);
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
// debounce: run after 300 ms without a new "typed"
|
|
@@ -84,7 +84,11 @@ const searchOnce = timeout(5000, (query, { signal }) => api.search(query, { sign
|
|
|
84
84
|
// retry: up to 4 attempts, backoff makes the pause longer each time
|
|
85
85
|
const searchWithRetry = retry(searchOnce, {
|
|
86
86
|
times: 4,
|
|
87
|
-
delay: backoff({
|
|
87
|
+
delay: backoff({
|
|
88
|
+
base: 200,
|
|
89
|
+
max: 5000,
|
|
90
|
+
jitter: true,
|
|
91
|
+
}),
|
|
88
92
|
});
|
|
89
93
|
|
|
90
94
|
// latest: a newer search aborts the whole retry chain
|
|
@@ -25,13 +25,11 @@ const bus = createBus(); // the bus: events and handlers
|
|
|
25
25
|
const searchLatest = latest(async (query, { signal }) => {
|
|
26
26
|
// signal: pass it on so the browser cancels the request itself
|
|
27
27
|
const items = await api.search(query, { signal });
|
|
28
|
-
|
|
28
|
+
render(items);
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
// debounce: run searchLatest after 300 ms without a new "typed"
|
|
32
32
|
bus.on("typed", debounce(300, searchLatest));
|
|
33
|
-
|
|
34
|
-
bus.on("results", (items) => render(items));
|
|
35
33
|
```
|
|
36
34
|
|
|
37
35
|
What to replace: `api.search` is your own request function. It must accept `{ signal }` and give it to `fetch`. `render` shows the items.
|
package/docs/effects.md
CHANGED
|
@@ -124,7 +124,11 @@ import { backoff, retry } from "nanoeffects/effects";
|
|
|
124
124
|
const loadWithRetry = retry(loadUser, {
|
|
125
125
|
times: 5,
|
|
126
126
|
// backoff — pause 200, 400, 800, … ms, never over 5 s, a random part of it
|
|
127
|
-
delay: backoff({
|
|
127
|
+
delay: backoff({
|
|
128
|
+
base: 200,
|
|
129
|
+
max: 5000,
|
|
130
|
+
jitter: true,
|
|
131
|
+
}),
|
|
128
132
|
});
|
|
129
133
|
```
|
|
130
134
|
|
|
@@ -147,8 +151,11 @@ If handlers call the same helpers often, put them on the task once. A handler th
|
|
|
147
151
|
import { createBus } from "nanoeffects";
|
|
148
152
|
import { all, delay, effectsPlugin, race } from "nanoeffects/effects";
|
|
149
153
|
|
|
150
|
-
// effectsPlugin — add race, all and delay to the context of every task
|
|
151
|
-
const
|
|
154
|
+
// effectsPlugin — add race, all and delay to the context of every task
|
|
155
|
+
const effects = effectsPlugin({ race, all, delay });
|
|
156
|
+
|
|
157
|
+
// use returns the same bus, typed with the new fields
|
|
158
|
+
const bus = createBus().use(effects);
|
|
152
159
|
|
|
153
160
|
bus.on("load", async (id, { race, all, delay }) => {
|
|
154
161
|
// race — same as race(ctx, …)
|
package/docs/observable.md
CHANGED
|
@@ -70,6 +70,56 @@ bus.on(queryChanged, (query) => console.log("search for", query));
|
|
|
70
70
|
|
|
71
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
72
|
|
|
73
|
+
## Read and write one event with a Subject
|
|
74
|
+
|
|
75
|
+
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.
|
|
76
|
+
|
|
77
|
+
`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.
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
import { createBus } from "nanoeffects";
|
|
81
|
+
import { Subject } from "nanoeffects/observable";
|
|
82
|
+
|
|
83
|
+
const bus = createBus(); // the bus: events and handlers
|
|
84
|
+
|
|
85
|
+
const typed = new Subject(bus, "typed"); // Subject — the event "typed" as a stream you can also write to
|
|
86
|
+
|
|
87
|
+
const subscription = typed.subscribe((text) => console.log(text)); // like bus.on("typed", …)
|
|
88
|
+
typed.next("hello"); // like bus.emit("typed", "hello"): bus.on handlers and subscribers both run
|
|
89
|
+
subscription.unsubscribe(); // removes this subscription only
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- `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.
|
|
93
|
+
- A subscriber that is added later does not get a value emitted before. Add `startWith` or `shareReplay` in RxJS when it should.
|
|
94
|
+
- 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.
|
|
95
|
+
|
|
96
|
+
### Extend it for a named stream
|
|
97
|
+
|
|
98
|
+
Problem: the event name is repeated wherever the stream is made. A subclass fixes it once.
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
import { createBus } from "nanoeffects";
|
|
102
|
+
import { Subject } from "nanoeffects/observable";
|
|
103
|
+
|
|
104
|
+
class TypedText extends Subject {
|
|
105
|
+
constructor(bus) {
|
|
106
|
+
super(bus, "typed"); // the event is fixed by the class
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
next(text) {
|
|
110
|
+
super.next(text.trim()); // a method of the subclass replaces the one of Subject
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const bus = createBus(); // the bus: events and handlers
|
|
115
|
+
const typed = new TypedText(bus);
|
|
116
|
+
typed.next(" hello "); // emits "typed" with "hello"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Use it inside a class with a bus
|
|
120
|
+
|
|
121
|
+
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)).
|
|
122
|
+
|
|
73
123
|
## Do the work in a handler, not in `switchMap`
|
|
74
124
|
|
|
75
125
|
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/docs/oop.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# Use the bus from a class
|
|
2
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.
|
|
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. 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:
|
|
4
|
+
|
|
5
|
+
| You want | Use |
|
|
6
|
+
| -------- | --- |
|
|
7
|
+
| every `new Service()` to be an independent emitter | `extends Emitter` |
|
|
8
|
+
| the instances to share a bus, or to use plugins | `extends extendBus(bus)` |
|
|
4
9
|
|
|
5
10
|
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
11
|
|
|
@@ -8,7 +13,68 @@ If your logic is plain functions, stay with [modules](actions.md#mount-domains-w
|
|
|
8
13
|
|
|
9
14
|
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
15
|
|
|
11
|
-
##
|
|
16
|
+
## Make an independent emitter with `Emitter`
|
|
17
|
+
|
|
18
|
+
`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`.
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
// video-service.js
|
|
22
|
+
import { Emitter } from "nanoeffects/oop";
|
|
23
|
+
|
|
24
|
+
// Emitter — a base class: each new VideoService() gets on, once, off, emit, dispose and its own bus
|
|
25
|
+
export class VideoService extends Emitter {
|
|
26
|
+
constructor() {
|
|
27
|
+
super();
|
|
28
|
+
// this.on — a subscription of this instance; dispose() removes it
|
|
29
|
+
this.on("played", () => console.log("playing"));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
import { VideoService } from "./video-service";
|
|
36
|
+
|
|
37
|
+
const first = new VideoService();
|
|
38
|
+
const second = new VideoService();
|
|
39
|
+
first.on("paused", () => console.log("first paused"));
|
|
40
|
+
second.emit("paused"); // nothing: the instances do not share a bus
|
|
41
|
+
first.emit("paused"); // logs "first paused"
|
|
42
|
+
first.dispose(); // its subscriptions are gone
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
What you get:
|
|
46
|
+
|
|
47
|
+
- `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.
|
|
48
|
+
- `@On` and `@Once` work on its methods (see below). They are subscribed when the base constructor runs, so before the fields of your subclass.
|
|
49
|
+
- `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.
|
|
50
|
+
- 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).
|
|
51
|
+
- 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).
|
|
52
|
+
|
|
53
|
+
### Type the events (optional)
|
|
54
|
+
|
|
55
|
+
`Emitter` takes the events of the class as a type parameter.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
// video-service.ts
|
|
59
|
+
import { Emitter } from "nanoeffects/oop";
|
|
60
|
+
|
|
61
|
+
// VideoEvents — checker only: the payload of each event of this class
|
|
62
|
+
type VideoEvents = { "video/time": number; "video/paused": void };
|
|
63
|
+
|
|
64
|
+
export class VideoService extends Emitter<VideoEvents> {
|
|
65
|
+
constructor() {
|
|
66
|
+
super();
|
|
67
|
+
// seconds is a number because "video/time" carries one
|
|
68
|
+
this.on("video/time", (seconds) => console.log(seconds));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const video = new VideoService();
|
|
73
|
+
video.emit("video/time", 12); // checked against VideoEvents
|
|
74
|
+
video.emit("video/paused"); // no payload
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Give a class the methods of a shared bus
|
|
12
78
|
|
|
13
79
|
`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
80
|
|
|
@@ -69,9 +135,33 @@ What you get:
|
|
|
69
135
|
- `bus` is the bus, and `Class.bus` is the same one.
|
|
70
136
|
- `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
137
|
|
|
138
|
+
### Extend the result with no class of your own
|
|
139
|
+
|
|
140
|
+
`extendBus(bus)` without a class returns a class with the methods of that bus and nothing else. Extend it as it is.
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
// service.js
|
|
144
|
+
import { createBus } from "nanoeffects";
|
|
145
|
+
import { extendBus } from "nanoeffects/oop";
|
|
146
|
+
|
|
147
|
+
const bus = createBus(); // the bus that the services share
|
|
148
|
+
|
|
149
|
+
// extendBus(bus) — a class with on, once, off, emit and dispose over this bus
|
|
150
|
+
export class AudioService extends extendBus(bus) {
|
|
151
|
+
constructor() {
|
|
152
|
+
super();
|
|
153
|
+
this.on("muted", () => console.log("muted")); // ends with dispose()
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const audio = new AudioService();
|
|
158
|
+
bus.emit("muted"); // the instances share this bus: logs "muted"
|
|
159
|
+
audio.dispose();
|
|
160
|
+
```
|
|
161
|
+
|
|
72
162
|
## Let the class make its bus
|
|
73
163
|
|
|
74
|
-
Leave the bus out and the class gets its own, shared by all of its instances.
|
|
164
|
+
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.
|
|
75
165
|
|
|
76
166
|
```js
|
|
77
167
|
// notifier.js
|
|
@@ -113,6 +203,37 @@ stop(); // the function on returned still works, and does nothing now
|
|
|
113
203
|
|
|
114
204
|
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
205
|
|
|
206
|
+
## Stream an event with a `Subject`
|
|
207
|
+
|
|
208
|
+
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.
|
|
209
|
+
|
|
210
|
+
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)).
|
|
211
|
+
|
|
212
|
+
```js
|
|
213
|
+
// typing.js
|
|
214
|
+
import { createBus } from "nanoeffects";
|
|
215
|
+
import { Subject } from "nanoeffects/observable";
|
|
216
|
+
import { extendBus } from "nanoeffects/oop";
|
|
217
|
+
|
|
218
|
+
const bus = createBus(); // the bus: events and handlers
|
|
219
|
+
|
|
220
|
+
class TypingBase {
|
|
221
|
+
constructor() {
|
|
222
|
+
this.typed = new Subject(this, "typed"); // Subject — the event "typed" as a stream; this is the source
|
|
223
|
+
this.typed.subscribe((text) => console.log(text));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const Typing = extendBus(bus, TypingBase); // extendBus — a class with on, emit and dispose
|
|
228
|
+
|
|
229
|
+
const typing = new Typing();
|
|
230
|
+
typing.typed.next("a"); // logs "a"
|
|
231
|
+
typing.dispose(); // ends the subscription of the Subject too
|
|
232
|
+
typing.typed.next("b"); // nothing is logged
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
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.
|
|
236
|
+
|
|
116
237
|
## Keep a method of your own
|
|
117
238
|
|
|
118
239
|
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.
|
|
@@ -285,4 +406,5 @@ service.emit("order/placed", { id: "o1" }); // checked against Events
|
|
|
285
406
|
|
|
286
407
|
- `this` is not bound in a handler that you pass by hand (see above).
|
|
287
408
|
- The methods of a plugin used after `extendBus` are not on the class. Use the plugins first, then `extendBus`.
|
|
288
|
-
-
|
|
409
|
+
- `Emitter` has no methods of plugins, because its bus is made inside. Use `extendBus(bus)` for them.
|
|
410
|
+
- Mixing the methods into an object that exists, and decorators for fields or parameters are not part of this entry.
|
package/docs/plugins.md
CHANGED
|
@@ -42,7 +42,9 @@ import { createBus } from "nanoeffects";
|
|
|
42
42
|
import { delay, effectsPlugin, race } from "nanoeffects/effects";
|
|
43
43
|
|
|
44
44
|
// effectsPlugin — race and delay are bound to the task of each handler
|
|
45
|
-
const
|
|
45
|
+
const effects = effectsPlugin({ race, delay });
|
|
46
|
+
|
|
47
|
+
const bus = createBus().use(effects);
|
|
46
48
|
|
|
47
49
|
bus.on("load", async (id, { race, delay }) => {
|
|
48
50
|
const result = await race({
|
|
@@ -148,8 +150,12 @@ With a tracer on the bus, the plugin lets a trace say which subscription began a
|
|
|
148
150
|
import { createBus } from "nanoeffects";
|
|
149
151
|
import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
150
152
|
|
|
153
|
+
// createTracer: sends each trace event to the logger; consoleLogger: prints one line each
|
|
154
|
+
const trace = createTracer(consoleLogger());
|
|
155
|
+
|
|
151
156
|
// tracePlugin — names the roots, groups and listeners of the trace
|
|
152
|
-
const bus = createBus({ trace
|
|
157
|
+
const bus = createBus({ trace })
|
|
158
|
+
.use(tracePlugin());
|
|
153
159
|
|
|
154
160
|
bus.on("name/changed", saveName, { traceId: "saveNotifier" }); // a listener you can find in the log
|
|
155
161
|
```
|
package/docs/tracing.md
CHANGED
|
@@ -66,7 +66,10 @@ bus.on("load", async (id, ctx) => {
|
|
|
66
66
|
import { createBus } from "nanoeffects";
|
|
67
67
|
import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
68
68
|
|
|
69
|
-
|
|
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 })
|
|
70
73
|
.use(tracePlugin()); // tracePlugin: reports each emit as an event
|
|
71
74
|
```
|
|
72
75
|
|
|
@@ -95,7 +98,9 @@ import { createAction, emitterPlugin } from "nanoeffects/actions";
|
|
|
95
98
|
import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
96
99
|
|
|
97
100
|
// createTracer: reports every task phase to the console
|
|
98
|
-
const
|
|
101
|
+
const trace = createTracer(consoleLogger());
|
|
102
|
+
|
|
103
|
+
const bus = createBus({ trace })
|
|
99
104
|
.use(tracePlugin()) // tracePlugin: makes the root task that traceId names
|
|
100
105
|
.use(emitterPlugin());
|
|
101
106
|
|
|
@@ -136,8 +141,12 @@ import { createBus } from "nanoeffects";
|
|
|
136
141
|
import { createCommand } from "nanoeffects/actions";
|
|
137
142
|
import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
138
143
|
|
|
144
|
+
// createTracer: sends each trace event to the logger; consoleLogger: prints one line each
|
|
145
|
+
const trace = createTracer(consoleLogger());
|
|
146
|
+
|
|
139
147
|
// tracePlugin: reads traceGroupId from the scope of a call
|
|
140
|
-
const bus = createBus({ trace
|
|
148
|
+
const bus = createBus({ trace })
|
|
149
|
+
.use(tracePlugin());
|
|
141
150
|
|
|
142
151
|
// the scope names the flow; traceGroupId is for the tracer, not a field of ctx
|
|
143
152
|
const pay = createCommand("checkout/pay", async () => {}, {
|
|
@@ -163,7 +172,11 @@ Give the listener a `traceId` when you register it. The plugin makes each call o
|
|
|
163
172
|
import { createBus } from "nanoeffects";
|
|
164
173
|
import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
165
174
|
|
|
166
|
-
|
|
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());
|
|
167
180
|
|
|
168
181
|
// traceId: the name of this listener in the trace
|
|
169
182
|
bus.on("name/changed", saveName, { traceId: "saveNotifier" });
|
|
@@ -270,7 +283,10 @@ export const bus = createBus({ trace: createTracer(logger) })
|
|
|
270
283
|
import { reduxDevtools } from "nanoeffects/trace";
|
|
271
284
|
|
|
272
285
|
// debugging a race: which calls did a policy cancel or drop?
|
|
273
|
-
reduxDevtools({
|
|
286
|
+
reduxDevtools({
|
|
287
|
+
name: "app",
|
|
288
|
+
policies: ["cancel", "drop"],
|
|
289
|
+
});
|
|
274
290
|
|
|
275
291
|
// every stage of every policy, one function for the choice
|
|
276
292
|
reduxDevtools({
|
|
@@ -279,10 +295,18 @@ reduxDevtools({
|
|
|
279
295
|
});
|
|
280
296
|
|
|
281
297
|
// the old log: every stage, and the effects below the handlers
|
|
282
|
-
reduxDevtools({
|
|
298
|
+
reduxDevtools({
|
|
299
|
+
name: "app",
|
|
300
|
+
policies: true,
|
|
301
|
+
depth: 1,
|
|
302
|
+
payload: "all",
|
|
303
|
+
});
|
|
283
304
|
|
|
284
305
|
// keep the payloads out of the extension
|
|
285
|
-
reduxDevtools({
|
|
306
|
+
reduxDevtools({
|
|
307
|
+
name: "app",
|
|
308
|
+
payload: "none",
|
|
309
|
+
});
|
|
286
310
|
```
|
|
287
311
|
|
|
288
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/llms-full.txt
CHANGED
|
@@ -628,7 +628,9 @@ import { createBus } from "nanoeffects";
|
|
|
628
628
|
import { delay, effectsPlugin, race } from "nanoeffects/effects";
|
|
629
629
|
|
|
630
630
|
// effectsPlugin — race and delay are bound to the task of each handler
|
|
631
|
-
const
|
|
631
|
+
const effects = effectsPlugin({ race, delay });
|
|
632
|
+
|
|
633
|
+
const bus = createBus().use(effects);
|
|
632
634
|
|
|
633
635
|
bus.on("load", async (id, { race, delay }) => {
|
|
634
636
|
const result = await race({
|
|
@@ -734,8 +736,12 @@ With a tracer on the bus, the plugin lets a trace say which subscription began a
|
|
|
734
736
|
import { createBus } from "nanoeffects";
|
|
735
737
|
import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
736
738
|
|
|
739
|
+
// createTracer: sends each trace event to the logger; consoleLogger: prints one line each
|
|
740
|
+
const trace = createTracer(consoleLogger());
|
|
741
|
+
|
|
737
742
|
// tracePlugin — names the roots, groups and listeners of the trace
|
|
738
|
-
const bus = createBus({ trace
|
|
743
|
+
const bus = createBus({ trace })
|
|
744
|
+
.use(tracePlugin());
|
|
739
745
|
|
|
740
746
|
bus.on("name/changed", saveName, { traceId: "saveNotifier" }); // a listener you can find in the log
|
|
741
747
|
```
|
|
@@ -1124,7 +1130,7 @@ const bus = createBus(); // the bus: events and handlers
|
|
|
1124
1130
|
// latest: a newer search aborts the request in flight
|
|
1125
1131
|
const searchLatest = latest(async (query, { signal }) => {
|
|
1126
1132
|
const items = await api.search(query, { signal });
|
|
1127
|
-
|
|
1133
|
+
render(items);
|
|
1128
1134
|
});
|
|
1129
1135
|
|
|
1130
1136
|
// debounce: run after 300 ms without a new "typed"
|
|
@@ -1148,7 +1154,11 @@ const searchOnce = timeout(5000, (query, { signal }) => api.search(query, { sign
|
|
|
1148
1154
|
// retry: up to 4 attempts, backoff makes the pause longer each time
|
|
1149
1155
|
const searchWithRetry = retry(searchOnce, {
|
|
1150
1156
|
times: 4,
|
|
1151
|
-
delay: backoff({
|
|
1157
|
+
delay: backoff({
|
|
1158
|
+
base: 200,
|
|
1159
|
+
max: 5000,
|
|
1160
|
+
jitter: true,
|
|
1161
|
+
}),
|
|
1152
1162
|
});
|
|
1153
1163
|
|
|
1154
1164
|
// latest: a newer search aborts the whole retry chain
|
|
@@ -1204,13 +1214,11 @@ const bus = createBus(); // the bus: events and handlers
|
|
|
1204
1214
|
const searchLatest = latest(async (query, { signal }) => {
|
|
1205
1215
|
// signal: pass it on so the browser cancels the request itself
|
|
1206
1216
|
const items = await api.search(query, { signal });
|
|
1207
|
-
|
|
1217
|
+
render(items);
|
|
1208
1218
|
});
|
|
1209
1219
|
|
|
1210
1220
|
// debounce: run searchLatest after 300 ms without a new "typed"
|
|
1211
1221
|
bus.on("typed", debounce(300, searchLatest));
|
|
1212
|
-
|
|
1213
|
-
bus.on("results", (items) => render(items));
|
|
1214
1222
|
```
|
|
1215
1223
|
|
|
1216
1224
|
What to replace: `api.search` is your own request function. It must accept `{ signal }` and give it to `fetch`. `render` shows the items.
|
|
@@ -1545,7 +1553,11 @@ import { backoff, retry } from "nanoeffects/effects";
|
|
|
1545
1553
|
const loadWithRetry = retry(loadUser, {
|
|
1546
1554
|
times: 5,
|
|
1547
1555
|
// backoff — pause 200, 400, 800, … ms, never over 5 s, a random part of it
|
|
1548
|
-
delay: backoff({
|
|
1556
|
+
delay: backoff({
|
|
1557
|
+
base: 200,
|
|
1558
|
+
max: 5000,
|
|
1559
|
+
jitter: true,
|
|
1560
|
+
}),
|
|
1549
1561
|
});
|
|
1550
1562
|
```
|
|
1551
1563
|
|
|
@@ -1568,8 +1580,11 @@ If handlers call the same helpers often, put them on the task once. A handler th
|
|
|
1568
1580
|
import { createBus } from "nanoeffects";
|
|
1569
1581
|
import { all, delay, effectsPlugin, race } from "nanoeffects/effects";
|
|
1570
1582
|
|
|
1571
|
-
// effectsPlugin — add race, all and delay to the context of every task
|
|
1572
|
-
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);
|
|
1573
1588
|
|
|
1574
1589
|
bus.on("load", async (id, { race, all, delay }) => {
|
|
1575
1590
|
// race — same as race(ctx, …)
|
|
@@ -3414,7 +3429,12 @@ Source: docs/oop.md
|
|
|
3414
3429
|
|
|
3415
3430
|
# Use the bus from a class
|
|
3416
3431
|
|
|
3417
|
-
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)` |
|
|
3418
3438
|
|
|
3419
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.
|
|
3420
3440
|
|
|
@@ -3422,7 +3442,68 @@ If your logic is plain functions, stay with [modules](actions.md#mount-domains-w
|
|
|
3422
3442
|
|
|
3423
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.
|
|
3424
3444
|
|
|
3425
|
-
##
|
|
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
|
|
3426
3507
|
|
|
3427
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`.
|
|
3428
3509
|
|
|
@@ -3483,9 +3564,33 @@ What you get:
|
|
|
3483
3564
|
- `bus` is the bus, and `Class.bus` is the same one.
|
|
3484
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.
|
|
3485
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
|
+
|
|
3486
3591
|
## Let the class make its bus
|
|
3487
3592
|
|
|
3488
|
-
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.
|
|
3489
3594
|
|
|
3490
3595
|
```js
|
|
3491
3596
|
// notifier.js
|
|
@@ -3527,6 +3632,37 @@ stop(); // the function on returned still works, and does nothing now
|
|
|
3527
3632
|
|
|
3528
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.
|
|
3529
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
|
+
|
|
3530
3666
|
## Keep a method of your own
|
|
3531
3667
|
|
|
3532
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.
|
|
@@ -3699,7 +3835,8 @@ service.emit("order/placed", { id: "o1" }); // checked against Events
|
|
|
3699
3835
|
|
|
3700
3836
|
- `this` is not bound in a handler that you pass by hand (see above).
|
|
3701
3837
|
- The methods of a plugin used after `extendBus` are not on the class. Use the plugins first, then `extendBus`.
|
|
3702
|
-
-
|
|
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.
|
|
3703
3840
|
|
|
3704
3841
|
---
|
|
3705
3842
|
Source: docs/typescript.md
|
|
@@ -4613,7 +4750,10 @@ bus.on("load", async (id, ctx) => {
|
|
|
4613
4750
|
import { createBus } from "nanoeffects";
|
|
4614
4751
|
import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
4615
4752
|
|
|
4616
|
-
|
|
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 })
|
|
4617
4757
|
.use(tracePlugin()); // tracePlugin: reports each emit as an event
|
|
4618
4758
|
```
|
|
4619
4759
|
|
|
@@ -4642,7 +4782,9 @@ import { createAction, emitterPlugin } from "nanoeffects/actions";
|
|
|
4642
4782
|
import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
4643
4783
|
|
|
4644
4784
|
// createTracer: reports every task phase to the console
|
|
4645
|
-
const
|
|
4785
|
+
const trace = createTracer(consoleLogger());
|
|
4786
|
+
|
|
4787
|
+
const bus = createBus({ trace })
|
|
4646
4788
|
.use(tracePlugin()) // tracePlugin: makes the root task that traceId names
|
|
4647
4789
|
.use(emitterPlugin());
|
|
4648
4790
|
|
|
@@ -4683,8 +4825,12 @@ import { createBus } from "nanoeffects";
|
|
|
4683
4825
|
import { createCommand } from "nanoeffects/actions";
|
|
4684
4826
|
import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
4685
4827
|
|
|
4828
|
+
// createTracer: sends each trace event to the logger; consoleLogger: prints one line each
|
|
4829
|
+
const trace = createTracer(consoleLogger());
|
|
4830
|
+
|
|
4686
4831
|
// tracePlugin: reads traceGroupId from the scope of a call
|
|
4687
|
-
const bus = createBus({ trace
|
|
4832
|
+
const bus = createBus({ trace })
|
|
4833
|
+
.use(tracePlugin());
|
|
4688
4834
|
|
|
4689
4835
|
// the scope names the flow; traceGroupId is for the tracer, not a field of ctx
|
|
4690
4836
|
const pay = createCommand("checkout/pay", async () => {}, {
|
|
@@ -4710,7 +4856,11 @@ Give the listener a `traceId` when you register it. The plugin makes each call o
|
|
|
4710
4856
|
import { createBus } from "nanoeffects";
|
|
4711
4857
|
import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
4712
4858
|
|
|
4713
|
-
|
|
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());
|
|
4714
4864
|
|
|
4715
4865
|
// traceId: the name of this listener in the trace
|
|
4716
4866
|
bus.on("name/changed", saveName, { traceId: "saveNotifier" });
|
|
@@ -4817,7 +4967,10 @@ export const bus = createBus({ trace: createTracer(logger) })
|
|
|
4817
4967
|
import { reduxDevtools } from "nanoeffects/trace";
|
|
4818
4968
|
|
|
4819
4969
|
// debugging a race: which calls did a policy cancel or drop?
|
|
4820
|
-
reduxDevtools({
|
|
4970
|
+
reduxDevtools({
|
|
4971
|
+
name: "app",
|
|
4972
|
+
policies: ["cancel", "drop"],
|
|
4973
|
+
});
|
|
4821
4974
|
|
|
4822
4975
|
// every stage of every policy, one function for the choice
|
|
4823
4976
|
reduxDevtools({
|
|
@@ -4826,10 +4979,18 @@ reduxDevtools({
|
|
|
4826
4979
|
});
|
|
4827
4980
|
|
|
4828
4981
|
// the old log: every stage, and the effects below the handlers
|
|
4829
|
-
reduxDevtools({
|
|
4982
|
+
reduxDevtools({
|
|
4983
|
+
name: "app",
|
|
4984
|
+
policies: true,
|
|
4985
|
+
depth: 1,
|
|
4986
|
+
payload: "all",
|
|
4987
|
+
});
|
|
4830
4988
|
|
|
4831
4989
|
// keep the payloads out of the extension
|
|
4832
|
-
reduxDevtools({
|
|
4990
|
+
reduxDevtools({
|
|
4991
|
+
name: "app",
|
|
4992
|
+
payload: "none",
|
|
4993
|
+
});
|
|
4833
4994
|
```
|
|
4834
4995
|
|
|
4835
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.
|
|
@@ -4938,6 +5099,56 @@ bus.on(queryChanged, (query) => console.log("search for", query));
|
|
|
4938
5099
|
|
|
4939
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.
|
|
4940
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
|
+
|
|
4941
5152
|
## Do the work in a handler, not in `switchMap`
|
|
4942
5153
|
|
|
4943
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`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nanoeffects",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Cancel stale async work and fix race conditions in event handlers: a typed event bus where every handler is a task with an AbortSignal, plus latest, debounce, throttle, queue, retry, timeout and poll. About 1.4 kB gzip.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"event-bus",
|
|
@@ -226,7 +226,7 @@
|
|
|
226
226
|
{
|
|
227
227
|
"name": "oop (class entry)",
|
|
228
228
|
"path": "dist/oop.js",
|
|
229
|
-
"limit": "2.
|
|
229
|
+
"limit": "2.6 kB"
|
|
230
230
|
},
|
|
231
231
|
{
|
|
232
232
|
"name": "scope (plugin and effect)",
|