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
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# Остановка работы на backend, когда клиент ушёл
|
|
2
|
+
|
|
3
|
+
> **[Уровень C](README.md#виды-проектов)**: сложная бизнес-логика. Сначала прочитайте: [Руководство](guide.md#1-создание-шины-событий).
|
|
4
|
+
|
|
5
|
+
Работа backend на Node.js (в примерах [Fastify](https://fastify.dev)) для запроса, сокета или задачи из очереди должна прекращаться, когда она никому не нужна. Один приём в трёх местах: запрос, WebSocket и воркер.
|
|
6
|
+
|
|
7
|
+
Нужны Node 18 или новее, `fastify` и `nanoeffects`. `api` ниже — ваш клиент другого сервиса: замените его своими запросами.
|
|
8
|
+
|
|
9
|
+
## Зачем это backend
|
|
10
|
+
|
|
11
|
+
Закрытая вкладка браузера останавливает свои запросы. Backend так не умеет: когда клиент ушёл, запросы, которые backend сделал для него, и их повторы продолжаются. На загруженном backend это лишняя работа и замедление для остальных клиентов.
|
|
12
|
+
|
|
13
|
+
Решение: один `AbortController` на каждую единицу работы клиента и одно правило — **когда клиент уходит, прервите контроллер.** `signalPlugin` позволяет `bus.emit` принять этот сигнал, и `timeout`, `retry` и сам запрос останавливаются.
|
|
14
|
+
|
|
15
|
+
| Единица работы | Создаётся, когда | Прерывается, когда |
|
|
16
|
+
| -------------- | ---------------- | ------------------ |
|
|
17
|
+
| HTTP-запрос | стартует маршрут | соединение закрылось до ответа |
|
|
18
|
+
| WebSocket | сокет подключился | сокет закрылся |
|
|
19
|
+
| Задача из очереди | задача принята | backend выключается |
|
|
20
|
+
|
|
21
|
+
## Шина и action
|
|
22
|
+
|
|
23
|
+
Шина и action лежат в файлах, которые ничего не знают о HTTP, так что тот же action может выполняться в маршруте, тесте или воркере.
|
|
24
|
+
|
|
25
|
+
```js
|
|
26
|
+
// bus.js
|
|
27
|
+
import { createBus } from "nanoeffects";
|
|
28
|
+
import { signalPlugin } from "nanoeffects/signal";
|
|
29
|
+
|
|
30
|
+
// createBus — шина, общая для backend
|
|
31
|
+
// signalPlugin — позволяет emit принимать { signal }, чтобы закрытое соединение могло прервать работу
|
|
32
|
+
export const bus = createBus().use(signalPlugin());
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Action загружает пользователя: до трёх попыток, каждая прерывается через пять секунд. `ctx.signal` — сигнал задачи: он прерывается, когда прерывается контроллер запроса.
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
// user.js
|
|
39
|
+
import { createAction } from "nanoeffects/actions";
|
|
40
|
+
import { retry, timeout } from "nanoeffects/effects";
|
|
41
|
+
|
|
42
|
+
// createAction — action с обработчиком: emit возвращает то, что вернул обработчик
|
|
43
|
+
export const getUser = createAction("user/get", async (id, ctx) => {
|
|
44
|
+
// одна попытка: запрос с сигналом timeout, чтобы тот мог его остановить
|
|
45
|
+
const load = ({ signal }) => api.getUser(id, { signal });
|
|
46
|
+
// timeout — провалить попытку через 5 с
|
|
47
|
+
const attempt = () => timeout(ctx, 5000, load);
|
|
48
|
+
// retry — до 3 попыток с паузой 500 мс
|
|
49
|
+
return retry(ctx, attempt, { times: 3, delay: 500 });
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Остановка работы запроса
|
|
54
|
+
|
|
55
|
+
<!-- docs-check: continue -->
|
|
56
|
+
|
|
57
|
+
Создайте контроллер при старте маршрута. Слушайте `close` у **ответа** (`reply.raw`): он наступает, когда соединение заканчивается. `request.raw` тоже отправляет `close`, но когда тело запроса прочитано, а не когда клиент ушёл, поэтому он не подходит.
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
// server.js
|
|
61
|
+
import Fastify from "fastify";
|
|
62
|
+
import { bus } from "./bus";
|
|
63
|
+
import { getUser } from "./user";
|
|
64
|
+
|
|
65
|
+
export const app = Fastify();
|
|
66
|
+
|
|
67
|
+
// контроллеры запросов, на которые ещё отвечают, для остановки backend ниже
|
|
68
|
+
export const inFlight = new Set();
|
|
69
|
+
|
|
70
|
+
app.get("/users/:id", async (request, reply) => {
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
inFlight.add(controller);
|
|
73
|
+
|
|
74
|
+
// close у ответа — соединение закончилось; если ответ не был завершён, клиент ушёл
|
|
75
|
+
reply.raw.on("close", () => {
|
|
76
|
+
inFlight.delete(controller);
|
|
77
|
+
if (!reply.raw.writableFinished) {
|
|
78
|
+
controller.abort();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
// emit с { signal } — прерывание контроллера прерывает action: timeout, retry и сам запрос
|
|
84
|
+
const call = getUser(request.params.id);
|
|
85
|
+
return await bus.emit(call, { signal: controller.signal });
|
|
86
|
+
} catch (error) {
|
|
87
|
+
// отвечать некому, когда клиент закрыл соединение
|
|
88
|
+
if (reply.raw.destroyed) {
|
|
89
|
+
return reply;
|
|
90
|
+
}
|
|
91
|
+
return reply.code(502).send({ error: String(error) });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
После прерывания action отклоняется с причиной прерывания. Блок `catch` выше игнорирует её, когда соединения уже нет, потому что отвечать некому.
|
|
97
|
+
|
|
98
|
+
## Остановка backend
|
|
99
|
+
|
|
100
|
+
<!-- docs-check: continue -->
|
|
101
|
+
|
|
102
|
+
Храните контроллеры запросов в полёте в множестве. По SIGTERM (его посылает контейнер) или SIGINT (Ctrl+C) прервите их все и закройте приложение. Прерванный запрос отвечает ошибкой, а `app.close()` ждёт окончания соединений.
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
// shutdown.js
|
|
106
|
+
import { app, inFlight } from "./server";
|
|
107
|
+
|
|
108
|
+
// SIGTERM посылает контейнер, SIGINT — это Ctrl+C
|
|
109
|
+
for (const name of ["SIGTERM", "SIGINT"]) {
|
|
110
|
+
process.on(name, async () => {
|
|
111
|
+
for (const controller of inFlight) {
|
|
112
|
+
controller.abort();
|
|
113
|
+
}
|
|
114
|
+
await app.close();
|
|
115
|
+
process.exit(0);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Маршрут выше наполняет `inFlight` и очищает его в обработчике `close`. [Пример api-server](../../examples/api-server) делает то же самое, с лимитом в две секунды на закрытие.
|
|
121
|
+
|
|
122
|
+
## Привязка WebSocket к его работе
|
|
123
|
+
|
|
124
|
+
<!-- docs-check: continue -->
|
|
125
|
+
|
|
126
|
+
Сокет живёт минуты, и в нём работает несколько задач (подписка, heartbeat), поэтому контроллер принадлежит сокету, а каждый его `emit` получает тот же сигнал. Закрытие сокета останавливает все задачи.
|
|
127
|
+
|
|
128
|
+
Сначала action. Оно отправляет цену сразу, а затем каждую секунду, пока его задача не будет прервана.
|
|
129
|
+
|
|
130
|
+
```js
|
|
131
|
+
// prices.js
|
|
132
|
+
import { createAction } from "nanoeffects/actions";
|
|
133
|
+
import { poll } from "nanoeffects/effects";
|
|
134
|
+
import { latest } from "nanoeffects/policies";
|
|
135
|
+
|
|
136
|
+
// createAction — action с обработчиком
|
|
137
|
+
// latest — новая подписка останавливает старую; key — но только старую того же сокета
|
|
138
|
+
export const watchPrices = createAction(
|
|
139
|
+
"prices/watch",
|
|
140
|
+
latest(
|
|
141
|
+
async (call, ctx) => {
|
|
142
|
+
// poll — вызывать функцию сразу и каждую секунду, пока задача не будет прервана
|
|
143
|
+
await poll(ctx, () => call.send(`${call.symbol}: ${Math.random()}`), 1000, {
|
|
144
|
+
leading: true,
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
{ key: (call) => call.socketId },
|
|
148
|
+
),
|
|
149
|
+
);
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Без `key` одна политика обслуживает весь action, и новая подписка на одном сокете остановила бы подписку на другом.
|
|
153
|
+
|
|
154
|
+
Затем маршрут. Каждый сокет получает идентификатор и контроллер.
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
// sockets.js
|
|
158
|
+
import websocket from "@fastify/websocket";
|
|
159
|
+
import Fastify from "fastify";
|
|
160
|
+
import { bus } from "./bus";
|
|
161
|
+
import { watchPrices } from "./prices";
|
|
162
|
+
|
|
163
|
+
const sockets = Fastify();
|
|
164
|
+
sockets.register(websocket);
|
|
165
|
+
|
|
166
|
+
let lastSocketId = 0;
|
|
167
|
+
|
|
168
|
+
sockets.register(async (instance) => {
|
|
169
|
+
instance.get("/prices", { websocket: true }, (socket) => {
|
|
170
|
+
lastSocketId += 1;
|
|
171
|
+
const socketId = lastSocketId;
|
|
172
|
+
const controller = new AbortController();
|
|
173
|
+
const { signal } = controller;
|
|
174
|
+
|
|
175
|
+
// close — клиент ушёл: остановить всё, что запущено для этого сокета
|
|
176
|
+
socket.on("close", () => controller.abort());
|
|
177
|
+
|
|
178
|
+
socket.on("message", (data) => {
|
|
179
|
+
const send = (text) => socket.send(text);
|
|
180
|
+
const call = watchPrices({ socketId, symbol: String(data), send });
|
|
181
|
+
// emit с { signal } — вызов останавливается, когда сокет закрывается
|
|
182
|
+
bus.emit(call, { signal });
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
[Пример ws-server](../../examples/ws-server) добавляет закрытие молчащего сокета, собранное из `race`, `take` и `delay`.
|
|
189
|
+
|
|
190
|
+
## Воркер очереди
|
|
191
|
+
|
|
192
|
+
Воркер принимает задачи от клиента, сразу отвечает и продолжает работу в фоне. `createJob` с `queue` ограничивает число одновременных задач, `retry` и `timeout` позволяют сдаваться, а исходы задачи (`pending`, `success`, `fail`, `cancel`) помогают их считать. Передавайте каждому `emit` задачи один и тот же сигнал: его прерывание при остановке отменит оставшиеся задачи.
|
|
193
|
+
|
|
194
|
+
Важная деталь: вызов, который ещё ждёт в очереди, когда сигнал прерывается, не стартует, и задача не отправляет для него исхода. Если итоги должны сходиться, считайте такие вызовы сами.
|
|
195
|
+
|
|
196
|
+
В [примере job-worker](../../examples/job-worker) есть всё: счётчики, `GET /stats` и остановка в три шага (перестать принимать задачи, подождать, прервать остальные).
|
|
197
|
+
|
|
198
|
+
## Примеры
|
|
199
|
+
|
|
200
|
+
| Папка | Что показывает |
|
|
201
|
+
| ----- | -------------- |
|
|
202
|
+
| [`api-server`](../../examples/api-server) | Backend на Fastify поверх PokeAPI: `timeout`, `retry`, `allSettled` и прерывание, когда клиент ушёл |
|
|
203
|
+
| [`ws-server`](../../examples/ws-server) | Backend с WebSocket: по контроллеру на сокет, `latest` с `key`, закрытие простаивающего |
|
|
204
|
+
| [`job-worker`](../../examples/job-worker) | Воркер очереди: `queue`, `retry`, `timeout`, счётчики по исходам, остановка в три шага |
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Проект из шаблона
|
|
2
|
+
|
|
3
|
+
> **[Уровень A](README.md#виды-проектов)**: небольшое приложение, виджет или MVP.
|
|
4
|
+
|
|
5
|
+
Одна команда создаёт работающий проект, и Vite с шиной подключать вручную не нужно. Проект — небольшое приложение с полем поиска. Оно ждёт, пока вы перестанете печатать (`debounce`), и прерывает старый запрос при новом (`latest`).
|
|
6
|
+
|
|
7
|
+
Нужен Node 18 или новее.
|
|
8
|
+
|
|
9
|
+
## Создание проекта
|
|
10
|
+
|
|
11
|
+
Замените `my-app` на имя новой папки.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm create nanoeffects@latest my-app
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm create nanoeffects my-app
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
yarn create nanoeffects my-app
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bun create nanoeffects my-app
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Если шаблон не указан, команда спросит его. Папка должна быть новой или пустой, иначе команда остановится и не перезапишет файлы.
|
|
30
|
+
|
|
31
|
+
Затем выполните шаги, которые напечатала команда:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
cd my-app
|
|
35
|
+
npm install
|
|
36
|
+
npm run dev
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Откройте адрес, который напечатает Vite, и наберите что-нибудь в поле. Поддельный backend отвечает 800 мс, поэтому видно, как прерывается старый запрос.
|
|
40
|
+
|
|
41
|
+
## Выбор шаблона
|
|
42
|
+
|
|
43
|
+
Передайте `--template`, чтобы пропустить вопрос. С npm перед параметром ставьте `--`.
|
|
44
|
+
|
|
45
|
+
| Шаблон | Что показывает | Когда брать |
|
|
46
|
+
| ------ | -------------- | ----------- |
|
|
47
|
+
| `vanilla` | Шина, одно событие и обработчик с `debounce` и `latest`, обычный DOM-код | Вы без фреймворка или хотите увидеть одну шину |
|
|
48
|
+
| `react` | Тот же обработчик с `useEmitter` и `useEventState`, а также `usePolicy` для сохранения с debounce в компоненте | Вы пишете приложение на React |
|
|
49
|
+
| `vanilla-ts` | Приложение `vanilla` на TypeScript | Хотите, чтобы компилятор проверял события и обработчики |
|
|
50
|
+
| `react-ts` | Приложение `react` на TypeScript | Вы пишете приложение на React с TypeScript |
|
|
51
|
+
| `domains-ts` | Actions с обработчиками, job и модули; логика разложена по доменным папкам, а React — тонкий слой | В приложении больше одной фичи, и логику нужно вынести из UI |
|
|
52
|
+
| `vue-ts` | Приложение `react-ts` на Vue 3, с `watchEmitter` и `useEventRef` из `nanoeffects/vue` | Вы пишете приложение на Vue 3 |
|
|
53
|
+
| `svelte-ts` | Приложение `react-ts` на Svelte 5, с `eventStore` из `nanoeffects/svelte` | Вы пишете приложение на Svelte |
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm create nanoeffects@latest my-app -- --template react
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
В шаблонах `react` `useEventState` хранит найденные элементы в компоненте. Это не глобальный store: другой компонент получит свою копию. Чтобы делить значение между компонентами, возьмите state manager: [State managers](stores.md).
|
|
60
|
+
|
|
61
|
+
TypeScript-шаблоны — те же приложения с теми же файлами в `.ts` и `.tsx`. События создаёт `createAction<Payload>`, поэтому обработчики и `useEventState` берут типы из событий, и аннотации в компоненте не нужны. Перед сборкой `npm run build` проверяет типы (`tsc --noEmit`).
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm create nanoeffects@latest my-app -- --template react-ts
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Начало с Vue
|
|
68
|
+
|
|
69
|
+
`vue-ts` — приложение поиска из `react-ts` на Vue 3 с `<script setup lang="ts">`. Шина использует `vuePlugin` из `nanoeffects/vue`: `watchEmitter` отправляет запрос в шину при его изменении, а `useEventRef` хранит результат поиска в `ref`. Как и `useEventState` в React, `useEventRef` — [не store](vue.md#это-не-store). Значение, общее для нескольких компонентов, храните в store, например в Pinia.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm create nanoeffects@latest my-app -- --template vue-ts
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`npm run build` проверяет типы файлов `.vue` через `vue-tsc`. Ему нужен TypeScript 5, поэтому шаблон от него зависит.
|
|
76
|
+
|
|
77
|
+
### Начало со Svelte
|
|
78
|
+
|
|
79
|
+
`svelte-ts` — приложение поиска из `react-ts` на Svelte 5. Шина использует `sveltePlugin` из `nanoeffects/svelte`: `eventStore` превращает событие в store Svelte. В компоненте `$found` читает его, а Svelte сам подписывается и отписывается. Запрос уходит в шину из обработчика `oninput`, поэтому при монтировании компонента ничего не отправляется. Store слушает, только пока его что-то читает, и [не является store приложения](svelte.md#это-не-store-приложения). Значение, общее для нескольких компонентов, храните в своём store.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm create nanoeffects@latest my-app -- --template svelte-ts
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`npm run build` проверяет типы файлов `.svelte` через `svelte-check`. Ему нужен TypeScript 5, поэтому шаблон от него зависит.
|
|
86
|
+
|
|
87
|
+
### Начало с доменами
|
|
88
|
+
|
|
89
|
+
`domains-ts` — не то же поле поиска на TypeScript. Он показывает, где живёт логика, когда в приложении несколько фич. Каждый домен — папка в `src/domains/`: события домена, их обработка и модуль, который её подключает. React-компоненты только отправляют события и читают значения.
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm create nanoeffects@latest my-app -- --template domains-ts
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
| Путь | Задача |
|
|
96
|
+
| ---- | ------ |
|
|
97
|
+
| `src/bus.ts` | Создаёт шину. Не импортирует ни события, ни домены. |
|
|
98
|
+
| `src/app.ts` | Подключает домены через `modulePlugin`. |
|
|
99
|
+
| `src/domains/catalog/` | Поиск товаров: `catalog.ts` (события и action с обработчиком), `catalog-handlers.ts` (`debounce` и `latest`), `catalog-module.ts`. |
|
|
100
|
+
| `src/domains/checkout/` | Оформление заказа: `checkout.ts` (job), `checkout-handlers.ts` (`leading` и `retry`), `checkout-store.ts` (state, записываемое из исходов задачи), `checkout-module.ts`. |
|
|
101
|
+
| `src/ui/` | Компоненты. Ни один из них не вызывает поддельный backend из `src/api.ts`. |
|
|
102
|
+
|
|
103
|
+
Пример побольше — чат, где части реагируют друг на друга, — в [Чат с сущностями, фичами, модулями и небольшим backend](chat-example.md). Слоёв там больше, чем в `domains-ts`. Когда домены должны реагировать друг на друга, чат делит их на сущности и фичи, и домены не импортируют друг друга.
|
|
104
|
+
|
|
105
|
+
Actions, jobs и модули объясняет страница [Actions, jobs и модули](actions.md). Шаблон хранит найденные товары в провайдере Context с одним `useEventState`, а state заказа — в отдельном store, потому что `useEventState` — [не store](react.md#последнее-значение-события-как-state).
|
|
106
|
+
|
|
107
|
+
## Файлы
|
|
108
|
+
|
|
109
|
+
Каждый шаблон, кроме `domains-ts`, делит код как [Руководство](guide.md): один файл — одна задача. В TypeScript-шаблонах файлы имеют расширение `.ts` (`.tsx` у компонентов React, `.vue` у Vue, `.svelte` у Svelte).
|
|
110
|
+
|
|
111
|
+
| Файл | Задача |
|
|
112
|
+
| ---- | ------ |
|
|
113
|
+
| `src/bus.js` | Создаёт шину. Не импортирует события. |
|
|
114
|
+
| `src/search.js` | Называет события через `createAction`. Не импортирует шину. |
|
|
115
|
+
| `src/search-handlers.js` | Обрабатывает события через `debounce` и `latest`. Знает и шину, и события. |
|
|
116
|
+
| `src/api.js` | Поддельный backend. Замените его своими запросами. |
|
|
117
|
+
|
|
118
|
+
Каждый вызов nanoeffects в этих файлах снабжён комментарием. Чтобы адаптировать проект, замените `src/api.js`, затем переименуйте события и обработчик.
|