nanoeffects 0.29.0 → 0.30.1
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 +6 -5
- package/dist/{action-creator-jeEqIKgK.d.ts → action-creator-6rlBTmH9.d.ts} +27 -2
- package/dist/actions.d.ts +3 -3
- package/dist/actions.js +1 -1
- package/dist/{bus-0uYn_kRj.js → bus-DFlmNJ6h.js} +1 -1
- package/dist/context-B5Z2RpJt.js +1 -0
- package/dist/{context-types-Bme1mzkn.d.ts → context-types-DKNc5mkS.d.ts} +1 -1
- package/dist/context.d.ts +2 -2
- package/dist/dev.d.ts +44 -3
- package/dist/dev.js +2 -1
- package/dist/effects.d.ts +2 -2
- package/dist/effects.js +1 -1
- package/dist/emit-key-C6FL7Ewn.js +1 -0
- package/dist/guard.d.ts +59 -0
- package/dist/guard.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/match.d.ts +1 -1
- package/dist/{module-BWMW83qk.d.ts → module-G39MaVOt.d.ts} +2 -2
- package/dist/notrace/{action-creator-xpQ_I2h8.d.ts → action-creator-BrMSO3RP.d.ts} +1 -0
- package/dist/notrace/actions.d.ts +2 -2
- package/dist/notrace/effects.d.ts +2 -2
- package/dist/notrace/index.d.ts +1 -1
- package/dist/notrace/{plugin-6LhNx0th.d.ts → plugin-7GUL2EW5.d.ts} +1 -1
- package/dist/notrace/policies.d.ts +1 -1
- package/dist/notrace/redux.d.ts +1 -1
- package/dist/observable.d.ts +1 -1
- package/dist/oop.d.ts +1 -1
- package/dist/oop.js +1 -1
- package/dist/{plugin-BaErevLy.d.ts → plugin-BWNP23hr.d.ts} +2 -2
- package/dist/policies.d.ts +2 -2
- package/dist/policies.js +1 -1
- package/dist/production/dev.js +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/redux.d.ts +1 -1
- package/dist/scope.d.ts +2 -2
- package/dist/signal.d.ts +2 -2
- package/dist/signal.js +1 -1
- package/dist/site-DLvUoXs3.js +1 -0
- package/dist/svelte.d.ts +1 -1
- package/dist/trace.d.ts +151 -2
- package/dist/trace.js +3 -1
- package/dist/{types-D5xsR37j.d.ts → types-N_cdRr1r.d.ts} +2 -2
- package/dist/vue.d.ts +1 -1
- package/dist/{with-policy-C6BIiPIi.js → with-policy-CeM1rtES.js} +1 -1
- package/dist/{with-policy-BZVRBUOm.d.ts → with-policy-CthiBp_z.d.ts} +1 -1
- package/docs/README.md +62 -31
- package/docs/actions.md +44 -27
- package/docs/cancel-stale-requests.md +2 -0
- package/docs/chat-example.md +3 -1
- package/docs/chat-widget.md +2 -0
- package/docs/context.md +321 -9
- package/docs/debounce-and-abort.md +3 -1
- package/docs/debugging-loops.md +283 -0
- package/docs/effects.md +255 -138
- package/docs/errors.md +4 -2
- package/docs/examples.md +60 -0
- package/docs/guide.md +39 -22
- package/docs/hmr.md +35 -1
- package/docs/loop-guard.md +109 -0
- package/docs/migrating-from-effector.md +4 -2
- package/docs/migrating-from-mitt.md +6 -4
- package/docs/migrating-from-rxjs.md +2 -0
- package/docs/migrating-from-saga.md +2 -0
- package/docs/motivation.md +14 -21
- package/docs/observable.md +2 -0
- package/docs/oop.md +3 -1
- package/docs/package.md +4 -1
- package/docs/pass-the-signal.md +281 -0
- package/docs/plugins.md +22 -5
- package/docs/policies.md +73 -46
- package/docs/prevent-double-submit.md +3 -1
- package/docs/query-libraries.md +3 -1
- package/docs/react.md +42 -13
- package/docs/recipes.md +528 -0
- package/docs/ru/README.md +62 -0
- package/docs/ru/actions.md +412 -0
- package/docs/ru/cancel-stale-requests.md +106 -0
- package/docs/ru/chat-example.md +259 -0
- package/docs/ru/chat-widget.md +131 -0
- package/docs/ru/context.md +401 -0
- package/docs/ru/debounce-and-abort.md +105 -0
- package/docs/ru/debugging-loops.md +283 -0
- package/docs/ru/effects.md +538 -0
- package/docs/ru/errors.md +36 -0
- package/docs/ru/examples.md +60 -0
- package/docs/ru/guide.md +530 -0
- package/docs/ru/hmr.md +83 -0
- package/docs/ru/loop-guard.md +109 -0
- package/docs/ru/migrating-from-effector.md +339 -0
- package/docs/ru/migrating-from-mitt.md +99 -0
- package/docs/ru/migrating-from-rxjs.md +197 -0
- package/docs/ru/migrating-from-saga.md +97 -0
- package/docs/ru/motivation.md +47 -0
- package/docs/ru/observable.md +233 -0
- package/docs/ru/oop.md +431 -0
- package/docs/ru/package.md +42 -0
- package/docs/ru/pass-the-signal.md +281 -0
- package/docs/ru/plugins.md +398 -0
- package/docs/ru/policies.md +201 -0
- package/docs/ru/prevent-double-submit.md +122 -0
- package/docs/ru/query-libraries.md +392 -0
- package/docs/ru/react.md +309 -0
- package/docs/ru/recipes.md +528 -0
- package/docs/ru/semantics.md +19 -0
- package/docs/ru/server.md +204 -0
- package/docs/ru/starter.md +118 -0
- package/docs/ru/stores.md +673 -0
- package/docs/ru/svelte.md +78 -0
- package/docs/ru/tracing.md +466 -0
- package/docs/ru/typescript.md +170 -0
- package/docs/ru/vue.md +92 -0
- package/docs/semantics.md +2 -0
- package/docs/server.md +2 -0
- package/docs/starter.md +2 -0
- package/docs/stores.md +2 -0
- package/docs/svelte.md +3 -1
- package/docs/tracing.md +58 -1
- package/docs/typescript.md +2 -0
- package/docs/vue.md +3 -1
- package/llms-full.txt +2008 -440
- package/llms.txt +11 -8
- package/package.json +22 -5
- package/dist/context-DtUnzC-1.js +0 -1
- package/docs/scope.md +0 -278
- package/docs/stability.md +0 -90
package/docs/ru/oop.md
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
# Работа с ООП: шина в классе
|
|
2
|
+
|
|
3
|
+
> **[Уровень D](README.md#виды-проектов)**: доменная логика вне UI. Сначала прочитайте: [Руководство](guide.md#1-создание-шины-событий).
|
|
4
|
+
|
|
5
|
+
**Доменная логика живёт в классах**: в сервисе, store или контроллере, которым нужна шина внутри методов. Есть два способа дать классу методы шины. Оба запоминают подписки каждого экземпляра, поэтому `dispose()` завершает их все:
|
|
6
|
+
|
|
7
|
+
| Что нужно | Использовать |
|
|
8
|
+
| --------- | ------------ |
|
|
9
|
+
| чтобы каждый `new Service()` был независимым эмиттером | `extends Emitter` |
|
|
10
|
+
| чтобы экземпляры делили шину или использовали плагины | `extends extendBus(bus)` |
|
|
11
|
+
|
|
12
|
+
Если ваша логика — обычные функции, оставайтесь с [модулями](actions.md#модули): модуль — та же идея без класса.
|
|
13
|
+
|
|
14
|
+
## На этой странице
|
|
15
|
+
|
|
16
|
+
- [Проблема без этого](#проблема-без-этого)
|
|
17
|
+
- [Независимый эмиттер через `Emitter`](#независимый-эмиттер-через-emitter)
|
|
18
|
+
- [Типы событий](#типы-событий)
|
|
19
|
+
- [Методы общей шины в классе](#методы-общей-шины-в-классе)
|
|
20
|
+
- [Расширение результата без собственного класса](#расширение-результата-без-собственного-класса)
|
|
21
|
+
- [Собственная шина класса](#собственная-шина-класса)
|
|
22
|
+
- [Завершение подписок через `dispose`](#завершение-подписок-через-dispose)
|
|
23
|
+
- [Событие как поток через `Subject`](#событие-как-поток-через-subject)
|
|
24
|
+
- [Собственный метод](#собственный-метод)
|
|
25
|
+
- [Подписка метода через `@On` и `@Once`](#подписка-метода-через-on-и-once)
|
|
26
|
+
- [Декоратор класса `@WithBus`](#декоратор-класса-withbus)
|
|
27
|
+
- [Метод, переданный вручную](#метод-переданный-вручную)
|
|
28
|
+
- [Типы экземпляров](#типы-экземпляров)
|
|
29
|
+
- [Что не сделано](#что-не-сделано)
|
|
30
|
+
|
|
31
|
+
## Проблема без этого
|
|
32
|
+
|
|
33
|
+
Класс, который слушает шину, держит ссылку на неё и хранит все функции отписки в списке, чтобы вызвать их, когда объект уходит. Забудете одну — и старый объект продолжает обрабатывать события. `extendBus` делает и то и другое за вас.
|
|
34
|
+
|
|
35
|
+
## Независимый эмиттер через `Emitter`
|
|
36
|
+
|
|
37
|
+
`Emitter` — базовый класс. У каждого экземпляра класса, который его расширяет, своя шина: события одного экземпляра никогда не доходят до другого. Это форма `EventEmitter` из Node.
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
// video-service.js
|
|
41
|
+
import { Emitter } from "nanoeffects/oop";
|
|
42
|
+
|
|
43
|
+
// Emitter — базовый класс: каждый new VideoService() получает on, once, off, emit, dispose и свою шину
|
|
44
|
+
export class VideoService extends Emitter {
|
|
45
|
+
constructor() {
|
|
46
|
+
super();
|
|
47
|
+
// this.on — подписка этого экземпляра; dispose() её снимает
|
|
48
|
+
this.on("played", () => console.log("playing"));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
import { VideoService } from "./video-service";
|
|
55
|
+
|
|
56
|
+
const first = new VideoService();
|
|
57
|
+
const second = new VideoService();
|
|
58
|
+
first.on("paused", () => console.log("first paused"));
|
|
59
|
+
second.emit("paused"); // ничего: экземпляры не делят шину
|
|
60
|
+
first.emit("paused"); // выводит "first paused"
|
|
61
|
+
first.dispose(); // его подписки сняты
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Что вы получаете:
|
|
65
|
+
|
|
66
|
+
- `on`, `once`, `off`, `emit`, `dispose` и `bus` (шина этого экземпляра). `instance[Symbol.dispose]()` делает то же, что `dispose()`, так что `using video = new VideoService()` работает там, где среда выполнения это поддерживает.
|
|
67
|
+
- `@On` и `@Once` работают на его методах (см. ниже). Подписки делаются, когда выполняется конструктор базового класса, то есть раньше полей вашего подкласса.
|
|
68
|
+
- `on`, `once`, `off` и `emit` привязаны к экземпляру, так что `const { emit } = video` работает. Подкласс заменяет один из них свойством-стрелкой (`emit = (…) => …`); TypeScript не позволяет переобъявить его методом. `dispose` — метод, и его можно переопределить как обычно.
|
|
69
|
+
- Методов плагинов нет: шина создаётся внутри, поэтому плагинов на ней нет. Для плагинов или для шины, которую делят несколько экземпляров или классов, используйте `extendBus` (следующие разделы).
|
|
70
|
+
- Для потока одного события см. [`Subject`](observable.md#чтение-и-запись-одного-события-через-subject) и [Событие как поток через `Subject`](#событие-как-поток-через-subject).
|
|
71
|
+
|
|
72
|
+
### Типы событий
|
|
73
|
+
|
|
74
|
+
`Emitter` принимает события класса как параметр типа.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
// video-service.ts
|
|
78
|
+
import { Emitter } from "nanoeffects/oop";
|
|
79
|
+
|
|
80
|
+
// VideoEvents — только для проверки типов: payload каждого события этого класса
|
|
81
|
+
type VideoEvents = { "video/time": number; "video/paused": void };
|
|
82
|
+
|
|
83
|
+
export class VideoService extends Emitter<VideoEvents> {
|
|
84
|
+
constructor() {
|
|
85
|
+
super();
|
|
86
|
+
// seconds — число, потому что "video/time" несёт число
|
|
87
|
+
this.on("video/time", (seconds) => console.log(seconds));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const video = new VideoService();
|
|
92
|
+
video.emit("video/time", 12); // проверяется по VideoEvents
|
|
93
|
+
video.emit("video/paused"); // без payload
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Методы общей шины в классе
|
|
97
|
+
|
|
98
|
+
`extendBus(bus, Base)` возвращает подкласс `Base`. У его экземпляров есть методы шины, а аргументы `new` идут в конструктор `Base`.
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
// bus.js
|
|
102
|
+
import { createBus } from "nanoeffects";
|
|
103
|
+
|
|
104
|
+
// createBus — шина, которую делят сервисы приложения
|
|
105
|
+
export const bus = createBus();
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
// search.js
|
|
110
|
+
import { createAction } from "nanoeffects/actions";
|
|
111
|
+
|
|
112
|
+
// createAction — события домена поиска
|
|
113
|
+
export const queryChanged = createAction("search/query-changed");
|
|
114
|
+
export const resultsLoaded = createAction("search/results-loaded");
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
// search-service.js
|
|
119
|
+
import { extendBus } from "nanoeffects/oop";
|
|
120
|
+
import { bus } from "./bus";
|
|
121
|
+
import { queryChanged, resultsLoaded } from "./search";
|
|
122
|
+
|
|
123
|
+
class SearchServiceBase {
|
|
124
|
+
constructor(api) {
|
|
125
|
+
this.api = api;
|
|
126
|
+
// this.on — подписка этого экземпляра; dispose() её снимает
|
|
127
|
+
this.on(queryChanged, (query, ctx) => this.search(query, ctx)); // стрелка сохраняет this
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async search(query, { signal, emit }) {
|
|
131
|
+
const items = await this.api.search(query, { signal });
|
|
132
|
+
emit(resultsLoaded(items));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// extendBus — подкласс, у экземпляров которого есть on, once, off, emit и dispose
|
|
137
|
+
export const SearchService = extendBus(bus, SearchServiceBase);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
import { queryChanged } from "./search";
|
|
142
|
+
import { SearchService } from "./search-service";
|
|
143
|
+
|
|
144
|
+
const service = new SearchService(api); // аргументы идут в SearchServiceBase
|
|
145
|
+
service.emit(queryChanged("nano")); // та же шина, что и bus.emit
|
|
146
|
+
service.dispose(); // его подписки сняты
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Что вы получаете:
|
|
150
|
+
|
|
151
|
+
- Класс сохраняет имя `Base`, а его экземпляры — экземпляры `Base`.
|
|
152
|
+
- `on`, `once`, `off` и `emit` действуют на шину. Их можно деструктурировать: `const { emit } = service`.
|
|
153
|
+
- Остальные методы шины, например `emitter` из [`emitterPlugin`](plugins.md), тоже есть. Считаются только плагины, подключённые **до** `extendBus`: имена берутся при его запуске.
|
|
154
|
+
- `bus` — это шина, а `Class.bus` — та же самая.
|
|
155
|
+
- `this.on` работает в конструкторе `Base`. Методы находятся на прототипе подкласса, который уже является прототипом экземпляра.
|
|
156
|
+
|
|
157
|
+
### Расширение результата без собственного класса
|
|
158
|
+
|
|
159
|
+
`extendBus(bus)` без класса возвращает класс с методами этой шины и больше ничем. Расширяйте его как есть.
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
// service.js
|
|
163
|
+
import { createBus } from "nanoeffects";
|
|
164
|
+
import { extendBus } from "nanoeffects/oop";
|
|
165
|
+
|
|
166
|
+
const bus = createBus(); // шина, которую делят сервисы
|
|
167
|
+
|
|
168
|
+
// extendBus(bus) — класс с on, once, off, emit и dispose поверх этой шины
|
|
169
|
+
export class AudioService extends extendBus(bus) {
|
|
170
|
+
constructor() {
|
|
171
|
+
super();
|
|
172
|
+
this.on("muted", () => console.log("muted")); // заканчивается через dispose()
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const audio = new AudioService();
|
|
177
|
+
bus.emit("muted"); // экземпляры делят эту шину: выводит "muted"
|
|
178
|
+
audio.dispose();
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Собственная шина класса
|
|
182
|
+
|
|
183
|
+
Опустите шину, и класс получит свою, общую для всех его экземпляров. Это одна шина на класс; `Emitter` делает по одной на экземпляр.
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
// notifier.js
|
|
187
|
+
import { extendBus } from "nanoeffects/oop";
|
|
188
|
+
|
|
189
|
+
class NotifierBase {}
|
|
190
|
+
|
|
191
|
+
// extendBus без шины — новая шина для этого класса; у двух классов две шины
|
|
192
|
+
export const Notifier = extendBus(NotifierBase);
|
|
193
|
+
|
|
194
|
+
const first = new Notifier();
|
|
195
|
+
const second = new Notifier();
|
|
196
|
+
second.on("notified", (message) => console.log(message));
|
|
197
|
+
first.emit("notified", "hello"); // экземпляры делят Notifier.bus
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Передавайте шину, когда нужны плагины (`contextPlugin`, `scopePlugin`) или когда несколько классов общаются друг с другом.
|
|
201
|
+
|
|
202
|
+
## Завершение подписок через `dispose`
|
|
203
|
+
|
|
204
|
+
`dispose()` снимает каждую подписку, сделанную через `this.on` и `this.once` этого экземпляра, и больше ничего. Его безопасно вызывать дважды. `instance[Symbol.dispose]()` делает то же, так что `using service = new SearchService(api)` работает там, где среда выполнения это поддерживает.
|
|
205
|
+
|
|
206
|
+
```js
|
|
207
|
+
// dispose.js
|
|
208
|
+
import { createBus } from "nanoeffects";
|
|
209
|
+
import { extendBus } from "nanoeffects/oop";
|
|
210
|
+
|
|
211
|
+
const bus = createBus();
|
|
212
|
+
const Service = extendBus(bus, class {});
|
|
213
|
+
|
|
214
|
+
const service = new Service();
|
|
215
|
+
const stop = service.on("ping", () => console.log("service heard it"));
|
|
216
|
+
bus.on("ping", () => console.log("the bus heard it")); // не подписка сервиса
|
|
217
|
+
|
|
218
|
+
service.dispose();
|
|
219
|
+
bus.emit("ping"); // только "the bus heard it"
|
|
220
|
+
stop(); // функция, которую вернул on, всё ещё работает и теперь ничего не делает
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Подписка, которую вы сняли сами (функция, которую вернул `on`, `this.off` или сработавший `once`), забывается, так что экземпляр ничего для неё не держит.
|
|
224
|
+
|
|
225
|
+
## Событие как поток через `Subject`
|
|
226
|
+
|
|
227
|
+
Проблема: сервис отдаёт одно из своих событий коду для потоков, и подписка потока должна заканчиваться вместе с сервисом. `createObserve(this.bus)` так не сможет: он подписывается через шину, поэтому `dispose()` о нём не знает.
|
|
228
|
+
|
|
229
|
+
Передайте `this` в `Subject` как источник. Subject подписывается через `this.on`, так что подписка принадлежит экземпляру ([Чтение и запись одного события через Subject](observable.md#чтение-и-запись-одного-события-через-subject)).
|
|
230
|
+
|
|
231
|
+
```js
|
|
232
|
+
// typing.js
|
|
233
|
+
import { createBus } from "nanoeffects";
|
|
234
|
+
import { Subject } from "nanoeffects/observable";
|
|
235
|
+
import { extendBus } from "nanoeffects/oop";
|
|
236
|
+
|
|
237
|
+
const bus = createBus(); // шина: события и обработчики
|
|
238
|
+
|
|
239
|
+
class TypingBase {
|
|
240
|
+
constructor() {
|
|
241
|
+
this.typed = new Subject(this, "typed"); // Subject — событие "typed" как поток; this — источник
|
|
242
|
+
this.typed.subscribe((text) => console.log(text));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const Typing = extendBus(bus, TypingBase); // extendBus — класс с on, emit и dispose
|
|
247
|
+
|
|
248
|
+
const typing = new Typing();
|
|
249
|
+
typing.typed.next("a"); // выводит "a"
|
|
250
|
+
typing.dispose(); // завершает и подписку Subject
|
|
251
|
+
typing.typed.next("b"); // ничего не выводится
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
В TypeScript объявите `on` и `emit` в классе (`declare on: BusMethods<typeof bus>["on"]`, см. [Типизируйте экземпляры](#типы-экземпляров)), чтобы `this` подходил как источник.
|
|
255
|
+
|
|
256
|
+
## Собственный метод
|
|
257
|
+
|
|
258
|
+
Класс выигрывает любой конфликт имён. Если у `Base` есть `emit`, `dispose`, `on` или `bus`, ваш остаётся, а метод шины не добавляется. `Base` со своим `dispose` никогда не завершил бы подписки, поэтому вызывайте оттуда `disposeBus(this)`.
|
|
259
|
+
|
|
260
|
+
```js
|
|
261
|
+
// cache.js
|
|
262
|
+
import { createBus } from "nanoeffects";
|
|
263
|
+
import { disposeBus, extendBus } from "nanoeffects/oop";
|
|
264
|
+
|
|
265
|
+
const bus = createBus();
|
|
266
|
+
|
|
267
|
+
class CacheBase {
|
|
268
|
+
dispose() {
|
|
269
|
+
this.items.clear(); // ваша собственная очистка
|
|
270
|
+
disposeBus(this); // disposeBus — завершает подписки этого экземпляра
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
items = new Map();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export const Cache = extendBus(bus, CacheBase);
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`disposeBus` ничего не делает для значения, которое не является экземпляром такого класса.
|
|
280
|
+
|
|
281
|
+
## Подписка метода через `@On` и `@Once`
|
|
282
|
+
|
|
283
|
+
`On` здесь — декоратор. Главная точка входа тоже экспортирует тип `On` — тип `bus.on`. Это разные вещи из разных импортов. Если файлу нужны оба, переименуйте один: `import { On as OnDecorator } from "nanoeffects/oop"`.
|
|
284
|
+
|
|
285
|
+
Декоратор превращает метод в обработчик события для каждого экземпляра, с привязанным `this`. Вам не нужно писать `this.on` в конструкторе, а `dispose()` завершает и эту подписку.
|
|
286
|
+
|
|
287
|
+
Шина и события те же, что выше, на TypeScript:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
// bus.ts
|
|
291
|
+
import { createBus } from "nanoeffects";
|
|
292
|
+
|
|
293
|
+
export const bus = createBus();
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
// search.ts
|
|
298
|
+
import { createAction } from "nanoeffects/actions";
|
|
299
|
+
|
|
300
|
+
export const queryChanged = createAction<string>("search/query-changed");
|
|
301
|
+
export const resultsLoaded = createAction<string[]>("search/results-loaded");
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
```ts
|
|
305
|
+
// search-service.ts
|
|
306
|
+
import { latest } from "nanoeffects/policies";
|
|
307
|
+
import { extendBus, On, Once } from "nanoeffects/oop";
|
|
308
|
+
import { bus } from "./bus";
|
|
309
|
+
import { queryChanged, resultsLoaded } from "./search";
|
|
310
|
+
|
|
311
|
+
class SearchServiceBase {
|
|
312
|
+
// On — этот метод обрабатывает queryChanged для каждого экземпляра, this — экземпляр;
|
|
313
|
+
// latest — каждый новый запрос прерывает предыдущий
|
|
314
|
+
@On(queryChanged, latest)
|
|
315
|
+
async search(query: string, { signal, emit }: { signal: AbortSignal; emit: (event: unknown) => void }) {
|
|
316
|
+
const items = await api.search(query, { signal });
|
|
317
|
+
emit(resultsLoaded(items));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Once — срабатывает только на первое событие
|
|
321
|
+
@Once("search/ready")
|
|
322
|
+
ready() {
|
|
323
|
+
console.log("first ready");
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export const SearchService = extendBus(bus, SearchServiceBase);
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Что нужно знать:
|
|
331
|
+
|
|
332
|
+
- Событие — это имя или action creator, как для `bus.on`. Для action creator payload метода проверяется по нему.
|
|
333
|
+
- Второй аргумент, `wrap`, принимает привязанный обработчик и возвращает тот, на который подписываются: `latest` или `(handler) => debounce(300, handler)`. Для политики это необходимо: `latest(this.search)` оборачивает функцию без `this`, а `wrap` получает уже привязанный обработчик.
|
|
334
|
+
- Подписки делаются сразу после конструктора `Base`. Событие, отправленное внутри этого конструктора, до них не доходит.
|
|
335
|
+
- Подкласс наследует их. Метод, который подкласс переопределяет, подписывается, только если переопределение тоже декорировано.
|
|
336
|
+
- На классе, который сделал не `extendBus`, декоратор ничего не делает.
|
|
337
|
+
- Работают и декораторы языка, и `experimentalDecorators`. Ваш компилятор должен поддерживать тот, что вы используете: TypeScript 5 или новее для первого и параметр `experimentalDecorators` для второго.
|
|
338
|
+
|
|
339
|
+
Назовите метод для [трассировки](tracing.md#имя-обработчика) через `{ traceId }`, вторым аргументом или, после обёртки, третьим: `@On(queryChanged, { traceId: "searcher" })` или `@On(queryChanged, latest, { traceId: "searcher" })`. Для этого нужен `tracePlugin` на шине, а без него параметр игнорируется.
|
|
340
|
+
|
|
341
|
+
## Декоратор класса `@WithBus`
|
|
342
|
+
|
|
343
|
+
`@WithBus(bus)` делает то, что `extendBus(bus, Base)`, как декоратор класса. `@WithBus()` создаёт шину для класса.
|
|
344
|
+
|
|
345
|
+
<!-- docs-check: continue -->
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
// search-service.ts
|
|
349
|
+
import { type BusMethods, On, WithBus } from "nanoeffects/oop";
|
|
350
|
+
import { bus } from "./bus";
|
|
351
|
+
import { queryChanged } from "./search";
|
|
352
|
+
|
|
353
|
+
// TypeScript не меняет тип декорированного класса, поэтому скажите, что он получает;
|
|
354
|
+
// только для проверки типов: во время выполнения WithBus уже добавляет методы
|
|
355
|
+
interface SearchService extends BusMethods<typeof bus> {}
|
|
356
|
+
|
|
357
|
+
// WithBus — заменяет класс на extendBus(bus, SearchService)
|
|
358
|
+
@WithBus(bus)
|
|
359
|
+
class SearchService {
|
|
360
|
+
@On(queryChanged)
|
|
361
|
+
search(query: string) {
|
|
362
|
+
console.log(query);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export { SearchService };
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Когда можно, предпочитайте `extendBus(bus, Base)`: его типы берутся из вызова и `interface` не требуют.
|
|
370
|
+
|
|
371
|
+
## Метод, переданный вручную
|
|
372
|
+
|
|
373
|
+
Обработчик, переданный в `this.on`, вызывается так, как вызывает шина, без `this`. Метод, переданный «голым», теряет `this`, как в обычном JavaScript:
|
|
374
|
+
|
|
375
|
+
```js
|
|
376
|
+
// handlers.js
|
|
377
|
+
import { createBus } from "nanoeffects";
|
|
378
|
+
import { extendBus } from "nanoeffects/oop";
|
|
379
|
+
|
|
380
|
+
class BaseWithHandler {
|
|
381
|
+
name = "handler";
|
|
382
|
+
|
|
383
|
+
handle() {
|
|
384
|
+
console.log(this?.name);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
listen() {
|
|
388
|
+
this.on("ping", this.handle); // this потерян: выведет undefined
|
|
389
|
+
this.on("ping", () => this.handle()); // стрелка его сохраняет
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export const Listener = extendBus(createBus(), BaseWithHandler);
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Используйте стрелку или декорированный метод. Библиотека не привязывает `this` за вас: политика вроде `latest(this.handle)` оборачивает функцию, так что привязка работала бы в половине случаев.
|
|
397
|
+
|
|
398
|
+
## Типы экземпляров
|
|
399
|
+
|
|
400
|
+
`extendBus` возвращает класс, который принимает аргументы `Base`, а его экземпляры — это `Base` плюс `BusMethods<typeof bus>`: функциональные члены шины (`on`, `emit` и члены ранее подключённых плагинов) с `dispose` и `bus`. Член `Base` с тем же именем сохраняет свой тип.
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
// service.ts
|
|
404
|
+
import { createBus } from "nanoeffects";
|
|
405
|
+
import { extendBus } from "nanoeffects/oop";
|
|
406
|
+
|
|
407
|
+
type Events = { "order/placed": { id: string } };
|
|
408
|
+
|
|
409
|
+
// Events — только для проверки типов: payload шины
|
|
410
|
+
const bus = createBus<Events>();
|
|
411
|
+
|
|
412
|
+
class OrderServiceBase {
|
|
413
|
+
name: string;
|
|
414
|
+
|
|
415
|
+
constructor(name: string) {
|
|
416
|
+
this.name = name;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export const OrderService = extendBus(bus, OrderServiceBase);
|
|
421
|
+
|
|
422
|
+
const service = new OrderService("main"); // конструктор OrderServiceBase
|
|
423
|
+
service.emit("order/placed", { id: "o1" }); // проверяется по Events
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
## Что не сделано
|
|
427
|
+
|
|
428
|
+
- `this` не привязывается в обработчике, который вы передаёте вручную (см. выше).
|
|
429
|
+
- Методов плагина, подключённого после `extendBus`, на классе нет. Сначала подключите плагины, затем `extendBus`.
|
|
430
|
+
- У `Emitter` нет методов плагинов, потому что его шина создаётся внутри. Для них используйте `extendBus(bus)`.
|
|
431
|
+
- Подмешивание методов в существующий объект и декораторы полей или параметров в эту точку входа не входят.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Точки входа пакета
|
|
2
|
+
|
|
3
|
+
> **[Уровни A–F](README.md#виды-проектов)**: для любого проекта.
|
|
4
|
+
|
|
5
|
+
## Три основных импорта
|
|
6
|
+
|
|
7
|
+
| Импорт | Что внутри |
|
|
8
|
+
| ------ | ---------- |
|
|
9
|
+
| `nanoeffects` | `createBus`, типы |
|
|
10
|
+
| `nanoeffects/policies` | `latest`, `leading`, `debounce`, `throttle`, `queue` |
|
|
11
|
+
| `nanoeffects/effects` | `retry`, `timeout`, `poll`, `delay`, `take`, `all`, `allSettled`, `race`, `effectsPlugin` |
|
|
12
|
+
|
|
13
|
+
## Остальные импорты
|
|
14
|
+
|
|
15
|
+
| Импорт | Что внутри | Когда |
|
|
16
|
+
| ------ | ---------- | ----- |
|
|
17
|
+
| `nanoeffects/actions` | `createAction`, `createJob`, `createModule`, `modulePlugin`, `actionsPlugin` | `emit` должен вернуть результат или доменная логика уходит из UI |
|
|
18
|
+
| `nanoeffects/match` | `matchPlugin`: `on`, `once` и `off` принимают функцию-фильтр вместо имени события | подписываться нужно по условию, а не по имени |
|
|
19
|
+
| `nanoeffects/context` | `contextPlugin`: ваши собственные поля (store, клиент) на `ctx` каждой задачи, с типами по значению | store или клиент должен быть на каждой задаче |
|
|
20
|
+
| `nanoeffects/signal` | `signalPlugin`: `emit(call, { signal })` привязывает задачи этого `emit` к внешнему `AbortSignal` | внешний `AbortSignal` (запрос, React Query) должен останавливать задачи |
|
|
21
|
+
| `nanoeffects/oop` | `Emitter`, `extendBus`, `WithBus`, `On`, `Once`, `disposeBus`: методы шины на классе, подписки каждого экземпляра заканчиваются через `dispose()` | вы пишете классы, а не функции |
|
|
22
|
+
| `nanoeffects/scope` | `scopePlugin`, `effect`: поля для одного action, задачи, модуля или `emit` (типы `Scope`, `Effect`) | одному action или модулю нужны свои поля |
|
|
23
|
+
| `nanoeffects/redux` | `createEffectsMiddleware` | вы уже используете Redux |
|
|
24
|
+
| `nanoeffects/observable` | `createObserve`, `Subject` | нужен Observable (RxJS) |
|
|
25
|
+
| `nanoeffects/react` | `reactPlugin`: добавляет хук `useEmitter` на шину (нужен React 18 или новее) | компоненты React |
|
|
26
|
+
| `nanoeffects/svelte` | `sveltePlugin`: добавляет `eventStore` на шину (нужен Svelte 5 или новее) | компоненты Svelte |
|
|
27
|
+
| `nanoeffects/vue` | `vuePlugin`: добавляет `watchEmitter` и `useEventRef` на шину (нужен Vue 3.3 или новее) | компоненты Vue |
|
|
28
|
+
| `nanoeffects/dev` | `devPlugin` (в production ничего не делает) | нужно находить утечки подписок при разработке |
|
|
29
|
+
| `nanoeffects/trace` | `createTracer`, логгеры (`graphLogger` находит циклы событий) и `tracePlugin` (имена для трассировки) | нужно видеть, что выполнилось и почему |
|
|
30
|
+
| `nanoeffects/guard` | `loopGuardPlugin`: отклоняет emit, который превысил бы предел цепочки | цикл не должен ронять приложение |
|
|
31
|
+
|
|
32
|
+
Плагины с именами `...Plugin` подключаются через `bus.use`. Страница [Плагины](plugins.md) перечисляет их все, говорит, что каждый возвращает и в каком порядке их подключать.
|
|
33
|
+
|
|
34
|
+
## Документация для ИИ-ассистентов
|
|
35
|
+
|
|
36
|
+
В пакете есть текстовые файлы для ассистентов, которые пишут код с nanoeffects:
|
|
37
|
+
|
|
38
|
+
- `llms.txt`: короткий указатель. Правила, в которых сгенерированный код часто ошибается, и ссылки на все страницы документации.
|
|
39
|
+
- `llms-full.txt`: тот же обзор и текст всех связанных страниц в одном файле.
|
|
40
|
+
- `skills/nanoeffects/SKILL.md`: навык (skill) для ассистентов с такой поддержкой, например Claude Code. Он говорит, когда предлагать nanoeffects, а когда нет, и повторяет те же правила. Скопируйте папку в `.claude/skills/` проекта или в `~/.claude/skills/`.
|
|
41
|
+
|
|
42
|
+
Файлы лежат в `node_modules/nanoeffects/`. `llms-full.txt` можно загрузить с CDN, например `https://unpkg.com/nanoeffects/llms-full.txt`. Содержимое соответствует установленной версии. Файлы написаны на английском.
|