@fozy-labs/rx-toolkit 0.5.3-rc.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +143 -137
- package/dist/common/devtools/combineDevtools.js +3 -3
- package/dist/common/devtools/index.d.ts +3 -3
- package/dist/common/devtools/index.js +3 -3
- package/dist/common/devtools/reduxDevtools.d.ts +1 -1
- package/dist/common/devtools/reduxDevtools.js +17 -17
- package/dist/common/devtools/types.d.ts +0 -6
- package/dist/common/options/SharedOptions.d.ts +1 -0
- package/dist/common/options/SharedOptions.js +6 -0
- package/dist/common/options/index.d.ts +1 -1
- package/dist/common/options/index.js +1 -1
- package/dist/common/react/index.d.ts +2 -2
- package/dist/common/react/index.js +2 -2
- package/dist/common/react/useConstant.js +1 -1
- package/dist/common/utils/deepEqual.js +1 -1
- package/dist/common/utils/index.d.ts +3 -3
- package/dist/common/utils/index.js +3 -3
- package/dist/common/utils/shallowEqual.js +1 -1
- package/dist/index.d.ts +8 -7
- package/dist/index.js +8 -7
- package/dist/query/SKIP_TOKEN.js +1 -1
- package/dist/query/api/createCommand.d.ts +1 -1
- package/dist/query/api/createOperation.d.ts +1 -1
- package/dist/query/api/createOperation.js +1 -1
- package/dist/query/api/createResource.d.ts +1 -1
- package/dist/query/api/createResourceDuplicator.d.ts +1 -1
- package/dist/query/core/Command/Command.d.ts +7 -7
- package/dist/query/core/Command/Command.js +2 -2
- package/dist/query/core/Command/CommandAgent.d.ts +1 -1
- package/dist/query/core/Command/index.d.ts +2 -2
- package/dist/query/core/Command/index.js +2 -2
- package/dist/query/core/{Opertation → Operation}/Operation.d.ts +2 -2
- package/dist/query/core/{Opertation → Operation}/Operation.js +1 -1
- package/dist/query/core/{Opertation → Operation}/OperationAgent.d.ts +1 -1
- package/dist/query/core/{Opertation → Operation}/OperationAgent.js +1 -1
- package/dist/query/core/QueriesCache.d.ts +1 -1
- package/dist/query/core/QueriesCache.js +1 -1
- package/dist/query/core/QueriesLifetimeHooks.js +7 -7
- package/dist/query/core/Resource/Resource.d.ts +15 -15
- package/dist/query/core/Resource/Resource.js +7 -7
- package/dist/query/core/Resource/ResourceAgent.d.ts +1 -1
- package/dist/query/core/Resource/ResourceAgent.js +2 -2
- package/dist/query/core/Resource/ResourceDuplicator.d.ts +16 -16
- package/dist/query/core/Resource/ResourceDuplicator.js +18 -20
- package/dist/query/core/Resource/ResourceDuplicatorAgent.d.ts +5 -5
- package/dist/query/core/Resource/ResourceDuplicatorAgent.js +2 -2
- package/dist/query/core/Resource/ResourceRef.d.ts +2 -2
- package/dist/query/core/Resource/ResourceRef.js +12 -12
- package/dist/query/index.d.ts +11 -10
- package/dist/query/index.js +11 -10
- package/dist/query/lib/IndirectMap.js +4 -4
- package/dist/query/react/useCommandAgent.d.ts +2 -2
- package/dist/query/react/useOperationAgent.d.ts +1 -1
- package/dist/query/react/useOperationAgent.js +1 -1
- package/dist/query/react/useResourceAgent.d.ts +3 -3
- package/dist/query/react/useResourceAgent.js +1 -1
- package/dist/query/react/useResourceRef.d.ts +3 -3
- package/dist/query/react/useResourceRef.js +7 -2
- package/dist/query/types/Command.types.d.ts +1 -1
- package/dist/query/types/Operation.types.d.ts +1 -1
- package/dist/query/types/Resource.types.d.ts +7 -5
- package/dist/query/types/index.d.ts +4 -4
- package/dist/query/types/index.js +4 -4
- package/dist/query-v2/api/createApi.d.ts +10 -0
- package/dist/query-v2/api/createApi.js +83 -0
- package/dist/query-v2/core/common/CacheEntry.d.ts +29 -0
- package/dist/query-v2/core/common/CacheEntry.js +71 -0
- package/dist/query-v2/core/common/CacheMap.d.ts +38 -0
- package/dist/query-v2/core/common/CacheMap.js +127 -0
- package/dist/query-v2/core/common/LifecycleHooks.d.ts +22 -0
- package/dist/query-v2/core/common/LifecycleHooks.js +104 -0
- package/dist/query-v2/core/common/index.d.ts +3 -0
- package/dist/query-v2/core/common/index.js +3 -0
- package/dist/query-v2/core/index.d.ts +3 -0
- package/dist/query-v2/core/index.js +3 -0
- package/dist/query-v2/core/machines/Machine.d.ts +14 -0
- package/dist/query-v2/core/machines/Machine.js +33 -0
- package/dist/query-v2/core/machines/MachineError.d.ts +11 -0
- package/dist/query-v2/core/machines/MachineError.js +26 -0
- package/dist/query-v2/core/machines/MachineIdle.d.ts +8 -0
- package/dist/query-v2/core/machines/MachineIdle.js +19 -0
- package/dist/query-v2/core/machines/MachinePending.d.ts +12 -0
- package/dist/query-v2/core/machines/MachinePending.js +29 -0
- package/dist/query-v2/core/machines/MachineRefreshing.d.ts +14 -0
- package/dist/query-v2/core/machines/MachineRefreshing.js +46 -0
- package/dist/query-v2/core/machines/MachineSuccess.d.ts +16 -0
- package/dist/query-v2/core/machines/MachineSuccess.js +42 -0
- package/dist/query-v2/core/machines/MachineWithData.d.ts +18 -0
- package/dist/query-v2/core/machines/MachineWithData.js +40 -0
- package/dist/query-v2/core/machines/Patcher.d.ts +20 -0
- package/dist/query-v2/core/machines/Patcher.js +104 -0
- package/dist/query-v2/core/machines/index.d.ts +8 -0
- package/dist/query-v2/core/machines/index.js +8 -0
- package/dist/query-v2/core/resource/ResourceV2.d.ts +120 -0
- package/dist/query-v2/core/resource/ResourceV2.js +464 -0
- package/dist/query-v2/core/resource/ResourceV2Agent.d.ts +26 -0
- package/dist/query-v2/core/resource/ResourceV2Agent.js +132 -0
- package/dist/query-v2/core/resource/index.d.ts +2 -0
- package/dist/query-v2/core/resource/index.js +2 -0
- package/dist/query-v2/index.d.ts +11 -0
- package/dist/query-v2/index.js +17 -0
- package/dist/query-v2/lib/NO_VALUE.d.ts +2 -0
- package/dist/query-v2/lib/NO_VALUE.js +1 -0
- package/dist/query-v2/lib/SKIP_TOKEN.d.ts +2 -0
- package/dist/query-v2/lib/SKIP_TOKEN.js +1 -0
- package/dist/query-v2/lib/index.d.ts +4 -0
- package/dist/query-v2/lib/index.js +3 -0
- package/dist/query-v2/lib/stableStringify.d.ts +8 -0
- package/dist/query-v2/lib/stableStringify.js +23 -0
- package/dist/query-v2/plugins/ReactHooksPlugin.d.ts +25 -0
- package/dist/query-v2/plugins/ReactHooksPlugin.js +19 -0
- package/dist/query-v2/plugins/types.d.ts +1 -0
- package/dist/query-v2/plugins/types.js +1 -0
- package/dist/query-v2/react/__tests__/helpers.d.ts +12 -0
- package/dist/query-v2/react/__tests__/helpers.js +33 -0
- package/dist/query-v2/react/index.d.ts +2 -0
- package/dist/query-v2/react/index.js +2 -0
- package/dist/query-v2/react/useResourceV2Agent.d.ts +12 -0
- package/dist/query-v2/react/useResourceV2Agent.js +36 -0
- package/dist/query-v2/react/useResourceV2Ref.d.ts +12 -0
- package/dist/query-v2/react/useResourceV2Ref.js +57 -0
- package/dist/query-v2/snapshot/Snapshot.d.ts +13 -0
- package/dist/query-v2/snapshot/Snapshot.js +76 -0
- package/dist/query-v2/types/agent.types.d.ts +54 -0
- package/dist/query-v2/types/agent.types.js +1 -0
- package/dist/query-v2/types/api.types.d.ts +22 -0
- package/dist/query-v2/types/api.types.js +1 -0
- package/dist/query-v2/types/cache.types.d.ts +37 -0
- package/dist/query-v2/types/cache.types.js +1 -0
- package/dist/query-v2/types/index.d.ts +9 -0
- package/dist/query-v2/types/index.js +9 -0
- package/dist/query-v2/types/lifecycle.types.d.ts +25 -0
- package/dist/query-v2/types/lifecycle.types.js +1 -0
- package/dist/query-v2/types/machine.types.d.ts +67 -0
- package/dist/query-v2/types/machine.types.js +1 -0
- package/dist/query-v2/types/plugin.types.d.ts +38 -0
- package/dist/query-v2/types/plugin.types.js +1 -0
- package/dist/query-v2/types/resource.types.d.ts +35 -0
- package/dist/query-v2/types/resource.types.js +1 -0
- package/dist/query-v2/types/shared.types.d.ts +20 -0
- package/dist/query-v2/types/shared.types.js +1 -0
- package/dist/query-v2/types/snapshot.types.d.ts +21 -0
- package/dist/query-v2/types/snapshot.types.js +1 -0
- package/dist/signals/base/Batcher.js +9 -5
- package/dist/signals/base/ComputeCache.js +3 -3
- package/dist/signals/base/DependencyTracker.js +1 -1
- package/dist/signals/base/Devtools.d.ts +3 -2
- package/dist/signals/base/Devtools.js +54 -27
- package/dist/signals/base/Indexer.js +1 -1
- package/dist/signals/base/ReadonlySignal.js +1 -1
- package/dist/signals/base/SyncObservable.d.ts +1 -2
- package/dist/signals/base/SyncObservable.js +2 -5
- package/dist/signals/base/index.d.ts +6 -6
- package/dist/signals/base/index.js +6 -6
- package/dist/signals/index.d.ts +5 -4
- package/dist/signals/index.js +5 -4
- package/dist/signals/operators/index.d.ts +1 -1
- package/dist/signals/operators/index.js +1 -1
- package/dist/signals/react/index.d.ts +1 -1
- package/dist/signals/react/index.js +1 -1
- package/dist/signals/signals/Computed.d.ts +3 -4
- package/dist/signals/signals/Computed.js +18 -10
- package/dist/signals/signals/Effect.js +2 -1
- package/dist/signals/signals/LocalState.d.ts +3 -4
- package/dist/signals/signals/LocalState.js +8 -8
- package/dist/signals/signals/Signal.d.ts +7 -6
- package/dist/signals/signals/Signal.js +4 -1
- package/dist/signals/signals/State.d.ts +4 -5
- package/dist/signals/signals/State.js +23 -9
- package/dist/signals/signals/index.d.ts +5 -5
- package/dist/signals/signals/index.js +5 -6
- package/dist/signals/types/SignalOptions.d.ts +16 -0
- package/dist/signals/types/SignalOptions.js +1 -0
- package/dist/signals/types/index.d.ts +3 -1
- package/dist/signals/types/index.js +3 -1
- package/dist/signals/types/normalizeSignalOptions.d.ts +2 -0
- package/dist/signals/types/normalizeSignalOptions.js +10 -0
- package/dist/signals/types/signals.types.d.ts +2 -3
- package/docs/CHANGELOG.md +95 -90
- package/docs/CONTRIBUTING.md +230 -0
- package/docs/contributing/ai-assisted-development.md +47 -0
- package/docs/contributing/query-v2/README.md +379 -0
- package/docs/{release → contributing/release}/README.md +59 -59
- package/docs/devtools/README.md +228 -228
- package/docs/migrations/0.5.0.md +58 -58
- package/docs/migrations/query-v2.md +171 -0
- package/docs/options/README.md +92 -92
- package/docs/query/README.md +575 -573
- package/docs/query-v2/README.md +280 -0
- package/docs/query-v2/api-reference.md +235 -0
- package/docs/query-v2/optimistic-updates.md +148 -0
- package/docs/query-v2/ssr.md +130 -0
- package/docs/signals/README.md +300 -300
- package/docs/usage/react/README.md +309 -309
- package/package.json +85 -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
|
+
| Множественные патчи | Не поддерживаются | Очередь патчей |
|