@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
@@ -0,0 +1,280 @@
1
+ # RxQuery v2 (**experimental**)
2
+
3
+ > ⚠️ **Экспериментальный модуль.** API может измениться без предупреждения. Используйте с осторожностью в production.
4
+
5
+ RxQuery v2 — переработанная система управления асинхронными запросами и кэшированием данных в RxToolkit. В отличие от v1, v2 строится вокруг единой фабрики `createApi`, machine-based состояний и плагинной архитектуры.
6
+
7
+ ## Основные концепции
8
+
9
+ ### createApi
10
+
11
+ Точка входа для создания API-инстанса. Все ресурсы создаются через API, что обеспечивает общую конфигурацию, snapshot-поддержку и плагины.
12
+
13
+ ```typescript
14
+ import { unstable_queryV2 } from '@fozy-labs/rx-toolkit';
15
+
16
+ const api = unstable_queryV2.createApi({
17
+ keyPrefix: 'my-app',
18
+ cacheLifetime: 60_000,
19
+ plugins: [new unstable_queryV2.ReactHooksPlugin()],
20
+ });
21
+ ```
22
+
23
+ **Параметры:**
24
+
25
+ | Параметр | Тип | По умолчанию | Описание |
26
+ |----------|-----|-------------|----------|
27
+ | `keyPrefix` | `string \| null` | `null` | Префикс ключей для namespace-изоляции |
28
+ | `keyStrategy` | `'serialize' \| 'compare'` | `'serialize'` | Стратегия ключей кэша |
29
+ | `serializeArgs` | `(args) => string` | — | Кастомная сериализация аргументов |
30
+ | `compareArg` | `(a, b) => boolean` | — | Кастомное сравнение аргументов |
31
+ | `cacheLifetime` | `number` | `60000` | Время жизни кэша (мс) |
32
+ | `plugins` | `IPlugin[]` | `[]` | Массив плагинов |
33
+ | `initialSnapshot` | `TApiSnapshot \| null` | `null` | Начальный snapshot для SSR-гидрации |
34
+ | `maxSnapshotDataAge` | `number` | `300000` | Максимальный возраст данных snapshot (мс) |
35
+ | `doCacheArgs` | `boolean` | `false` | Кэшировать ли аргументы в кэш-записи |
36
+
37
+ ### ResourceV2
38
+
39
+ Ресурс — единица кэширования. Создаётся через `api.createResource()`. В отличие от v1, ресурс всегда принадлежит API-инстансу.
40
+
41
+ ```typescript
42
+ interface User {
43
+ id: string;
44
+ name: string;
45
+ }
46
+
47
+ const userResource = api.createResource<{ id: string }, User>({
48
+ key: 'users',
49
+ queryFn: async (args, { abortSignal }) => {
50
+ const res = await fetch(`/api/users/${args.id}`, { signal: abortSignal });
51
+ return res.json();
52
+ },
53
+ cacheLifetime: 30_000,
54
+ });
55
+ ```
56
+
57
+ **Параметры `createResource`:**
58
+
59
+ | Параметр | Тип | Описание |
60
+ |----------|-----|----------|
61
+ | `key` | `string` | Уникальный ключ ресурса (обязателен для SSR) |
62
+ | `queryFn` | `(args, tools) => Promise<TData>` | Функция запроса |
63
+ | `cacheLifetime` | `number` | Время жизни кэша (переопределяет API-уровень) |
64
+ | `serializeArgs` | `(args) => string` | Кастомная сериализация (переопределяет API-уровень) |
65
+ | `compareArg` | `(a, b) => boolean` | Кастомное сравнение (переопределяет API-уровень) |
66
+ | `onCacheEntryAdded` | `(args, tools) => void` | Хук при добавлении записи в кэш |
67
+ | `onQueryStarted` | `(args, tools) => void` | Хук при старте запроса |
68
+ | `beforeDevtoolsPush` | `(value, push) => void` | Перехват состояния перед отправкой в devtools |
69
+ | `maxSnapshotDataAge` | `number` | Возраст данных для snapshot |
70
+ | `doCacheArgs` | `boolean` | Кэшировать ли аргументы |
71
+
72
+ ### Agents (Агенты)
73
+
74
+ Agent — наблюдатель за ресурсом с поддержкой stale-while-revalidate. Предоставляет вычисляемое состояние с удобными флагами.
75
+
76
+ ```typescript
77
+ const agent = userResource.createAgent();
78
+ agent.start({ id: '1' });
79
+
80
+ // Реактивное состояние
81
+ const state = agent.state$();
82
+ console.log(state.status); // 'pending' | 'success' | ...
83
+ console.log(state.data); // TData | null
84
+ console.log(state.isLoading); // true/false
85
+ ```
86
+
87
+ **Состояние агента (`IResourceV2AgentState`):**
88
+
89
+ | Поле | Тип | Описание |
90
+ |------|-----|----------|
91
+ | `status` | `TMachineStatus` | Текущий статус машины |
92
+ | `data` | `TData \| null` | Текущие данные (могут быть устаревшими при обновлении) |
93
+ | `error` | `TError \| null` | Текущая ошибка |
94
+ | `args` | `TArgs \| null` | Текущие аргументы |
95
+ | `isLoading` | `boolean` | Индикатор загрузки |
96
+ | `isInitialLoading` | `boolean` | `true` только при первой загрузке (нет предыдущих данных) |
97
+ | `isRefreshing` | `boolean` | `true` при обновлении существующих данных |
98
+ | `isSuccess` | `boolean` | `true` когда данные доступны |
99
+ | `isError` | `boolean` | `true` при ошибке |
100
+ | `refreshError` | `TError \| null` | Ошибка фонового обновления (устаревшие данные сохранены) |
101
+
102
+ ### Machine States (Машина состояний)
103
+
104
+ В v2 состояние кэш-записи описывается машиной состояний вместо набора boolean-флагов:
105
+
106
+ | Состояние | Описание | Данные |
107
+ |-----------|----------|--------|
108
+ | `idle` | Начальное состояние | — |
109
+ | `pending` | Первый запрос выполняется | — |
110
+ | `success` | Данные загружены | ✅ |
111
+ | `error` | Запрос завершился с ошибкой | — |
112
+ | `refreshing` | Обновление при наличии данных | ✅ (устаревшие) |
113
+
114
+ Классы машин: `MachineIdle`, `MachinePending`, `MachineSuccess`, `MachineError`, `MachineRefreshing`.
115
+
116
+ ### Cache Strategies (Стратегии кэша)
117
+
118
+ Доступны две стратегии определения ключей кэша:
119
+
120
+ - **`serialize`** (по умолчанию) — аргументы сериализуются в строку (поддерживает SSR snapshots)
121
+ - **`compare`** — аргументы сравниваются функцией (не поддерживает snapshots)
122
+
123
+ ### SKIP_TOKEN
124
+
125
+ Специальный токен для пропуска запроса (полезен при условных запросах).
126
+
127
+ ```typescript
128
+ import { unstable_queryV2 } from '@fozy-labs/rx-toolkit';
129
+
130
+ const { SKIP } = unstable_queryV2;
131
+
132
+ // В React с плагином
133
+ const state = userResource.useResourceV2Agent(
134
+ userId ? { id: userId } : SKIP,
135
+ );
136
+ ```
137
+
138
+ ### Lifecycle Hooks (Хуки жизненного цикла)
139
+
140
+ #### onCacheEntryAdded
141
+
142
+ Вызывается при создании новой записи кэша. Полезен для WebSocket-подписок.
143
+
144
+ ```typescript
145
+ const resource = api.createResource({
146
+ key: 'messages',
147
+ queryFn: fetchMessages,
148
+ onCacheEntryAdded: async (args, { $cacheDataLoaded, $cacheEntryRemoved }) => {
149
+ await $cacheDataLoaded;
150
+ const ws = new WebSocket(`/ws/messages/${args.channelId}`);
151
+ await $cacheEntryRemoved;
152
+ ws.close();
153
+ },
154
+ });
155
+ ```
156
+
157
+ **Tools (onCacheEntryAdded):**
158
+
159
+ | Поле | Тип | Описание |
160
+ |------|-----|----------|
161
+ | `$cacheDataLoaded` | `Promise<TData>` | Разрешается при первом `MachineSuccess` |
162
+ | `$cacheEntryRemoved` | `Promise<void>` | Разрешается при удалении записи из кэша |
163
+ | `getCacheEntry()` | `() => TMachine` | Текущее состояние машины |
164
+
165
+ #### onQueryStarted
166
+
167
+ Вызывается при каждом старте запроса. Полезен для оптимистичных обновлений.
168
+
169
+ ```typescript
170
+ const resource = api.createResource({
171
+ key: 'todos',
172
+ queryFn: updateTodo,
173
+ onQueryStarted: async (args, { $queryFulfilled, getCacheEntry }) => {
174
+ // Оптимистичное обновление
175
+ const entry = getCacheEntry();
176
+ // ...
177
+ try {
178
+ await $queryFulfilled;
179
+ } catch {
180
+ // Откат
181
+ }
182
+ },
183
+ });
184
+ ```
185
+
186
+ **Tools (onQueryStarted):**
187
+
188
+ | Поле | Тип | Описание |
189
+ |------|-----|----------|
190
+ | `$queryFulfilled` | `Promise<{ data, isError }>` | Разрешается/отклоняется при завершении запроса |
191
+ | `getCacheEntry()` | `() => ICacheEntry` | Текущая запись кэша для патчинга |
192
+
193
+ ### Plugins (Плагины)
194
+
195
+ Плагинная архитектура позволяет расширять ресурсы дополнительными методами.
196
+
197
+ #### ReactHooksPlugin
198
+
199
+ Добавляет React-хуки к ресурсам:
200
+
201
+ ```typescript
202
+ const api = unstable_queryV2.createApi({
203
+ plugins: [new unstable_queryV2.ReactHooksPlugin()],
204
+ });
205
+
206
+ const userResource = api.createResource({ /* ... */ });
207
+
208
+ // Теперь доступны хуки:
209
+ function UserProfile({ userId }: { userId: string }) {
210
+ const state = userResource.useResourceV2Agent({ id: userId });
211
+ const ref = userResource.useResourceV2Ref({ id: userId });
212
+
213
+ if (state.isLoading) return <div>Загрузка...</div>;
214
+ if (state.isError) return <div>Ошибка</div>;
215
+
216
+ return <div>{state.data?.name}</div>;
217
+ }
218
+ ```
219
+
220
+ **Методы, добавляемые плагином:**
221
+
222
+ | Метод | Описание |
223
+ |-------|----------|
224
+ | `useResourceV2Agent(args)` | React-хук агента (реактивное состояние) |
225
+ | `useResourceV2Ref(args)` | React-хук ref (императивный доступ к кэш-записи) |
226
+
227
+ ### ResourceV2Ref (Ссылка на ресурс)
228
+
229
+ Ref предоставляет императивный доступ к конкретной записи кэша.
230
+
231
+ | Метод | Описание |
232
+ |-------|----------|
233
+ | `has` | Проверить наличие записи в кэше |
234
+ | `lock()` | Заблокировать запись (предотвратить удаление) |
235
+ | `invalidate()` | Инвалидировать (принудительный перезапрос) |
236
+ | `createPatch(fn)` | Создать оптимистичный патч |
237
+ | `create(data)` | Предзаполнить кэш данными |
238
+
239
+ ---
240
+
241
+ ## Быстрый старт
242
+
243
+ ```typescript
244
+ import { unstable_queryV2 } from '@fozy-labs/rx-toolkit';
245
+
246
+ // 1. Создаём API
247
+ const api = unstable_queryV2.createApi({
248
+ plugins: [new unstable_queryV2.ReactHooksPlugin()],
249
+ });
250
+
251
+ // 2. Создаём ресурс
252
+ const todoResource = api.createResource<void, { items: string[] }>({
253
+ key: 'todos',
254
+ queryFn: async (_args, { abortSignal }) => {
255
+ const res = await fetch('/api/todos', { signal: abortSignal });
256
+ return res.json();
257
+ },
258
+ });
259
+
260
+ // 3. Используем в React
261
+ function TodoList() {
262
+ const { data, isLoading, isError } = todoResource.useResourceV2Agent(undefined);
263
+
264
+ if (isLoading) return <div>Загрузка...</div>;
265
+ if (isError) return <div>Ошибка</div>;
266
+
267
+ return (
268
+ <ul>
269
+ {data?.items.map((item, i) => <li key={i}>{item}</li>)}
270
+ </ul>
271
+ );
272
+ }
273
+ ```
274
+
275
+ ## Дополнительные материалы
276
+
277
+ - [API Reference](./api-reference.md) — полная таблица типов и параметров
278
+ - [Оптимистичные обновления](./optimistic-updates.md) — гайд по патчам
279
+ - [SSR](./ssr.md) — серверный рендеринг и snapshots
280
+ - [Миграция с v1](../migrations/query-v2.md) — гайд по миграции
@@ -0,0 +1,235 @@
1
+ # API Reference — RxQuery v2
2
+
3
+ > ⚠️ **Экспериментальный модуль.** API может измениться.
4
+
5
+ ## createApi
6
+
7
+ Фабрика для создания API-инстанса.
8
+
9
+ ```typescript
10
+ function createApi<TPlugins extends IPlugin[] = []>(
11
+ options?: ICreateApiOptions<TPlugins>,
12
+ ): IApi<TPlugins>;
13
+ ```
14
+
15
+ ### ICreateApiOptions
16
+
17
+ | Параметр | Тип | По умолчанию | Описание |
18
+ |----------|-----|-------------|----------|
19
+ | `keyPrefix` | `string \| null` | `null` | Префикс ключей для namespace-изоляции |
20
+ | `keyStrategy` | `'serialize' \| 'compare'` | `'serialize'` | Стратегия ключей кэша |
21
+ | `serializeArgs` | `TSerializeArgsFn` | — | Кастомная сериализация аргументов |
22
+ | `compareArg` | `TCompareArgsFn` | — | Кастомное сравнение аргументов |
23
+ | `cacheLifetime` | `number` | `60000` | Время жизни кэша (мс) |
24
+ | `plugins` | `IPlugin[]` | `[]` | Массив плагинов |
25
+ | `initialSnapshot` | `TApiSnapshot \| null` | `null` | Начальный snapshot (SSR) |
26
+ | `maxSnapshotDataAge` | `number` | `300000` | Макс. возраст snapshot-данных (мс) |
27
+ | `doCacheArgs` | `boolean` | `false` | Кэшировать ли аргументы в кэш-записи |
28
+
29
+ ### IApi
30
+
31
+ | Метод | Сигнатура | Описание |
32
+ |-------|-----------|----------|
33
+ | `createResource` | `<TArgs, TData, TError>(options: IResourceV2Options) => IResourceV2 & PluginAugmentations` | Создать ресурс |
34
+ | `resetAll` | `() => void` | Сбросить все ресурсы |
35
+ | `getSnapshot` | `() => TApiSnapshot` | Получить snapshot для SSR |
36
+
37
+ ---
38
+
39
+ ## api.createResource
40
+
41
+ Создаёт ресурс, привязанный к API-инстансу.
42
+
43
+ ### IResourceV2Options
44
+
45
+ | Параметр | Тип | Описание |
46
+ |----------|-----|----------|
47
+ | `key` | `string` | Уникальный ключ ресурса (обязателен для SSR snapshots) |
48
+ | `queryFn` | `(args: TArgs, tools: TQueryFnTools) => Promise<TData>` | Функция запроса |
49
+ | `cacheLifetime` | `number` | Время жизни кэша (мс), переопределяет API-уровень |
50
+ | `serializeArgs` | `TSerializeArgsFn` | Кастомная сериализация (переопределяет API-уровень) |
51
+ | `compareArg` | `TCompareArgsFn` | Кастомное сравнение (переопределяет API-уровень) |
52
+ | `onCacheEntryAdded` | `TOnCacheEntryAdded<TArgs, TData>` | Хук при добавлении записи в кэш |
53
+ | `onQueryStarted` | `TOnQueryStarted<TArgs, TData>` | Хук при старте запроса |
54
+ | `beforeDevtoolsPush` | `TBeforeDevtoolsPushFn` | Перехват перед devtools |
55
+ | `maxSnapshotDataAge` | `number` | Возраст данных для snapshot (мс) |
56
+ | `doCacheArgs` | `boolean` | Кэшировать ли аргументы |
57
+
58
+ ### TQueryFnTools
59
+
60
+ | Поле | Тип | Описание |
61
+ |------|-----|----------|
62
+ | `abortSignal` | `AbortSignal` | Сигнал для отмены запроса |
63
+
64
+ ---
65
+
66
+ ## IResourceV2
67
+
68
+ Интерфейс ресурса (публичный API).
69
+
70
+ | Метод | Сигнатура | Описание |
71
+ |-------|-----------|----------|
72
+ | `createAgent` | `() => IResourceV2Agent` | Создать агента (observer + SWR) |
73
+ | `query` | `(args, doForce?) => Promise<ICacheEntry>` | Выполнить запрос |
74
+ | `query$` | `(args, doForce?) => TMachine` | Реактивный запрос (signal read) |
75
+ | `entry` | `(args, doInitiate?) => ICacheEntry \| null` | Получить запись кэша (нереактивно) |
76
+ | `entry$` | `(args, doInitiate?) => TMachine` | Получить запись кэша (реактивно) |
77
+ | `invalidate` | `(args) => void` | Инвалидировать запись |
78
+ | `compareArgs` | `(a, b) => boolean` | Сравнить аргументы |
79
+
80
+ ---
81
+
82
+ ## IResourceV2Agent
83
+
84
+ Агент — наблюдатель за ресурсом с stale-while-revalidate.
85
+
86
+ | Поле/Метод | Сигнатура | Описание |
87
+ |------------|-----------|----------|
88
+ | `state$` | `() => IResourceV2AgentState` | Реактивное состояние (computed signal) |
89
+ | `start` | `(args: TArgs \| SKIP_TOKEN) => Promise<void>` | Начать запрос с новыми аргументами |
90
+ | `compareArgs` | `(a, b) => boolean` | Сравнить аргументы |
91
+
92
+ ### IResourceV2AgentState
93
+
94
+ | Поле | Тип | Описание |
95
+ |------|-----|----------|
96
+ | `status` | `TMachineStatus` | Текущий статус: `'idle'` \| `'pending'` \| `'success'` \| `'error'` \| `'refreshing'` |
97
+ | `data` | `TData \| null` | Текущие данные |
98
+ | `error` | `TError \| null` | Текущая ошибка |
99
+ | `args` | `TArgs \| null` | Текущие аргументы |
100
+ | `isLoading` | `boolean` | Загрузка (pending или refreshing) |
101
+ | `isInitialLoading` | `boolean` | Первая загрузка (нет предыдущих данных) |
102
+ | `isRefreshing` | `boolean` | Обновление существующих данных |
103
+ | `isSuccess` | `boolean` | Данные доступны |
104
+ | `isError` | `boolean` | Ошибка |
105
+ | `refreshError` | `TError \| null` | Ошибка фонового обновления |
106
+
107
+ ---
108
+
109
+ ## IResourceV2Ref
110
+
111
+ Императивный доступ к записи кэша.
112
+
113
+ | Поле/Метод | Сигнатура | Описание |
114
+ |------------|-----------|----------|
115
+ | `has` | `boolean` (readonly) | Наличие записи в кэше |
116
+ | `lock` | `() => { unlock: () => void }` | Заблокировать запись |
117
+ | `invalidate` | `() => void` | Инвалидировать |
118
+ | `createPatch` | `(patchFn: TPatchFn<TData>) => { commit, abort } \| null` | Создать оптимистичный патч |
119
+ | `create` | `(data: TData) => void` | Предзаполнить кэш |
120
+
121
+ ---
122
+
123
+ ## Machine Classes
124
+
125
+ Классы машины состояний, определяющие текущее состояние кэш-записи.
126
+
127
+ | Класс | Статус | Данные | Ошибка | Описание |
128
+ |-------|--------|--------|--------|----------|
129
+ | `MachineIdle` | `'idle'` | `null` | `null` | Начальное состояние |
130
+ | `MachinePending` | `'pending'` | `null` | `null` | Запрос выполняется |
131
+ | `MachineSuccess` | `'success'` | `TData` | `null` | Данные загружены |
132
+ | `MachineError` | `'error'` | `null` | `TError` | Ошибка запроса |
133
+ | `MachineRefreshing` | `'refreshing'` | `TData` | `null` | Обновление с данными |
134
+
135
+ ### TMachineStatus
136
+
137
+ ```typescript
138
+ type TMachineStatus = 'idle' | 'pending' | 'success' | 'error' | 'refreshing';
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Lifecycle Hooks
144
+
145
+ ### TOnCacheEntryAdded
146
+
147
+ ```typescript
148
+ type TOnCacheEntryAdded<TArgs, TData> = (
149
+ args: TArgs,
150
+ tools: TCacheEntryAddedTools<TData>,
151
+ ) => void | Promise<void>;
152
+ ```
153
+
154
+ #### TCacheEntryAddedTools
155
+
156
+ | Поле | Тип | Описание |
157
+ |------|-----|----------|
158
+ | `$cacheDataLoaded` | `Promise<TData>` | Разрешается при первом `MachineSuccess` |
159
+ | `$cacheEntryRemoved` | `Promise<void>` | Разрешается при удалении записи |
160
+ | `getCacheEntry` | `() => TMachine` | Текущее состояние машины |
161
+
162
+ ### TOnQueryStarted
163
+
164
+ ```typescript
165
+ type TOnQueryStarted<TArgs, TData> = (
166
+ args: TArgs,
167
+ tools: TQueryStartedTools<TData>,
168
+ ) => void | Promise<void>;
169
+ ```
170
+
171
+ #### TQueryStartedTools
172
+
173
+ | Поле | Тип | Описание |
174
+ |------|-----|----------|
175
+ | `$queryFulfilled` | `Promise<{ data: TData; isError: false }>` | Разрешается при завершении запроса |
176
+ | `getCacheEntry` | `() => ICacheEntry` | Запись кэша для патчинга |
177
+
178
+ ---
179
+
180
+ ## Plugins
181
+
182
+ ### IPlugin
183
+
184
+ ```typescript
185
+ interface IPlugin {
186
+ readonly name: string;
187
+ install(context: IPluginContext): void;
188
+ augmentResource<TArgs, TData, TError>(
189
+ resource: IResourceV2<TArgs, TData, TError>,
190
+ options: IResourceV2Options<TArgs, TData, TError>,
191
+ ): Record<string, unknown>;
192
+ }
193
+ ```
194
+
195
+ ### ReactHooksPlugin
196
+
197
+ Добавляет React-хуки к ресурсам:
198
+
199
+ | Метод | Сигнатура | Описание |
200
+ |-------|-----------|----------|
201
+ | `useResourceV2Agent` | `(args: TArgs \| SKIP_TOKEN) => IResourceV2AgentState` | React-хук агента |
202
+ | `useResourceV2Ref` | `(args: TArgs \| SKIP_TOKEN) => IResourceV2Ref` | React-хук ref |
203
+
204
+ > **Standalone-импорт:** Хуки `useResourceV2Agent` и `useResourceV2Ref` доступны как отдельные функции без `ReactHooksPlugin`:
205
+ > ```typescript
206
+ > import { useResourceV2Agent } from '@fozy-labs/rx-toolkit/query-v2/react';
207
+ > const state = useResourceV2Agent(resource, args);
208
+ > ```
209
+
210
+ ---
211
+
212
+ ## Snapshot Types
213
+
214
+ ### TApiSnapshot
215
+
216
+ | Поле | Тип | Описание |
217
+ |------|-----|----------|
218
+ | `version` | `number` | Версия формата |
219
+ | `keyPrefix` | `string \| null` | Префикс ключей API |
220
+ | `resources` | `Record<string, TResourceSnapshot>` | Snapshots ресурсов |
221
+
222
+ ### TResourceSnapshot
223
+
224
+ | Поле | Тип | Описание |
225
+ |------|-----|----------|
226
+ | `entries` | `Record<string, TResourceV2SnapshotSlice>` | Записи кэша |
227
+
228
+ ### TResourceV2SnapshotSlice
229
+
230
+ | Поле | Тип | Описание |
231
+ |------|-----|----------|
232
+ | `status` | `'success'` | Всегда success (только успешные записи) |
233
+ | `args` | `unknown` | Аргументы запроса |
234
+ | `data` | `TData` | Данные |
235
+ | `updatedAt` | `number` | Timestamp обновления |
@@ -0,0 +1,148 @@
1
+ # Оптимистичные обновления — RxQuery v2
2
+
3
+ > ⚠️ **Экспериментальный модуль.** API может измениться.
4
+
5
+ Оптимистичные обновления позволяют мгновенно отобразить результат действия пользователя, не дожидаясь ответа сервера. В v2 это реализуется через механизм патчей (`createPatch` / `finishPatch`).
6
+
7
+ ## Основы
8
+
9
+ В RxQuery v2 каждая запись кэша поддерживает **очередь патчей** — Immer-based мутаций, которые накладываются поверх оригинальных данных. Патч можно подтвердить (`commit`) или откатить (`abort`).
10
+
11
+ ## Использование через Ref
12
+
13
+ Самый прямой способ — использовать `IResourceV2Ref`:
14
+
15
+ ```typescript
16
+ import { unstable_queryV2 } from '@fozy-labs/rx-toolkit';
17
+
18
+ const api = unstable_queryV2.createApi({
19
+ plugins: [new unstable_queryV2.ReactHooksPlugin()],
20
+ });
21
+
22
+ interface Todo {
23
+ id: number;
24
+ text: string;
25
+ completed: boolean;
26
+ }
27
+
28
+ const todosResource = api.createResource<void, { items: Todo[] }>({
29
+ key: 'todos',
30
+ queryFn: async (_args, { abortSignal }) => {
31
+ const res = await fetch('/api/todos', { signal: abortSignal });
32
+ return res.json();
33
+ },
34
+ });
35
+ ```
36
+
37
+ ### createPatch
38
+
39
+ Создаёт оптимистичный патч. Принимает функцию-рецепт Immer — мутации `draft` применяются мгновенно к отображаемым данным.
40
+
41
+ ```typescript
42
+ function ToggleTodo({ todo }: { todo: Todo }) {
43
+ const ref = todosResource.useResourceV2Ref(undefined);
44
+
45
+ const handleToggle = async () => {
46
+ // 1. Создаём оптимистичный патч
47
+ const patch = ref.createPatch((draft) => {
48
+ const item = draft.items.find(i => i.id === todo.id);
49
+ if (item) item.completed = !item.completed;
50
+ });
51
+ if (!patch) return;
52
+
53
+ try {
54
+ // 2. Отправляем на сервер
55
+ await fetch(`/api/todos/${todo.id}`, {
56
+ method: 'PATCH',
57
+ headers: { 'Content-Type': 'application/json' },
58
+ body: JSON.stringify({ completed: !todo.completed }),
59
+ });
60
+
61
+ // 3. Подтверждаем патч (данные остаются)
62
+ patch.commit();
63
+ } catch {
64
+ // 4. Откатываем при ошибке (данные возвращаются к оригиналу)
65
+ patch.abort();
66
+ }
67
+ };
68
+
69
+ return <button onClick={handleToggle}>{todo.text}</button>;
70
+ }
71
+ ```
72
+
73
+ ### Жизненный цикл патча
74
+
75
+ 1. **`createPatch(fn)`** — применяет Immer-рецепт к данным. UI мгновенно обновляется.
76
+ 2. **`commit()`** — подтверждает патч. Оригинальные данные обновляются.
77
+ 3. **`abort()`** — откатывает патч. Данные возвращаются к предыдущему состоянию.
78
+
79
+ ### Несколько патчей
80
+
81
+ Патчи накладываются в порядке создания. Каждый патч можно подтвердить или откатить независимо:
82
+
83
+ ```typescript
84
+ const patch1 = ref.createPatch(draft => { draft.items[0].text = 'Изменено 1'; });
85
+ const patch2 = ref.createPatch(draft => { draft.items[1].text = 'Изменено 2'; });
86
+
87
+ // Подтвердить первый, откатить второй
88
+ patch1?.commit();
89
+ patch2?.abort();
90
+ ```
91
+
92
+ ### Патч при отсутствии данных
93
+
94
+ Если запись кэша не содержит данных (статус не `success` / `refreshing`), `createPatch` вернёт `null`:
95
+
96
+ ```typescript
97
+ const patch = ref.createPatch(draft => { /* ... */ });
98
+ if (!patch) {
99
+ console.warn('Нет данных для патча');
100
+ return;
101
+ }
102
+ ```
103
+
104
+ ## Паттерн: оптимистичное обновление в компоненте
105
+
106
+ ```tsx
107
+ function TodoList() {
108
+ const state = todosResource.useResourceV2Agent(undefined);
109
+ const ref = todosResource.useResourceV2Ref(undefined);
110
+
111
+ const toggleTodo = async (todo: Todo) => {
112
+ const patch = ref.createPatch((draft) => {
113
+ const item = draft.items.find(i => i.id === todo.id);
114
+ if (item) item.completed = !item.completed;
115
+ });
116
+ if (!patch) return;
117
+
118
+ try {
119
+ await updateTodoOnServer(todo.id, { completed: !todo.completed });
120
+ patch.commit();
121
+ } catch {
122
+ patch.abort();
123
+ }
124
+ };
125
+
126
+ if (state.isLoading) return <div>Загрузка...</div>;
127
+ if (!state.data) return null;
128
+
129
+ return (
130
+ <ul>
131
+ {state.data.items.map(todo => (
132
+ <li key={todo.id} onClick={() => toggleTodo(todo)}>
133
+ {todo.completed ? '✅' : '⬜'} {todo.text}
134
+ </li>
135
+ ))}
136
+ </ul>
137
+ );
138
+ }
139
+ ```
140
+
141
+ ## Сравнение с v1
142
+
143
+ | Аспект | v1 | v2 |
144
+ |--------|----|----|
145
+ | Механизм | `resourceRef.patch()` + Command link | `ref.createPatch()` → `commit/abort` |
146
+ | Привязка | Через `link()` в Command | Напрямую через Ref |
147
+ | Отмена | Автоматическая через Command | Явный `abort()` |
148
+ | Множественные патчи | Не поддерживаются | Очередь патчей |