@vira-ui/react 1.1.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +411 -65
- package/dist/index.js +83 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +83 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +51 -50
package/README.md
CHANGED
|
@@ -1,65 +1,411 @@
|
|
|
1
|
-
# @vira-ui/react
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
1
|
+
# @vira-ui/react
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
**React хуки для Vira Reactive Protocol (VRP)**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@vira-ui/react)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](https://www.typescriptlang.org/)
|
|
10
|
+
|
|
11
|
+
**Синхронизация состояния между клиентом и сервером через WebSocket**
|
|
12
|
+
|
|
13
|
+
[Установка](#-установка) • [Быстрый старт](#-быстрый-старт) • [Документация](#-документация)
|
|
14
|
+
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 🎯 Что это?
|
|
20
|
+
|
|
21
|
+
**@vira-ui/react** предоставляет React хуки для работы с Vira Reactive Protocol (VRP) — протоколом для real-time синхронизации состояния между клиентом и сервером через WebSocket.
|
|
22
|
+
|
|
23
|
+
### Основные возможности
|
|
24
|
+
|
|
25
|
+
- ✅ **Автоматическая синхронизация** — состояние обновляется при изменениях на сервере
|
|
26
|
+
- ✅ **Двусторонняя связь** — можно отправлять события и обновления на сервер
|
|
27
|
+
- ✅ **Diff-патчи** — обновляются только изменённые части данных
|
|
28
|
+
- ✅ **Переподключение** — автоматическое восстановление соединения
|
|
29
|
+
- ✅ **TypeScript** — полная типизация
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 📦 Установка
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install @vira-ui/react @vira-ui/core react
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**Требования:**
|
|
40
|
+
- React 18.2.0+
|
|
41
|
+
- `@vira-ui/core` ^1.0.0
|
|
42
|
+
- Сервер с поддержкой Vira Reactive Protocol
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 🚀 Быстрый старт
|
|
47
|
+
|
|
48
|
+
### useViraState
|
|
49
|
+
|
|
50
|
+
Основной хук для синхронизации состояния:
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
import { useViraState } from '@vira-ui/react';
|
|
54
|
+
|
|
55
|
+
interface User {
|
|
56
|
+
id: string;
|
|
57
|
+
name: string;
|
|
58
|
+
email: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function UserProfile({ userId }: { userId: string }) {
|
|
62
|
+
const { data, sendUpdate, sendDiff, isConnected } = useViraState<User>(
|
|
63
|
+
`user:${userId}`,
|
|
64
|
+
{
|
|
65
|
+
initial: { id: userId, name: 'Guest', email: '' },
|
|
66
|
+
onOpen: () => console.log('Connected'),
|
|
67
|
+
deepMerge: true
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (!isConnected) return <div>Connecting...</div>;
|
|
72
|
+
if (!data) return <div>Loading...</div>;
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div>
|
|
76
|
+
<h1>{data.name}</h1>
|
|
77
|
+
<p>{data.email}</p>
|
|
78
|
+
<button onClick={() => sendDiff({ name: 'New Name' })}>
|
|
79
|
+
Update Name
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 📚 Документация
|
|
89
|
+
|
|
90
|
+
### useViraState
|
|
91
|
+
|
|
92
|
+
Подключается к VRP каналу и синхронизирует состояние.
|
|
93
|
+
|
|
94
|
+
**Параметры:**
|
|
95
|
+
- `channel` — имя канала (например: `"user:123"`, `"tasks"`)
|
|
96
|
+
- `options` — опции конфигурации
|
|
97
|
+
|
|
98
|
+
**Возвращает:**
|
|
99
|
+
- `data` — текущее состояние
|
|
100
|
+
- `sendUpdate(payload)` — полная замена состояния
|
|
101
|
+
- `sendDiff(patch)` — частичное обновление (merge)
|
|
102
|
+
- `sendEvent(name, payload)` — отправка события
|
|
103
|
+
- `isConnected` — статус соединения
|
|
104
|
+
- `isLoading` — статус загрузки
|
|
105
|
+
|
|
106
|
+
**Опции:**
|
|
107
|
+
- `initial` — начальное значение
|
|
108
|
+
- `apiUrl` — URL сервера (по умолчанию из `VITE_API_URL`)
|
|
109
|
+
- `authToken` — токен авторизации
|
|
110
|
+
- `deepMerge` — глубокое слияние для diff-патчей
|
|
111
|
+
- `enableMsgId` — поддержка idempotency
|
|
112
|
+
- `onOpen`, `onClose`, `onError` — колбэки событий соединения
|
|
113
|
+
|
|
114
|
+
### Примеры использования
|
|
115
|
+
|
|
116
|
+
#### Список элементов
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
import { useViraState } from '@vira-ui/react';
|
|
120
|
+
|
|
121
|
+
interface Task {
|
|
122
|
+
id: string;
|
|
123
|
+
title: string;
|
|
124
|
+
completed: boolean;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function TasksList() {
|
|
128
|
+
const { data, sendEvent, sendDiff } = useViraState<Task[]>(
|
|
129
|
+
'tasks',
|
|
130
|
+
{ initial: [] }
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const toggleTask = (taskId: string) => {
|
|
134
|
+
const task = data?.find(t => t.id === taskId);
|
|
135
|
+
if (task) {
|
|
136
|
+
sendDiff({ [taskId]: { completed: !task.completed } });
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const createTask = (title: string) => {
|
|
141
|
+
sendEvent('task.created', {
|
|
142
|
+
id: crypto.randomUUID(),
|
|
143
|
+
title,
|
|
144
|
+
completed: false
|
|
145
|
+
});
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div>
|
|
150
|
+
{data?.map(task => (
|
|
151
|
+
<div key={task.id}>
|
|
152
|
+
<input
|
|
153
|
+
type="checkbox"
|
|
154
|
+
checked={task.completed}
|
|
155
|
+
onChange={() => toggleTask(task.id)}
|
|
156
|
+
/>
|
|
157
|
+
<span>{task.title}</span>
|
|
158
|
+
</div>
|
|
159
|
+
))}
|
|
160
|
+
<button onClick={() => createTask('New Task')}>
|
|
161
|
+
Add Task
|
|
162
|
+
</button>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
#### Одиночный элемент
|
|
169
|
+
|
|
170
|
+
```tsx
|
|
171
|
+
function UserDetails({ userId }: { userId: string }) {
|
|
172
|
+
const { data, sendDiff, isConnected } = useViraState<User>(
|
|
173
|
+
`user:${userId}`,
|
|
174
|
+
{
|
|
175
|
+
initial: null,
|
|
176
|
+
deepMerge: true
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const updateName = (name: string) => {
|
|
181
|
+
sendDiff({ name });
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
if (!isConnected) {
|
|
185
|
+
return <div>Connecting to server...</div>;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!data) {
|
|
189
|
+
return <div>Loading user...</div>;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return (
|
|
193
|
+
<div>
|
|
194
|
+
<input
|
|
195
|
+
value={data.name}
|
|
196
|
+
onChange={(e) => updateName(e.target.value)}
|
|
197
|
+
/>
|
|
198
|
+
<p>Email: {data.email}</p>
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
#### С обработкой ошибок
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
function DataComponent({ channel }: { channel: string }) {
|
|
208
|
+
const { data, sendEvent, isConnected, error } = useViraState(
|
|
209
|
+
channel,
|
|
210
|
+
{
|
|
211
|
+
initial: null,
|
|
212
|
+
onError: (err) => {
|
|
213
|
+
console.error('VRP Error:', err);
|
|
214
|
+
// Можно показать уведомление пользователю
|
|
215
|
+
},
|
|
216
|
+
onClose: () => {
|
|
217
|
+
console.log('Connection closed, reconnecting...');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
if (error) {
|
|
223
|
+
return <div>Error: {error.message}</div>;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!isConnected) {
|
|
227
|
+
return <div>Reconnecting...</div>;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return <div>{/* ... */}</div>;
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
#### С авторизацией
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
function AuthenticatedComponent() {
|
|
238
|
+
const authToken = useAuthToken(); // Ваш хук для получения токена
|
|
239
|
+
|
|
240
|
+
const { data } = useViraState('protected:data', {
|
|
241
|
+
authToken,
|
|
242
|
+
onError: (err) => {
|
|
243
|
+
if (err.message.includes('unauthorized')) {
|
|
244
|
+
// Перенаправить на страницу входа
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
return <div>{/* ... */}</div>;
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## 🔄 Паттерны использования
|
|
256
|
+
|
|
257
|
+
### Real-time обновления
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
function LiveDashboard() {
|
|
261
|
+
const { data } = useViraState('dashboard:stats', {
|
|
262
|
+
initial: { users: 0, orders: 0 }
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Данные автоматически обновляются при изменениях на сервере
|
|
266
|
+
return (
|
|
267
|
+
<div>
|
|
268
|
+
<div>Users: {data?.users}</div>
|
|
269
|
+
<div>Orders: {data?.orders}</div>
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Оптимистичные обновления
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
function OptimisticUpdate() {
|
|
279
|
+
const { data, sendDiff } = useViraState('user:123', {
|
|
280
|
+
initial: { name: 'John' }
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const updateName = (newName: string) => {
|
|
284
|
+
// Сразу обновляем локально (оптимистично)
|
|
285
|
+
sendDiff({ name: newName });
|
|
286
|
+
|
|
287
|
+
// Сервер подтвердит или откатит изменение
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<input
|
|
292
|
+
value={data?.name}
|
|
293
|
+
onChange={(e) => updateName(e.target.value)}
|
|
294
|
+
/>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### События вместо обновлений
|
|
300
|
+
|
|
301
|
+
```tsx
|
|
302
|
+
function EventDriven() {
|
|
303
|
+
const { sendEvent } = useViraState('tasks', { initial: [] });
|
|
304
|
+
|
|
305
|
+
const handleComplete = (taskId: string) => {
|
|
306
|
+
// Отправляем событие вместо прямого обновления
|
|
307
|
+
sendEvent('task.completed', { taskId });
|
|
308
|
+
|
|
309
|
+
// Сервер обработает событие и обновит состояние
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
return <button onClick={() => handleComplete('123')}>Complete</button>;
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## 🔗 Интеграция
|
|
319
|
+
|
|
320
|
+
Обычно используется вместе с:
|
|
321
|
+
|
|
322
|
+
- **@vira-ui/core** — базовый фреймворк
|
|
323
|
+
- **@vira-ui/bindings-react** — компоненты с автоматическим связыванием
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## 📖 Примеры
|
|
328
|
+
|
|
329
|
+
### Kanban доска
|
|
330
|
+
|
|
331
|
+
```tsx
|
|
332
|
+
function KanbanBoard() {
|
|
333
|
+
const { data, sendEvent } = useViraState<Column[]>('kanban:board', {
|
|
334
|
+
initial: []
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const moveCard = (cardId: string, fromColumn: string, toColumn: string) => {
|
|
338
|
+
sendEvent('card.moved', {
|
|
339
|
+
cardId,
|
|
340
|
+
fromColumn,
|
|
341
|
+
toColumn
|
|
342
|
+
});
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
return (
|
|
346
|
+
<div className="kanban-board">
|
|
347
|
+
{data?.map(column => (
|
|
348
|
+
<Column
|
|
349
|
+
key={column.id}
|
|
350
|
+
column={column}
|
|
351
|
+
onMoveCard={moveCard}
|
|
352
|
+
/>
|
|
353
|
+
))}
|
|
354
|
+
</div>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Чат
|
|
360
|
+
|
|
361
|
+
```tsx
|
|
362
|
+
function ChatRoom({ roomId }: { roomId: string }) {
|
|
363
|
+
const { data, sendEvent } = useViraState<Message[]>(
|
|
364
|
+
`chat:${roomId}`,
|
|
365
|
+
{ initial: [] }
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const sendMessage = (text: string) => {
|
|
369
|
+
sendEvent('message.sent', {
|
|
370
|
+
id: crypto.randomUUID(),
|
|
371
|
+
text,
|
|
372
|
+
timestamp: Date.now()
|
|
373
|
+
});
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
return (
|
|
377
|
+
<div>
|
|
378
|
+
<div className="messages">
|
|
379
|
+
{data?.map(msg => (
|
|
380
|
+
<Message key={msg.id} message={msg} />
|
|
381
|
+
))}
|
|
382
|
+
</div>
|
|
383
|
+
<MessageInput onSend={sendMessage} />
|
|
384
|
+
</div>
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## 🔥 Best Practices
|
|
392
|
+
|
|
393
|
+
1. **Используйте типизацию** — всегда указывайте тип для `useViraState<T>`
|
|
394
|
+
2. **Обрабатывайте ошибки** — используйте `onError` для обработки ошибок соединения
|
|
395
|
+
3. **Оптимистичные обновления** — используйте `sendDiff` для мгновенного обновления UI
|
|
396
|
+
4. **События для действий** — используйте `sendEvent` для действий, которые должны обрабатываться на сервере
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## 📄 License
|
|
401
|
+
|
|
402
|
+
MIT
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## 🔗 Связанные пакеты
|
|
407
|
+
|
|
408
|
+
- [`@vira-ui/core`](../core/README.md) - Базовый фреймворк с VRP клиентом
|
|
409
|
+
- [`@vira-ui/bindings-react`](../bindings-react/README.md) - Компоненты с auto-binding
|
|
410
|
+
- [`@vira-ui/ui`](../ui/README.md) - UI компоненты
|
|
411
|
+
|
package/dist/index.js
CHANGED
|
@@ -90,6 +90,24 @@ function useViraState(channel, initialOrOptions) {
|
|
|
90
90
|
if (disablePooling) return null;
|
|
91
91
|
return (0, import_core.getViraConnectionPool)({ url: apiUrl, authToken, debug });
|
|
92
92
|
}, [disablePooling, apiUrl, authToken, debug]);
|
|
93
|
+
const loadInitialData = (0, import_react.useCallback)(async () => {
|
|
94
|
+
if (channel === "inventoryitem:") {
|
|
95
|
+
const maxRetries = 30;
|
|
96
|
+
let retries = 0;
|
|
97
|
+
const trySendRequest = () => {
|
|
98
|
+
if (transportRef.current && retries < maxRetries) {
|
|
99
|
+
transportRef.current.sendEvent("inventoryitem.list", {});
|
|
100
|
+
return;
|
|
101
|
+
} else if (retries >= maxRetries) {
|
|
102
|
+
} else {
|
|
103
|
+
retries++;
|
|
104
|
+
setTimeout(trySendRequest, 100);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
trySendRequest();
|
|
108
|
+
} else {
|
|
109
|
+
}
|
|
110
|
+
}, [channel]);
|
|
93
111
|
(0, import_react.useEffect)(() => {
|
|
94
112
|
if (!channel) return;
|
|
95
113
|
const handleMessage = (msg) => {
|
|
@@ -97,27 +115,79 @@ function useViraState(channel, initialOrOptions) {
|
|
|
97
115
|
case "update":
|
|
98
116
|
case "event":
|
|
99
117
|
if (msg.channel === channel) {
|
|
100
|
-
|
|
118
|
+
if (channel === "inventoryitem:" && msg.data && typeof msg.data === "object" && !msg.data.type && !msg.data.patch && !data) {
|
|
119
|
+
setData(msg.data);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (msg.data && typeof msg.data === "object" && msg.data.type === "diff" && msg.data.patch) {
|
|
123
|
+
setData((prev) => {
|
|
124
|
+
if (!prev) {
|
|
125
|
+
return msg.data.patch;
|
|
126
|
+
}
|
|
127
|
+
if (typeof prev === "object" && typeof msg.data.patch === "object") {
|
|
128
|
+
let newData = { ...prev };
|
|
129
|
+
for (const [itemId, itemData] of Object.entries(msg.data.patch)) {
|
|
130
|
+
if (itemData && typeof itemData === "object" && itemId !== "type" && itemId !== "patch") {
|
|
131
|
+
newData[itemId] = itemData;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return newData;
|
|
135
|
+
}
|
|
136
|
+
return prev;
|
|
137
|
+
});
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (msg.data && typeof msg.data === "object" && msg.data.type === "event") {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (msg.data && typeof msg.data === "object" && msg.data.patch) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (msg.data && typeof msg.data === "object" && msg.data.type === "inventory_list") {
|
|
147
|
+
setData(msg.data.items);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
setData((prev) => {
|
|
151
|
+
if (!prev) {
|
|
152
|
+
return msg.data;
|
|
153
|
+
}
|
|
154
|
+
if (typeof prev === "object" && typeof msg.data === "object") {
|
|
155
|
+
let mergedData;
|
|
156
|
+
if (useDeepMerge) {
|
|
157
|
+
mergedData = (0, import_core.deepMerge)(prev, msg.data);
|
|
158
|
+
} else {
|
|
159
|
+
mergedData = { ...prev, ...msg.data };
|
|
160
|
+
}
|
|
161
|
+
return mergedData;
|
|
162
|
+
}
|
|
163
|
+
return msg.data;
|
|
164
|
+
});
|
|
101
165
|
}
|
|
102
166
|
break;
|
|
103
167
|
case "diff":
|
|
104
168
|
if (msg.channel === channel) {
|
|
105
169
|
setData((prev) => {
|
|
170
|
+
if (!prev || !msg.patch || typeof msg.patch === "object" && (msg.patch.type === "diff" || msg.patch.type === "event")) {
|
|
171
|
+
return prev;
|
|
172
|
+
}
|
|
106
173
|
if (!prev) {
|
|
107
|
-
return
|
|
174
|
+
return null;
|
|
108
175
|
}
|
|
109
176
|
if (typeof prev === "object" && typeof msg.patch === "object") {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
177
|
+
let newData = { ...prev };
|
|
178
|
+
for (const [itemId, itemData] of Object.entries(msg.patch)) {
|
|
179
|
+
if (itemData && typeof itemData === "object" && itemId !== "type" && itemId !== "patch") {
|
|
180
|
+
newData[itemId] = itemData;
|
|
181
|
+
}
|
|
114
182
|
}
|
|
183
|
+
return newData;
|
|
115
184
|
}
|
|
116
|
-
return
|
|
185
|
+
return prev;
|
|
117
186
|
});
|
|
118
187
|
}
|
|
119
188
|
break;
|
|
120
189
|
}
|
|
190
|
+
;
|
|
121
191
|
};
|
|
122
192
|
if (pool) {
|
|
123
193
|
const unsubChannel = pool.subscribe(channel, handleMessage);
|
|
@@ -126,6 +196,7 @@ function useViraState(channel, initialOrOptions) {
|
|
|
126
196
|
setIsConnected(status.connected);
|
|
127
197
|
if (status.connected && !wasConnectedRef.current) {
|
|
128
198
|
wasConnectedRef.current = true;
|
|
199
|
+
loadInitialData();
|
|
129
200
|
onOpen?.();
|
|
130
201
|
}
|
|
131
202
|
if (!status.connected && wasConnectedRef.current) {
|
|
@@ -158,10 +229,15 @@ function useViraState(channel, initialOrOptions) {
|
|
|
158
229
|
const handleConnect = () => {
|
|
159
230
|
setIsConnected(true);
|
|
160
231
|
setError(null);
|
|
232
|
+
if (!wasConnectedRef.current) {
|
|
233
|
+
wasConnectedRef.current = true;
|
|
234
|
+
loadInitialData();
|
|
235
|
+
}
|
|
161
236
|
onOpen?.();
|
|
162
237
|
};
|
|
163
238
|
const handleDisconnect = (event) => {
|
|
164
239
|
setIsConnected(false);
|
|
240
|
+
wasConnectedRef.current = false;
|
|
165
241
|
onClose?.(event);
|
|
166
242
|
};
|
|
167
243
|
const handleError = (err) => {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/useViraState.ts"],"sourcesContent":["/**\r\n * @vira-ui/react\r\n * \r\n * Vira Framework - React hooks for Vira Reactive Protocol\r\n * \r\n * This package provides React hooks for VRP, built on top of @vira-ui/core.\r\n */\r\n\r\nexport {\r\n useViraState,\r\n useViraStream,\r\n} from './useViraState';\r\nexport type {\r\n UseViraStateOptions,\r\n} from './useViraState';\r\n\r\n","import { useEffect, useMemo, useRef, useState, useCallback } from 'react';\r\nimport {\r\n createViraClient,\r\n deepMerge,\r\n getViraConnectionPool,\r\n type ViraClient,\r\n type Message,\r\n type ViraConnectionPool,\r\n} from '@vira-ui/core';\r\n\r\nexport interface UseViraStateOptions<T = any> {\r\n /** Initial value for the state */\r\n initial?: T | null;\r\n /** Enable msgId support for idempotency */\r\n enableMsgId?: boolean;\r\n /** Callback when connection opens */\r\n onOpen?: () => void;\r\n /** Callback when connection closes */\r\n onClose?: (event: CloseEvent) => void;\r\n /** Callback when connection error occurs */\r\n onError?: (error: Error) => void;\r\n /** Use deep merge for diff patches (default: true) */\r\n deepMerge?: boolean;\r\n /** API URL (defaults to VITE_API_URL env or 'http://localhost:8080') */\r\n apiUrl?: string;\r\n /** Auth token for handshake */\r\n authToken?: string;\r\n /** Disable connection pooling (fallback to 1 WS per channel). Default: false */\r\n disablePooling?: boolean;\r\n /** Enable debug logs for VRP (console.debug). Default: env VITE_VRP_DEBUG === 'true' */\r\n debug?: boolean;\r\n}\r\n\r\n/**\r\n * Unified hook for Vira Reactive Protocol state management.\r\n * Replaces both useViraState and useViraStream.\r\n *\r\n * @example\r\n * ```tsx\r\n * // Basic usage\r\n * const { data, sendUpdate } = useViraState<MyType>('my-channel');\r\n *\r\n * // With options\r\n * const { data, sendUpdate, sendDiff } = useViraState<User>('user:123', {\r\n * initial: { name: 'Guest' },\r\n * enableMsgId: true,\r\n * onOpen: () => console.log('Connected'),\r\n * deepMerge: true\r\n * });\r\n * ```\r\n */\r\nexport function useViraState<T = any, C extends string = string>(\r\n channel: C,\r\n initialOrOptions?: T | null | UseViraStateOptions<T>\r\n): {\r\n /** Current state data */\r\n data: T | null;\r\n /** Send an event to the server */\r\n sendEvent: (name: string, payload: any, msgId?: string) => void;\r\n /** Send a full update (replaces state) */\r\n sendUpdate: (payload: T, msgId?: string) => void;\r\n /** Send a partial diff (merges with current state) */\r\n sendDiff: (patch: Partial<T>, msgId?: string) => void;\r\n /** Connection status */\r\n isConnected: boolean;\r\n /** Connection error, if any */\r\n error: Error | null;\r\n} {\r\n // Parse options (backward compatibility: second param can be initial value or options)\r\n const options: UseViraStateOptions<T> = useMemo(() => {\r\n if (initialOrOptions === null || initialOrOptions === undefined) {\r\n return {};\r\n }\r\n // If it's an object with known option keys, treat as options\r\n if (\r\n typeof initialOrOptions === 'object' &&\r\n !Array.isArray(initialOrOptions) &&\r\n ('enableMsgId' in initialOrOptions ||\r\n 'onOpen' in initialOrOptions ||\r\n 'onClose' in initialOrOptions ||\r\n 'onError' in initialOrOptions ||\r\n 'deepMerge' in initialOrOptions ||\r\n 'initial' in initialOrOptions ||\r\n 'apiUrl' in initialOrOptions ||\r\n 'authToken' in initialOrOptions ||\r\n 'disablePooling' in initialOrOptions ||\r\n 'debug' in initialOrOptions)\r\n ) {\r\n return initialOrOptions as UseViraStateOptions<T>;\r\n }\r\n // Otherwise, treat as initial value (backward compatibility)\r\n return { initial: initialOrOptions as T | null };\r\n }, [initialOrOptions]);\r\n\r\n const {\r\n initial = null,\r\n enableMsgId = false,\r\n onOpen,\r\n onClose,\r\n onError,\r\n deepMerge: useDeepMerge = true,\r\n apiUrl: apiUrlOption,\r\n authToken: authTokenOption,\r\n disablePooling = false,\r\n debug: debugOption,\r\n } = options;\r\n\r\n const [data, setData] = useState<T | null>(initial);\r\n const [isConnected, setIsConnected] = useState(false);\r\n const [error, setError] = useState<Error | null>(null);\r\n\r\n type Transport = {\r\n sendEvent: (name: string, payload: any, msgId?: string) => void;\r\n sendUpdate: (payload: T, msgId?: string) => void;\r\n sendDiff: (patch: Partial<T>, msgId?: string) => void;\r\n };\r\n\r\n const transportRef = useRef<Transport | null>(null);\r\n const clientRef = useRef<ViraClient | null>(null); // legacy non-pooled mode only\r\n const sessionRef = useRef<string | null>(null);\r\n const msgIdCounterRef = useRef(0);\r\n const wasConnectedRef = useRef(false);\r\n const lastErrorRef = useRef<Error | null>(null);\r\n\r\n // Use provided apiUrl or fallback to env or default\r\n // Note: import.meta is only available in ESM, so we check safely\r\n const apiUrl = useMemo(() => {\r\n if (apiUrlOption) return apiUrlOption;\r\n // Try to get from env if available (Vite/bundler environment)\r\n try {\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n const env = (globalThis as any).import?.meta?.env || (globalThis as any).process?.env;\r\n if (env?.VITE_API_URL) return env.VITE_API_URL;\r\n } catch {\r\n // Ignore if import.meta is not available\r\n }\r\n return 'http://localhost:8080';\r\n }, [apiUrlOption]);\r\n\r\n // Get authToken from options or try to get from env\r\n const authToken = useMemo(() => {\r\n if (authTokenOption !== undefined) return authTokenOption;\r\n try {\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n const env = (globalThis as any).import?.meta?.env || (globalThis as any).process?.env;\r\n return env?.VITE_AUTH_TOKEN || '';\r\n } catch {\r\n return '';\r\n }\r\n }, [authTokenOption]);\r\n\r\n const debug = useMemo(() => {\r\n if (debugOption !== undefined) return Boolean(debugOption);\r\n try {\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n const env = (globalThis as any).import?.meta?.env || (globalThis as any).process?.env;\r\n return String(env?.VITE_VRP_DEBUG || '').toLowerCase() === 'true';\r\n } catch {\r\n return false;\r\n }\r\n }, [debugOption]);\r\n\r\n const pool: ViraConnectionPool | null = useMemo(() => {\r\n if (disablePooling) return null;\r\n return getViraConnectionPool({ url: apiUrl, authToken, debug });\r\n }, [disablePooling, apiUrl, authToken, debug]);\r\n\r\n useEffect(() => {\r\n if (!channel) return;\r\n\r\n const handleMessage = (msg: Message) => {\r\n switch (msg.type) {\r\n case 'update':\r\n case 'event':\r\n if (msg.channel === channel) {\r\n setData(msg.data as T);\r\n }\r\n break;\r\n\r\n case 'diff':\r\n if (msg.channel === channel) {\r\n setData((prev) => {\r\n if (!prev) {\r\n return (msg.patch as T) || null;\r\n }\r\n\r\n // For objects, merge the patch\r\n if (typeof prev === 'object' && typeof msg.patch === 'object') {\r\n if (useDeepMerge) {\r\n // Deep merge preserves nested structures\r\n return deepMerge(prev as Record<string, any>, msg.patch as Partial<T>) as T;\r\n } else {\r\n // Shallow merge (backward compatible)\r\n return { ...(prev as any), ...(msg.patch as any) };\r\n }\r\n }\r\n\r\n // For primitives, replace entirely\r\n return (msg.patch as T) || prev;\r\n });\r\n }\r\n break;\r\n }\r\n };\r\n\r\n // --- Pooled mode (default) ---\r\n if (pool) {\r\n // Subscribe to messages for this channel\r\n const unsubChannel = pool.subscribe(channel, handleMessage);\r\n\r\n // Track shared connection status\r\n const unsubStatus = pool.onStatus((status) => {\r\n setError(status.error);\r\n setIsConnected(status.connected);\r\n\r\n // Fire callbacks only on transitions\r\n if (status.connected && !wasConnectedRef.current) {\r\n wasConnectedRef.current = true;\r\n onOpen?.();\r\n }\r\n if (!status.connected && wasConnectedRef.current) {\r\n wasConnectedRef.current = false;\r\n // We don't get a real CloseEvent from pooled status, so we pass a lightweight synthetic object\r\n const synthetic = (typeof CloseEvent !== 'undefined'\r\n ? new CloseEvent('close', { code: 1001, reason: 'pooled disconnect' })\r\n : ({ code: 1001, reason: 'pooled disconnect' } as any)) as any;\r\n onClose?.(synthetic);\r\n }\r\n if (status.error) {\r\n if (lastErrorRef.current !== status.error) {\r\n lastErrorRef.current = status.error;\r\n onError?.(status.error);\r\n }\r\n } else {\r\n lastErrorRef.current = null;\r\n }\r\n });\r\n\r\n // Wire transport for send* APIs\r\n transportRef.current = {\r\n sendEvent: (name, payload, msgId) => pool.sendEvent(channel, name, payload, msgId),\r\n sendUpdate: (payload, msgId) => pool.sendUpdate(channel, payload, msgId),\r\n sendDiff: (patch, msgId) => pool.sendDiff(channel, patch, msgId),\r\n };\r\n\r\n return () => {\r\n unsubStatus();\r\n unsubChannel();\r\n transportRef.current = null;\r\n setIsConnected(false);\r\n setError(null);\r\n };\r\n }\r\n\r\n // --- Legacy mode (1 WS per channel) ---\r\n const handleConnect = () => {\r\n setIsConnected(true);\r\n setError(null);\r\n onOpen?.();\r\n };\r\n\r\n const handleDisconnect = (event?: CloseEvent) => {\r\n setIsConnected(false);\r\n onClose?.(event!);\r\n };\r\n\r\n const handleError = (err: Error) => {\r\n setError(err);\r\n onError?.(err);\r\n };\r\n\r\n const client = createViraClient({\r\n url: apiUrl,\r\n channel,\r\n onMessage: handleMessage,\r\n onConnect: handleConnect,\r\n onDisconnect: handleDisconnect,\r\n onError: handleError,\r\n session: sessionRef.current,\r\n authToken,\r\n onSessionChange: (newSession) => {\r\n sessionRef.current = newSession;\r\n },\r\n });\r\n\r\n clientRef.current = client;\r\n transportRef.current = {\r\n sendEvent: (name, payload, msgId) => client.sendEvent(name, payload, msgId),\r\n sendUpdate: (payload, msgId) => client.sendUpdate(payload, msgId),\r\n sendDiff: (patch, msgId) => client.sendDiff(patch, msgId),\r\n };\r\n\r\n return () => {\r\n client.close();\r\n clientRef.current = null;\r\n transportRef.current = null;\r\n setIsConnected(false);\r\n setError(null);\r\n };\r\n }, [channel, apiUrl, authToken, onOpen, onClose, onError, useDeepMerge, pool]);\r\n\r\n // Generate msgId if enabled\r\n const generateMsgId = useCallback((): string | undefined => {\r\n if (!enableMsgId) return undefined;\r\n msgIdCounterRef.current++;\r\n return `${channel}:${Date.now()}:${msgIdCounterRef.current}`;\r\n }, [channel, enableMsgId]);\r\n\r\n const sendEvent = useCallback(\r\n (name: string, payload: any, msgId?: string) => {\r\n transportRef.current?.sendEvent(name, payload, msgId ?? generateMsgId());\r\n },\r\n [generateMsgId]\r\n );\r\n\r\n const sendUpdate = useCallback(\r\n (payload: T, msgId?: string) => {\r\n transportRef.current?.sendUpdate(payload, msgId ?? generateMsgId());\r\n },\r\n [generateMsgId]\r\n );\r\n\r\n const sendDiff = useCallback(\r\n (patch: Partial<T>, msgId?: string) => {\r\n transportRef.current?.sendDiff(patch, msgId ?? generateMsgId());\r\n },\r\n [generateMsgId]\r\n );\r\n\r\n return {\r\n data,\r\n sendEvent,\r\n sendUpdate,\r\n sendDiff,\r\n isConnected,\r\n error,\r\n };\r\n}\r\n\r\n/**\r\n * Legacy hook - use useViraState instead.\r\n * @deprecated Use useViraState with options instead\r\n */\r\nexport function useViraStream<T = any, C extends string = string>(\r\n channel: C,\r\n options?: UseViraStateOptions<T>\r\n) {\r\n return useViraState<T, C>(channel, { ...options, initial: null });\r\n}\r\n\r\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/useViraState.ts"],"sourcesContent":["/**\n * @vira-ui/react\n * \n * Vira Framework - React hooks for Vira Reactive Protocol\n * \n * This package provides React hooks for VRP, built on top of @vira-ui/core.\n */\n\nexport {\n useViraState,\n useViraStream,\n} from './useViraState';\nexport type {\n UseViraStateOptions,\n} from './useViraState';\n\n","import { useEffect, useMemo, useRef, useState, useCallback } from 'react';\nimport {\n createViraClient,\n deepMerge,\n getViraConnectionPool,\n type ViraClient,\n type Message,\n type ViraConnectionPool,\n type ViraPoolStatus,\n} from '@vira-ui/core';\n\nexport interface UseViraStateOptions<T = any> {\n /** Initial value for the state */\n initial?: T | null;\n /** Enable msgId support for idempotency */\n enableMsgId?: boolean;\n /** Callback when connection opens */\n onOpen?: () => void;\n /** Callback when connection closes */\n onClose?: (event: CloseEvent) => void;\n /** Callback when connection error occurs */\n onError?: (error: Error) => void;\n /** Use deep merge for diff patches (default: true) */\n deepMerge?: boolean;\n /** API URL (defaults to VITE_API_URL env or 'http://localhost:8080') */\n apiUrl?: string;\n /** Auth token for handshake */\n authToken?: string;\n /** Disable connection pooling (fallback to 1 WS per channel). Default: false */\n disablePooling?: boolean;\n /** Enable debug logs for VRP (console.debug). Default: env VITE_VRP_DEBUG === 'true' */\n debug?: boolean;\n}\n\n/**\n * Unified hook for Vira Reactive Protocol state management.\n * Replaces both useViraState and useViraStream.\n *\n * @example\n * ```tsx\n * // Basic usage\n * const { data, sendUpdate } = useViraState<MyType>('my-channel');\n *\n * // With options\n * const { data, sendUpdate, sendDiff } = useViraState<User>('user:123', {\n * initial: { name: 'Guest' },\n * enableMsgId: true,\n * onOpen: () => console.log('Connected'),\n * deepMerge: true\n * });\n * ```\n */\nexport function useViraState<T = any, C extends string = string>(\n channel: C,\n initialOrOptions?: T | null | UseViraStateOptions<T>\n): {\n /** Current state data */\n data: T | null;\n /** Send an event to the server */\n sendEvent: (name: string, payload: any, msgId?: string) => void;\n /** Send a full update (replaces state) */\n sendUpdate: (payload: T, msgId?: string) => void;\n /** Send a partial diff (merges with current state) */\n sendDiff: (patch: Partial<T>, msgId?: string) => void;\n /** Connection status */\n isConnected: boolean;\n /** Connection error, if any */\n error: Error | null;\n} {\n // Parse options (backward compatibility: second param can be initial value or options)\n const options: UseViraStateOptions<T> = useMemo(() => {\n if (initialOrOptions === null || initialOrOptions === undefined) {\n return {};\n }\n // If it's an object with known option keys, treat as options\n if (\n typeof initialOrOptions === 'object' &&\n !Array.isArray(initialOrOptions) &&\n ('enableMsgId' in initialOrOptions ||\n 'onOpen' in initialOrOptions ||\n 'onClose' in initialOrOptions ||\n 'onError' in initialOrOptions ||\n 'deepMerge' in initialOrOptions ||\n 'initial' in initialOrOptions ||\n 'apiUrl' in initialOrOptions ||\n 'authToken' in initialOrOptions ||\n 'disablePooling' in initialOrOptions ||\n 'debug' in initialOrOptions)\n ) {\n return initialOrOptions as UseViraStateOptions<T>;\n }\n // Otherwise, treat as initial value (backward compatibility)\n return { initial: initialOrOptions as T | null };\n }, [initialOrOptions]);\n\n const {\n initial = null,\n enableMsgId = false,\n onOpen,\n onClose,\n onError,\n deepMerge: useDeepMerge = true,\n apiUrl: apiUrlOption,\n authToken: authTokenOption,\n disablePooling = false,\n debug: debugOption,\n } = options;\n\n const [data, setData] = useState<T | null>(initial);\n const [isConnected, setIsConnected] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n type Transport = {\n sendEvent: (name: string, payload: any, msgId?: string) => void;\n sendUpdate: (payload: T, msgId?: string) => void;\n sendDiff: (patch: Partial<T>, msgId?: string) => void;\n };\n\n const transportRef = useRef<Transport | null>(null);\n const clientRef = useRef<ViraClient | null>(null); // legacy non-pooled mode only\n const sessionRef = useRef<string | null>(null);\n const msgIdCounterRef = useRef(0);\n const wasConnectedRef = useRef(false);\n const lastErrorRef = useRef<Error | null>(null);\n\n // Use provided apiUrl or fallback to env or default\n // Note: import.meta is only available in ESM, so we check safely\n const apiUrl = useMemo(() => {\n if (apiUrlOption) return apiUrlOption;\n // Try to get from env if available (Vite/bundler environment)\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const env = (globalThis as any).import?.meta?.env || (globalThis as any).process?.env;\n if (env?.VITE_API_URL) return env.VITE_API_URL;\n } catch {\n // Ignore if import.meta is not available\n }\n return 'http://localhost:8080';\n }, [apiUrlOption]);\n\n // Get authToken from options or try to get from env\n const authToken = useMemo(() => {\n if (authTokenOption !== undefined) return authTokenOption;\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const env = (globalThis as any).import?.meta?.env || (globalThis as any).process?.env;\n return env?.VITE_AUTH_TOKEN || '';\n } catch {\n return '';\n }\n }, [authTokenOption]);\n\n const debug = useMemo(() => {\n if (debugOption !== undefined) return Boolean(debugOption);\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const env = (globalThis as any).import?.meta?.env || (globalThis as any).process?.env;\n return String(env?.VITE_VRP_DEBUG || '').toLowerCase() === 'true';\n } catch {\n return false;\n }\n }, [debugOption]);\n\n const pool: ViraConnectionPool | null = useMemo(() => {\n if (disablePooling) return null;\n return getViraConnectionPool({ url: apiUrl, authToken, debug });\n }, [disablePooling, apiUrl, authToken, debug]);\n\n // ДОБАВИТЬ ЭТУ ФУНКЦИЮ:\n const loadInitialData = useCallback(async () => {\n\n\n // Если это канал товаров, ждем транспорт и запрашиваем данные\n if (channel === 'inventoryitem:') {\n\n // Ждем до 3 секунд пока транспорт будет готов\n const maxRetries = 30;\n let retries = 0;\n\n const trySendRequest = () => {\n if (transportRef.current && retries < maxRetries) {\n transportRef.current.sendEvent('inventoryitem.list', {});\n return;\n } else if (retries >= maxRetries) {\n } else {\n retries++;\n setTimeout(trySendRequest, 100);\n }\n };\n\n trySendRequest();\n } else {\n }\n }, [channel]);\n\n\n\n\n useEffect(() => {\n if (!channel) return;\n\n const handleMessage = (msg: Message) => {\n\n switch (msg.type) {\n case 'update':\n case 'event':\n if (msg.channel === channel) {\n // НОВАЯ ЛОГИКА: Если это первое update с полными данными товаров\n if (channel === 'inventoryitem:' && msg.data && typeof msg.data === 'object' &&\n !msg.data.type && !msg.data.patch && !data) {\n setData(msg.data as T);\n return;\n }\n\n // Handle diff patches that come as update messages\n if (msg.data && typeof msg.data === 'object' && msg.data.type === 'diff' && msg.data.patch) {\n\n setData((prev) => {\n\n if (!prev) {\n return msg.data.patch as T;\n }\n\n // Merge diff данных с существующими данными\n if (typeof prev === 'object' && typeof msg.data.patch === 'object') {\n let newData = { ...(prev as any) };\n\n // Обновляем только элементы из diff patch\n for (const [itemId, itemData] of Object.entries(msg.data.patch)) {\n if (itemData && typeof itemData === 'object' && itemId !== 'type' && itemId !== 'patch') {\n newData[itemId] = itemData;\n }\n }\n\n return newData as T;\n }\n\n return prev;\n });\n return;\n }\n\n // Handle events that come as update messages (ignore service events)\n if (msg.data && typeof msg.data === 'object' && msg.data.type === 'event') {\n return;\n }\n\n // ПРОВЕРКА: Игнорируем другие служебные сообщения\n if (msg.data && typeof msg.data === 'object' && msg.data.patch) {\n return; // Выходим без обновления данных\n }\n\n // Обработка запроса начальных данных\n if (msg.data && typeof msg.data === 'object' && msg.data.type === 'inventory_list') {\n setData(msg.data.items as T);\n return;\n }\n\n\n setData((prev) => {\n\n if (!prev) {\n return msg.data as T;\n }\n\n if (typeof prev === 'object' && typeof msg.data === 'object') {\n let mergedData;\n if (useDeepMerge) {\n mergedData = deepMerge(prev as Record<string, any>, msg.data as Record<string, any>) as T;\n } else {\n mergedData = { ...(prev as any), ...(msg.data as any) };\n }\n return mergedData;\n }\n\n return msg.data as T;\n });\n }\n break;\n\n\n case 'diff':\n if (msg.channel === channel) {\n // СПЕЦИАЛЬНАЯ ЛОГИКА для diff - только для товаров\n setData((prev) => {\n\n // Игнорируем служебные diff (с type: 'diff')\n if (!prev || !msg.patch ||\n (typeof msg.patch === 'object' && (msg.patch.type === 'diff' || msg.patch.type === 'event'))) {\n return prev;\n }\n\n if (!prev) {\n return null; // Возвращаем null для соответствия типу T | null\n }\n\n // Merge diff данных с существующими товарами\n if (typeof prev === 'object' && typeof msg.patch === 'object') {\n let newData = { ...(prev as any) };\n\n // Обновляем только товары из diff\n for (const [itemId, itemData] of Object.entries(msg.patch)) {\n if (itemData && typeof itemData === 'object' && itemId !== 'type' && itemId !== 'patch') {\n newData[itemId] = itemData;\n }\n }\n\n return newData as T;\n }\n\n return prev;\n });\n }\n break;\n };\n };\n\n // --- Pooled mode (default) ---\n if (pool) {\n // Subscribe to messages for this channel\n const unsubChannel = pool.subscribe(channel, handleMessage);\n\n // Track shared connection status\n const unsubStatus = pool.onStatus((status: ViraPoolStatus) => {\n setError(status.error);\n setIsConnected(status.connected);\n\n // Fire callbacks only on transitions\n if (status.connected && !wasConnectedRef.current) {\n wasConnectedRef.current = true;\n loadInitialData();\n onOpen?.();\n }\n if (!status.connected && wasConnectedRef.current) {\n wasConnectedRef.current = false;\n // We don't get a real CloseEvent from pooled status, so we pass a lightweight synthetic object\n const synthetic = (typeof CloseEvent !== 'undefined'\n ? new CloseEvent('close', { code: 1001, reason: 'pooled disconnect' })\n : ({ code: 1001, reason: 'pooled disconnect' } as any)) as any;\n onClose?.(synthetic);\n }\n if (status.error) {\n if (lastErrorRef.current !== status.error) {\n lastErrorRef.current = status.error;\n onError?.(status.error);\n }\n } else {\n lastErrorRef.current = null;\n }\n });\n\n // Wire transport for send* APIs\n transportRef.current = {\n sendEvent: (name, payload, msgId) => pool.sendEvent(channel, name, payload, msgId),\n sendUpdate: (payload, msgId) => pool.sendUpdate(channel, payload, msgId),\n sendDiff: (patch, msgId) => pool.sendDiff(channel, patch, msgId),\n };\n\n return () => {\n unsubStatus();\n unsubChannel();\n transportRef.current = null;\n setIsConnected(false);\n setError(null);\n };\n }\n\n // --- Legacy mode (1 WS per channel) ---\n const handleConnect = () => {\n setIsConnected(true);\n setError(null);\n if (!wasConnectedRef.current) {\n wasConnectedRef.current = true;\n loadInitialData();\n }\n onOpen?.();\n };\n\n const handleDisconnect = (event?: CloseEvent) => {\n setIsConnected(false);\n wasConnectedRef.current = false;\n onClose?.(event!);\n };\n\n const handleError = (err: Error) => {\n setError(err);\n onError?.(err);\n };\n\n const client = createViraClient({\n url: apiUrl,\n channel,\n onMessage: handleMessage,\n onConnect: handleConnect,\n onDisconnect: handleDisconnect,\n onError: handleError,\n session: sessionRef.current,\n authToken,\n onSessionChange: (newSession: string | null) => {\n sessionRef.current = newSession;\n },\n });\n\n clientRef.current = client;\n transportRef.current = {\n sendEvent: (name, payload, msgId) => client.sendEvent(name, payload, msgId),\n sendUpdate: (payload, msgId) => client.sendUpdate(payload, msgId),\n sendDiff: (patch, msgId) => client.sendDiff(patch, msgId),\n };\n\n return () => {\n client.close();\n clientRef.current = null;\n transportRef.current = null;\n setIsConnected(false);\n setError(null);\n };\n }, [channel, apiUrl, authToken, onOpen, onClose, onError, useDeepMerge, pool]);\n\n // Generate msgId if enabled\n const generateMsgId = useCallback((): string | undefined => {\n if (!enableMsgId) return undefined;\n msgIdCounterRef.current++;\n return `${channel}:${Date.now()}:${msgIdCounterRef.current}`;\n }, [channel, enableMsgId]);\n\n const sendEvent = useCallback(\n (name: string, payload: any, msgId?: string) => {\n transportRef.current?.sendEvent(name, payload, msgId ?? generateMsgId());\n },\n [generateMsgId]\n );\n\n const sendUpdate = useCallback(\n (payload: T, msgId?: string) => {\n transportRef.current?.sendUpdate(payload, msgId ?? generateMsgId());\n },\n [generateMsgId]\n );\n\n const sendDiff = useCallback(\n (patch: Partial<T>, msgId?: string) => {\n transportRef.current?.sendDiff(patch, msgId ?? generateMsgId());\n },\n [generateMsgId]\n );\n\n return {\n data,\n sendEvent,\n sendUpdate,\n sendDiff,\n isConnected,\n error,\n };\n}\n\n/**\n * Legacy hook - use useViraState instead.\n * @deprecated Use useViraState with options instead\n */\nexport function useViraStream<T = any, C extends string = string>(\n channel: C,\n options?: UseViraStateOptions<T>\n) {\n return useViraState<T, C>(channel, { ...options, initial: null });\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -67,6 +67,24 @@ function useViraState(channel, initialOrOptions) {
|
|
|
67
67
|
if (disablePooling) return null;
|
|
68
68
|
return getViraConnectionPool({ url: apiUrl, authToken, debug });
|
|
69
69
|
}, [disablePooling, apiUrl, authToken, debug]);
|
|
70
|
+
const loadInitialData = useCallback(async () => {
|
|
71
|
+
if (channel === "inventoryitem:") {
|
|
72
|
+
const maxRetries = 30;
|
|
73
|
+
let retries = 0;
|
|
74
|
+
const trySendRequest = () => {
|
|
75
|
+
if (transportRef.current && retries < maxRetries) {
|
|
76
|
+
transportRef.current.sendEvent("inventoryitem.list", {});
|
|
77
|
+
return;
|
|
78
|
+
} else if (retries >= maxRetries) {
|
|
79
|
+
} else {
|
|
80
|
+
retries++;
|
|
81
|
+
setTimeout(trySendRequest, 100);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
trySendRequest();
|
|
85
|
+
} else {
|
|
86
|
+
}
|
|
87
|
+
}, [channel]);
|
|
70
88
|
useEffect(() => {
|
|
71
89
|
if (!channel) return;
|
|
72
90
|
const handleMessage = (msg) => {
|
|
@@ -74,27 +92,79 @@ function useViraState(channel, initialOrOptions) {
|
|
|
74
92
|
case "update":
|
|
75
93
|
case "event":
|
|
76
94
|
if (msg.channel === channel) {
|
|
77
|
-
|
|
95
|
+
if (channel === "inventoryitem:" && msg.data && typeof msg.data === "object" && !msg.data.type && !msg.data.patch && !data) {
|
|
96
|
+
setData(msg.data);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (msg.data && typeof msg.data === "object" && msg.data.type === "diff" && msg.data.patch) {
|
|
100
|
+
setData((prev) => {
|
|
101
|
+
if (!prev) {
|
|
102
|
+
return msg.data.patch;
|
|
103
|
+
}
|
|
104
|
+
if (typeof prev === "object" && typeof msg.data.patch === "object") {
|
|
105
|
+
let newData = { ...prev };
|
|
106
|
+
for (const [itemId, itemData] of Object.entries(msg.data.patch)) {
|
|
107
|
+
if (itemData && typeof itemData === "object" && itemId !== "type" && itemId !== "patch") {
|
|
108
|
+
newData[itemId] = itemData;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return newData;
|
|
112
|
+
}
|
|
113
|
+
return prev;
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (msg.data && typeof msg.data === "object" && msg.data.type === "event") {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (msg.data && typeof msg.data === "object" && msg.data.patch) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (msg.data && typeof msg.data === "object" && msg.data.type === "inventory_list") {
|
|
124
|
+
setData(msg.data.items);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
setData((prev) => {
|
|
128
|
+
if (!prev) {
|
|
129
|
+
return msg.data;
|
|
130
|
+
}
|
|
131
|
+
if (typeof prev === "object" && typeof msg.data === "object") {
|
|
132
|
+
let mergedData;
|
|
133
|
+
if (useDeepMerge) {
|
|
134
|
+
mergedData = deepMerge(prev, msg.data);
|
|
135
|
+
} else {
|
|
136
|
+
mergedData = { ...prev, ...msg.data };
|
|
137
|
+
}
|
|
138
|
+
return mergedData;
|
|
139
|
+
}
|
|
140
|
+
return msg.data;
|
|
141
|
+
});
|
|
78
142
|
}
|
|
79
143
|
break;
|
|
80
144
|
case "diff":
|
|
81
145
|
if (msg.channel === channel) {
|
|
82
146
|
setData((prev) => {
|
|
147
|
+
if (!prev || !msg.patch || typeof msg.patch === "object" && (msg.patch.type === "diff" || msg.patch.type === "event")) {
|
|
148
|
+
return prev;
|
|
149
|
+
}
|
|
83
150
|
if (!prev) {
|
|
84
|
-
return
|
|
151
|
+
return null;
|
|
85
152
|
}
|
|
86
153
|
if (typeof prev === "object" && typeof msg.patch === "object") {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
154
|
+
let newData = { ...prev };
|
|
155
|
+
for (const [itemId, itemData] of Object.entries(msg.patch)) {
|
|
156
|
+
if (itemData && typeof itemData === "object" && itemId !== "type" && itemId !== "patch") {
|
|
157
|
+
newData[itemId] = itemData;
|
|
158
|
+
}
|
|
91
159
|
}
|
|
160
|
+
return newData;
|
|
92
161
|
}
|
|
93
|
-
return
|
|
162
|
+
return prev;
|
|
94
163
|
});
|
|
95
164
|
}
|
|
96
165
|
break;
|
|
97
166
|
}
|
|
167
|
+
;
|
|
98
168
|
};
|
|
99
169
|
if (pool) {
|
|
100
170
|
const unsubChannel = pool.subscribe(channel, handleMessage);
|
|
@@ -103,6 +173,7 @@ function useViraState(channel, initialOrOptions) {
|
|
|
103
173
|
setIsConnected(status.connected);
|
|
104
174
|
if (status.connected && !wasConnectedRef.current) {
|
|
105
175
|
wasConnectedRef.current = true;
|
|
176
|
+
loadInitialData();
|
|
106
177
|
onOpen?.();
|
|
107
178
|
}
|
|
108
179
|
if (!status.connected && wasConnectedRef.current) {
|
|
@@ -135,10 +206,15 @@ function useViraState(channel, initialOrOptions) {
|
|
|
135
206
|
const handleConnect = () => {
|
|
136
207
|
setIsConnected(true);
|
|
137
208
|
setError(null);
|
|
209
|
+
if (!wasConnectedRef.current) {
|
|
210
|
+
wasConnectedRef.current = true;
|
|
211
|
+
loadInitialData();
|
|
212
|
+
}
|
|
138
213
|
onOpen?.();
|
|
139
214
|
};
|
|
140
215
|
const handleDisconnect = (event) => {
|
|
141
216
|
setIsConnected(false);
|
|
217
|
+
wasConnectedRef.current = false;
|
|
142
218
|
onClose?.(event);
|
|
143
219
|
};
|
|
144
220
|
const handleError = (err) => {
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/useViraState.ts"],"sourcesContent":["import { useEffect, useMemo, useRef, useState, useCallback } from 'react';\r\nimport {\r\n createViraClient,\r\n deepMerge,\r\n getViraConnectionPool,\r\n type ViraClient,\r\n type Message,\r\n type ViraConnectionPool,\r\n} from '@vira-ui/core';\r\n\r\nexport interface UseViraStateOptions<T = any> {\r\n /** Initial value for the state */\r\n initial?: T | null;\r\n /** Enable msgId support for idempotency */\r\n enableMsgId?: boolean;\r\n /** Callback when connection opens */\r\n onOpen?: () => void;\r\n /** Callback when connection closes */\r\n onClose?: (event: CloseEvent) => void;\r\n /** Callback when connection error occurs */\r\n onError?: (error: Error) => void;\r\n /** Use deep merge for diff patches (default: true) */\r\n deepMerge?: boolean;\r\n /** API URL (defaults to VITE_API_URL env or 'http://localhost:8080') */\r\n apiUrl?: string;\r\n /** Auth token for handshake */\r\n authToken?: string;\r\n /** Disable connection pooling (fallback to 1 WS per channel). Default: false */\r\n disablePooling?: boolean;\r\n /** Enable debug logs for VRP (console.debug). Default: env VITE_VRP_DEBUG === 'true' */\r\n debug?: boolean;\r\n}\r\n\r\n/**\r\n * Unified hook for Vira Reactive Protocol state management.\r\n * Replaces both useViraState and useViraStream.\r\n *\r\n * @example\r\n * ```tsx\r\n * // Basic usage\r\n * const { data, sendUpdate } = useViraState<MyType>('my-channel');\r\n *\r\n * // With options\r\n * const { data, sendUpdate, sendDiff } = useViraState<User>('user:123', {\r\n * initial: { name: 'Guest' },\r\n * enableMsgId: true,\r\n * onOpen: () => console.log('Connected'),\r\n * deepMerge: true\r\n * });\r\n * ```\r\n */\r\nexport function useViraState<T = any, C extends string = string>(\r\n channel: C,\r\n initialOrOptions?: T | null | UseViraStateOptions<T>\r\n): {\r\n /** Current state data */\r\n data: T | null;\r\n /** Send an event to the server */\r\n sendEvent: (name: string, payload: any, msgId?: string) => void;\r\n /** Send a full update (replaces state) */\r\n sendUpdate: (payload: T, msgId?: string) => void;\r\n /** Send a partial diff (merges with current state) */\r\n sendDiff: (patch: Partial<T>, msgId?: string) => void;\r\n /** Connection status */\r\n isConnected: boolean;\r\n /** Connection error, if any */\r\n error: Error | null;\r\n} {\r\n // Parse options (backward compatibility: second param can be initial value or options)\r\n const options: UseViraStateOptions<T> = useMemo(() => {\r\n if (initialOrOptions === null || initialOrOptions === undefined) {\r\n return {};\r\n }\r\n // If it's an object with known option keys, treat as options\r\n if (\r\n typeof initialOrOptions === 'object' &&\r\n !Array.isArray(initialOrOptions) &&\r\n ('enableMsgId' in initialOrOptions ||\r\n 'onOpen' in initialOrOptions ||\r\n 'onClose' in initialOrOptions ||\r\n 'onError' in initialOrOptions ||\r\n 'deepMerge' in initialOrOptions ||\r\n 'initial' in initialOrOptions ||\r\n 'apiUrl' in initialOrOptions ||\r\n 'authToken' in initialOrOptions ||\r\n 'disablePooling' in initialOrOptions ||\r\n 'debug' in initialOrOptions)\r\n ) {\r\n return initialOrOptions as UseViraStateOptions<T>;\r\n }\r\n // Otherwise, treat as initial value (backward compatibility)\r\n return { initial: initialOrOptions as T | null };\r\n }, [initialOrOptions]);\r\n\r\n const {\r\n initial = null,\r\n enableMsgId = false,\r\n onOpen,\r\n onClose,\r\n onError,\r\n deepMerge: useDeepMerge = true,\r\n apiUrl: apiUrlOption,\r\n authToken: authTokenOption,\r\n disablePooling = false,\r\n debug: debugOption,\r\n } = options;\r\n\r\n const [data, setData] = useState<T | null>(initial);\r\n const [isConnected, setIsConnected] = useState(false);\r\n const [error, setError] = useState<Error | null>(null);\r\n\r\n type Transport = {\r\n sendEvent: (name: string, payload: any, msgId?: string) => void;\r\n sendUpdate: (payload: T, msgId?: string) => void;\r\n sendDiff: (patch: Partial<T>, msgId?: string) => void;\r\n };\r\n\r\n const transportRef = useRef<Transport | null>(null);\r\n const clientRef = useRef<ViraClient | null>(null); // legacy non-pooled mode only\r\n const sessionRef = useRef<string | null>(null);\r\n const msgIdCounterRef = useRef(0);\r\n const wasConnectedRef = useRef(false);\r\n const lastErrorRef = useRef<Error | null>(null);\r\n\r\n // Use provided apiUrl or fallback to env or default\r\n // Note: import.meta is only available in ESM, so we check safely\r\n const apiUrl = useMemo(() => {\r\n if (apiUrlOption) return apiUrlOption;\r\n // Try to get from env if available (Vite/bundler environment)\r\n try {\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n const env = (globalThis as any).import?.meta?.env || (globalThis as any).process?.env;\r\n if (env?.VITE_API_URL) return env.VITE_API_URL;\r\n } catch {\r\n // Ignore if import.meta is not available\r\n }\r\n return 'http://localhost:8080';\r\n }, [apiUrlOption]);\r\n\r\n // Get authToken from options or try to get from env\r\n const authToken = useMemo(() => {\r\n if (authTokenOption !== undefined) return authTokenOption;\r\n try {\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n const env = (globalThis as any).import?.meta?.env || (globalThis as any).process?.env;\r\n return env?.VITE_AUTH_TOKEN || '';\r\n } catch {\r\n return '';\r\n }\r\n }, [authTokenOption]);\r\n\r\n const debug = useMemo(() => {\r\n if (debugOption !== undefined) return Boolean(debugOption);\r\n try {\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n const env = (globalThis as any).import?.meta?.env || (globalThis as any).process?.env;\r\n return String(env?.VITE_VRP_DEBUG || '').toLowerCase() === 'true';\r\n } catch {\r\n return false;\r\n }\r\n }, [debugOption]);\r\n\r\n const pool: ViraConnectionPool | null = useMemo(() => {\r\n if (disablePooling) return null;\r\n return getViraConnectionPool({ url: apiUrl, authToken, debug });\r\n }, [disablePooling, apiUrl, authToken, debug]);\r\n\r\n useEffect(() => {\r\n if (!channel) return;\r\n\r\n const handleMessage = (msg: Message) => {\r\n switch (msg.type) {\r\n case 'update':\r\n case 'event':\r\n if (msg.channel === channel) {\r\n setData(msg.data as T);\r\n }\r\n break;\r\n\r\n case 'diff':\r\n if (msg.channel === channel) {\r\n setData((prev) => {\r\n if (!prev) {\r\n return (msg.patch as T) || null;\r\n }\r\n\r\n // For objects, merge the patch\r\n if (typeof prev === 'object' && typeof msg.patch === 'object') {\r\n if (useDeepMerge) {\r\n // Deep merge preserves nested structures\r\n return deepMerge(prev as Record<string, any>, msg.patch as Partial<T>) as T;\r\n } else {\r\n // Shallow merge (backward compatible)\r\n return { ...(prev as any), ...(msg.patch as any) };\r\n }\r\n }\r\n\r\n // For primitives, replace entirely\r\n return (msg.patch as T) || prev;\r\n });\r\n }\r\n break;\r\n }\r\n };\r\n\r\n // --- Pooled mode (default) ---\r\n if (pool) {\r\n // Subscribe to messages for this channel\r\n const unsubChannel = pool.subscribe(channel, handleMessage);\r\n\r\n // Track shared connection status\r\n const unsubStatus = pool.onStatus((status) => {\r\n setError(status.error);\r\n setIsConnected(status.connected);\r\n\r\n // Fire callbacks only on transitions\r\n if (status.connected && !wasConnectedRef.current) {\r\n wasConnectedRef.current = true;\r\n onOpen?.();\r\n }\r\n if (!status.connected && wasConnectedRef.current) {\r\n wasConnectedRef.current = false;\r\n // We don't get a real CloseEvent from pooled status, so we pass a lightweight synthetic object\r\n const synthetic = (typeof CloseEvent !== 'undefined'\r\n ? new CloseEvent('close', { code: 1001, reason: 'pooled disconnect' })\r\n : ({ code: 1001, reason: 'pooled disconnect' } as any)) as any;\r\n onClose?.(synthetic);\r\n }\r\n if (status.error) {\r\n if (lastErrorRef.current !== status.error) {\r\n lastErrorRef.current = status.error;\r\n onError?.(status.error);\r\n }\r\n } else {\r\n lastErrorRef.current = null;\r\n }\r\n });\r\n\r\n // Wire transport for send* APIs\r\n transportRef.current = {\r\n sendEvent: (name, payload, msgId) => pool.sendEvent(channel, name, payload, msgId),\r\n sendUpdate: (payload, msgId) => pool.sendUpdate(channel, payload, msgId),\r\n sendDiff: (patch, msgId) => pool.sendDiff(channel, patch, msgId),\r\n };\r\n\r\n return () => {\r\n unsubStatus();\r\n unsubChannel();\r\n transportRef.current = null;\r\n setIsConnected(false);\r\n setError(null);\r\n };\r\n }\r\n\r\n // --- Legacy mode (1 WS per channel) ---\r\n const handleConnect = () => {\r\n setIsConnected(true);\r\n setError(null);\r\n onOpen?.();\r\n };\r\n\r\n const handleDisconnect = (event?: CloseEvent) => {\r\n setIsConnected(false);\r\n onClose?.(event!);\r\n };\r\n\r\n const handleError = (err: Error) => {\r\n setError(err);\r\n onError?.(err);\r\n };\r\n\r\n const client = createViraClient({\r\n url: apiUrl,\r\n channel,\r\n onMessage: handleMessage,\r\n onConnect: handleConnect,\r\n onDisconnect: handleDisconnect,\r\n onError: handleError,\r\n session: sessionRef.current,\r\n authToken,\r\n onSessionChange: (newSession) => {\r\n sessionRef.current = newSession;\r\n },\r\n });\r\n\r\n clientRef.current = client;\r\n transportRef.current = {\r\n sendEvent: (name, payload, msgId) => client.sendEvent(name, payload, msgId),\r\n sendUpdate: (payload, msgId) => client.sendUpdate(payload, msgId),\r\n sendDiff: (patch, msgId) => client.sendDiff(patch, msgId),\r\n };\r\n\r\n return () => {\r\n client.close();\r\n clientRef.current = null;\r\n transportRef.current = null;\r\n setIsConnected(false);\r\n setError(null);\r\n };\r\n }, [channel, apiUrl, authToken, onOpen, onClose, onError, useDeepMerge, pool]);\r\n\r\n // Generate msgId if enabled\r\n const generateMsgId = useCallback((): string | undefined => {\r\n if (!enableMsgId) return undefined;\r\n msgIdCounterRef.current++;\r\n return `${channel}:${Date.now()}:${msgIdCounterRef.current}`;\r\n }, [channel, enableMsgId]);\r\n\r\n const sendEvent = useCallback(\r\n (name: string, payload: any, msgId?: string) => {\r\n transportRef.current?.sendEvent(name, payload, msgId ?? generateMsgId());\r\n },\r\n [generateMsgId]\r\n );\r\n\r\n const sendUpdate = useCallback(\r\n (payload: T, msgId?: string) => {\r\n transportRef.current?.sendUpdate(payload, msgId ?? generateMsgId());\r\n },\r\n [generateMsgId]\r\n );\r\n\r\n const sendDiff = useCallback(\r\n (patch: Partial<T>, msgId?: string) => {\r\n transportRef.current?.sendDiff(patch, msgId ?? generateMsgId());\r\n },\r\n [generateMsgId]\r\n );\r\n\r\n return {\r\n data,\r\n sendEvent,\r\n sendUpdate,\r\n sendDiff,\r\n isConnected,\r\n error,\r\n };\r\n}\r\n\r\n/**\r\n * Legacy hook - use useViraState instead.\r\n * @deprecated Use useViraState with options instead\r\n */\r\nexport function useViraStream<T = any, C extends string = string>(\r\n channel: C,\r\n options?: UseViraStateOptions<T>\r\n) {\r\n return useViraState<T, C>(channel, { ...options, initial: null });\r\n}\r\n\r\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/useViraState.ts"],"sourcesContent":["import { useEffect, useMemo, useRef, useState, useCallback } from 'react';\nimport {\n createViraClient,\n deepMerge,\n getViraConnectionPool,\n type ViraClient,\n type Message,\n type ViraConnectionPool,\n type ViraPoolStatus,\n} from '@vira-ui/core';\n\nexport interface UseViraStateOptions<T = any> {\n /** Initial value for the state */\n initial?: T | null;\n /** Enable msgId support for idempotency */\n enableMsgId?: boolean;\n /** Callback when connection opens */\n onOpen?: () => void;\n /** Callback when connection closes */\n onClose?: (event: CloseEvent) => void;\n /** Callback when connection error occurs */\n onError?: (error: Error) => void;\n /** Use deep merge for diff patches (default: true) */\n deepMerge?: boolean;\n /** API URL (defaults to VITE_API_URL env or 'http://localhost:8080') */\n apiUrl?: string;\n /** Auth token for handshake */\n authToken?: string;\n /** Disable connection pooling (fallback to 1 WS per channel). Default: false */\n disablePooling?: boolean;\n /** Enable debug logs for VRP (console.debug). Default: env VITE_VRP_DEBUG === 'true' */\n debug?: boolean;\n}\n\n/**\n * Unified hook for Vira Reactive Protocol state management.\n * Replaces both useViraState and useViraStream.\n *\n * @example\n * ```tsx\n * // Basic usage\n * const { data, sendUpdate } = useViraState<MyType>('my-channel');\n *\n * // With options\n * const { data, sendUpdate, sendDiff } = useViraState<User>('user:123', {\n * initial: { name: 'Guest' },\n * enableMsgId: true,\n * onOpen: () => console.log('Connected'),\n * deepMerge: true\n * });\n * ```\n */\nexport function useViraState<T = any, C extends string = string>(\n channel: C,\n initialOrOptions?: T | null | UseViraStateOptions<T>\n): {\n /** Current state data */\n data: T | null;\n /** Send an event to the server */\n sendEvent: (name: string, payload: any, msgId?: string) => void;\n /** Send a full update (replaces state) */\n sendUpdate: (payload: T, msgId?: string) => void;\n /** Send a partial diff (merges with current state) */\n sendDiff: (patch: Partial<T>, msgId?: string) => void;\n /** Connection status */\n isConnected: boolean;\n /** Connection error, if any */\n error: Error | null;\n} {\n // Parse options (backward compatibility: second param can be initial value or options)\n const options: UseViraStateOptions<T> = useMemo(() => {\n if (initialOrOptions === null || initialOrOptions === undefined) {\n return {};\n }\n // If it's an object with known option keys, treat as options\n if (\n typeof initialOrOptions === 'object' &&\n !Array.isArray(initialOrOptions) &&\n ('enableMsgId' in initialOrOptions ||\n 'onOpen' in initialOrOptions ||\n 'onClose' in initialOrOptions ||\n 'onError' in initialOrOptions ||\n 'deepMerge' in initialOrOptions ||\n 'initial' in initialOrOptions ||\n 'apiUrl' in initialOrOptions ||\n 'authToken' in initialOrOptions ||\n 'disablePooling' in initialOrOptions ||\n 'debug' in initialOrOptions)\n ) {\n return initialOrOptions as UseViraStateOptions<T>;\n }\n // Otherwise, treat as initial value (backward compatibility)\n return { initial: initialOrOptions as T | null };\n }, [initialOrOptions]);\n\n const {\n initial = null,\n enableMsgId = false,\n onOpen,\n onClose,\n onError,\n deepMerge: useDeepMerge = true,\n apiUrl: apiUrlOption,\n authToken: authTokenOption,\n disablePooling = false,\n debug: debugOption,\n } = options;\n\n const [data, setData] = useState<T | null>(initial);\n const [isConnected, setIsConnected] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n type Transport = {\n sendEvent: (name: string, payload: any, msgId?: string) => void;\n sendUpdate: (payload: T, msgId?: string) => void;\n sendDiff: (patch: Partial<T>, msgId?: string) => void;\n };\n\n const transportRef = useRef<Transport | null>(null);\n const clientRef = useRef<ViraClient | null>(null); // legacy non-pooled mode only\n const sessionRef = useRef<string | null>(null);\n const msgIdCounterRef = useRef(0);\n const wasConnectedRef = useRef(false);\n const lastErrorRef = useRef<Error | null>(null);\n\n // Use provided apiUrl or fallback to env or default\n // Note: import.meta is only available in ESM, so we check safely\n const apiUrl = useMemo(() => {\n if (apiUrlOption) return apiUrlOption;\n // Try to get from env if available (Vite/bundler environment)\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const env = (globalThis as any).import?.meta?.env || (globalThis as any).process?.env;\n if (env?.VITE_API_URL) return env.VITE_API_URL;\n } catch {\n // Ignore if import.meta is not available\n }\n return 'http://localhost:8080';\n }, [apiUrlOption]);\n\n // Get authToken from options or try to get from env\n const authToken = useMemo(() => {\n if (authTokenOption !== undefined) return authTokenOption;\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const env = (globalThis as any).import?.meta?.env || (globalThis as any).process?.env;\n return env?.VITE_AUTH_TOKEN || '';\n } catch {\n return '';\n }\n }, [authTokenOption]);\n\n const debug = useMemo(() => {\n if (debugOption !== undefined) return Boolean(debugOption);\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const env = (globalThis as any).import?.meta?.env || (globalThis as any).process?.env;\n return String(env?.VITE_VRP_DEBUG || '').toLowerCase() === 'true';\n } catch {\n return false;\n }\n }, [debugOption]);\n\n const pool: ViraConnectionPool | null = useMemo(() => {\n if (disablePooling) return null;\n return getViraConnectionPool({ url: apiUrl, authToken, debug });\n }, [disablePooling, apiUrl, authToken, debug]);\n\n // ДОБАВИТЬ ЭТУ ФУНКЦИЮ:\n const loadInitialData = useCallback(async () => {\n\n\n // Если это канал товаров, ждем транспорт и запрашиваем данные\n if (channel === 'inventoryitem:') {\n\n // Ждем до 3 секунд пока транспорт будет готов\n const maxRetries = 30;\n let retries = 0;\n\n const trySendRequest = () => {\n if (transportRef.current && retries < maxRetries) {\n transportRef.current.sendEvent('inventoryitem.list', {});\n return;\n } else if (retries >= maxRetries) {\n } else {\n retries++;\n setTimeout(trySendRequest, 100);\n }\n };\n\n trySendRequest();\n } else {\n }\n }, [channel]);\n\n\n\n\n useEffect(() => {\n if (!channel) return;\n\n const handleMessage = (msg: Message) => {\n\n switch (msg.type) {\n case 'update':\n case 'event':\n if (msg.channel === channel) {\n // НОВАЯ ЛОГИКА: Если это первое update с полными данными товаров\n if (channel === 'inventoryitem:' && msg.data && typeof msg.data === 'object' &&\n !msg.data.type && !msg.data.patch && !data) {\n setData(msg.data as T);\n return;\n }\n\n // Handle diff patches that come as update messages\n if (msg.data && typeof msg.data === 'object' && msg.data.type === 'diff' && msg.data.patch) {\n\n setData((prev) => {\n\n if (!prev) {\n return msg.data.patch as T;\n }\n\n // Merge diff данных с существующими данными\n if (typeof prev === 'object' && typeof msg.data.patch === 'object') {\n let newData = { ...(prev as any) };\n\n // Обновляем только элементы из diff patch\n for (const [itemId, itemData] of Object.entries(msg.data.patch)) {\n if (itemData && typeof itemData === 'object' && itemId !== 'type' && itemId !== 'patch') {\n newData[itemId] = itemData;\n }\n }\n\n return newData as T;\n }\n\n return prev;\n });\n return;\n }\n\n // Handle events that come as update messages (ignore service events)\n if (msg.data && typeof msg.data === 'object' && msg.data.type === 'event') {\n return;\n }\n\n // ПРОВЕРКА: Игнорируем другие служебные сообщения\n if (msg.data && typeof msg.data === 'object' && msg.data.patch) {\n return; // Выходим без обновления данных\n }\n\n // Обработка запроса начальных данных\n if (msg.data && typeof msg.data === 'object' && msg.data.type === 'inventory_list') {\n setData(msg.data.items as T);\n return;\n }\n\n\n setData((prev) => {\n\n if (!prev) {\n return msg.data as T;\n }\n\n if (typeof prev === 'object' && typeof msg.data === 'object') {\n let mergedData;\n if (useDeepMerge) {\n mergedData = deepMerge(prev as Record<string, any>, msg.data as Record<string, any>) as T;\n } else {\n mergedData = { ...(prev as any), ...(msg.data as any) };\n }\n return mergedData;\n }\n\n return msg.data as T;\n });\n }\n break;\n\n\n case 'diff':\n if (msg.channel === channel) {\n // СПЕЦИАЛЬНАЯ ЛОГИКА для diff - только для товаров\n setData((prev) => {\n\n // Игнорируем служебные diff (с type: 'diff')\n if (!prev || !msg.patch ||\n (typeof msg.patch === 'object' && (msg.patch.type === 'diff' || msg.patch.type === 'event'))) {\n return prev;\n }\n\n if (!prev) {\n return null; // Возвращаем null для соответствия типу T | null\n }\n\n // Merge diff данных с существующими товарами\n if (typeof prev === 'object' && typeof msg.patch === 'object') {\n let newData = { ...(prev as any) };\n\n // Обновляем только товары из diff\n for (const [itemId, itemData] of Object.entries(msg.patch)) {\n if (itemData && typeof itemData === 'object' && itemId !== 'type' && itemId !== 'patch') {\n newData[itemId] = itemData;\n }\n }\n\n return newData as T;\n }\n\n return prev;\n });\n }\n break;\n };\n };\n\n // --- Pooled mode (default) ---\n if (pool) {\n // Subscribe to messages for this channel\n const unsubChannel = pool.subscribe(channel, handleMessage);\n\n // Track shared connection status\n const unsubStatus = pool.onStatus((status: ViraPoolStatus) => {\n setError(status.error);\n setIsConnected(status.connected);\n\n // Fire callbacks only on transitions\n if (status.connected && !wasConnectedRef.current) {\n wasConnectedRef.current = true;\n loadInitialData();\n onOpen?.();\n }\n if (!status.connected && wasConnectedRef.current) {\n wasConnectedRef.current = false;\n // We don't get a real CloseEvent from pooled status, so we pass a lightweight synthetic object\n const synthetic = (typeof CloseEvent !== 'undefined'\n ? new CloseEvent('close', { code: 1001, reason: 'pooled disconnect' })\n : ({ code: 1001, reason: 'pooled disconnect' } as any)) as any;\n onClose?.(synthetic);\n }\n if (status.error) {\n if (lastErrorRef.current !== status.error) {\n lastErrorRef.current = status.error;\n onError?.(status.error);\n }\n } else {\n lastErrorRef.current = null;\n }\n });\n\n // Wire transport for send* APIs\n transportRef.current = {\n sendEvent: (name, payload, msgId) => pool.sendEvent(channel, name, payload, msgId),\n sendUpdate: (payload, msgId) => pool.sendUpdate(channel, payload, msgId),\n sendDiff: (patch, msgId) => pool.sendDiff(channel, patch, msgId),\n };\n\n return () => {\n unsubStatus();\n unsubChannel();\n transportRef.current = null;\n setIsConnected(false);\n setError(null);\n };\n }\n\n // --- Legacy mode (1 WS per channel) ---\n const handleConnect = () => {\n setIsConnected(true);\n setError(null);\n if (!wasConnectedRef.current) {\n wasConnectedRef.current = true;\n loadInitialData();\n }\n onOpen?.();\n };\n\n const handleDisconnect = (event?: CloseEvent) => {\n setIsConnected(false);\n wasConnectedRef.current = false;\n onClose?.(event!);\n };\n\n const handleError = (err: Error) => {\n setError(err);\n onError?.(err);\n };\n\n const client = createViraClient({\n url: apiUrl,\n channel,\n onMessage: handleMessage,\n onConnect: handleConnect,\n onDisconnect: handleDisconnect,\n onError: handleError,\n session: sessionRef.current,\n authToken,\n onSessionChange: (newSession: string | null) => {\n sessionRef.current = newSession;\n },\n });\n\n clientRef.current = client;\n transportRef.current = {\n sendEvent: (name, payload, msgId) => client.sendEvent(name, payload, msgId),\n sendUpdate: (payload, msgId) => client.sendUpdate(payload, msgId),\n sendDiff: (patch, msgId) => client.sendDiff(patch, msgId),\n };\n\n return () => {\n client.close();\n clientRef.current = null;\n transportRef.current = null;\n setIsConnected(false);\n setError(null);\n };\n }, [channel, apiUrl, authToken, onOpen, onClose, onError, useDeepMerge, pool]);\n\n // Generate msgId if enabled\n const generateMsgId = useCallback((): string | undefined => {\n if (!enableMsgId) return undefined;\n msgIdCounterRef.current++;\n return `${channel}:${Date.now()}:${msgIdCounterRef.current}`;\n }, [channel, enableMsgId]);\n\n const sendEvent = useCallback(\n (name: string, payload: any, msgId?: string) => {\n transportRef.current?.sendEvent(name, payload, msgId ?? generateMsgId());\n },\n [generateMsgId]\n );\n\n const sendUpdate = useCallback(\n (payload: T, msgId?: string) => {\n transportRef.current?.sendUpdate(payload, msgId ?? generateMsgId());\n },\n [generateMsgId]\n );\n\n const sendDiff = useCallback(\n (patch: Partial<T>, msgId?: string) => {\n transportRef.current?.sendDiff(patch, msgId ?? generateMsgId());\n },\n [generateMsgId]\n );\n\n return {\n data,\n sendEvent,\n sendUpdate,\n sendDiff,\n isConnected,\n error,\n };\n}\n\n/**\n * Legacy hook - use useViraState instead.\n * @deprecated Use useViraState with options instead\n */\nexport function useViraStream<T = any, C extends string = string>(\n channel: C,\n options?: UseViraStateOptions<T>\n) {\n return useViraState<T, C>(channel, { ...options, initial: null });\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,50 +1,51 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@vira-ui/react",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "Vira Framework - React hooks for Vira Reactive Protocol",
|
|
5
|
-
"author": "Vira Team",
|
|
6
|
-
"license": "MIT",
|
|
7
|
-
"repository": {
|
|
8
|
-
"type": "git",
|
|
9
|
-
"url": "https://github.com/skrolikov/vira-core"
|
|
10
|
-
},
|
|
11
|
-
"keywords": [
|
|
12
|
-
"vira",
|
|
13
|
-
"react",
|
|
14
|
-
"hooks",
|
|
15
|
-
"vrp",
|
|
16
|
-
"reactive",
|
|
17
|
-
"websocket"
|
|
18
|
-
],
|
|
19
|
-
"main": "./dist/index.js",
|
|
20
|
-
"module": "./dist/index.mjs",
|
|
21
|
-
"types": "./dist/index.d.ts",
|
|
22
|
-
"exports": {
|
|
23
|
-
".": {
|
|
24
|
-
"types": "./dist/index.d.ts",
|
|
25
|
-
"import": "./dist/index.mjs",
|
|
26
|
-
"require": "./dist/index.js"
|
|
27
|
-
}
|
|
28
|
-
},
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
},
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@vira-ui/react",
|
|
3
|
+
"version": "1.1.1",
|
|
4
|
+
"description": "Vira Framework - React hooks for Vira Reactive Protocol",
|
|
5
|
+
"author": "Vira Team",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/skrolikov/vira-core"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"vira",
|
|
13
|
+
"react",
|
|
14
|
+
"hooks",
|
|
15
|
+
"vrp",
|
|
16
|
+
"reactive",
|
|
17
|
+
"websocket"
|
|
18
|
+
],
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"module": "./dist/index.mjs",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.mjs",
|
|
26
|
+
"require": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"react": "^18.2.0",
|
|
31
|
+
"@vira-ui/core": "1.1.1"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": "^18.2.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/react": "^18.2.0",
|
|
38
|
+
"tsup": "^8.0.0",
|
|
39
|
+
"typescript": "^5.3.3"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist"
|
|
43
|
+
],
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsup",
|
|
49
|
+
"dev": "tsup --watch"
|
|
50
|
+
}
|
|
51
|
+
}
|