@fozy-labs/rx-toolkit 0.5.3-rc.2 → 0.5.3

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