nanoeffects 0.28.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-CcAEvUnw.d.ts → action-creator-6rlBTmH9.d.ts} +60 -33
- package/dist/actions.d.ts +18 -18
- package/dist/actions.js +1 -1
- package/dist/bus-DFlmNJ6h.js +1 -0
- package/dist/context-B5Z2RpJt.js +1 -0
- package/dist/context-types-DKNc5mkS.d.ts +31 -0
- package/dist/context.d.ts +8 -3
- package/dist/context.js +1 -1
- package/dist/{delay-18LJ8jSk.js → delay-5OhJZZQ0.js} +1 -1
- 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 +2 -2
- package/dist/index.js +1 -1
- package/dist/match.d.ts +3 -3
- package/dist/match.js +1 -1
- package/dist/{module-C_oEXw1n.d.ts → module-G39MaVOt.d.ts} +38 -38
- package/dist/notrace/{action-creator-DxOvVo8I.d.ts → action-creator-BrMSO3RP.d.ts} +35 -32
- package/dist/notrace/actions.d.ts +52 -52
- package/dist/notrace/actions.js +1 -1
- package/dist/notrace/{bus-DmKn4SDD.js → bus-37nlVljc.js} +1 -1
- package/dist/notrace/effects.d.ts +2 -2
- package/dist/notrace/index.d.ts +2 -2
- package/dist/notrace/index.js +1 -1
- package/dist/notrace/{plugin-lJjjd-SN.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 +43 -1
- package/dist/observable.js +1 -1
- package/dist/onAbort-BgMfspK4.js +1 -0
- package/dist/oop.d.ts +3 -3
- package/dist/oop.js +1 -1
- package/dist/{plugin-CuvIgjnX.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 +9 -9
- package/dist/signal-field-key-CWT-3OXC.js +1 -0
- package/dist/signal.d.ts +34 -6
- 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 +170 -6
- package/dist/trace.js +3 -1
- package/dist/{types-FV8fuvI9.d.ts → types-N_cdRr1r.d.ts} +2 -2
- package/dist/vue.d.ts +1 -1
- package/dist/{with-policy-D-SQRkPh.js → with-policy-CeM1rtES.js} +1 -1
- package/dist/{with-policy-3bhFLEXs.d.ts → with-policy-CthiBp_z.d.ts} +1 -1
- package/docs/README.md +65 -22
- package/docs/actions.md +56 -13
- package/docs/cancel-stale-requests.md +2 -0
- package/docs/chat-example.md +15 -1
- package/docs/chat-widget.md +9 -7
- package/docs/context.md +352 -8
- package/docs/debounce-and-abort.md +3 -1
- package/docs/debugging-loops.md +283 -0
- package/docs/effects.md +259 -131
- package/docs/errors.md +4 -2
- package/docs/examples.md +60 -0
- package/docs/guide.md +57 -22
- package/docs/hmr.md +35 -1
- package/docs/loop-guard.md +109 -0
- package/docs/migrating-from-effector.md +15 -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 +45 -0
- package/docs/oop.md +20 -1
- package/docs/package.md +25 -13
- package/docs/pass-the-signal.md +281 -0
- package/docs/plugins.md +90 -15
- package/docs/policies.md +73 -46
- package/docs/prevent-double-submit.md +3 -1
- package/docs/query-libraries.md +276 -6
- package/docs/react.md +50 -11
- 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 +12 -0
- package/docs/server.md +5 -3
- package/docs/starter.md +2 -0
- package/docs/stores.md +2 -0
- package/docs/svelte.md +3 -1
- package/docs/tracing.md +132 -7
- package/docs/typescript.md +3 -1
- package/docs/vue.md +3 -1
- package/llms-full.txt +2718 -545
- package/llms.txt +17 -14
- package/package.json +25 -8
- package/skills/nanoeffects/SKILL.md +2 -2
- package/dist/bus-DlpEluzd.js +0 -1
- package/dist/context-UcmrAIhP.js +0 -1
- package/dist/context-types-hCKgU8sC.d.ts +0 -14
- package/docs/scope.md +0 -266
- package/docs/stability.md +0 -90
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
# Плагины шины
|
|
2
|
+
|
|
3
|
+
> **[Уровень F](README.md#виды-проектов)**: платформа, SDK или долгоживущее приложение. Сначала прочитайте: [Руководство](guide.md#1-создание-шины-событий).
|
|
4
|
+
|
|
5
|
+
Плагин добавляет к шине одну вещь: поля на `ctx`, метод, реестр обработчиков, диагностику. Для небольшой шины плагины не нужны: хватает `createBus()`.
|
|
6
|
+
|
|
7
|
+
## На этой странице
|
|
8
|
+
|
|
9
|
+
- [Подключение плагина](#подключение-плагина)
|
|
10
|
+
- [Плагин по задаче](#плагин-по-задаче)
|
|
11
|
+
- [Основные плагины](#основные-плагины)
|
|
12
|
+
- [Store или клиент на каждом обработчике: `contextPlugin`](#store-или-клиент-на-каждом-обработчике-contextplugin)
|
|
13
|
+
- [`race` и `delay` без `ctx`: `effectsPlugin`](#race-и-delay-без-ctx-effectsplugin)
|
|
14
|
+
- [Привязка и замена обработчика: `actionsPlugin`](#привязка-и-замена-обработчика-actionsplugin)
|
|
15
|
+
- [Домен как единое целое: `modulePlugin`](#домен-как-единое-целое-moduleplugin)
|
|
16
|
+
- [Дополнительные плагины](#дополнительные-плагины)
|
|
17
|
+
- [Собственные поля action: `scopePlugin`](#собственные-поля-action-scopeplugin)
|
|
18
|
+
- [Внешний сигнал для emit и подписки: `signalPlugin`](#внешний-сигнал-для-emit-и-подписки-signalplugin)
|
|
19
|
+
- [Имена источника и потока трассировки: `tracePlugin`](#имена-источника-и-потока-трассировки-traceplugin)
|
|
20
|
+
- [Подписка на группу событий: `matchPlugin`](#подписка-на-группу-событий-matchplugin)
|
|
21
|
+
- [Шина для state manager: `emitterPlugin`](#шина-для-state-manager-emitterplugin)
|
|
22
|
+
- [Сообщение шине об изменении значения в React: `reactPlugin`](#сообщение-шине-об-изменении-значения-в-react-reactplugin)
|
|
23
|
+
- [Поиск утечек при разработке: `devPlugin`](#поиск-утечек-при-разработке-devplugin)
|
|
24
|
+
- [Обрыв цикла: `loopGuardPlugin`](#обрыв-цикла-loopguardplugin)
|
|
25
|
+
- [Все плагины вместе](#все-плагины-вместе)
|
|
26
|
+
- [Порядок подключения](#порядок-подключения)
|
|
27
|
+
- [Что не является плагином](#что-не-является-плагином)
|
|
28
|
+
- [Собственный плагин](#собственный-плагин)
|
|
29
|
+
|
|
30
|
+
## Подключение плагина
|
|
31
|
+
|
|
32
|
+
Плагин — функция шины. `bus.use(plugin)` вызывает её с шиной и возвращает то, что вернул плагин. Большинство плагинов возвращают ту же шину, поэтому их можно сцеплять: `createBus().use(a).use(b)`. Некоторые возвращают своё (реестр, приложение, функцию `disconnect`): сохраните результат.
|
|
33
|
+
|
|
34
|
+
В TypeScript сохраняйте результат `contextPlugin` и `effectsPlugin`: тип шины с новыми полями — это тип, который они возвращают (`const bus = createBus().use(contextPlugin({ store }))`).
|
|
35
|
+
|
|
36
|
+
## Плагин по задаче
|
|
37
|
+
|
|
38
|
+
Подходящей строки нет — плагин не нужен.
|
|
39
|
+
|
|
40
|
+
| Мне нужно | Использовать |
|
|
41
|
+
| --------- | ------------ |
|
|
42
|
+
| Store, клиент или логгер на каждом обработчике | [`contextPlugin`](#store-или-клиент-на-каждом-обработчике-contextplugin) |
|
|
43
|
+
| `race`, `all` или `delay` без передачи `ctx` | [`effectsPlugin`](#race-и-delay-без-ctx-effectsplugin) |
|
|
44
|
+
| Тест, который заменяет обработчик | [`actionsPlugin`](#привязка-и-замена-обработчика-actionsplugin) |
|
|
45
|
+
| Один домен, подключаемый и отключаемый как единое целое | [`modulePlugin`](#домен-как-единое-целое-moduleplugin) |
|
|
46
|
+
| Поля только для одного action или модуля | [`scopePlugin`](#собственные-поля-action-scopeplugin) |
|
|
47
|
+
| Задачи, которые останавливаются, когда отменён запрос, сокет или query | [`signalPlugin`](#внешний-сигнал-для-emit-и-подписки-signalplugin) |
|
|
48
|
+
| Одна подписка на группу событий | [`matchPlugin`](#подписка-на-группу-событий-matchplugin) |
|
|
49
|
+
| Callback для state manager, который отправляет события | [`emitterPlugin`](#шина-для-state-manager-emitterplugin) |
|
|
50
|
+
| Компонент React, который отправляет своё значение на шину | [`reactPlugin`](#сообщение-шине-об-изменении-значения-в-react-reactplugin) |
|
|
51
|
+
| Имена в трассировке | [`tracePlugin`](#имена-источника-и-потока-трассировки-traceplugin) |
|
|
52
|
+
| Предупреждения об утечках при разработке | [`devPlugin`](#поиск-утечек-при-разработке-devplugin) |
|
|
53
|
+
| Цикл не должен ронять приложение | [`loopGuardPlugin`](#обрыв-цикла-loopguardplugin) |
|
|
54
|
+
|
|
55
|
+
## Основные плагины
|
|
56
|
+
|
|
57
|
+
| Плагин | Что даёт | Импорт | Что возвращает `bus.use` |
|
|
58
|
+
| ------ | -------- | ------ | ------------------------ |
|
|
59
|
+
| [`contextPlugin`](#store-или-клиент-на-каждом-обработчике-contextplugin) | Ваши собственные поля (store, клиент, логгер) на `ctx` каждого обработчика. | `nanoeffects` | Ту же шину, типизированную полями |
|
|
60
|
+
| [`effectsPlugin`](#race-и-delay-без-ctx-effectsplugin) | `race`, `all`, `delay` и ваши собственные эффекты на `ctx`, вызываемые без передачи `ctx`. | `nanoeffects/effects` | Ту же шину, типизированную эффектами |
|
|
61
|
+
| [`actionsPlugin`](#привязка-и-замена-обработчика-actionsplugin) | Обработчики actions в реестре, чтобы тест мог привязать или заменить один. | `nanoeffects/actions` | Реестр `{ run, handle }` |
|
|
62
|
+
| [`modulePlugin`](#домен-как-единое-целое-moduleplugin) | Модуль: подписки, обработчики и задачи одного домена, подключаемые и отключаемые одним вызовом. | `nanoeffects/actions` | Приложение `{ mount, unmount, isMounted, dispose, actions, createModule }` |
|
|
63
|
+
|
|
64
|
+
### Store или клиент на каждом обработчике: `contextPlugin`
|
|
65
|
+
|
|
66
|
+
Каждая задача получает на `ctx` одни и те же поля: store, API-клиент, логгер. Типы берутся из значений. Подробнее в [Полях контекста](context.md).
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
import { createBus } from "nanoeffects";
|
|
70
|
+
import { contextPlugin } from "nanoeffects/context";
|
|
71
|
+
|
|
72
|
+
// contextPlugin — каждый обработчик получает ctx.store
|
|
73
|
+
const bus = createBus().use(contextPlugin({ store }));
|
|
74
|
+
|
|
75
|
+
bus.on("save", (draft, { store }) => store.put(draft));
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Клиент, который должен останавливаться вместе с задачей, например экземпляр `axios`, передаётся как `fromSignal((signal) => axios.create({ signal }))` из `nanoeffects/signal`: он строится из сигнала каждой задачи, когда задача его читает. Подробнее в [Поле, построенное из сигнала задачи](context.md#поле-построенное-из-сигнала-задачи).
|
|
79
|
+
|
|
80
|
+
### `race` и `delay` без `ctx`: `effectsPlugin`
|
|
81
|
+
|
|
82
|
+
Эффекты вроде `race`, `all`, `take` и `delay` привязаны к задаче, так что обработчик вызывает `race({ … })`, а не `race(ctx, { … })`. Ваши собственные функции, принимающие `{ signal }` первым, тоже работают. Подробнее в [Эффектах](effects.md#effectsplugin).
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
import { createBus } from "nanoeffects";
|
|
86
|
+
import { delay, effectsPlugin, race } from "nanoeffects/effects";
|
|
87
|
+
|
|
88
|
+
// effectsPlugin — race и delay привязаны к задаче каждого обработчика
|
|
89
|
+
const effects = effectsPlugin({ race, delay });
|
|
90
|
+
|
|
91
|
+
const bus = createBus().use(effects);
|
|
92
|
+
|
|
93
|
+
bus.on("load", async (id, { race, delay }) => {
|
|
94
|
+
const result = await race({
|
|
95
|
+
data: (child) => api.get(id, child),
|
|
96
|
+
timeout: () => delay(5000),
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Привязка и замена обработчика: `actionsPlugin`
|
|
102
|
+
|
|
103
|
+
Обработчики actions могут жить в реестре, принадлежащем шине. У action может не быть обработчика, а тест или другой слой привязывает его. Подробнее в [Actions](actions.md#привязка-обработчика-позже).
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
import { createBus } from "nanoeffects";
|
|
107
|
+
import { actionsPlugin, createAction } from "nanoeffects/actions";
|
|
108
|
+
|
|
109
|
+
const getUser = createAction("user/get"); // обработчика пока нет: событие
|
|
110
|
+
const bus = createBus();
|
|
111
|
+
|
|
112
|
+
// actionsPlugin — реестр обработчиков этой шины
|
|
113
|
+
const actions = bus.use(actionsPlugin());
|
|
114
|
+
|
|
115
|
+
actions.handle(getUser, async (id) => ({ id })); // побеждает последняя привязка
|
|
116
|
+
const user = await bus.emit(getUser("1")); // { id: "1" }
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Домен как единое целое: `modulePlugin`
|
|
120
|
+
|
|
121
|
+
Модуль группирует подписки, привязанные обработчики и фоновые задачи одного домена и завершает все их, когда отключается. Модули также делают возможной [hot reload](hmr.md). Подробнее в [Модулях](actions.md#модули).
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
import { createBus } from "nanoeffects";
|
|
125
|
+
import { modulePlugin } from "nanoeffects/actions";
|
|
126
|
+
|
|
127
|
+
const bus = createBus();
|
|
128
|
+
|
|
129
|
+
// modulePlugin — подключает userModule в конце этого блока или при первом использовании, если оно раньше, и возвращает приложение
|
|
130
|
+
const app = bus.use(modulePlugin([userModule]));
|
|
131
|
+
|
|
132
|
+
app.unmount(userModule); // его подписки, привязанные actions и задачи исчезли
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Дополнительные плагины
|
|
136
|
+
|
|
137
|
+
| Плагин | Что даёт | Импорт | Что возвращает `bus.use` |
|
|
138
|
+
| ------ | -------- | ------ | ------------------------ |
|
|
139
|
+
| [`scopePlugin`](#собственные-поля-action-scopeplugin) | Собственные поля для одного action, задачи или модуля и способ заменить их в одном `emit`. | `nanoeffects/scope` | Ту же шину |
|
|
140
|
+
| [`signalPlugin`](#внешний-сигнал-для-emit-и-подписки-signalplugin) | `emit(call, { signal })`, который прерывает свои задачи, и `on(event, handler, { signal })`, который отписывает обработчик, когда внешний сигнал прерывается. | `nanoeffects/signal` | Ту же шину, типизированную новым `emit` |
|
|
141
|
+
| [`matchPlugin`](#подписка-на-группу-событий-matchplugin) | `on`, который слушает группу событий, выбираемую функцией. | `nanoeffects/match` | Ту же шину, типизированную новыми перегрузками |
|
|
142
|
+
| [`emitterPlugin`](#шина-для-state-manager-emitterplugin) | Callback, который отправляет action, чтобы отдать его state manager. | `nanoeffects/actions` | Ту же шину с `emitter` |
|
|
143
|
+
| [`tracePlugin`](#имена-источника-и-потока-трассировки-traceplugin) | `traceId` и `traceGroupId` для трассировки: корень эмиттера, поток, именованный обработчик. | `nanoeffects/trace` | Ту же шину |
|
|
144
|
+
| [`reactPlugin`](#сообщение-шине-об-изменении-значения-в-react-reactplugin) | Хук React, который отправляет событие, когда меняется значение компонента. | `nanoeffects/react` | Ту же шину с `useEmitter` |
|
|
145
|
+
| [`devPlugin`](#поиск-утечек-при-разработке-devplugin) | Предупреждения о накапливающихся подписках и сообщения о модулях при разработке. | `nanoeffects/dev` | Функцию, которая его отключает |
|
|
146
|
+
| [`loopGuardPlugin`](#обрыв-цикла-loopguardplugin) | Отклоняет emit, который превысил бы предел цепочки, чтобы цикл закончился. | `nanoeffects/guard` | Ту же шину |
|
|
147
|
+
|
|
148
|
+
### Собственные поля action: `scopePlugin`
|
|
149
|
+
|
|
150
|
+
Action, job или модуль могут нести свои поля и эффекты, а один `emit` может их заменить. Так тест подставляет подделку для одного вызова. Подробнее в [Областях (scope)](context.md#scope-поля-для-action-модуля-и-вызова).
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
import { createBus } from "nanoeffects";
|
|
154
|
+
import { createAction } from "nanoeffects/actions";
|
|
155
|
+
import { scopePlugin } from "nanoeffects/scope";
|
|
156
|
+
|
|
157
|
+
// scopePlugin — включает scope action на этой шине
|
|
158
|
+
const bus = createBus().use(scopePlugin());
|
|
159
|
+
|
|
160
|
+
// последний аргумент: поля только этого action
|
|
161
|
+
const getUser = createAction("user/get", (id, { api }) => api.getUser(id), {
|
|
162
|
+
api,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const fakeApi = { getUser: async (id) => ({ id, name: "test" }) };
|
|
166
|
+
bus.emit(getUser("1"), { api: fakeApi }); // поддельный api только для этого вызова
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Внешний сигнал для emit и подписки: `signalPlugin`
|
|
170
|
+
|
|
171
|
+
Библиотека вроде React Query владеет запросом и даёт вам `AbortSignal`. `emit` принимает его как `{ signal }` и прерывает запущенные им задачи, когда сигнал прерывается, так что `timeout`, `retry` и сам запрос тоже останавливаются. Подробнее в [nanoeffects с React Query, SWR и RTK Query](query-libraries.md).
|
|
172
|
+
|
|
173
|
+
```js
|
|
174
|
+
import { createBus } from "nanoeffects";
|
|
175
|
+
import { createAction } from "nanoeffects/actions";
|
|
176
|
+
import { signalPlugin } from "nanoeffects/signal";
|
|
177
|
+
|
|
178
|
+
// signalPlugin — emit принимает { signal } для action или задачи
|
|
179
|
+
const bus = createBus().use(signalPlugin());
|
|
180
|
+
|
|
181
|
+
const getUser = createAction("user/get", (id, { signal }) => api.getUser(id, { signal }));
|
|
182
|
+
|
|
183
|
+
const controller = new AbortController();
|
|
184
|
+
bus.emit(getUser("1"), { signal: controller.signal }); // запускает action
|
|
185
|
+
controller.abort(); // ctx.signal обработчика прерывается вместе с ним
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Тот же сигнал может завершить и подписку. `on` и `once` принимают `{ signal }` в параметрах, как `addEventListener`, так что коду, у которого есть сигнал, не нужна функция `unbind`. Подписка снимается, когда сигнал прерывается; уже выполняющийся обработчик не прерывается. Уже прерванный сигнал не добавляет подписку.
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
import { createBus } from "nanoeffects";
|
|
192
|
+
import { signalPlugin } from "nanoeffects/signal";
|
|
193
|
+
|
|
194
|
+
// signalPlugin — on и once принимают { signal } в параметрах
|
|
195
|
+
const bus = createBus().use(signalPlugin());
|
|
196
|
+
|
|
197
|
+
const controller = new AbortController();
|
|
198
|
+
bus.on("saved", (draft) => console.log(draft), { signal: controller.signal }); // слушает до прерывания
|
|
199
|
+
controller.abort(); // подписка снята: bus.get("saved") пуст
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Без плагина параметр `signal` игнорируется, и подписка остаётся. `off(event, handler)` отписывает обработчик, но не обрывает его связь с сигналом; для этого вызовите функцию, которую вернул `on`.
|
|
203
|
+
|
|
204
|
+
### Имена источника и потока трассировки: `tracePlugin`
|
|
205
|
+
|
|
206
|
+
Когда на шине есть трассировщик, плагин позволяет трассировке сказать, какая подписка начала цепочку (`traceId` у `useEmitter` и `bus.emitter`), проследить один поток по `traceGroupId` и показать, какой обработчик выполнился (`bus.on(event, handler, { traceId })`). Без плагина параметры игнорируются. Подробнее в [Трассировке](tracing.md#какая-подписка-начала-цепочку).
|
|
207
|
+
|
|
208
|
+
```js
|
|
209
|
+
import { createBus } from "nanoeffects";
|
|
210
|
+
import { consoleLogger, createTracer, tracePlugin } from "nanoeffects/trace";
|
|
211
|
+
|
|
212
|
+
// createTracer: отправляет каждое событие трассировки логгеру; consoleLogger: печатает по одной строке на каждое
|
|
213
|
+
const trace = createTracer(consoleLogger());
|
|
214
|
+
|
|
215
|
+
// tracePlugin — называет корни, группы и обработчики трассировки
|
|
216
|
+
const bus = createBus({ trace })
|
|
217
|
+
.use(tracePlugin());
|
|
218
|
+
|
|
219
|
+
bus.on("name/changed", saveName, { traceId: "saveNotifier" }); // обработчик, который можно найти в логе
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Подписка на группу событий: `matchPlugin`
|
|
223
|
+
|
|
224
|
+
`on`, `once` и `off` принимают функцию, выбирающую события, например каждое событие `user/*`. Функция-type guard сужает тип payload. Подписка слушает каждое событие, поэтому для нескольких известных событий дешевле `on(creator, handler)`. Подробнее в [Руководстве](guide.md#фильтрация-событий-для-подписки).
|
|
225
|
+
|
|
226
|
+
```js
|
|
227
|
+
import { createBus } from "nanoeffects";
|
|
228
|
+
import { matchPlugin } from "nanoeffects/match";
|
|
229
|
+
|
|
230
|
+
// matchPlugin — on принимает функцию, которая выбирает события
|
|
231
|
+
const bus = createBus().use(matchPlugin());
|
|
232
|
+
|
|
233
|
+
bus.on(
|
|
234
|
+
(action) => action.type.startsWith("user/"),
|
|
235
|
+
(payload, ctx) => console.log(ctx.type, payload),
|
|
236
|
+
);
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Шина для state manager: `emitterPlugin`
|
|
240
|
+
|
|
241
|
+
`bus.emitter(creator)` — callback, отправляющий action. State manager принимает его как callback подписки, так что изменение store становится событием. Подробнее в [Stores](stores.md#события-из-state-manager).
|
|
242
|
+
|
|
243
|
+
```js
|
|
244
|
+
import { atom } from "nanostores";
|
|
245
|
+
import { createBus } from "nanoeffects";
|
|
246
|
+
import { createAction, emitterPlugin } from "nanoeffects/actions";
|
|
247
|
+
|
|
248
|
+
// emitterPlugin — добавляет bus.emitter
|
|
249
|
+
const bus = createBus().use(emitterPlugin());
|
|
250
|
+
const nameChanged = createAction("name/changed");
|
|
251
|
+
const $name = atom("");
|
|
252
|
+
|
|
253
|
+
$name.listen(bus.emitter(nameChanged)); // каждое изменение $name отправляет nameChanged
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Сообщение шине об изменении значения в React: `reactPlugin`
|
|
257
|
+
|
|
258
|
+
`useEmitter(event, value, options?)` отправляет событие после коммита рендера с новым значением. Он не отправляет при монтировании и не отправляет дважды под StrictMode. Параметры — `{ equals, traceId, traceGroupId }`; последние два называют эту отправку в [трассировке](tracing.md#какая-подписка-начала-цепочку). Нужен React 18 или новее. Подробнее в [Stores](stores.md#react-одно-значение-на-несколько-компонентов).
|
|
259
|
+
|
|
260
|
+
```jsx
|
|
261
|
+
import { useState } from "react";
|
|
262
|
+
import { createBus } from "nanoeffects";
|
|
263
|
+
import { reactPlugin } from "nanoeffects/react";
|
|
264
|
+
|
|
265
|
+
// reactPlugin — добавляет bus.useEmitter; деструктуризация даёт свободный хук
|
|
266
|
+
const bus = createBus().use(reactPlugin());
|
|
267
|
+
const { useEmitter } = bus;
|
|
268
|
+
|
|
269
|
+
function Profile() {
|
|
270
|
+
const [name, setName] = useState("");
|
|
271
|
+
useEmitter("name/changed", name); // отправляет после рендера с новым именем
|
|
272
|
+
return <input value={name} onChange={(event) => setName(event.target.value)} />;
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Поиск утечек при разработке: `devPlugin`
|
|
277
|
+
|
|
278
|
+
Он предупреждает о подписках, которые добавили и не сняли, и сообщает о модулях, которые подключаются, заменяются и отключаются. В production-сборке `nanoeffects/dev` пуст, так что вызов можно оставить. Подробнее в [Hot reload](hmr.md#сообщения-об-утечках-через-devplugin).
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
import { createBus } from "nanoeffects";
|
|
282
|
+
import { modulePlugin } from "nanoeffects/actions";
|
|
283
|
+
import { devPlugin } from "nanoeffects/dev";
|
|
284
|
+
|
|
285
|
+
const bus = createBus();
|
|
286
|
+
bus.use(modulePlugin([userModule]));
|
|
287
|
+
|
|
288
|
+
// devPlugin — предупреждения и сообщения в консоли при разработке;
|
|
289
|
+
// он находит приложение modulePlugin на шине, какой бы из двух вы ни подключили первым
|
|
290
|
+
const disconnect = bus.use(devPlugin());
|
|
291
|
+
|
|
292
|
+
disconnect(); // останавливает диагностику
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Обрыв цикла: `loopGuardPlugin`
|
|
296
|
+
|
|
297
|
+
Два обработчика, которые отправляют события друг друга, не остановятся сами, а цикл из микрозадач вешает страницу. Плагин следит за цепочкой emit и отклоняет тот emit, который повторил бы событие слишком часто или сделал бы цепочку слишком длинной. Трассировщик ему не нужен, поэтому он может работать и в production. Подробнее: [Обрыв цикла](loop-guard.md).
|
|
298
|
+
|
|
299
|
+
```js
|
|
300
|
+
import { createBus } from "nanoeffects";
|
|
301
|
+
import { loopGuardPlugin } from "nanoeffects/guard";
|
|
302
|
+
|
|
303
|
+
// loopGuardPlugin — отклоняет emit, который поставил бы одно событие в цепочку 50-й раз
|
|
304
|
+
const bus = createBus().use(loopGuardPlugin({ maxRepeats: 50 }));
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Все плагины вместе
|
|
308
|
+
|
|
309
|
+
Одна шина со всеми плагинами, которым не нужен React, в отдельном файле. События приложения остаются в других файлах. Плагины работают в любом порядке; в следующем разделе сказано, что это значит для модулей.
|
|
310
|
+
|
|
311
|
+
```js
|
|
312
|
+
// bus.js
|
|
313
|
+
import { createBus } from "nanoeffects";
|
|
314
|
+
import { contextPlugin } from "nanoeffects/context";
|
|
315
|
+
import {
|
|
316
|
+
actionsPlugin,
|
|
317
|
+
emitterPlugin,
|
|
318
|
+
modulePlugin,
|
|
319
|
+
} from "nanoeffects/actions";
|
|
320
|
+
import { devPlugin } from "nanoeffects/dev";
|
|
321
|
+
import { all, effectsPlugin, race } from "nanoeffects/effects";
|
|
322
|
+
import { matchPlugin } from "nanoeffects/match";
|
|
323
|
+
import { scopePlugin } from "nanoeffects/scope";
|
|
324
|
+
|
|
325
|
+
export const bus = createBus()
|
|
326
|
+
.use(contextPlugin({ store })) // ctx.store на каждой задаче
|
|
327
|
+
.use(effectsPlugin({ race, all })) // ctx.race и ctx.all, привязанные к задаче
|
|
328
|
+
.use(scopePlugin()) // scope для actions, jobs, модулей и emit
|
|
329
|
+
.use(matchPlugin()) // bus.on принимает функцию, которая выбирает события
|
|
330
|
+
.use(emitterPlugin()); // bus.emitter(creator)
|
|
331
|
+
|
|
332
|
+
// actionsPlugin — реестр обработчиков actions; сохраните его, чтобы привязывать обработчики позже
|
|
333
|
+
export const actions = bus.use(actionsPlugin());
|
|
334
|
+
|
|
335
|
+
// modulePlugin — подключает userModule в конце этого блока; сохраните app, чтобы подключать и отключать позже
|
|
336
|
+
export const app = bus.use(modulePlugin([userModule]));
|
|
337
|
+
|
|
338
|
+
// devPlugin — сообщает об утечках и модулях при разработке; сохраните disconnect, чтобы его убрать
|
|
339
|
+
export const disconnect = bus.use(devPlugin());
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
## Порядок подключения
|
|
343
|
+
|
|
344
|
+
Плагины дают один и тот же результат в любом порядке.
|
|
345
|
+
|
|
346
|
+
Из-за позднего подключения модулей происходят две вещи:
|
|
347
|
+
|
|
348
|
+
- **Модули стартуют в конце блока.** `modulePlugin([userModule])` подключает `userModule`, когда синхронный блок, создавший его, закончился, или раньше, если сначала использовались приложение или шина: первый `bus.emit` либо первый вызов `app.mount`, `app.unmount`, `app.isMounted`, `app.dispose` или чтение `app.actions`. К этому моменту каждый плагин, подключённый в том же блоке, уже на шине, так что у модуля и задач, которые он порождает, есть поля, эффекты и scope плагинов, подключённых после `modulePlugin`, а `m.on(fn, handler)` работает с `matchPlugin`. Плагин, подключённый после `await`, может опоздать: подключайте каждый плагин до первого `await`. Без модулей `modulePlugin()` ничего не запускает до первого использования.
|
|
349
|
+
- **Подписки сохраняют своё место.** `bus.on` после `bus.use(modulePlugin(...))` удерживается, пока модули не подключены, а затем выполняется в том порядке, в котором вы его написали. Поэтому подписки модуля остаются впереди зарегистрированных после плагина и позади зарегистрированных до него. Функция, которую возвращает `bus.on`, работает и до, и после.
|
|
350
|
+
|
|
351
|
+
Остальным плагинам порядок не нужен по другой причине:
|
|
352
|
+
|
|
353
|
+
- `actionsPlugin` привязывает свой список в реестре, который есть у шины, так что он может идти до или после `modulePlugin`. Собственный `bus.actions = { run }` заменяется.
|
|
354
|
+
- `effectsPlugin` и `contextPlugin` не заменяют поле, которое уже есть на `ctx`, так что scope побеждает в любом порядке.
|
|
355
|
+
- `devPlugin()` находит приложение, которое `modulePlugin` положил на шину (`bus.app`). `devPlugin({ app })` по-прежнему работает.
|
|
356
|
+
- `matchPlugin` и `devPlugin` оба оборачивают `bus.on`; `devPlugin` считает функцию, выбирающую события, под `"*"`, а его `disconnect` оставляет обёртку, которая была положена поверх него.
|
|
357
|
+
|
|
358
|
+
Имя, которое уже является полем контекста шины, — ошибка типов, когда `contextPlugin` или `effectsPlugin` добавляет его снова.
|
|
359
|
+
|
|
360
|
+
Тест кладёт каждый плагин на шину во многих порядках и проверяет, что делает каждый (`src/tests/plugin-matrix.test.ts`). Если какой-то порядок что-то ломает, это баг.
|
|
361
|
+
|
|
362
|
+
## Что не является плагином
|
|
363
|
+
|
|
364
|
+
Эти вещи подключаются без `bus.use`:
|
|
365
|
+
|
|
366
|
+
- `createEffectsMiddleware(bus, { filter })` из `nanoeffects/redux` — middleware Redux для существующей шины. См. [State managers](stores.md#подключение-redux-toolkit-к-шине).
|
|
367
|
+
- `createBus({ trace })` принимает трассировщик из `nanoeffects/trace`. См. [Трассировку](tracing.md).
|
|
368
|
+
- `extendBus(bus, Base)` из `nanoeffects/oop` даёт классу методы шины, а подписки каждого экземпляра заканчиваются через `dispose()`. Он читает шину как есть, так что сначала подключите плагины. См. [Шина в классе](oop.md).
|
|
369
|
+
- `createObserve(bus)` из `nanoeffects/observable` превращает событие в Observable, а `events(bus, event)` — в цикл `for await`. См. [Observable](observable.md).
|
|
370
|
+
|
|
371
|
+
## Собственный плагин
|
|
372
|
+
|
|
373
|
+
Плагин — функция шины, которая возвращает нужное вам. Шина не хранит список плагинов и не может убрать подключённый плагин. Если он нужен, плагин сам возвращает `stop` или `disconnect`.
|
|
374
|
+
|
|
375
|
+
```js
|
|
376
|
+
// count-plugin.js
|
|
377
|
+
// ваш собственный плагин: функция шины, которая возвращает то, что вам нужно
|
|
378
|
+
export const countPlugin = (bus) => {
|
|
379
|
+
let count = 0;
|
|
380
|
+
const unbind = bus.on("*", () => {
|
|
381
|
+
count += 1;
|
|
382
|
+
});
|
|
383
|
+
return { count: () => count, stop: unbind };
|
|
384
|
+
};
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
```js
|
|
388
|
+
import { createBus } from "nanoeffects";
|
|
389
|
+
import { countPlugin } from "./count-plugin";
|
|
390
|
+
|
|
391
|
+
const bus = createBus();
|
|
392
|
+
// use — вызывает countPlugin(bus) и возвращает его результат
|
|
393
|
+
const counter = bus.use(countPlugin);
|
|
394
|
+
|
|
395
|
+
bus.emit("greet", "Ada");
|
|
396
|
+
console.log(counter.count()); // 1
|
|
397
|
+
counter.stop();
|
|
398
|
+
```
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# Политики
|
|
2
|
+
|
|
3
|
+
> **[Уровень B](README.md#виды-проектов)**: SPA с async UI. Сначала прочитайте: [Руководство](guide.md#1-создание-шины-событий).
|
|
4
|
+
|
|
5
|
+
В SPA обработчики накладываются друг на друга: поиск по мере набора, повторная отправка, уход со страницы. **Политика** оборачивает обработчик и решает, что делать с наложением.
|
|
6
|
+
|
|
7
|
+
Начните с `latest`. Остальные политики — в таблице и разделах ниже.
|
|
8
|
+
|
|
9
|
+
## `latest`: прерывание предыдущего запуска
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { createBus } from "nanoeffects";
|
|
13
|
+
import { latest } from "nanoeffects/policies";
|
|
14
|
+
|
|
15
|
+
const bus = createBus();
|
|
16
|
+
|
|
17
|
+
// latest — новый запуск прерывает старый
|
|
18
|
+
bus.on(
|
|
19
|
+
"search",
|
|
20
|
+
latest(async (query, { signal }) => {
|
|
21
|
+
// signal — позволяет latest отменить этот запрос, а не только отбросить его результат
|
|
22
|
+
await api.search(query, { signal });
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Передайте `signal` в `fetch` (и подобные вызовы), чтобы запрос отменялся.
|
|
28
|
+
|
|
29
|
+
## Таблица политик
|
|
30
|
+
|
|
31
|
+
Каждая политика принимает `key` в параметрах (см. [`key`: разделение state по ключу](#key-разделение-state-по-ключу)).
|
|
32
|
+
|
|
33
|
+
| Политика | Накладывающиеся вызовы |
|
|
34
|
+
| -------- | ---------------------- |
|
|
35
|
+
| `latest(fn)` | предыдущий запуск прерван, результат отброшен |
|
|
36
|
+
| `leading(fn)` | новые вызовы отбрасываются, пока идёт запуск |
|
|
37
|
+
| `debounce(ms, fn, options?)` | запуск после `ms` тишины |
|
|
38
|
+
| `throttle(ms, fn, options?)` | не больше одного запуска за `ms` |
|
|
39
|
+
| `queue(fn, options?)` | один за другим, по порядку |
|
|
40
|
+
|
|
41
|
+
## Справочник политик
|
|
42
|
+
|
|
43
|
+
По одной записи на политику: что она делает и ссылка на полный рецепт.
|
|
44
|
+
|
|
45
|
+
### `latest`
|
|
46
|
+
|
|
47
|
+
Новый вызов прерывает старый и отбрасывает его результат.
|
|
48
|
+
|
|
49
|
+
Сигнатура: `latest(fn, options?)`.
|
|
50
|
+
|
|
51
|
+
Подробнее: [`latest`: прерывание предыдущего запуска](#latest-прерывание-предыдущего-запуска).
|
|
52
|
+
|
|
53
|
+
### `leading`
|
|
54
|
+
|
|
55
|
+
Новые вызовы отбрасываются, пока идёт запуск.
|
|
56
|
+
|
|
57
|
+
Сигнатура: `leading(fn, options?)`.
|
|
58
|
+
|
|
59
|
+
Подробнее: [Защита от повторной отправки](prevent-double-submit.md).
|
|
60
|
+
|
|
61
|
+
### `debounce`
|
|
62
|
+
|
|
63
|
+
Запускается после паузы.
|
|
64
|
+
|
|
65
|
+
Сигнатура: `debounce(ms, fn, options?)`.
|
|
66
|
+
|
|
67
|
+
Подробнее: [Настройка debounce и throttle](#настройка-debounce-и-throttle).
|
|
68
|
+
|
|
69
|
+
### `throttle`
|
|
70
|
+
|
|
71
|
+
Не больше одного запуска в течение `ms`.
|
|
72
|
+
|
|
73
|
+
Сигнатура: `throttle(ms, fn, options?)`.
|
|
74
|
+
|
|
75
|
+
Подробнее: [Настройка debounce и throttle](#настройка-debounce-и-throttle).
|
|
76
|
+
|
|
77
|
+
### `queue`
|
|
78
|
+
|
|
79
|
+
Формирует очередь вызовов: они выполняются один за другим, по порядку.
|
|
80
|
+
|
|
81
|
+
Сигнатура: `queue(fn, options?)`.
|
|
82
|
+
|
|
83
|
+
Подробнее: [Очередь событий через `queue`](#очередь-событий-через-queue).
|
|
84
|
+
|
|
85
|
+
### `key`
|
|
86
|
+
|
|
87
|
+
Параметр каждой политики. Он разделяет state политики по признаку, например по id: вызовы с разными ключами не влияют друг на друга.
|
|
88
|
+
|
|
89
|
+
Сигнатура: `latest(fn, { key: (payload) => payload.id })`.
|
|
90
|
+
|
|
91
|
+
Подробнее: [`key`: разделение state по ключу](#key-разделение-state-по-ключу).
|
|
92
|
+
|
|
93
|
+
Чтобы применить политику к своей функции без шины, см. [`withPolicy`: вызов политики вне шины](#withpolicy-вызов-политики-вне-шины).
|
|
94
|
+
|
|
95
|
+
## Настройка debounce и throttle
|
|
96
|
+
|
|
97
|
+
`debounce` и `throttle` принимают два параметра: `leading` и `trailing`. Они решают, какой вызов серии выполнится. `leading` выполняет первый вызов серии сразу. `trailing` выполняет последний вызов в конце серии. Обе функции построены по тому же алгоритму, что в lodash (и patronum), и ведут себя точно так же.
|
|
98
|
+
|
|
99
|
+
**Throttle** (`throttle(ms, fn)`) выполняет вызов сразу, если до него `ms` ничего не выполнялось, удерживает вызовы, пришедшие в течение `ms`, и выполняет последний из них, когда окно закрывается. При стабильном потоке запуск идёт каждые `ms`.
|
|
100
|
+
|
|
101
|
+
- `leading` — выполнить первый вызов серии сразу. По умолчанию `true`. При `false` даже первый вызов ждёт конца своего окна.
|
|
102
|
+
- `trailing` — выполнить последний удержанный вызов, когда окно закрывается. По умолчанию `true`. При `false` вызовы, сделанные во время окна, отбрасываются.
|
|
103
|
+
|
|
104
|
+
**Debounce** (`debounce(ms, fn)`) выполняет последний вызов серии после `ms` тишины.
|
|
105
|
+
|
|
106
|
+
- `trailing` — выполнить последний вызов после `ms` тишины. По умолчанию `true`.
|
|
107
|
+
- `leading` — выполнить ещё и первый вызов серии сразу. По умолчанию `false`. Завершающий запуск тогда происходит, только если следуют ещё вызовы.
|
|
108
|
+
- `leading: true, trailing: false` — только первый вызов серии.
|
|
109
|
+
- Оба `false` — ничего не запускается.
|
|
110
|
+
|
|
111
|
+
## `key`: разделение state по ключу
|
|
112
|
+
|
|
113
|
+
`key` даёт каждому ключу своё state. Вызовы с разными ключами не влияют друг на друга. Простаивающие ключи удаляются, поэтому ключей может быть сколько угодно (идентификаторы, URL).
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
import { createBus } from "nanoeffects";
|
|
117
|
+
import { latest, queue } from "nanoeffects/policies";
|
|
118
|
+
|
|
119
|
+
const bus = createBus();
|
|
120
|
+
|
|
121
|
+
// один запуск latest на идентификатор пользователя
|
|
122
|
+
bus.on(
|
|
123
|
+
"load",
|
|
124
|
+
latest(loadUser, { key: (user) => user.id }),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// загрузки одного и того же файла идут одна за другой
|
|
128
|
+
bus.on(
|
|
129
|
+
"upload",
|
|
130
|
+
queue(upload, { key: (file) => file.name }),
|
|
131
|
+
);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Очередь событий через `queue`
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
import { createBus } from "nanoeffects";
|
|
138
|
+
import { queue } from "nanoeffects/policies";
|
|
139
|
+
|
|
140
|
+
const bus = createBus();
|
|
141
|
+
|
|
142
|
+
// queue — один за другим, по порядку
|
|
143
|
+
bus.on(
|
|
144
|
+
"save",
|
|
145
|
+
queue(async (draft, { signal }) => {
|
|
146
|
+
await api.save(draft, { signal });
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
bus.emit("save", draftA); // выполняется сразу
|
|
151
|
+
bus.emit("save", draftB); // ждёт draftA, затем выполняется
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
| Параметр | Значение |
|
|
155
|
+
| -------- | -------- |
|
|
156
|
+
| `concurrency` | сколько вызовов могут идти одновременно (по умолчанию `1`) |
|
|
157
|
+
| `limit` | сколько вызовов могут ждать |
|
|
158
|
+
| `overflow` | когда очередь полна: `"drop-new"` (по умолчанию) или `"drop-old"` |
|
|
159
|
+
|
|
160
|
+
Отброшенный вызов завершается значением `undefined`. `queue(fn, { limit: 1, overflow: "drop-old" })` оставляет только последний ожидающий вызов. `limit: 0` не даёт никому ждать.
|
|
161
|
+
|
|
162
|
+
Каждый вызов возвращает Promise с результатом собственного запуска. Ошибка отклоняет только этот вызов; очередь продолжает работу.
|
|
163
|
+
|
|
164
|
+
**Прерывание:**
|
|
165
|
+
|
|
166
|
+
- Ожидающий вызов, чья задача прервана, пропускается и завершается значением `undefined`.
|
|
167
|
+
- Когда прерывается выполняющийся вызов, его слот освобождается сразу, а результат отбрасывается.
|
|
168
|
+
- Передавайте `signal` в то, что делает `fn`. `fn`, который его игнорирует, может пересечься со следующим запуском.
|
|
169
|
+
|
|
170
|
+
## `withPolicy`: вызов политики вне шины
|
|
171
|
+
|
|
172
|
+
**Проблема.** Нужен обработчик ввода с debounce или запрос по принципу «побеждает последний» для вашей собственной функции, но шины в коде нет: DOM-обработчик, callback клика, функция в сервисе. Политике на каждом вызове нужен контекст, поэтому `debounce(300, search)("re")` сам по себе не работает.
|
|
173
|
+
|
|
174
|
+
**Когда.** Одному полю ввода, кнопке или функции нужна политика и больше ничего из библиотеки. Если шина у вас уже есть, проще `bus.on(event, policy)`. В компоненте React берите [`usePolicy`](react.md#обработчик-с-debounce-между-рендерами): он ещё и сохраняет функцию между рендерами.
|
|
175
|
+
|
|
176
|
+
`withPolicy` принимает политику и возвращает обычную функцию. Ей не нужны ни шина, ни аргумент с контекстом, и она сохраняет state политики между вызовами.
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
import { debounce, withPolicy } from "nanoeffects/policies";
|
|
180
|
+
|
|
181
|
+
const input = document.querySelector("#search");
|
|
182
|
+
|
|
183
|
+
// debounce — ждёт 300 мс тишины, затем запускает callback
|
|
184
|
+
const policy = debounce(300, async (query, { signal }) => {
|
|
185
|
+
const items = await api.search(query, { signal });
|
|
186
|
+
render(items);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// withPolicy — обычная функция: без шины и без аргумента с контекстом
|
|
190
|
+
const onInput = withPolicy(policy);
|
|
191
|
+
|
|
192
|
+
input.addEventListener("input", (event) => onInput(event.target.value));
|
|
193
|
+
|
|
194
|
+
// cancel — отбрасывает ожидающий вызов и прерывает выполняющийся запрос, например при закрытии страницы
|
|
195
|
+
window.addEventListener("pagehide", () => onInput.cancel());
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
- У callback та же форма `(payload, ctx)`, что и для шины. `ctx.signal`, `ctx.abort` и `ctx.run` работают. `ctx.emit` и `ctx.on` ничего не делают, потому что шины нет.
|
|
199
|
+
- Каждый вызов возвращает Promise с результатом. Вызов, отброшенный политикой, завершается значением `undefined`.
|
|
200
|
+
- `cancel()` прерывает каждый вызов, который ещё не закончился. Вызовы после него снова работают.
|
|
201
|
+
- Там, где есть `Symbol.dispose`, `using onInput = withPolicy(policy)` вызывает `cancel()` в конце области видимости.
|