@fozy-labs/rx-toolkit 0.4.18 → 0.5.0-rc.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.
Files changed (77) hide show
  1. package/README.md +20 -19
  2. package/dist/common/devtools/reduxDevtools.d.ts +17 -0
  3. package/dist/common/devtools/reduxDevtools.js +98 -17
  4. package/dist/common/devtools/types.d.ts +1 -0
  5. package/dist/common/options/DefaultOptions.d.ts +0 -2
  6. package/dist/common/options/DefaultOptions.js +0 -2
  7. package/dist/common/options/SharedOptions.d.ts +0 -2
  8. package/dist/common/options/SharedOptions.js +0 -1
  9. package/dist/query/api/resetAllQueriesCache.d.ts +1 -0
  10. package/dist/query/api/resetAllQueriesCache.js +4 -0
  11. package/dist/query/core/Opertation/Operation.js +10 -7
  12. package/dist/query/core/Opertation/OperationAgent.d.ts +0 -2
  13. package/dist/query/core/Opertation/OperationAgent.js +4 -21
  14. package/dist/query/core/QueriesCache.d.ts +1 -1
  15. package/dist/query/core/QueriesCache.js +2 -6
  16. package/dist/query/core/{CleanAllQueriesSignal.d.ts → ResetAllQueriesSignal.d.ts} +1 -1
  17. package/dist/query/core/ResetAllQueriesSignal.js +11 -0
  18. package/dist/query/core/Resource/Resource.js +7 -3
  19. package/dist/query/core/Resource/ResourceAgent.d.ts +2 -3
  20. package/dist/query/core/Resource/ResourceAgent.js +62 -29
  21. package/dist/query/index.d.ts +1 -1
  22. package/dist/query/index.js +1 -1
  23. package/dist/query/lib/IndirectMap.d.ts +1 -1
  24. package/dist/query/lib/IndirectMap.js +1 -1
  25. package/dist/query/lib/ReactiveCache.d.ts +1 -1
  26. package/dist/query/lib/ReactiveCache.js +2 -2
  27. package/dist/query/react/useOperationAgent.js +2 -5
  28. package/dist/query/react/useResourceAgent.js +1 -4
  29. package/dist/query/types/Operation.types.d.ts +1 -3
  30. package/dist/query/types/Resource.types.d.ts +1 -3
  31. package/dist/signals/base/Batcher.d.ts +1 -1
  32. package/dist/signals/base/Batcher.js +1 -1
  33. package/dist/signals/base/DependencyTracker.d.ts +18 -0
  34. package/dist/signals/base/DependencyTracker.js +13 -0
  35. package/dist/signals/base/Devtools.d.ts +1 -1
  36. package/dist/signals/base/Devtools.js +13 -1
  37. package/dist/signals/base/ReadonlySignal.d.ts +5 -7
  38. package/dist/signals/base/ReadonlySignal.js +20 -12
  39. package/dist/signals/base/index.d.ts +1 -2
  40. package/dist/signals/base/index.js +1 -2
  41. package/dist/signals/operators/index.d.ts +0 -2
  42. package/dist/signals/operators/index.js +0 -2
  43. package/dist/signals/operators/signalize.d.ts +2 -2
  44. package/dist/signals/operators/signalize.js +1 -1
  45. package/dist/signals/react/useSignal.d.ts +6 -2
  46. package/dist/signals/react/useSignal.js +2 -21
  47. package/dist/signals/signals/Computed.d.ts +13 -11
  48. package/dist/signals/signals/Computed.js +79 -26
  49. package/dist/signals/signals/Effect.d.ts +11 -7
  50. package/dist/signals/signals/Effect.js +60 -58
  51. package/dist/signals/signals/LocalSignal.d.ts +14 -7
  52. package/dist/signals/signals/LocalSignal.js +53 -36
  53. package/dist/signals/signals/Signal.d.ts +13 -37
  54. package/dist/signals/signals/Signal.js +44 -58
  55. package/dist/signals/types/index.d.ts +1 -0
  56. package/dist/signals/types/index.js +1 -0
  57. package/dist/signals/types/signals.types.d.ts +16 -0
  58. package/docs/CHANGELOG.md +32 -0
  59. package/docs/devtools/README.md +162 -29
  60. package/docs/migrations/0.5.0.md +73 -0
  61. package/docs/options/README.md +89 -0
  62. package/docs/query/README.md +425 -89
  63. package/docs/release/README.md +58 -6
  64. package/docs/signals/README.md +207 -34
  65. package/docs/usage/react/README.md +261 -49
  66. package/package.json +1 -1
  67. package/dist/query/api/cleanAllQueriesCache.d.ts +0 -1
  68. package/dist/query/api/cleanAllQueriesCache.js +0 -4
  69. package/dist/query/core/CleanAllQueriesSignal.js +0 -11
  70. package/dist/signals/base/Tracker.d.ts +0 -10
  71. package/dist/signals/base/Tracker.js +0 -7
  72. package/dist/signals/base/types.d.ts +0 -23
  73. package/dist/signals/operators/filterUpdates.d.ts +0 -5
  74. package/dist/signals/operators/filterUpdates.js +0 -18
  75. package/dist/signals/operators/mapSignals.d.ts +0 -3
  76. package/dist/signals/operators/mapSignals.js +0 -10
  77. /package/dist/signals/{base/types.js → types/signals.types.js} +0 -0
@@ -1,6 +1,6 @@
1
1
  # RxQuery
2
2
 
3
- RxQuery - система для управления запросами и кэшированием данных в RxToolkit. Она состоит из двух основных компонентов: **Resources** и **Operations**.
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
- Ref - это абстракция, для взаимодействия с ресурсом.
41
+
42
+ Ref — это абстракция для взаимодействия с элементом кэша ресурса напрямую.
43
43
 
44
44
  **Особенности:**
45
- - Операции использует ref под капотом, чтобы управлять связанным ресурсом.
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
- async queryFn(args, tools) {
58
- const response = await fetch(`/api/users/${args.id}`, {
59
- signal: tools.abortSignal
60
- });
61
- return response.json();
62
- },
63
- select: (data) => ({
64
- id: data.id,
65
- name: data.name,
66
- email: data.email
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
- - `queryFn(args, tools)` — функция выполнения запроса
73
- - `select(data)` опциональная функция трансформации данных
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
- optimisticUpdate(draft, args) {
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
- - `queryFn(args)` — функция выполнения операции
106
- - `link(ref)` опциональная функция связывания с ресурсами
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
- export type LinkOptions<D extends OperationDefinition, RD extends ResourceDefinition> = {
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
- * @optional @default false
177
+ * Инвалидация кэша после выполнения операции.
178
+ * При true кэш будет очищен и ресурс перезагрузится
179
+ * @default false
133
180
  */
134
181
  invalidate?: boolean;
135
182
 
136
183
  /**
137
- * Флаг для блокировки ресурса во время выполнения операции.
138
- * При true - ресурс будет заблокирован и не сможет выполнять новые запросы
139
- * @optional @default false
184
+ * Блокировка ресурса во время выполнения операции.
185
+ * При true ресурс не сможет выполнять новые запросы
186
+ * @default false
140
187
  */
141
188
  lock?: boolean;
142
189
 
143
190
  /**
144
- * Функция для обновления кэша ресурса после успешного выполнения операции.
145
- * Получает draft объект для мутации, аргументы операции и результат операции
146
- * @optional
191
+ * Обновление кэша ПОСЛЕ успешного выполнения операции.
192
+ * Использует Immer для иммутабельных обновлений
147
193
  */
148
194
  update?: (tools: {
149
- /** Immer draft объект для мутации кэша ресурса */
150
- draft: RD["Data"];
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
- /** Immer draft объект для мутации кэша ресурса */
164
- draft: RD["Data"];
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
- /** Успешно ли завершен последний запрос (false по умолчанию) */
272
+
273
+ /** Успешно ли завершен последний запрос */
200
274
  isSuccess: boolean;
201
- /** Произошла ли ошибка последнего запроса (false по умолчанию) */
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
- /** Данные, полученные в результате запроса (или select данных) */
284
+
285
+ /** Данные (или select данных) */
210
286
  data: D["Data"] | undefined;
211
- /** Аргументы запроса */
212
- args: D["Args"] | undefined; // TODO undefined - это костыль для сведения типов, его быть не должно
287
+
288
+ /** Аргументы последнего запроса */
289
+ args: D["Args"] | undefined;
213
290
  }
291
+ ```
292
+
293
+ ### OperationQueryState
214
294
 
215
- /**
216
- * Состояние выполнения операции
217
- */
218
- export type OperationQueryState<D extends OperationDefinition> = {
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
+