@fozy-labs/rx-toolkit 0.4.17 → 0.5.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -19
- package/dist/common/devtools/reduxDevtools.d.ts +17 -0
- package/dist/common/devtools/reduxDevtools.js +98 -17
- package/dist/common/devtools/types.d.ts +1 -0
- package/dist/common/options/DefaultOptions.d.ts +0 -2
- package/dist/common/options/DefaultOptions.js +0 -2
- package/dist/common/options/SharedOptions.d.ts +0 -2
- package/dist/common/options/SharedOptions.js +0 -1
- package/dist/query/api/resetAllQueriesCache.d.ts +1 -0
- package/dist/query/api/resetAllQueriesCache.js +4 -0
- package/dist/query/core/Opertation/Operation.js +10 -7
- package/dist/query/core/Opertation/OperationAgent.d.ts +0 -2
- package/dist/query/core/Opertation/OperationAgent.js +4 -21
- package/dist/query/core/QueriesCache.d.ts +1 -1
- package/dist/query/core/QueriesCache.js +2 -6
- package/dist/query/core/{CleanAllQueriesSignal.d.ts → ResetAllQueriesSignal.d.ts} +1 -1
- package/dist/query/core/ResetAllQueriesSignal.js +11 -0
- package/dist/query/core/Resource/Resource.js +7 -3
- package/dist/query/core/Resource/ResourceAgent.d.ts +2 -3
- package/dist/query/core/Resource/ResourceAgent.js +62 -29
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +1 -1
- package/dist/query/lib/IndirectMap.d.ts +1 -1
- package/dist/query/lib/IndirectMap.js +1 -1
- package/dist/query/lib/ReactiveCache.d.ts +1 -1
- package/dist/query/lib/ReactiveCache.js +2 -2
- package/dist/query/react/useOperationAgent.js +2 -5
- package/dist/query/react/useResourceAgent.js +1 -4
- package/dist/query/types/Operation.types.d.ts +1 -3
- package/dist/query/types/Resource.types.d.ts +1 -3
- package/dist/signals/base/Batcher.d.ts +1 -1
- package/dist/signals/base/Batcher.js +1 -1
- package/dist/signals/base/DependencyTracker.d.ts +18 -0
- package/dist/signals/base/DependencyTracker.js +13 -0
- package/dist/signals/base/Devtools.d.ts +1 -1
- package/dist/signals/base/Devtools.js +13 -1
- package/dist/signals/base/ReadonlySignal.d.ts +5 -7
- package/dist/signals/base/ReadonlySignal.js +20 -12
- package/dist/signals/base/index.d.ts +1 -2
- package/dist/signals/base/index.js +1 -2
- package/dist/signals/operators/index.d.ts +0 -2
- package/dist/signals/operators/index.js +0 -2
- package/dist/signals/operators/signalize.d.ts +2 -2
- package/dist/signals/operators/signalize.js +1 -1
- package/dist/signals/react/useSignal.d.ts +6 -2
- package/dist/signals/react/useSignal.js +2 -21
- package/dist/signals/signals/Computed.d.ts +13 -11
- package/dist/signals/signals/Computed.js +79 -26
- package/dist/signals/signals/Effect.d.ts +11 -7
- package/dist/signals/signals/Effect.js +60 -58
- package/dist/signals/signals/LocalSignal.d.ts +14 -7
- package/dist/signals/signals/LocalSignal.js +52 -33
- package/dist/signals/signals/Signal.d.ts +13 -37
- package/dist/signals/signals/Signal.js +44 -58
- package/dist/signals/types/index.d.ts +1 -0
- package/dist/signals/types/index.js +1 -0
- package/dist/signals/types/signals.types.d.ts +16 -0
- package/docs/CHANGELOG.md +32 -0
- package/docs/devtools/README.md +162 -29
- package/docs/migrations/0.5.0.md +73 -0
- package/docs/options/README.md +89 -0
- package/docs/query/README.md +425 -89
- package/docs/release/README.md +58 -6
- package/docs/signals/README.md +207 -34
- package/docs/usage/react/README.md +261 -49
- package/package.json +5 -5
- package/dist/query/api/cleanAllQueriesCache.d.ts +0 -1
- package/dist/query/api/cleanAllQueriesCache.js +0 -4
- package/dist/query/core/CleanAllQueriesSignal.js +0 -11
- package/dist/signals/base/Tracker.d.ts +0 -10
- package/dist/signals/base/Tracker.js +0 -7
- package/dist/signals/base/types.d.ts +0 -23
- package/dist/signals/operators/filterUpdates.d.ts +0 -5
- package/dist/signals/operators/filterUpdates.js +0 -18
- package/dist/signals/operators/mapSignals.d.ts +0 -3
- package/dist/signals/operators/mapSignals.js +0 -10
- /package/dist/signals/{base/types.js → types/signals.types.js} +0 -0
package/docs/query/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# RxQuery
|
|
2
2
|
|
|
3
|
-
RxQuery
|
|
3
|
+
RxQuery — система для управления асинхронными запросами и кэшированием данных в RxToolkit. Она состоит из двух основных компонентов: **Resources** и **Operations**.
|
|
4
4
|
|
|
5
5
|
## Основные концепции
|
|
6
6
|
|
|
@@ -13,6 +13,7 @@ Resources предназначены для реактивного кэширо
|
|
|
13
13
|
- Поддержка AbortController для отмены запросов
|
|
14
14
|
- Реактивные обновления состояния
|
|
15
15
|
- Оптимистичные обновления
|
|
16
|
+
- Гибкое управление временем жизни кэша
|
|
16
17
|
|
|
17
18
|
### Operations (Операции)
|
|
18
19
|
|
|
@@ -23,28 +24,29 @@ Operations представляют одноразовые операции ил
|
|
|
23
24
|
- Связывание с ресурсами для их обновления
|
|
24
25
|
- Поддержка оптимистичных обновлений
|
|
25
26
|
- Возможность блокировки связанных ресурсов
|
|
27
|
+
- Автоматический откат при ошибках
|
|
26
28
|
|
|
27
29
|
### Agents (Агенты)
|
|
28
|
-
Agents представляют собой интеллектуальные обертки над ресурсами (или операциями),
|
|
29
|
-
которые обеспечивают более удобную работу с состояниями запросов для потребителей.
|
|
30
30
|
|
|
31
|
+
Agents представляют собой интеллектуальные обертки над ресурсами (или операциями), которые обеспечивают более удобную работу с состояниями запросов для потребителей.
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
**Основная проблема, которую решают агенты:**
|
|
33
34
|
|
|
34
|
-
Кэш ресурсов
|
|
35
|
-
|
|
36
|
-
- `isInitialLoading` должно быть true только при первой загрузке ресурса, но не при переключении между разными аргументами
|
|
35
|
+
Кэш ресурсов содержит "сырые" состояния отдельных запросов, но потребителям нужна более высокоуровневая логика:
|
|
36
|
+
- `isInitialLoading` должно быть true только при первой загрузке ресурса
|
|
37
37
|
- При смене аргументов запроса нужно показывать данные предыдущего запроса, пока загружается новый
|
|
38
38
|
- Состояние загрузки должно отражать контекст использования, а не просто состояние кэша
|
|
39
39
|
|
|
40
|
-
|
|
41
40
|
### ResourceRef (Ссылка на ресурс)
|
|
42
|
-
|
|
41
|
+
|
|
42
|
+
Ref — это абстракция для взаимодействия с элементом кэша ресурса напрямую.
|
|
43
43
|
|
|
44
44
|
**Особенности:**
|
|
45
|
-
- Операции
|
|
46
|
-
- Ref
|
|
45
|
+
- Операции используют ref под капотом для управления связанным ресурсом
|
|
46
|
+
- Ref может ссылаться на отсутствующий элемент кэша
|
|
47
|
+
- Позволяет выполнять patch-транзакции для оптимистичных обновлений
|
|
47
48
|
|
|
49
|
+
---
|
|
48
50
|
|
|
49
51
|
## API
|
|
50
52
|
|
|
@@ -53,30 +55,62 @@ Ref - это абстракция, для взаимодействия с рес
|
|
|
53
55
|
Создает новый ресурс для кэширования данных.
|
|
54
56
|
|
|
55
57
|
```typescript
|
|
58
|
+
import { createResource } from '@fozy-labs/rx-toolkit';
|
|
59
|
+
|
|
60
|
+
interface User {
|
|
61
|
+
id: string;
|
|
62
|
+
name: string;
|
|
63
|
+
email: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
56
66
|
const userResource = createResource<{ id: string }, User>({
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
async queryFn(args, tools) {
|
|
68
|
+
const response = await fetch(`/api/users/${args.id}`, {
|
|
69
|
+
signal: tools.abortSignal // Поддержка отмены запроса
|
|
70
|
+
});
|
|
71
|
+
return response.json();
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Опционально: трансформация данных
|
|
75
|
+
select: (data) => ({
|
|
76
|
+
id: data.id,
|
|
77
|
+
name: data.name,
|
|
78
|
+
email: data.email
|
|
79
|
+
}),
|
|
80
|
+
|
|
81
|
+
// Опционально: время жизни кэша (по умолчанию 60 секунд)
|
|
82
|
+
cacheLifetime: 30000, // 30 секунд
|
|
83
|
+
|
|
84
|
+
// Опционально: имя для devtools
|
|
85
|
+
devtoolsName: 'user-resource',
|
|
86
|
+
|
|
87
|
+
// Опционально: кастомное сравнение аргументов
|
|
88
|
+
compareArgsFn: (args1, args2) => args1.id === args2.id,
|
|
68
89
|
});
|
|
69
90
|
```
|
|
70
91
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
92
|
+
**Параметры createResource:**
|
|
93
|
+
|
|
94
|
+
| Параметр | Тип | Описание |
|
|
95
|
+
|----------|-----|----------|
|
|
96
|
+
| `queryFn` | `(args, tools) => Promise<Result>` | Функция выполнения запроса |
|
|
97
|
+
| `select` | `(data) => Selected` | Опциональная функция трансформации данных |
|
|
98
|
+
| `cacheLifetime` | `number \| false` | Время жизни кэша в мс (default: 60000). `false` — кэш не удаляется |
|
|
99
|
+
| `compareArgsFn` | `(args1, args2) => boolean` | Кастомная функция сравнения аргументов |
|
|
100
|
+
| `onCacheEntryAdded` | `(args, tools) => void` | Хук при добавлении элемента в кэш |
|
|
101
|
+
| `onQueryStarted` | `(args, tools) => void` | Хук при старте запроса |
|
|
102
|
+
| `devtoolsName` | `string \| false` | Имя для devtools (`false` — отключить) |
|
|
103
|
+
|
|
104
|
+
**Tools в queryFn:**
|
|
105
|
+
- `abortSignal` — AbortSignal для отмены запроса
|
|
74
106
|
|
|
75
107
|
### createOperation
|
|
76
108
|
|
|
77
109
|
Создает новую операцию для выполнения мутаций.
|
|
78
110
|
|
|
79
111
|
```typescript
|
|
112
|
+
import { createOperation } from '@fozy-labs/rx-toolkit';
|
|
113
|
+
|
|
80
114
|
const updateUser = createOperation<
|
|
81
115
|
{ id: string; data: Partial<User> },
|
|
82
116
|
User
|
|
@@ -89,30 +123,43 @@ const updateUser = createOperation<
|
|
|
89
123
|
});
|
|
90
124
|
return response.json();
|
|
91
125
|
},
|
|
126
|
+
|
|
127
|
+
// Связывание с ресурсами
|
|
92
128
|
link(add) {
|
|
93
129
|
add({
|
|
94
130
|
resource: userResource,
|
|
95
131
|
forwardArgs: (args) => ({ id: args.id }),
|
|
96
|
-
|
|
132
|
+
// Обновление кэша после успешного запроса
|
|
133
|
+
update({ draft, args, data }) {
|
|
97
134
|
Object.assign(draft, args.data);
|
|
98
135
|
},
|
|
99
136
|
});
|
|
100
|
-
}
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
devtoolsName: 'update-user',
|
|
101
140
|
});
|
|
102
141
|
```
|
|
103
142
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
143
|
+
**Параметры createOperation:**
|
|
144
|
+
|
|
145
|
+
| Параметр | Тип | Описание |
|
|
146
|
+
|---------------------|-----------------------------|-----------------------------------------------|
|
|
147
|
+
| `queryFn` | `(args) => Promise<Result>` | Функция выполнения операции |
|
|
148
|
+
| `select` | `(data) => Selected` | Опциональная функция трансформации результата |
|
|
149
|
+
| `link` | `(add) => void` | Функция связывания с ресурсами |
|
|
150
|
+
| `cacheLifetime` | `number \| false` | Время жизни кэша операции (default: 1000) |
|
|
151
|
+
| `onCacheEntryAdded` | `(args, tools) => void` | Хук при добавлении в кэш |
|
|
152
|
+
| `onQueryStarted` | `(args, tools) => void` | Хук при старте операции |
|
|
153
|
+
| `devtoolsName` | `string \| false` | Имя для devtools |
|
|
107
154
|
|
|
155
|
+
---
|
|
108
156
|
|
|
109
157
|
## Свойства Link
|
|
110
|
-
```typescript
|
|
111
158
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
159
|
+
Link позволяет связывать операции с ресурсами для автоматического обновления кэша:
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
type LinkOptions<D, RD> = {
|
|
116
163
|
/**
|
|
117
164
|
* Целевой ресурс, с которым связывается операция
|
|
118
165
|
* @required
|
|
@@ -121,115 +168,404 @@ export type LinkOptions<D extends OperationDefinition, RD extends ResourceDefini
|
|
|
121
168
|
|
|
122
169
|
/**
|
|
123
170
|
* Функция для получения аргументов ресурса из аргументов операции.
|
|
124
|
-
* Используется для определения какой
|
|
171
|
+
* Используется для определения какой элемент в кэше нужно обновить
|
|
125
172
|
* @required
|
|
126
173
|
*/
|
|
127
174
|
forwardArgs: (args: D["Args"]) => RD["Args"];
|
|
128
175
|
|
|
129
176
|
/**
|
|
130
|
-
*
|
|
131
|
-
* При true
|
|
132
|
-
* @
|
|
177
|
+
* Инвалидация кэша после выполнения операции.
|
|
178
|
+
* При true — кэш будет очищен и ресурс перезагрузится
|
|
179
|
+
* @default false
|
|
133
180
|
*/
|
|
134
181
|
invalidate?: boolean;
|
|
135
182
|
|
|
136
183
|
/**
|
|
137
|
-
*
|
|
138
|
-
* При true
|
|
139
|
-
* @
|
|
184
|
+
* Блокировка ресурса во время выполнения операции.
|
|
185
|
+
* При true — ресурс не сможет выполнять новые запросы
|
|
186
|
+
* @default false
|
|
140
187
|
*/
|
|
141
188
|
lock?: boolean;
|
|
142
189
|
|
|
143
190
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* @optional
|
|
191
|
+
* Обновление кэша ПОСЛЕ успешного выполнения операции.
|
|
192
|
+
* Использует Immer для иммутабельных обновлений
|
|
147
193
|
*/
|
|
148
194
|
update?: (tools: {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
args: D["Args"];
|
|
153
|
-
/** Результат выполнения операции */
|
|
154
|
-
data: D["Data"];
|
|
195
|
+
draft: RD["Data"]; // Immer draft для мутации
|
|
196
|
+
args: D["Args"]; // Аргументы операции
|
|
197
|
+
data: D["Data"]; // Результат операции
|
|
155
198
|
}) => void | RD["Data"];
|
|
156
199
|
|
|
157
200
|
/**
|
|
158
|
-
*
|
|
159
|
-
* Позволяет обновить UI
|
|
160
|
-
* @optional
|
|
201
|
+
* Оптимистичное обновление ДО выполнения операции.
|
|
202
|
+
* Позволяет обновить UI немедленно
|
|
161
203
|
*/
|
|
162
204
|
optimisticUpdate?: (tools: {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
/** Аргументы, с которыми была вызвана операция */
|
|
166
|
-
args: D["Args"];
|
|
205
|
+
draft: RD["Data"]; // Immer draft для мутации
|
|
206
|
+
args: D["Args"]; // Аргументы операции
|
|
167
207
|
}) => void | RD["Data"];
|
|
168
208
|
|
|
169
209
|
/**
|
|
170
|
-
*
|
|
171
|
-
* Используется когда операция создает новую
|
|
172
|
-
* @optional
|
|
210
|
+
* Создание нового элемента в кэше.
|
|
211
|
+
* Используется когда операция создает новую сущность
|
|
173
212
|
*/
|
|
174
213
|
create?: (tools: {
|
|
175
|
-
/** Аргументы, с которыми была вызвана операция */
|
|
176
214
|
args: D["Args"];
|
|
177
|
-
/** Результат выполнения операции */
|
|
178
215
|
data: D["Data"];
|
|
179
216
|
}) => RD["Data"] | Promise<RD["Data"]>;
|
|
180
217
|
};
|
|
181
218
|
```
|
|
182
219
|
|
|
220
|
+
### Пример: Оптимистичные обновления
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
const toggleCartItem = createOperation({
|
|
224
|
+
queryFn: async (args: { id: string; enabled: boolean }) => {
|
|
225
|
+
return fetch(`/api/cart/toggle`, {
|
|
226
|
+
method: 'POST',
|
|
227
|
+
body: JSON.stringify(args)
|
|
228
|
+
}).then(r => r.json());
|
|
229
|
+
},
|
|
230
|
+
link(add) {
|
|
231
|
+
add({
|
|
232
|
+
resource: cartResource,
|
|
233
|
+
forwardArgs: () => undefined, // Корзина без параметров
|
|
234
|
+
|
|
235
|
+
// Оптимистичное обновление — UI обновится мгновенно
|
|
236
|
+
optimisticUpdate: ({ draft, args }) => {
|
|
237
|
+
const item = draft.items.find(i => i.id === args.id);
|
|
238
|
+
if (item) {
|
|
239
|
+
item.enabled = args.enabled;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// При ошибке изменения автоматически откатятся
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
183
249
|
|
|
184
250
|
## Состояния запросов
|
|
185
251
|
|
|
186
|
-
|
|
252
|
+
### ResourceQueryState
|
|
253
|
+
|
|
254
|
+
Состояние запроса ресурса через агента:
|
|
187
255
|
|
|
188
256
|
```typescript
|
|
189
|
-
|
|
190
|
-
* Состояние запроса ресурса
|
|
191
|
-
*/
|
|
192
|
-
export type ResourceQueryState<D extends ResourceDefinition> = {
|
|
257
|
+
type ResourceQueryState<D> = {
|
|
193
258
|
/** Инициализирован ли хотя бы один запрос */
|
|
194
259
|
isInitiated: boolean;
|
|
195
|
-
|
|
260
|
+
|
|
261
|
+
/** Любая загрузка (первая или повторная) */
|
|
196
262
|
isLoading: boolean;
|
|
263
|
+
|
|
264
|
+
/** Первая загрузка (данных еще не было) */
|
|
265
|
+
isInitialLoading: boolean;
|
|
266
|
+
|
|
267
|
+
/** Перезагрузка (данные уже есть) */
|
|
268
|
+
isReloading: boolean;
|
|
269
|
+
|
|
197
270
|
/** Завершен ли запрос */
|
|
198
271
|
isDone: boolean;
|
|
199
|
-
|
|
272
|
+
|
|
273
|
+
/** Успешно ли завершен последний запрос */
|
|
200
274
|
isSuccess: boolean;
|
|
201
|
-
|
|
275
|
+
|
|
276
|
+
/** Произошла ли ошибка последнего запроса */
|
|
202
277
|
isError: boolean;
|
|
203
|
-
|
|
278
|
+
|
|
279
|
+
/** Заблокирован ли ресурс операцией */
|
|
204
280
|
isLocked: boolean;
|
|
205
|
-
|
|
206
|
-
isReloading: boolean;
|
|
281
|
+
|
|
207
282
|
/** Оригинал ошибки, если есть */
|
|
208
283
|
error: unknown | undefined;
|
|
209
|
-
|
|
284
|
+
|
|
285
|
+
/** Данные (или select данных) */
|
|
210
286
|
data: D["Data"] | undefined;
|
|
211
|
-
|
|
212
|
-
|
|
287
|
+
|
|
288
|
+
/** Аргументы последнего запроса */
|
|
289
|
+
args: D["Args"] | undefined;
|
|
213
290
|
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### OperationQueryState
|
|
214
294
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
295
|
+
Состояние выполнения операции:
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
type OperationQueryState<D> = {
|
|
299
|
+
isInitiated: boolean;
|
|
220
300
|
isLoading: boolean;
|
|
221
|
-
/** Завершена ли операция */
|
|
222
301
|
isDone: boolean;
|
|
223
|
-
/** Успешно ли завершена операция (false по умолчанию) */
|
|
224
302
|
isSuccess: boolean;
|
|
225
|
-
/** Произошла ли ошибка при выполнении операции (false по умолчанию) */
|
|
226
303
|
isError: boolean;
|
|
227
|
-
/** Оригинал ошибки, если есть */
|
|
228
304
|
error: unknown | undefined;
|
|
229
|
-
/** Результат выполнения операции */
|
|
230
305
|
data: D["Data"] | undefined;
|
|
231
|
-
/** Аргументы операции */
|
|
232
|
-
args: D["Args"];
|
|
233
306
|
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## ResourceRef API
|
|
312
|
+
|
|
313
|
+
ResourceRef предоставляет низкоуровневый доступ к элементу кэша:
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
type ResourceRefInstanse<D> = {
|
|
317
|
+
/** Проверка наличия элемента в кэше */
|
|
318
|
+
get has(): boolean;
|
|
319
|
+
|
|
320
|
+
/** Блокировка ресурса (возвращает функцию разблокировки) */
|
|
321
|
+
lock(): { unlock: () => void };
|
|
322
|
+
|
|
323
|
+
/** Снятие одной блокировки */
|
|
324
|
+
unlockOne(): void;
|
|
325
|
+
|
|
326
|
+
/** Patch-транзакция для изменения данных */
|
|
327
|
+
patch(patchFn: (data: D['Data']) => void): ResourceTransaction | null;
|
|
328
|
+
|
|
329
|
+
/** Инвалидация (очистка) кэша */
|
|
330
|
+
invalidate(): void;
|
|
331
|
+
|
|
332
|
+
/** Создание элемента в кэше с данными */
|
|
333
|
+
create(data: D['Data']): void;
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Patch-транзакции
|
|
234
338
|
|
|
339
|
+
Транзакции позволяют делать изменения с возможностью отката:
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
type ResourceTransaction = {
|
|
343
|
+
patches: ImmerPatch[] // Патчи изменений
|
|
344
|
+
inversePatches: ImmerPatch[] // Патчи для отката
|
|
345
|
+
status: 'pending' | 'committed' | 'aborted'
|
|
346
|
+
abort(): void // Откатить изменения
|
|
347
|
+
commit(): void // Подтвердить изменения
|
|
348
|
+
}
|
|
235
349
|
```
|
|
350
|
+
|
|
351
|
+
**Пример использования транзакций:**
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
import { useResourceRef } from '@fozy-labs/rx-toolkit';
|
|
355
|
+
|
|
356
|
+
function TodoList() {
|
|
357
|
+
const todoRef = useResourceRef(todoResource, undefined);
|
|
358
|
+
const [pendingChanges, setPendingChanges] = useState([]);
|
|
359
|
+
|
|
360
|
+
const handleToggle = (itemId: number) => {
|
|
361
|
+
const transaction = todoRef.patch((draft) => {
|
|
362
|
+
const item = draft.items.find(i => i.id === itemId);
|
|
363
|
+
if (item) item.completed = !item.completed;
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
if (transaction) {
|
|
367
|
+
setPendingChanges(prev => [...prev, {
|
|
368
|
+
id: itemId,
|
|
369
|
+
transaction
|
|
370
|
+
}]);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const commitChange = (id: number) => {
|
|
375
|
+
const change = pendingChanges.find(c => c.id === id);
|
|
376
|
+
change?.transaction.commit();
|
|
377
|
+
setPendingChanges(prev => prev.filter(c => c.id !== id));
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const abortChange = (id: number) => {
|
|
381
|
+
const change = pendingChanges.find(c => c.id === id);
|
|
382
|
+
change?.transaction.abort(); // Данные вернутся к исходным
|
|
383
|
+
setPendingChanges(prev => prev.filter(c => c.id !== id));
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
## Lifecycle хуки
|
|
391
|
+
|
|
392
|
+
### onCacheEntryAdded
|
|
393
|
+
|
|
394
|
+
Вызывается при добавлении нового элемента в кэш:
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
const userResource = createResource({
|
|
398
|
+
queryFn: fetchUser,
|
|
399
|
+
|
|
400
|
+
onCacheEntryAdded(args, { $cacheDataLoaded, $cacheEntryRemoved, dataChanged$ }) {
|
|
401
|
+
// args — аргументы запроса
|
|
402
|
+
|
|
403
|
+
// Ожидание первой загрузки данных
|
|
404
|
+
$cacheDataLoaded.then(() => {
|
|
405
|
+
console.log('Данные загружены в кэш');
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// Ожидание удаления из кэша
|
|
409
|
+
$cacheEntryRemoved.then(() => {
|
|
410
|
+
console.log('Элемент удален из кэша');
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Подписка на изменения данных
|
|
414
|
+
const sub = dataChanged$.subscribe(data => {
|
|
415
|
+
console.log('Данные изменились:', data);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### onQueryStarted
|
|
422
|
+
|
|
423
|
+
Вызывается при старте каждого запроса:
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
const userResource = createResource({
|
|
427
|
+
queryFn: fetchUser,
|
|
428
|
+
|
|
429
|
+
async onQueryStarted(args, { $queryFulfilled }) {
|
|
430
|
+
console.log('Запрос начат с аргументами:', args);
|
|
431
|
+
|
|
432
|
+
const result = await $queryFulfilled;
|
|
433
|
+
|
|
434
|
+
if (result.isError) {
|
|
435
|
+
console.error('Ошибка запроса:', result.error);
|
|
436
|
+
} else {
|
|
437
|
+
console.log('Запрос успешен:', result.data);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
## Утилиты
|
|
446
|
+
|
|
447
|
+
### resetAllQueriesCache
|
|
448
|
+
|
|
449
|
+
Сбрасывает кэш всех ресурсов в приложении:
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
import { resetAllQueriesCache } from '@fozy-labs/rx-toolkit';
|
|
453
|
+
|
|
454
|
+
function LogoutButton() {
|
|
455
|
+
const handleLogout = () => {
|
|
456
|
+
// Очистить все кэшированные данные при выходе
|
|
457
|
+
resetAllQueriesCache();
|
|
458
|
+
navigate('/login');
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
return <button onClick={handleLogout}>Выйти</button>;
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### SKIP токен
|
|
466
|
+
|
|
467
|
+
Используется для условного пропуска запроса:
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
import { useResourceAgent, SKIP } from '@fozy-labs/rx-toolkit';
|
|
471
|
+
|
|
472
|
+
function UserProfile({ userId }: { userId: string | null }) {
|
|
473
|
+
// Запрос будет выполнен только если userId не null
|
|
474
|
+
const userQuery = useResourceAgent(
|
|
475
|
+
userResource,
|
|
476
|
+
userId ? { id: userId } : SKIP
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
if (!userId) return <div>Выберите пользователя</div>;
|
|
480
|
+
if (userQuery.isLoading) return <div>Загрузка...</div>;
|
|
481
|
+
|
|
482
|
+
return <div>{userQuery.data?.name}</div>;
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
## Примеры
|
|
489
|
+
|
|
490
|
+
### Корзина покупок с оптимистичными обновлениями
|
|
491
|
+
|
|
492
|
+
```typescript
|
|
493
|
+
import { createResource, createOperation, useResourceAgent, useOperationAgent } from '@fozy-labs/rx-toolkit';
|
|
494
|
+
|
|
495
|
+
const cartResource = createResource({
|
|
496
|
+
queryFn: () => fetch('/api/cart').then(r => r.json()),
|
|
497
|
+
devtoolsName: 'cart'
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
const toggleCartItem = createOperation({
|
|
501
|
+
queryFn: (args: { id: string; enabled: boolean }) =>
|
|
502
|
+
fetch('/api/cart/toggle', {
|
|
503
|
+
method: 'POST',
|
|
504
|
+
body: JSON.stringify(args)
|
|
505
|
+
}).then(r => r.json()),
|
|
506
|
+
|
|
507
|
+
link(add) {
|
|
508
|
+
add({
|
|
509
|
+
resource: cartResource,
|
|
510
|
+
forwardArgs: () => undefined,
|
|
511
|
+
optimisticUpdate: ({ draft, args }) => {
|
|
512
|
+
const item = draft.items.find(i => i.id === args.id);
|
|
513
|
+
if (item) item.enabled = args.enabled;
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
function ShoppingCart() {
|
|
520
|
+
const cartQuery = useResourceAgent(cartResource, undefined);
|
|
521
|
+
const [toggleItem, toggleState] = useOperationAgent(toggleCartItem);
|
|
522
|
+
|
|
523
|
+
return (
|
|
524
|
+
<div>
|
|
525
|
+
{cartQuery.data?.items.map(item => (
|
|
526
|
+
<div key={item.id}>
|
|
527
|
+
<span>{item.name}</span>
|
|
528
|
+
<button onClick={() => toggleItem({
|
|
529
|
+
id: item.id,
|
|
530
|
+
enabled: !item.enabled
|
|
531
|
+
})}>
|
|
532
|
+
{item.enabled ? 'Убрать' : 'Добавить'}
|
|
533
|
+
</button>
|
|
534
|
+
</div>
|
|
535
|
+
))}
|
|
536
|
+
</div>
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
### Зависимые запросы
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
const userResource = createResource({
|
|
545
|
+
queryFn: (args: { id: number }) => fetch(`/api/users/${args.id}`).then(r => r.json()),
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
const userStatsResource = createResource({
|
|
549
|
+
queryFn: (args: { userId: number; period: string }) =>
|
|
550
|
+
fetch(`/api/users/${args.userId}/stats?period=${args.period}`).then(r => r.json()),
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
function UserDashboard({ userId }: { userId: number }) {
|
|
554
|
+
const [period, setPeriod] = useState('daily');
|
|
555
|
+
|
|
556
|
+
const userQuery = useResourceAgent(userResource, { id: userId });
|
|
557
|
+
|
|
558
|
+
// Запрос статистики выполняется только после загрузки пользователя
|
|
559
|
+
const statsQuery = useResourceAgent(
|
|
560
|
+
userStatsResource,
|
|
561
|
+
userQuery.isSuccess ? { userId, period } : SKIP
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
// ...
|
|
565
|
+
}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
## React интеграция
|
|
569
|
+
|
|
570
|
+
См. [React интеграция](../usage/react/README.md) для подробной информации о React хуках.
|
|
571
|
+
|