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,401 @@
|
|
|
1
|
+
# Работа с контекстом
|
|
2
|
+
|
|
3
|
+
> **[Уровень D](README.md#виды-проектов)**: доменная логика вне UI. Сначала прочитайте: [Руководство](guide.md#1-создание-шины-событий).
|
|
4
|
+
|
|
5
|
+
Страница нужна, когда одна зависимость (store, клиент, логгер) требуется многим обработчикам, или когда одному action, модулю или вызову нужны свои поля.
|
|
6
|
+
|
|
7
|
+
## На этой странице
|
|
8
|
+
|
|
9
|
+
- [Поля контекста](#поля-контекста)
|
|
10
|
+
- [Значения во время выполнения](#значения-во-время-выполнения)
|
|
11
|
+
- [Типы полей](#типы-полей)
|
|
12
|
+
- [Поле, построенное из сигнала задачи](#поле-построенное-из-сигнала-задачи)
|
|
13
|
+
- [Scope: поля для action, модуля и вызова](#scope-поля-для-action-модуля-и-вызова)
|
|
14
|
+
- [Включение scope](#включение-scope)
|
|
15
|
+
- [Собственные поля action](#собственные-поля-action)
|
|
16
|
+
- [Привязка эффекта к задаче](#привязка-эффекта-к-задаче)
|
|
17
|
+
- [Замена поля для одного вызова](#замена-поля-для-одного-вызова)
|
|
18
|
+
- [Собственные поля action, задачи и модуля](#собственные-поля-action-задачи-и-модуля)
|
|
19
|
+
- [Какое значение побеждает](#какое-значение-побеждает)
|
|
20
|
+
- [Обобщённый эффект](#обобщённый-эффект)
|
|
21
|
+
- [Типы полей scope](#типы-полей-scope)
|
|
22
|
+
- [Ограничения](#ограничения)
|
|
23
|
+
|
|
24
|
+
## Поля контекста
|
|
25
|
+
|
|
26
|
+
### Значения во время выполнения
|
|
27
|
+
|
|
28
|
+
<!-- docs-check: new -->
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
import { createBus } from "nanoeffects";
|
|
32
|
+
|
|
33
|
+
// createBus({ context }) — каждая задача получает эти поля
|
|
34
|
+
const bus = createBus({ context: { store } });
|
|
35
|
+
|
|
36
|
+
bus.on("save", (draft, { store }) => {
|
|
37
|
+
store.put(draft);
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Здесь `store` — ваш объект: store, API-клиент, логгер, что угодно. В JavaScript этого достаточно. Поля есть у обработчиков, `ctx.run`, `ctx.on`, actions, модулей и дочерних задач эффектов и политик.
|
|
42
|
+
|
|
43
|
+
Поля библиотеки (`signal`, `emit`, `run`, …) побеждают при конфликте имён. `context` может быть функцией `(task) => fields`, когда значение зависит от задачи.
|
|
44
|
+
|
|
45
|
+
Чтобы `race`, `all`, `take` и подобные были на каждой задаче без передачи `ctx`, см. [`effectsPlugin`](effects.md#effectsplugin).
|
|
46
|
+
|
|
47
|
+
Чтобы дать поля **одному** action, задаче или модулю либо заменить их для одного `emit`, см. [области (scope)](context.md#scope-поля-для-action-модуля-и-вызова).
|
|
48
|
+
|
|
49
|
+
### Типы полей
|
|
50
|
+
|
|
51
|
+
<!-- docs-check: new -->
|
|
52
|
+
|
|
53
|
+
В TypeScript `contextPlugin` (из `nanoeffects/context`) добавляет поля в каждую задачу **этой** шины и берёт их типы из переданного значения. Карта событий описывает только события. Она ничего не связывает в приложении: у другой шины могут быть другие поля или никаких.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { createBus } from "nanoeffects";
|
|
57
|
+
import { contextPlugin } from "nanoeffects/context";
|
|
58
|
+
|
|
59
|
+
// Events — имя события → payload
|
|
60
|
+
type Events = { save: Draft };
|
|
61
|
+
|
|
62
|
+
// contextPlugin — каждая задача получает ctx.store; тип — это тип `store`
|
|
63
|
+
const bus = createBus<Events>().use(contextPlugin({ store }));
|
|
64
|
+
|
|
65
|
+
bus.on("save", (draft, { store }) => store.put(draft));
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- Функция `(task) => fields` делает поля для каждой задачи отдельно. Она получает `{ type, payload }`.
|
|
69
|
+
- Несколько вызовов `contextPlugin`, `effectsPlugin` и параметр `context` в `createBus` добавляют свои поля в один и тот же контекст.
|
|
70
|
+
- Повтор имени, которое у шины уже есть, — ошибка типов. Поля библиотеки (`signal`, `emit`, `run`, …) побеждают во время выполнения, а уже существующее поле контекста не заменяется.
|
|
71
|
+
- Плагин возвращает ту же шину, поэтому сохраняйте результат цепочки: `const bus = createBus().use(contextPlugin({ store }))`.
|
|
72
|
+
|
|
73
|
+
### Поле, построенное из сигнала задачи
|
|
74
|
+
|
|
75
|
+
<!-- docs-check: new -->
|
|
76
|
+
|
|
77
|
+
Проблема: HTTP-клиент или дескриптор базы данных должен останавливаться вместе с прерванной задачей. Без помощи каждый обработчик передаёт `ctx.signal` вручную, и один забытый вызов оставляет запрос работать.
|
|
78
|
+
|
|
79
|
+
`fromSignal(make)` из `nanoeffects/signal` помечает поле `contextPlugin` как значение, которое строится из сигнала задачи. `make` запускается не для шины, а для каждой задачи, которая читает поле, — один раз, с `ctx.signal` этой задачи.
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
import { createBus } from "nanoeffects";
|
|
83
|
+
import { contextPlugin } from "nanoeffects/context";
|
|
84
|
+
import { fromSignal } from "nanoeffects/signal";
|
|
85
|
+
import axios from "axios";
|
|
86
|
+
|
|
87
|
+
// contextPlugin — каждая задача получает эти поля
|
|
88
|
+
// fromSignal — http строится из сигнала той задачи, которая читает ctx.http
|
|
89
|
+
const bus = createBus().use(
|
|
90
|
+
contextPlugin({
|
|
91
|
+
logger: console, // обычное поле: один и тот же объект для каждой задачи
|
|
92
|
+
http: fromSignal((signal) => axios.create({ signal })),
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
bus.on("user/open", async (id, ctx) => {
|
|
97
|
+
const response = await ctx.http.get(`/users/${id}`); // прерывается вместе с задачей
|
|
98
|
+
ctx.logger.log(response.data);
|
|
99
|
+
});
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
- `make` запускается при первом чтении, поэтому обработчик, который никогда не использует `ctx.http`, ничего за него не платит.
|
|
103
|
+
- Задача, которую запускает `ctx.run`, строит своё значение из своего сигнала, так что прерывание дочерней задачи останавливает только её запросы.
|
|
104
|
+
- Поле не копируется через `{ ...ctx }`, и `Object.keys(ctx)` его не перечисляет. Если вы копируете контекст, сначала прочитайте `ctx.http`.
|
|
105
|
+
- Без `contextPlugin` маркер `fromSignal` — просто объект.
|
|
106
|
+
|
|
107
|
+
#### Поля в карте событий
|
|
108
|
+
|
|
109
|
+
`createBus<Events>({ context: { store } })` тоже типизирует поля, но через карту событий. `WithContext` нужен только для проверки типов: он говорит, какие поля есть у обработчиков этой шины. Берите его, когда `use` вызвать нельзя, например для шины, которую создаёт `createEffectsMiddleware`.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { createBus, type WithContext } from "nanoeffects";
|
|
113
|
+
|
|
114
|
+
// WithContext — только для проверки типов: у обработчиков есть и ctx.store
|
|
115
|
+
type Events = { save: Draft } & WithContext<{ store: Store }>;
|
|
116
|
+
|
|
117
|
+
const bus = createBus<Events>({ context: { store } });
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Scope: поля для action, модуля и вызова
|
|
121
|
+
|
|
122
|
+
Страница нужна, когда одному action нужен свой клиент, модулю — свой логгер, а тесту — своя подделка, и поле на всей шине ([поля контекста](context.md)) слишком широко.
|
|
123
|
+
|
|
124
|
+
**Scope** — обычный объект. Каждый его ключ попадает на `ctx` как есть (`cache`, `api`, `logger`), кроме `$effects`. Его функции принимают `{ signal }` первым параметром, как `retry(ctx, fn)`, и попадают на `ctx` привязанными к задаче: вы пишете `ctx.fetchJson(url)` вместо `fetchJson(ctx, url)`.
|
|
125
|
+
|
|
126
|
+
Scope вы задаёте тому, кто его использует, а вызов может его заменить. Больше ничего не меняется: обработчики по-прежнему получают `(payload, ctx)`.
|
|
127
|
+
|
|
128
|
+
### Включение scope
|
|
129
|
+
|
|
130
|
+
<!-- docs-check: new -->
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
// bus.js
|
|
134
|
+
import { createBus } from "nanoeffects";
|
|
135
|
+
import { scopePlugin } from "nanoeffects/scope";
|
|
136
|
+
|
|
137
|
+
// scopePlugin — включает scope ниже на этой шине; возвращает ту же шину
|
|
138
|
+
export const bus = createBus().use(scopePlugin());
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Шина лежит в одном файле, а actions — в файлах по имени домена, поэтому следующий пример импортирует `bus` отсюда. Каждый следующий раздел держит свои `bus.js` и файл домена, как будто это отдельное приложение.
|
|
142
|
+
|
|
143
|
+
- Без `scopePlugin` scope **игнорируется**: ни ошибки, ни предупреждения. Если поля нет на `ctx`, проверьте это в первую очередь.
|
|
144
|
+
- Плагин работает в любом порядке с другими: scope побеждает `effectsPlugin`, `contextPlugin` и `createBus({ context })`.
|
|
145
|
+
|
|
146
|
+
### Собственные поля action
|
|
147
|
+
|
|
148
|
+
<!-- docs-check: new -->
|
|
149
|
+
|
|
150
|
+
<!-- docs-check: continue -->
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
// user.js
|
|
154
|
+
import { createAction } from "nanoeffects/actions";
|
|
155
|
+
|
|
156
|
+
// createAction — третий аргумент это scope этого action
|
|
157
|
+
export const getUser = createAction(
|
|
158
|
+
"user/get",
|
|
159
|
+
(id, { api, cache }) => {
|
|
160
|
+
// api и cache есть на ctx только для этого action
|
|
161
|
+
return cache.get(id) ?? api.getUser(id);
|
|
162
|
+
},
|
|
163
|
+
{ api, cache: new Map() },
|
|
164
|
+
);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```js
|
|
168
|
+
import { bus } from "./bus";
|
|
169
|
+
import { getUser } from "./user";
|
|
170
|
+
|
|
171
|
+
const user = await bus.emit(getUser("1")); // у обработчика есть api и cache
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Подписчики `"user/get"` получают те же поля, когда action отправляется, потому что поля принадлежат вызову.
|
|
175
|
+
|
|
176
|
+
### Привязка эффекта к задаче
|
|
177
|
+
|
|
178
|
+
<!-- docs-check: new -->
|
|
179
|
+
|
|
180
|
+
Эффект — это функция с `{ signal }` первым параметром. В `$effects` он привязан к задаче, поэтому останавливается вместе с ней.
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
// feed.js
|
|
184
|
+
import { createAction } from "nanoeffects/actions";
|
|
185
|
+
import { delay, race } from "nanoeffects/effects";
|
|
186
|
+
|
|
187
|
+
// ваш собственный эффект: первый параметр — задача (используется только её signal)
|
|
188
|
+
const fetchJson = (ctx, url) =>
|
|
189
|
+
fetch(url, { signal: ctx.signal }).then((response) => response.json());
|
|
190
|
+
|
|
191
|
+
export const loadFeed = createAction(
|
|
192
|
+
"feed/load",
|
|
193
|
+
(url, { fetchJson, race, delay }) =>
|
|
194
|
+
// race, delay, fetchJson — привязаны к этой задаче: аргумента ctx нет
|
|
195
|
+
race({
|
|
196
|
+
feed: () => fetchJson(url),
|
|
197
|
+
timeout: () => delay(5000),
|
|
198
|
+
}),
|
|
199
|
+
// $effects — встроенные и ваши собственные функции в одном объекте
|
|
200
|
+
{ $effects: { fetchJson, race, delay } },
|
|
201
|
+
);
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
```js
|
|
205
|
+
// bus.js
|
|
206
|
+
import { createBus } from "nanoeffects";
|
|
207
|
+
import { scopePlugin } from "nanoeffects/scope";
|
|
208
|
+
|
|
209
|
+
export const bus = createBus().use(scopePlugin());
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
```js
|
|
213
|
+
import { bus } from "./bus";
|
|
214
|
+
import { loadFeed } from "./feed";
|
|
215
|
+
|
|
216
|
+
const result = await bus.emit(loadFeed("/feed"));
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
- В `$effects` кладите только функции, которые принимают `ctx` первым. Обычной функции (форматтер, логгер) место в `context`.
|
|
220
|
+
- Функция под любым другим ключом копируется как есть; функция в `$effects` привязывается.
|
|
221
|
+
- `$effects` — единственное имя со смыслом. Поле `effects` — обычное поле: `{ effects: { retries: 3 } }` добавляет этот объект в `ctx`, и ничего в нём не привязывается.
|
|
222
|
+
|
|
223
|
+
### Замена поля для одного вызова
|
|
224
|
+
|
|
225
|
+
<!-- docs-check: new -->
|
|
226
|
+
|
|
227
|
+
Второй аргумент `emit` — scope для задач **этого** вызова. Так обычно подставляют тесту подделку.
|
|
228
|
+
|
|
229
|
+
```js
|
|
230
|
+
// bus.js
|
|
231
|
+
import { createBus } from "nanoeffects";
|
|
232
|
+
import { scopePlugin } from "nanoeffects/scope";
|
|
233
|
+
|
|
234
|
+
export const bus = createBus().use(scopePlugin());
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
```js
|
|
238
|
+
// user.js
|
|
239
|
+
import { createAction } from "nanoeffects/actions";
|
|
240
|
+
|
|
241
|
+
export const getUser = createAction(
|
|
242
|
+
"user/get",
|
|
243
|
+
(id, { api }) => api.getUser(id),
|
|
244
|
+
{ api },
|
|
245
|
+
);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
```js
|
|
249
|
+
import { bus } from "./bus";
|
|
250
|
+
import { getUser } from "./user";
|
|
251
|
+
|
|
252
|
+
// настоящий api для этого вызова
|
|
253
|
+
await bus.emit(getUser("1"));
|
|
254
|
+
|
|
255
|
+
// второй аргумент — поддельный api, только для этого вызова
|
|
256
|
+
const fakeApi = { getUser: async (id) => ({ id, name: "test" }) };
|
|
257
|
+
await bus.emit(getUser("1"), { api: fakeApi });
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Правила для второго аргумента:
|
|
261
|
+
|
|
262
|
+
- Он работает с вызовом action (`emit(getUser("1"), scope)`, `ctx.emit(loaded(user), scope)`).
|
|
263
|
+
- Он не работает со строковой формой: в `emit("user/get", payload)` второй аргумент — это payload.
|
|
264
|
+
- Он может заменить и `$effects`: `{ $effects: { delay: instantDelay } }`.
|
|
265
|
+
- Поля библиотеки (`signal`, `emit`, …) в нём игнорируются.
|
|
266
|
+
|
|
267
|
+
### Собственные поля action, задачи и модуля
|
|
268
|
+
|
|
269
|
+
<!-- docs-check: new -->
|
|
270
|
+
|
|
271
|
+
```js
|
|
272
|
+
// user.js
|
|
273
|
+
import { createAction, createJob } from "nanoeffects/actions";
|
|
274
|
+
|
|
275
|
+
// createAction — подписчики вызовов этого action получают поля
|
|
276
|
+
export const userLoaded = createAction("user/loaded", { store });
|
|
277
|
+
|
|
278
|
+
// createJob — scope это четвёртый аргумент, после исходов;
|
|
279
|
+
// обработчик и подписчики исхода получают его поля
|
|
280
|
+
export const loadUser = createJob(
|
|
281
|
+
"user/load",
|
|
282
|
+
fetchUser,
|
|
283
|
+
{ success: userLoaded, fail: "user/failed" },
|
|
284
|
+
{ logger },
|
|
285
|
+
);
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
```js
|
|
289
|
+
// audit-module.js
|
|
290
|
+
import { createModule } from "nanoeffects/actions";
|
|
291
|
+
import { userLoaded } from "./user";
|
|
292
|
+
|
|
293
|
+
// createModule — третий аргумент; всё, чем владеет модуль, получает поля:
|
|
294
|
+
// подписки m.on, обработчики m.handle, задачи m.spawn
|
|
295
|
+
export const auditModule = createModule(
|
|
296
|
+
"audit",
|
|
297
|
+
(m) => {
|
|
298
|
+
m.on(userLoaded, (user, { logger }) => logger.info(user.id));
|
|
299
|
+
},
|
|
300
|
+
{ logger },
|
|
301
|
+
);
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Если у action-исхода (`success` или `fail`) есть свой scope, его подписчики видят поля задачи, поверх которых лежат собственные поля action; обработчик задачи видит только поля задачи.
|
|
305
|
+
|
|
306
|
+
Scope модуля действует только внутри модуля: подписчик, зарегистрированный вне модуля, его не видит.
|
|
307
|
+
|
|
308
|
+
### Какое значение побеждает
|
|
309
|
+
|
|
310
|
+
<!-- docs-check: new -->
|
|
311
|
+
|
|
312
|
+
Когда одно и то же поле задано в нескольких местах, побеждает самое сильное:
|
|
313
|
+
|
|
314
|
+
| Сила | Где задано |
|
|
315
|
+
| ---- | ---------- |
|
|
316
|
+
| 1 (сильнейшее) | второй аргумент `emit` |
|
|
317
|
+
| 2 | scope action или задачи |
|
|
318
|
+
| 3 | scope модуля, которому принадлежит обработчик |
|
|
319
|
+
| 4 (слабейшее) | шина: `createBus({ context })`, `contextPlugin`, `effectsPlugin` |
|
|
320
|
+
|
|
321
|
+
Поля библиотеки (`signal`, `abort`, `emit`, `on`, `off`, `run`, `type`, `payload`) никогда не заменяются. Имя `traceGroupId` зарезервировано: его читает трассировщик, и на `ctx` оно не попадает ([Проследите один поток](tracing.md#один-поток-через-tracegroupid)). У дочерней задачи (`ctx.run`, попытка `retry`) есть поля её родителя.
|
|
322
|
+
|
|
323
|
+
### Обобщённый эффект
|
|
324
|
+
|
|
325
|
+
<!-- docs-check: new -->
|
|
326
|
+
|
|
327
|
+
В `$effects` функция теряет первый параметр, и TypeScript не может сохранить при этом обобщение: `<T>(items: T[]) => T` становится `(items: unknown[]) => unknown`. JavaScript это не затрагивает. Для точного типа есть два способа.
|
|
328
|
+
|
|
329
|
+
`effect(fn)`: пометьте функцию при объявлении и кладите в scope как любое другое значение. Аннотации не нужны, поэтому редактор JavaScript тоже показывает точную подсказку.
|
|
330
|
+
|
|
331
|
+
```js
|
|
332
|
+
// pick.js
|
|
333
|
+
import { createAction } from "nanoeffects/actions";
|
|
334
|
+
import { effect } from "nanoeffects/scope";
|
|
335
|
+
|
|
336
|
+
// effect — помечает функцию, чтобы scope привязал её к задаче; редактор сохраняет её обобщение
|
|
337
|
+
const first = effect((ctx, items) => items[0]);
|
|
338
|
+
|
|
339
|
+
export const pick = createAction("pick", (items, { first }) => first(items), {
|
|
340
|
+
first, // обычный ключ scope, а не запись в `$effects`
|
|
341
|
+
});
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Вызывайте `effect` при объявлении функции, как выше, а не прямо в scope: TypeScript не выводит scope из обобщённого вызова внутри него.
|
|
345
|
+
|
|
346
|
+
`Effect<F>` (TypeScript): опишите, как выглядит эффект **после** привязки, и положите его в `$effects`.
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
import { createAction } from "nanoeffects/actions";
|
|
350
|
+
import type { Effect } from "nanoeffects/effects";
|
|
351
|
+
|
|
352
|
+
// First — только для проверки типов: как обработчик вызывает эффект (без ctx)
|
|
353
|
+
type First = <T>(items: T[]) => T;
|
|
354
|
+
|
|
355
|
+
// Effect<First> — функция принимает задачу первой, а после привязки типизируется как First
|
|
356
|
+
const first: Effect<First> = (ctx, items) => items[0];
|
|
357
|
+
|
|
358
|
+
const pick = createAction(
|
|
359
|
+
"pick",
|
|
360
|
+
(id: string, { first }) => first([id]), // string
|
|
361
|
+
{ $effects: { first } },
|
|
362
|
+
);
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
`Effect<F>` и `effect` меняют то, что видит проверка типов. Во время выполнения функция та же.
|
|
366
|
+
|
|
367
|
+
### Типы полей scope
|
|
368
|
+
|
|
369
|
+
<!-- docs-check: new -->
|
|
370
|
+
|
|
371
|
+
Обработчик action, задачи или модуля со scope видит поля scope в `ctx` с типами, выведенными из переданных значений. `WithContext` писать не нужно.
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
import { createAction } from "nanoeffects/actions";
|
|
375
|
+
|
|
376
|
+
type User = { id: string; name: string };
|
|
377
|
+
|
|
378
|
+
const getUser = createAction(
|
|
379
|
+
"user/get",
|
|
380
|
+
async (id: string, ctx) => {
|
|
381
|
+
ctx.cache; // Map<string, User>, из scope ниже
|
|
382
|
+
return ctx.cache.get(id);
|
|
383
|
+
},
|
|
384
|
+
{ cache: new Map<string, User>() },
|
|
385
|
+
);
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
- Поле scope заменяет одноимённое поле шины и в типе тоже.
|
|
389
|
+
- Подмена в `emit` должна совпадать с типом заменяемого поля: `emit(getUser("1"), { cache: 1 })` — ошибка.
|
|
390
|
+
- У поля, которое не объявили ни scope, ни шина, нет типа для сверки, поэтому `emit(call, { unknown: 1 })` — ошибка в TypeScript.
|
|
391
|
+
- `bus.on(creator, fn)` для action со scope типизирует `ctx` его полями.
|
|
392
|
+
|
|
393
|
+
### Ограничения
|
|
394
|
+
|
|
395
|
+
<!-- docs-check: new -->
|
|
396
|
+
|
|
397
|
+
- Нет плагина — нет scope: см. [Включение scope](#включение-scope).
|
|
398
|
+
- Полей шины нет в обработчике action, созданного через `createAction(…, scope)`, если вы не типизируете `ctx` сами; поля scope есть.
|
|
399
|
+
- Строковая форма `emit` scope не принимает.
|
|
400
|
+
- `effect(fn)` работает только как значение scope, а не как запись в `$effects`.
|
|
401
|
+
- Scope на action, задаче или модуле типизируется без событий шины, поэтому `take` в его `$effects` принимает любое имя события.
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Пауза между запросами
|
|
2
|
+
|
|
3
|
+
> **[Уровень B](README.md#виды-проектов)**: SPA с async UI. Сначала прочитайте: [Политики](policies.md).
|
|
4
|
+
|
|
5
|
+
Debounce пользовательского ввода (поле поиска, автосохранение, обработчик resize), когда запрос в полёте должен останавливаться при новом вводе. Один `debounce` ждёт паузы, но не отменяет уже стартовавший запрос. Как совместить две политики, в каком порядке их ставить и когда нужен `throttle`.
|
|
6
|
+
|
|
7
|
+
## Политики
|
|
8
|
+
|
|
9
|
+
| Политика | Что делает | Чего не делает |
|
|
10
|
+
| -------- | ---------- | -------------- |
|
|
11
|
+
| `debounce(ms, fn)` | запускает `fn` через `ms` без нового вызова | не отменяет запуск, который уже начался |
|
|
12
|
+
| `latest(fn)` | прерывает предыдущий запуск, когда начинается новый | не ждёт паузы |
|
|
13
|
+
|
|
14
|
+
Для поля поиска используйте обе. `debounce` отправляет меньше запросов. `latest` отменяет медленный, когда пользователь снова печатает после паузы.
|
|
15
|
+
|
|
16
|
+
## Совместное использование debounce и latest
|
|
17
|
+
|
|
18
|
+
Поставьте `debounce` снаружи, а `latest` внутри. Сначала событие ждёт паузы. Когда запуск начинается, он прерывает предыдущий запуск.
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import { createBus } from "nanoeffects";
|
|
22
|
+
import { debounce, latest } from "nanoeffects/policies";
|
|
23
|
+
|
|
24
|
+
const bus = createBus(); // шина: события и обработчики
|
|
25
|
+
|
|
26
|
+
// latest: новый запуск прерывает старый запрос
|
|
27
|
+
const searchLatest = latest(async (query, { signal }) => {
|
|
28
|
+
// signal: передайте его дальше, чтобы браузер сам отменил запрос
|
|
29
|
+
const items = await api.search(query, { signal });
|
|
30
|
+
render(items);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// debounce: запуск searchLatest через 300 мс без нового "typed"
|
|
34
|
+
bus.on("typed", debounce(300, searchLatest));
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Что заменить: `api.search` — ваша функция запроса. Она должна принимать `{ signal }` и передавать его в `fetch`. `render` показывает элементы.
|
|
38
|
+
|
|
39
|
+
Результат: ввод `a`, `ab`, `abc` в пределах 300 мс отправляет один запрос для `abc`. Если пользователь печатает `abcd`, пока этот запрос идёт, запрос для `abc` прерывается, а после паузы стартует запрос для `abcd`.
|
|
40
|
+
|
|
41
|
+
## Первый вызов сразу
|
|
42
|
+
|
|
43
|
+
По умолчанию `debounce` ждёт даже перед первым вызовом. Чтобы первое нажатие срабатывало сразу, добавьте `leading: true`. Первый вызов серии выполняется немедленно, последний — после паузы.
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
import { createBus } from "nanoeffects";
|
|
47
|
+
import { debounce } from "nanoeffects/policies";
|
|
48
|
+
|
|
49
|
+
const bus = createBus(); // шина: события и обработчики
|
|
50
|
+
|
|
51
|
+
const showSuggestions = (query) => console.log("suggest", query);
|
|
52
|
+
|
|
53
|
+
// debounce с leading: первый вызов выполняется сразу, последний — после 300 мс тишины
|
|
54
|
+
bus.on("typed", debounce(300, showSuggestions, { leading: true }));
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`leading` и `trailing` следуют lodash. Все сочетания: [Политики](policies.md#настройка-debounce-и-throttle).
|
|
58
|
+
|
|
59
|
+
## Непрерывный ввод: throttle
|
|
60
|
+
|
|
61
|
+
`debounce` не срабатывает, пока вызовы продолжают приходить. Для прокрутки и перетаскивания нужны обновления во время движения. Возьмите `throttle`: не больше одного запуска за период.
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
import { createBus } from "nanoeffects";
|
|
65
|
+
import { throttle } from "nanoeffects/policies";
|
|
66
|
+
|
|
67
|
+
const bus = createBus(); // шина: события и обработчики
|
|
68
|
+
|
|
69
|
+
const saveScroll = (position) => localStorage.setItem("scroll", String(position));
|
|
70
|
+
|
|
71
|
+
// throttle: не больше одного запуска за 100 мс, пока "scrolled" продолжает приходить
|
|
72
|
+
bus.on("scrolled", throttle(100, saveScroll));
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Debounce без шины
|
|
76
|
+
|
|
77
|
+
Если шины нет, оберните ту же пару в `withPolicy`. Получится обычная функция для обработчика ввода.
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
import { debounce, latest, withPolicy } from "nanoeffects/policies";
|
|
81
|
+
|
|
82
|
+
// latest: новый запуск прерывает старый запрос
|
|
83
|
+
const searchLatest = latest(async (query, { signal }) => {
|
|
84
|
+
const items = await api.search(query, { signal });
|
|
85
|
+
console.log(items);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// withPolicy: debounce на 300 мс, вызывается как обычная функция
|
|
89
|
+
const onTyped = withPolicy(debounce(300, searchLatest));
|
|
90
|
+
|
|
91
|
+
onTyped("a");
|
|
92
|
+
onTyped("ab"); // выполнится только "ab", через 300 мс
|
|
93
|
+
|
|
94
|
+
// cancel: отбросить ожидающий вызов и прервать выполняющийся запрос, например при уходе со страницы
|
|
95
|
+
onTyped.cancel();
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
В компоненте React берите [`usePolicy`](react.md#обработчик-с-debounce-между-рендерами): он ещё и отменяет вызовы при размонтировании.
|
|
99
|
+
|
|
100
|
+
## Что проверить, если отмены нет
|
|
101
|
+
|
|
102
|
+
- Запрос не получил `signal`: `latest` отбросит старый результат, но запрос продолжится.
|
|
103
|
+
- Порядок перепутан. Пишите `debounce(300, latest(handler))`: `debounce` принимает события, `latest` охраняет запрос.
|
|
104
|
+
- Несколько полей используют одно событие. Дайте каждому своё имя события или задайте `{ key }`, например `latest(handler, { key: (input) => input.id })`.
|
|
105
|
+
|