@vira-ui/react 1.0.2 → 1.1.0
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 +34 -715
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +66 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +71 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,746 +1,65 @@
|
|
|
1
1
|
# @vira-ui/react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React-хуки для работы с Vira Reactive Protocol (VRP). Синхронизация состояния между клиентом и сервером через WebSocket.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Зачем нужен
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
[](LICENSE)
|
|
9
|
-
[](https://www.typescriptlang.org/)
|
|
7
|
+
Пакет предоставляет простой способ подключения React-компонентов к серверу через WebSocket:
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
- **Автоматическая синхронизация** — состояние обновляется при изменениях на сервере
|
|
10
|
+
- **Двусторонняя связь** — можно отправлять события и обновления на сервер
|
|
11
|
+
- **Diff-патчи** — обновляются только изменённые части данных
|
|
12
|
+
- **Переподключение** — автоматическое восстановление соединения
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
## Основные возможности
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
### useViraState
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
## 🎯 Что это?
|
|
20
|
-
|
|
21
|
-
**@vira-ui/react** — это пакет React хуков для работы с **Vira Reactive Protocol (VRP)**. Он предоставляет простой способ синхронизации состояния между клиентом и сервером в реальном времени через WebSocket.
|
|
22
|
-
|
|
23
|
-
### Основные возможности
|
|
24
|
-
|
|
25
|
-
- ✅ **useViraState** — универсальный хук для работы с VRP каналами
|
|
26
|
-
- ✅ **Автоматическая синхронизация** — состояние обновляется автоматически при изменениях на сервере
|
|
27
|
-
- ✅ **Типизация** — полная поддержка TypeScript
|
|
28
|
-
- ✅ **Idempotency** — поддержка msgId для предотвращения дублирования сообщений
|
|
29
|
-
- ✅ **Deep merge** — умное слияние частичных обновлений
|
|
30
|
-
- ✅ **Reconnection** — автоматическое переподключение при разрыве связи
|
|
31
|
-
- ✅ **Session management** — управление сессиями для восстановления состояния
|
|
32
|
-
|
|
33
|
-
---
|
|
34
|
-
|
|
35
|
-
## 📦 Установка
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
npm install @vira-ui/react @vira-ui/core react
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
**Требования:**
|
|
42
|
-
- React 18.2.0+
|
|
43
|
-
- @vira-ui/core 1.0.1+
|
|
44
|
-
- TypeScript 5.3+ (рекомендуется)
|
|
45
|
-
|
|
46
|
-
---
|
|
47
|
-
|
|
48
|
-
## 🚀 Быстрый старт
|
|
49
|
-
|
|
50
|
-
### 1. Базовое использование
|
|
51
|
-
|
|
52
|
-
```tsx
|
|
53
|
-
import { useViraState } from "@vira-ui/react";
|
|
54
|
-
|
|
55
|
-
function MyComponent() {
|
|
56
|
-
const { data, sendUpdate, sendDiff, sendEvent } = useViraState<User>(
|
|
57
|
-
"user:123"
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
if (!data) {
|
|
61
|
-
return <div>Loading...</div>;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<div>
|
|
66
|
-
<h1>{data.name}</h1>
|
|
67
|
-
<button onClick={() => sendDiff({ name: "New Name" })}>
|
|
68
|
-
Обновить имя
|
|
69
|
-
</button>
|
|
70
|
-
</div>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### 2. С опциями
|
|
76
|
-
|
|
77
|
-
```tsx
|
|
78
|
-
import { useViraState } from "@vira-ui/react";
|
|
79
|
-
|
|
80
|
-
function MyComponent() {
|
|
81
|
-
const { data, sendUpdate, sendDiff, isConnected, error } = useViraState<User>(
|
|
82
|
-
"user:123",
|
|
83
|
-
{
|
|
84
|
-
initial: { name: "Guest", email: "" },
|
|
85
|
-
enableMsgId: true,
|
|
86
|
-
deepMerge: true,
|
|
87
|
-
onOpen: () => console.log("Connected"),
|
|
88
|
-
onClose: () => console.log("Disconnected"),
|
|
89
|
-
onError: (err) => console.error("Error:", err),
|
|
90
|
-
apiUrl: "http://localhost:8080",
|
|
91
|
-
authToken: "your-token",
|
|
92
|
-
}
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
if (error) {
|
|
96
|
-
return <div>Error: {error.message}</div>;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (!isConnected) {
|
|
100
|
-
return <div>Connecting...</div>;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return <div>{data?.name}</div>;
|
|
104
|
-
}
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### 3. С несколькими каналами
|
|
108
|
-
|
|
109
|
-
```tsx
|
|
110
|
-
import { useViraState } from "@vira-ui/react";
|
|
111
|
-
|
|
112
|
-
function Dashboard() {
|
|
113
|
-
const user = useViraState<User>("user:123");
|
|
114
|
-
const notifications = useViraState<Notification[]>("notifications:123");
|
|
115
|
-
const tasks = useViraState<Task[]>("tasks:123");
|
|
116
|
-
|
|
117
|
-
return (
|
|
118
|
-
<div>
|
|
119
|
-
<UserProfile data={user.data} />
|
|
120
|
-
<NotificationsList data={notifications.data} />
|
|
121
|
-
<TasksList data={tasks.data} />
|
|
122
|
-
</div>
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
---
|
|
128
|
-
|
|
129
|
-
## 📚 API Reference
|
|
130
|
-
|
|
131
|
-
### `useViraState<T>(channel, options?)`
|
|
132
|
-
|
|
133
|
-
Универсальный хук для работы с VRP каналами.
|
|
134
|
-
|
|
135
|
-
**Параметры:**
|
|
136
|
-
|
|
137
|
-
- `channel` (string) — имя канала (например, `"user:123"`, `"tasks:456"`)
|
|
138
|
-
- `options` (UseViraStateOptions, опционально) — опции хука
|
|
139
|
-
|
|
140
|
-
**Возвращает:**
|
|
141
|
-
|
|
142
|
-
```tsx
|
|
143
|
-
{
|
|
144
|
-
// Текущее состояние данных
|
|
145
|
-
data: T | null;
|
|
146
|
-
|
|
147
|
-
// Отправить событие на сервер
|
|
148
|
-
sendEvent: (name: string, payload: any, msgId?: string) => void;
|
|
149
|
-
|
|
150
|
-
// Отправить полное обновление (заменяет состояние)
|
|
151
|
-
sendUpdate: (payload: T, msgId?: string) => void;
|
|
152
|
-
|
|
153
|
-
// Отправить частичное обновление (сливается с текущим состоянием)
|
|
154
|
-
sendDiff: (patch: Partial<T>, msgId?: string) => void;
|
|
155
|
-
|
|
156
|
-
// Статус подключения
|
|
157
|
-
isConnected: boolean;
|
|
158
|
-
|
|
159
|
-
// Ошибка подключения, если есть
|
|
160
|
-
error: Error | null;
|
|
161
|
-
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
**Опции:**
|
|
165
|
-
|
|
166
|
-
```tsx
|
|
167
|
-
interface UseViraStateOptions<T> {
|
|
168
|
-
// Начальное значение состояния
|
|
169
|
-
initial?: T | null;
|
|
170
|
-
|
|
171
|
-
// Включить поддержку msgId для идемпотентности
|
|
172
|
-
enableMsgId?: boolean;
|
|
173
|
-
|
|
174
|
-
// Callback при открытии соединения
|
|
175
|
-
onOpen?: () => void;
|
|
176
|
-
|
|
177
|
-
// Callback при закрытии соединения
|
|
178
|
-
onClose?: (event: CloseEvent) => void;
|
|
179
|
-
|
|
180
|
-
// Callback при ошибке соединения
|
|
181
|
-
onError?: (error: Error) => void;
|
|
182
|
-
|
|
183
|
-
// Использовать deep merge для diff патчей (по умолчанию: true)
|
|
184
|
-
deepMerge?: boolean;
|
|
185
|
-
|
|
186
|
-
// URL API (по умолчанию: VITE_API_URL или 'http://localhost:8080')
|
|
187
|
-
apiUrl?: string;
|
|
188
|
-
|
|
189
|
-
// Auth token для handshake
|
|
190
|
-
authToken?: string;
|
|
191
|
-
}
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
---
|
|
195
|
-
|
|
196
|
-
## 🎯 Примеры использования
|
|
197
|
-
|
|
198
|
-
### Пример 1: Простое обновление состояния
|
|
18
|
+
Подключается к каналу и синхронизирует состояние:
|
|
199
19
|
|
|
200
20
|
```tsx
|
|
201
|
-
import { useViraState } from
|
|
202
|
-
|
|
203
|
-
interface Counter {
|
|
204
|
-
count: number;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function CounterComponent() {
|
|
208
|
-
const { data, sendDiff } = useViraState<Counter>("counter:demo");
|
|
209
|
-
|
|
210
|
-
const increment = () => {
|
|
211
|
-
sendDiff({ count: (data?.count || 0) + 1 });
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
return (
|
|
215
|
-
<div>
|
|
216
|
-
<p>Count: {data?.count || 0}</p>
|
|
217
|
-
<button onClick={increment}>Increment</button>
|
|
218
|
-
</div>
|
|
219
|
-
);
|
|
220
|
-
}
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
### Пример 2: Работа с пользователем
|
|
224
|
-
|
|
225
|
-
```tsx
|
|
226
|
-
import { useViraState } from "@vira-ui/react";
|
|
227
|
-
|
|
228
|
-
interface User {
|
|
229
|
-
id: string;
|
|
230
|
-
name: string;
|
|
231
|
-
email: string;
|
|
232
|
-
avatar?: string;
|
|
233
|
-
}
|
|
21
|
+
import { useViraState } from '@vira-ui/react';
|
|
234
22
|
|
|
235
23
|
function UserProfile({ userId }: { userId: string }) {
|
|
236
|
-
const { data,
|
|
24
|
+
const { data, sendUpdate, sendDiff, isConnected } = useViraState<User>(
|
|
237
25
|
`user:${userId}`,
|
|
238
26
|
{
|
|
239
|
-
initial: {
|
|
240
|
-
|
|
27
|
+
initial: { name: 'Guest' },
|
|
28
|
+
onOpen: () => console.log('Connected'),
|
|
29
|
+
deepMerge: true
|
|
241
30
|
}
|
|
242
31
|
);
|
|
243
32
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
const updateEmail = (newEmail: string) => {
|
|
249
|
-
sendDiff({ email: newEmail });
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
const uploadAvatar = (avatarUrl: string) => {
|
|
253
|
-
sendEvent("user.avatar.upload", { avatarUrl });
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
if (!isConnected) {
|
|
257
|
-
return <div>Connecting...</div>;
|
|
258
|
-
}
|
|
33
|
+
if (!isConnected) return <div>Connecting...</div>;
|
|
34
|
+
if (!data) return <div>Loading...</div>;
|
|
259
35
|
|
|
260
36
|
return (
|
|
261
37
|
<div>
|
|
262
|
-
<
|
|
263
|
-
<
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
/>
|
|
267
|
-
<input
|
|
268
|
-
value={data?.email || ""}
|
|
269
|
-
onChange={(e) => updateEmail(e.target.value)}
|
|
270
|
-
/>
|
|
271
|
-
</div>
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
### Пример 3: Kanban доска
|
|
277
|
-
|
|
278
|
-
```tsx
|
|
279
|
-
import { useViraState } from "@vira-ui/react";
|
|
280
|
-
|
|
281
|
-
interface KanbanBoard {
|
|
282
|
-
id: string;
|
|
283
|
-
title: string;
|
|
284
|
-
columns: Column[];
|
|
285
|
-
cards: Record<string, Card>;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function KanbanBoard({ boardId }: { boardId: string }) {
|
|
289
|
-
const { data, sendEvent, sendDiff } = useViraState<KanbanBoard>(
|
|
290
|
-
`kanban:${boardId}`,
|
|
291
|
-
{
|
|
292
|
-
enableMsgId: true,
|
|
293
|
-
deepMerge: true,
|
|
294
|
-
}
|
|
295
|
-
);
|
|
296
|
-
|
|
297
|
-
const createCard = (columnId: string, title: string) => {
|
|
298
|
-
sendEvent("kanban.card.create", {
|
|
299
|
-
boardId,
|
|
300
|
-
columnId,
|
|
301
|
-
title,
|
|
302
|
-
at: Date.now(),
|
|
303
|
-
});
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
const moveCard = (
|
|
307
|
-
cardId: string,
|
|
308
|
-
fromColumnId: string,
|
|
309
|
-
toColumnId: string,
|
|
310
|
-
newOrder: number
|
|
311
|
-
) => {
|
|
312
|
-
sendEvent("kanban.card.move", {
|
|
313
|
-
boardId,
|
|
314
|
-
cardId,
|
|
315
|
-
fromColumnId,
|
|
316
|
-
toColumnId,
|
|
317
|
-
newOrder,
|
|
318
|
-
at: Date.now(),
|
|
319
|
-
});
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
const updateCard = (cardId: string, updates: Partial<Card>) => {
|
|
323
|
-
sendDiff({
|
|
324
|
-
cards: {
|
|
325
|
-
[cardId]: updates,
|
|
326
|
-
},
|
|
327
|
-
});
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
if (!data) {
|
|
331
|
-
return <div>Loading board...</div>;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
return (
|
|
335
|
-
<div>
|
|
336
|
-
<h1>{data.title}</h1>
|
|
337
|
-
{data.columns.map((column) => (
|
|
338
|
-
<Column
|
|
339
|
-
key={column.id}
|
|
340
|
-
column={column}
|
|
341
|
-
cards={getColumnCards(data, column.id)}
|
|
342
|
-
onCreateCard={(title) => createCard(column.id, title)}
|
|
343
|
-
onMoveCard={moveCard}
|
|
344
|
-
onUpdateCard={updateCard}
|
|
345
|
-
/>
|
|
346
|
-
))}
|
|
347
|
-
</div>
|
|
348
|
-
);
|
|
349
|
-
}
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
### Пример 4: Чат приложение
|
|
353
|
-
|
|
354
|
-
```tsx
|
|
355
|
-
import { useViraState } from "@vira-ui/react";
|
|
356
|
-
|
|
357
|
-
interface Chat {
|
|
358
|
-
messages: Message[];
|
|
359
|
-
participants: User[];
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function ChatRoom({ roomId }: { roomId: string }) {
|
|
363
|
-
const { data, sendEvent, sendDiff } = useViraState<Chat>(`chat:${roomId}`, {
|
|
364
|
-
initial: { messages: [], participants: [] },
|
|
365
|
-
enableMsgId: true,
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
const sendMessage = (text: string) => {
|
|
369
|
-
sendEvent("chat.message.send", {
|
|
370
|
-
roomId,
|
|
371
|
-
text,
|
|
372
|
-
timestamp: Date.now(),
|
|
373
|
-
});
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
const addParticipant = (user: User) => {
|
|
377
|
-
sendDiff({
|
|
378
|
-
participants: [...(data?.participants || []), user],
|
|
379
|
-
});
|
|
380
|
-
};
|
|
381
|
-
|
|
382
|
-
return (
|
|
383
|
-
<div>
|
|
384
|
-
<div>
|
|
385
|
-
{data?.messages.map((msg) => (
|
|
386
|
-
<MessageBubble key={msg.id} message={msg} />
|
|
387
|
-
))}
|
|
388
|
-
</div>
|
|
389
|
-
<MessageInput onSend={sendMessage} />
|
|
390
|
-
</div>
|
|
391
|
-
);
|
|
392
|
-
}
|
|
393
|
-
```
|
|
394
|
-
|
|
395
|
-
### Пример 5: Редактирование документа
|
|
396
|
-
|
|
397
|
-
```tsx
|
|
398
|
-
import { useViraState } from "@vira-ui/react";
|
|
399
|
-
|
|
400
|
-
interface Document {
|
|
401
|
-
id: string;
|
|
402
|
-
title: string;
|
|
403
|
-
content: string;
|
|
404
|
-
version: number;
|
|
405
|
-
updatedAt: number;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function DocumentEditor({ docId }: { docId: string }) {
|
|
409
|
-
const { data, sendDiff, isConnected } = useViraState<Document>(
|
|
410
|
-
`document:${docId}`,
|
|
411
|
-
{
|
|
412
|
-
enableMsgId: true,
|
|
413
|
-
deepMerge: true,
|
|
414
|
-
onOpen: () => console.log("Document loaded"),
|
|
415
|
-
}
|
|
416
|
-
);
|
|
417
|
-
|
|
418
|
-
const updateTitle = (title: string) => {
|
|
419
|
-
sendDiff({
|
|
420
|
-
title,
|
|
421
|
-
updatedAt: Date.now(),
|
|
422
|
-
});
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
const updateContent = (content: string) => {
|
|
426
|
-
sendDiff({
|
|
427
|
-
content,
|
|
428
|
-
updatedAt: Date.now(),
|
|
429
|
-
});
|
|
430
|
-
};
|
|
431
|
-
|
|
432
|
-
if (!isConnected || !data) {
|
|
433
|
-
return <div>Loading document...</div>;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return (
|
|
437
|
-
<div>
|
|
438
|
-
<input
|
|
439
|
-
value={data.title}
|
|
440
|
-
onChange={(e) => updateTitle(e.target.value)}
|
|
441
|
-
/>
|
|
442
|
-
<textarea
|
|
443
|
-
value={data.content}
|
|
444
|
-
onChange={(e) => updateContent(e.target.value)}
|
|
445
|
-
/>
|
|
446
|
-
<div>Version: {data.version}</div>
|
|
38
|
+
<h1>{data.name}</h1>
|
|
39
|
+
<button onClick={() => sendDiff({ name: 'New Name' })}>
|
|
40
|
+
Update Name
|
|
41
|
+
</button>
|
|
447
42
|
</div>
|
|
448
43
|
);
|
|
449
44
|
}
|
|
450
45
|
```
|
|
451
46
|
|
|
452
|
-
###
|
|
453
|
-
|
|
454
|
-
```tsx
|
|
455
|
-
import { useViraState } from "@vira-ui/react";
|
|
456
|
-
|
|
457
|
-
function RobustComponent() {
|
|
458
|
-
const { data, sendDiff, isConnected, error } = useViraState<MyData>(
|
|
459
|
-
"my:channel",
|
|
460
|
-
{
|
|
461
|
-
onError: (err) => {
|
|
462
|
-
console.error("Connection error:", err);
|
|
463
|
-
// Можно отправить в систему мониторинга
|
|
464
|
-
// sendToMonitoring(err);
|
|
465
|
-
},
|
|
466
|
-
onClose: (event) => {
|
|
467
|
-
console.log("Connection closed:", event.code, event.reason);
|
|
468
|
-
// Можно показать уведомление пользователю
|
|
469
|
-
},
|
|
470
|
-
}
|
|
471
|
-
);
|
|
472
|
-
|
|
473
|
-
if (error) {
|
|
474
|
-
return (
|
|
475
|
-
<div>
|
|
476
|
-
<p>Ошибка подключения: {error.message}</p>
|
|
477
|
-
<button onClick={() => window.location.reload()}>
|
|
478
|
-
Перезагрузить
|
|
479
|
-
</button>
|
|
480
|
-
</div>
|
|
481
|
-
);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
if (!isConnected) {
|
|
485
|
-
return <div>Подключение...</div>;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
return <div>{/* Ваш контент */}</div>;
|
|
489
|
-
}
|
|
490
|
-
```
|
|
491
|
-
|
|
492
|
-
---
|
|
493
|
-
|
|
494
|
-
## 🔧 Конфигурация
|
|
495
|
-
|
|
496
|
-
### Переменные окружения
|
|
497
|
-
|
|
498
|
-
```env
|
|
499
|
-
# URL API сервера
|
|
500
|
-
VITE_API_URL=http://localhost:8080
|
|
501
|
-
|
|
502
|
-
# Auth token для подключения
|
|
503
|
-
VITE_AUTH_TOKEN=your-secret-token
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
### Программная конфигурация
|
|
507
|
-
|
|
508
|
-
```tsx
|
|
509
|
-
import { useViraState } from "@vira-ui/react";
|
|
510
|
-
|
|
511
|
-
function MyComponent() {
|
|
512
|
-
const { data } = useViraState("my:channel", {
|
|
513
|
-
apiUrl: "https://api.example.com",
|
|
514
|
-
authToken: "your-token",
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
---
|
|
520
|
-
|
|
521
|
-
## 🎯 Vira Reactive Protocol (VRP)
|
|
522
|
-
|
|
523
|
-
### Типы сообщений
|
|
524
|
-
|
|
525
|
-
VRP поддерживает следующие типы сообщений:
|
|
526
|
-
|
|
527
|
-
#### 1. `update` — полное обновление состояния
|
|
528
|
-
|
|
529
|
-
```tsx
|
|
530
|
-
sendUpdate({ name: "John", age: 30 });
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
#### 2. `diff` — частичное обновление
|
|
534
|
-
|
|
535
|
-
```tsx
|
|
536
|
-
sendDiff({ name: "Jane" }); // Обновит только name
|
|
537
|
-
```
|
|
538
|
-
|
|
539
|
-
#### 3. `event` — событие
|
|
540
|
-
|
|
541
|
-
```tsx
|
|
542
|
-
sendEvent("user.created", { userId: "123" });
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
### Каналы
|
|
546
|
-
|
|
547
|
-
Каналы используются для изоляции данных:
|
|
548
|
-
|
|
549
|
-
```tsx
|
|
550
|
-
// Пользователь
|
|
551
|
-
"user:123"
|
|
552
|
-
|
|
553
|
-
// Задачи
|
|
554
|
-
"tasks:456"
|
|
555
|
-
|
|
556
|
-
// Уведомления
|
|
557
|
-
"notifications:123"
|
|
558
|
-
|
|
559
|
-
// Демо канал
|
|
560
|
-
"demo"
|
|
561
|
-
```
|
|
562
|
-
|
|
563
|
-
### Idempotency (msgId)
|
|
564
|
-
|
|
565
|
-
Для предотвращения дублирования сообщений используйте `enableMsgId`:
|
|
566
|
-
|
|
567
|
-
```tsx
|
|
568
|
-
const { sendUpdate } = useViraState("my:channel", {
|
|
569
|
-
enableMsgId: true, // Автоматически генерирует msgId
|
|
570
|
-
});
|
|
571
|
-
|
|
572
|
-
// Или вручную
|
|
573
|
-
sendUpdate(data, "custom-msg-id");
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
### Deep Merge
|
|
577
|
-
|
|
578
|
-
По умолчанию используется deep merge для diff обновлений:
|
|
579
|
-
|
|
580
|
-
```tsx
|
|
581
|
-
// Текущее состояние
|
|
582
|
-
{ user: { name: "John", age: 30 } }
|
|
583
|
-
|
|
584
|
-
// Отправляем diff
|
|
585
|
-
sendDiff({ user: { name: "Jane" } });
|
|
586
|
-
|
|
587
|
-
// Результат (deep merge)
|
|
588
|
-
{ user: { name: "Jane", age: 30 } }
|
|
589
|
-
```
|
|
590
|
-
|
|
591
|
-
Для shallow merge:
|
|
592
|
-
|
|
593
|
-
```tsx
|
|
594
|
-
useViraState("my:channel", {
|
|
595
|
-
deepMerge: false, // Shallow merge
|
|
596
|
-
});
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
---
|
|
600
|
-
|
|
601
|
-
## 🔥 Best Practices
|
|
602
|
-
|
|
603
|
-
### 1. Используйте типизацию
|
|
604
|
-
|
|
605
|
-
```tsx
|
|
606
|
-
// ✅ Хорошо
|
|
607
|
-
interface User {
|
|
608
|
-
id: string;
|
|
609
|
-
name: string;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
const { data } = useViraState<User>("user:123");
|
|
613
|
-
|
|
614
|
-
// ❌ Плохо
|
|
615
|
-
const { data } = useViraState("user:123");
|
|
616
|
-
```
|
|
617
|
-
|
|
618
|
-
### 2. Используйте enableMsgId для критичных операций
|
|
619
|
-
|
|
620
|
-
```tsx
|
|
621
|
-
// ✅ Хорошо
|
|
622
|
-
useViraState("payment:123", {
|
|
623
|
-
enableMsgId: true, // Предотвращает дублирование платежей
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
// ❌ Плохо
|
|
627
|
-
useViraState("payment:123"); // Может привести к дублированию
|
|
628
|
-
```
|
|
629
|
-
|
|
630
|
-
### 3. Обрабатывайте ошибки
|
|
631
|
-
|
|
632
|
-
```tsx
|
|
633
|
-
// ✅ Хорошо
|
|
634
|
-
const { data, error, isConnected } = useViraState("my:channel", {
|
|
635
|
-
onError: (err) => console.error(err),
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
if (error) {
|
|
639
|
-
return <ErrorComponent error={error} />;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// ❌ Плохо
|
|
643
|
-
const { data } = useViraState("my:channel");
|
|
644
|
-
// Нет обработки ошибок
|
|
645
|
-
```
|
|
646
|
-
|
|
647
|
-
### 4. Используйте initial для лучшего UX
|
|
648
|
-
|
|
649
|
-
```tsx
|
|
650
|
-
// ✅ Хорошо
|
|
651
|
-
useViraState("user:123", {
|
|
652
|
-
initial: { name: "Loading...", email: "" },
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
// ❌ Плохо
|
|
656
|
-
useViraState("user:123");
|
|
657
|
-
// Пользователь видит пустой экран
|
|
658
|
-
```
|
|
659
|
-
|
|
660
|
-
### 5. Используйте sendDiff для частичных обновлений
|
|
661
|
-
|
|
662
|
-
```tsx
|
|
663
|
-
// ✅ Хорошо
|
|
664
|
-
sendDiff({ name: "New Name" }); // Обновляет только name
|
|
665
|
-
|
|
666
|
-
// ❌ Плохо
|
|
667
|
-
sendUpdate({ ...data, name: "New Name" }); // Отправляет весь объект
|
|
668
|
-
```
|
|
669
|
-
|
|
670
|
-
---
|
|
671
|
-
|
|
672
|
-
## ❓ FAQ
|
|
673
|
-
|
|
674
|
-
**Q: В чём разница между `sendUpdate` и `sendDiff`?**
|
|
675
|
-
|
|
676
|
-
A: `sendUpdate` заменяет всё состояние целиком, а `sendDiff` сливает изменения с текущим состоянием. Используйте `sendDiff` для частичных обновлений.
|
|
677
|
-
|
|
678
|
-
**Q: Что такое msgId и зачем он нужен?**
|
|
679
|
-
|
|
680
|
-
A: `msgId` — это уникальный идентификатор сообщения для предотвращения дублирования. Включите `enableMsgId: true` для критичных операций.
|
|
681
|
-
|
|
682
|
-
**Q: Как работает deep merge?**
|
|
683
|
-
|
|
684
|
-
A: Deep merge рекурсивно сливает объекты, сохраняя вложенные структуры. Например, `{ user: { name: "Jane" } }` обновит только `name`, не затрагивая другие поля `user`.
|
|
685
|
-
|
|
686
|
-
**Q: Можно ли использовать несколько каналов одновременно?**
|
|
687
|
-
|
|
688
|
-
A: Да! Просто вызовите `useViraState` несколько раз с разными каналами.
|
|
689
|
-
|
|
690
|
-
**Q: Что происходит при разрыве соединения?**
|
|
691
|
-
|
|
692
|
-
A: Клиент автоматически пытается переподключиться. Вы можете отслеживать статус через `isConnected` и обрабатывать через `onClose` callback.
|
|
693
|
-
|
|
694
|
-
**Q: Как работает сессия?**
|
|
695
|
-
|
|
696
|
-
A: Сессия сохраняется между переподключениями, что позволяет восстановить подписки и состояние.
|
|
697
|
-
|
|
698
|
-
**Q: Можно ли использовать без TypeScript?**
|
|
699
|
-
|
|
700
|
-
A: Да, но TypeScript рекомендуется для лучшего DX и безопасности типов.
|
|
701
|
-
|
|
702
|
-
---
|
|
703
|
-
|
|
704
|
-
## 🛣️ Roadmap
|
|
705
|
-
|
|
706
|
-
### v1.1 (В разработке)
|
|
707
|
-
- [ ] Поддержка batch операций
|
|
708
|
-
- [ ] Оптимистичные обновления
|
|
709
|
-
- [ ] Offline queue
|
|
710
|
-
- [ ] Compression для больших payload
|
|
711
|
-
|
|
712
|
-
### v1.2 (Планируется)
|
|
713
|
-
- [ ] DevTools интеграция
|
|
714
|
-
- [ ] Метрики и мониторинг
|
|
715
|
-
- [ ] Поддержка подписок на несколько каналов
|
|
716
|
-
- [ ] WebRTC fallback
|
|
717
|
-
|
|
718
|
-
---
|
|
719
|
-
|
|
720
|
-
## 📄 License
|
|
721
|
-
|
|
722
|
-
MIT
|
|
723
|
-
|
|
724
|
-
---
|
|
725
|
-
|
|
726
|
-
## 🤝 Contributing
|
|
727
|
-
|
|
728
|
-
Мы приветствуем вклад! Пожалуйста, прочитайте [CONTRIBUTING.md](../../CONTRIBUTING.md) для деталей.
|
|
729
|
-
|
|
730
|
-
---
|
|
731
|
-
|
|
732
|
-
## 📞 Support
|
|
733
|
-
|
|
734
|
-
- **GitHub Issues**: [Создать issue](https://github.com/skrolikov/vira-core/issues)
|
|
735
|
-
- **Discussions**: [Обсуждения](https://github.com/skrolikov/vira-core/discussions)
|
|
47
|
+
### Отправка данных
|
|
736
48
|
|
|
737
|
-
|
|
49
|
+
- `sendUpdate(payload)` — полная замена состояния
|
|
50
|
+
- `sendDiff(patch)` — частичное обновление (merge)
|
|
51
|
+
- `sendEvent(name, payload)` — отправка события
|
|
738
52
|
|
|
739
|
-
|
|
53
|
+
### Опции
|
|
740
54
|
|
|
741
|
-
|
|
55
|
+
- `initial` — начальное значение
|
|
56
|
+
- `apiUrl` — URL сервера (по умолчанию из `VITE_API_URL`)
|
|
57
|
+
- `authToken` — токен авторизации
|
|
58
|
+
- `deepMerge` — глубокое слияние для diff-патчей
|
|
59
|
+
- `enableMsgId` — поддержка idempotency
|
|
60
|
+
- `onOpen`, `onClose`, `onError` — колбэки событий соединения
|
|
742
61
|
|
|
743
|
-
|
|
62
|
+
## Использование
|
|
744
63
|
|
|
745
|
-
|
|
64
|
+
Требует сервер с поддержкой Vira Reactive Protocol. Обычно используется вместе с `@vira-ui/core` и `@vira-ui/bindings-react`.
|
|
746
65
|
|
package/dist/index.d.mts
CHANGED
|
@@ -15,6 +15,10 @@ interface UseViraStateOptions<T = any> {
|
|
|
15
15
|
apiUrl?: string;
|
|
16
16
|
/** Auth token for handshake */
|
|
17
17
|
authToken?: string;
|
|
18
|
+
/** Disable connection pooling (fallback to 1 WS per channel). Default: false */
|
|
19
|
+
disablePooling?: boolean;
|
|
20
|
+
/** Enable debug logs for VRP (console.debug). Default: env VITE_VRP_DEBUG === 'true' */
|
|
21
|
+
debug?: boolean;
|
|
18
22
|
}
|
|
19
23
|
/**
|
|
20
24
|
* Unified hook for Vira Reactive Protocol state management.
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ interface UseViraStateOptions<T = any> {
|
|
|
15
15
|
apiUrl?: string;
|
|
16
16
|
/** Auth token for handshake */
|
|
17
17
|
authToken?: string;
|
|
18
|
+
/** Disable connection pooling (fallback to 1 WS per channel). Default: false */
|
|
19
|
+
disablePooling?: boolean;
|
|
20
|
+
/** Enable debug logs for VRP (console.debug). Default: env VITE_VRP_DEBUG === 'true' */
|
|
21
|
+
debug?: boolean;
|
|
18
22
|
}
|
|
19
23
|
/**
|
|
20
24
|
* Unified hook for Vira Reactive Protocol state management.
|
package/dist/index.js
CHANGED
|
@@ -33,7 +33,7 @@ function useViraState(channel, initialOrOptions) {
|
|
|
33
33
|
if (initialOrOptions === null || initialOrOptions === void 0) {
|
|
34
34
|
return {};
|
|
35
35
|
}
|
|
36
|
-
if (typeof initialOrOptions === "object" && !Array.isArray(initialOrOptions) && ("enableMsgId" in initialOrOptions || "onOpen" in initialOrOptions || "onClose" in initialOrOptions || "onError" in initialOrOptions || "deepMerge" in initialOrOptions || "initial" in initialOrOptions || "apiUrl" in initialOrOptions || "authToken" in initialOrOptions)) {
|
|
36
|
+
if (typeof initialOrOptions === "object" && !Array.isArray(initialOrOptions) && ("enableMsgId" in initialOrOptions || "onOpen" in initialOrOptions || "onClose" in initialOrOptions || "onError" in initialOrOptions || "deepMerge" in initialOrOptions || "initial" in initialOrOptions || "apiUrl" in initialOrOptions || "authToken" in initialOrOptions || "disablePooling" in initialOrOptions || "debug" in initialOrOptions)) {
|
|
37
37
|
return initialOrOptions;
|
|
38
38
|
}
|
|
39
39
|
return { initial: initialOrOptions };
|
|
@@ -46,14 +46,19 @@ function useViraState(channel, initialOrOptions) {
|
|
|
46
46
|
onError,
|
|
47
47
|
deepMerge: useDeepMerge = true,
|
|
48
48
|
apiUrl: apiUrlOption,
|
|
49
|
-
authToken: authTokenOption
|
|
49
|
+
authToken: authTokenOption,
|
|
50
|
+
disablePooling = false,
|
|
51
|
+
debug: debugOption
|
|
50
52
|
} = options;
|
|
51
53
|
const [data, setData] = (0, import_react.useState)(initial);
|
|
52
54
|
const [isConnected, setIsConnected] = (0, import_react.useState)(false);
|
|
53
55
|
const [error, setError] = (0, import_react.useState)(null);
|
|
56
|
+
const transportRef = (0, import_react.useRef)(null);
|
|
54
57
|
const clientRef = (0, import_react.useRef)(null);
|
|
55
58
|
const sessionRef = (0, import_react.useRef)(null);
|
|
56
59
|
const msgIdCounterRef = (0, import_react.useRef)(0);
|
|
60
|
+
const wasConnectedRef = (0, import_react.useRef)(false);
|
|
61
|
+
const lastErrorRef = (0, import_react.useRef)(null);
|
|
57
62
|
const apiUrl = (0, import_react.useMemo)(() => {
|
|
58
63
|
if (apiUrlOption) return apiUrlOption;
|
|
59
64
|
try {
|
|
@@ -72,6 +77,19 @@ function useViraState(channel, initialOrOptions) {
|
|
|
72
77
|
return "";
|
|
73
78
|
}
|
|
74
79
|
}, [authTokenOption]);
|
|
80
|
+
const debug = (0, import_react.useMemo)(() => {
|
|
81
|
+
if (debugOption !== void 0) return Boolean(debugOption);
|
|
82
|
+
try {
|
|
83
|
+
const env = globalThis.import?.meta?.env || globalThis.process?.env;
|
|
84
|
+
return String(env?.VITE_VRP_DEBUG || "").toLowerCase() === "true";
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}, [debugOption]);
|
|
89
|
+
const pool = (0, import_react.useMemo)(() => {
|
|
90
|
+
if (disablePooling) return null;
|
|
91
|
+
return (0, import_core.getViraConnectionPool)({ url: apiUrl, authToken, debug });
|
|
92
|
+
}, [disablePooling, apiUrl, authToken, debug]);
|
|
75
93
|
(0, import_react.useEffect)(() => {
|
|
76
94
|
if (!channel) return;
|
|
77
95
|
const handleMessage = (msg) => {
|
|
@@ -101,6 +119,42 @@ function useViraState(channel, initialOrOptions) {
|
|
|
101
119
|
break;
|
|
102
120
|
}
|
|
103
121
|
};
|
|
122
|
+
if (pool) {
|
|
123
|
+
const unsubChannel = pool.subscribe(channel, handleMessage);
|
|
124
|
+
const unsubStatus = pool.onStatus((status) => {
|
|
125
|
+
setError(status.error);
|
|
126
|
+
setIsConnected(status.connected);
|
|
127
|
+
if (status.connected && !wasConnectedRef.current) {
|
|
128
|
+
wasConnectedRef.current = true;
|
|
129
|
+
onOpen?.();
|
|
130
|
+
}
|
|
131
|
+
if (!status.connected && wasConnectedRef.current) {
|
|
132
|
+
wasConnectedRef.current = false;
|
|
133
|
+
const synthetic = typeof CloseEvent !== "undefined" ? new CloseEvent("close", { code: 1001, reason: "pooled disconnect" }) : { code: 1001, reason: "pooled disconnect" };
|
|
134
|
+
onClose?.(synthetic);
|
|
135
|
+
}
|
|
136
|
+
if (status.error) {
|
|
137
|
+
if (lastErrorRef.current !== status.error) {
|
|
138
|
+
lastErrorRef.current = status.error;
|
|
139
|
+
onError?.(status.error);
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
lastErrorRef.current = null;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
transportRef.current = {
|
|
146
|
+
sendEvent: (name, payload, msgId) => pool.sendEvent(channel, name, payload, msgId),
|
|
147
|
+
sendUpdate: (payload, msgId) => pool.sendUpdate(channel, payload, msgId),
|
|
148
|
+
sendDiff: (patch, msgId) => pool.sendDiff(channel, patch, msgId)
|
|
149
|
+
};
|
|
150
|
+
return () => {
|
|
151
|
+
unsubStatus();
|
|
152
|
+
unsubChannel();
|
|
153
|
+
transportRef.current = null;
|
|
154
|
+
setIsConnected(false);
|
|
155
|
+
setError(null);
|
|
156
|
+
};
|
|
157
|
+
}
|
|
104
158
|
const handleConnect = () => {
|
|
105
159
|
setIsConnected(true);
|
|
106
160
|
setError(null);
|
|
@@ -128,13 +182,19 @@ function useViraState(channel, initialOrOptions) {
|
|
|
128
182
|
}
|
|
129
183
|
});
|
|
130
184
|
clientRef.current = client;
|
|
185
|
+
transportRef.current = {
|
|
186
|
+
sendEvent: (name, payload, msgId) => client.sendEvent(name, payload, msgId),
|
|
187
|
+
sendUpdate: (payload, msgId) => client.sendUpdate(payload, msgId),
|
|
188
|
+
sendDiff: (patch, msgId) => client.sendDiff(patch, msgId)
|
|
189
|
+
};
|
|
131
190
|
return () => {
|
|
132
191
|
client.close();
|
|
133
192
|
clientRef.current = null;
|
|
193
|
+
transportRef.current = null;
|
|
134
194
|
setIsConnected(false);
|
|
135
195
|
setError(null);
|
|
136
196
|
};
|
|
137
|
-
}, [channel, apiUrl, authToken, onOpen, onClose, onError, useDeepMerge]);
|
|
197
|
+
}, [channel, apiUrl, authToken, onOpen, onClose, onError, useDeepMerge, pool]);
|
|
138
198
|
const generateMsgId = (0, import_react.useCallback)(() => {
|
|
139
199
|
if (!enableMsgId) return void 0;
|
|
140
200
|
msgIdCounterRef.current++;
|
|
@@ -142,19 +202,19 @@ function useViraState(channel, initialOrOptions) {
|
|
|
142
202
|
}, [channel, enableMsgId]);
|
|
143
203
|
const sendEvent = (0, import_react.useCallback)(
|
|
144
204
|
(name, payload, msgId) => {
|
|
145
|
-
|
|
205
|
+
transportRef.current?.sendEvent(name, payload, msgId ?? generateMsgId());
|
|
146
206
|
},
|
|
147
207
|
[generateMsgId]
|
|
148
208
|
);
|
|
149
209
|
const sendUpdate = (0, import_react.useCallback)(
|
|
150
210
|
(payload, msgId) => {
|
|
151
|
-
|
|
211
|
+
transportRef.current?.sendUpdate(payload, msgId ?? generateMsgId());
|
|
152
212
|
},
|
|
153
213
|
[generateMsgId]
|
|
154
214
|
);
|
|
155
215
|
const sendDiff = (0, import_react.useCallback)(
|
|
156
216
|
(patch, msgId) => {
|
|
157
|
-
|
|
217
|
+
transportRef.current?.sendDiff(patch, msgId ?? generateMsgId());
|
|
158
218
|
},
|
|
159
219
|
[generateMsgId]
|
|
160
220
|
);
|
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 { createViraClient, deepMerge, type ViraClient, type Message } 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}\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 ) {\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 } = 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 const clientRef = useRef<ViraClient | null>(null);\r\n const sessionRef = useRef<string | null>(null);\r\n const msgIdCounterRef = useRef(0);\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 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 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\r\n return () => {\r\n client.close();\r\n clientRef.current = null;\r\n setIsConnected(false);\r\n setError(null);\r\n };\r\n }, [channel, apiUrl, authToken, onOpen, onClose, onError, useDeepMerge]);\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 clientRef.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 clientRef.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 clientRef.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":["/**\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":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
// src/useViraState.ts
|
|
2
2
|
import { useEffect, useMemo, useRef, useState, useCallback } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createViraClient,
|
|
5
|
+
deepMerge,
|
|
6
|
+
getViraConnectionPool
|
|
7
|
+
} from "@vira-ui/core";
|
|
4
8
|
function useViraState(channel, initialOrOptions) {
|
|
5
9
|
const options = useMemo(() => {
|
|
6
10
|
if (initialOrOptions === null || initialOrOptions === void 0) {
|
|
7
11
|
return {};
|
|
8
12
|
}
|
|
9
|
-
if (typeof initialOrOptions === "object" && !Array.isArray(initialOrOptions) && ("enableMsgId" in initialOrOptions || "onOpen" in initialOrOptions || "onClose" in initialOrOptions || "onError" in initialOrOptions || "deepMerge" in initialOrOptions || "initial" in initialOrOptions || "apiUrl" in initialOrOptions || "authToken" in initialOrOptions)) {
|
|
13
|
+
if (typeof initialOrOptions === "object" && !Array.isArray(initialOrOptions) && ("enableMsgId" in initialOrOptions || "onOpen" in initialOrOptions || "onClose" in initialOrOptions || "onError" in initialOrOptions || "deepMerge" in initialOrOptions || "initial" in initialOrOptions || "apiUrl" in initialOrOptions || "authToken" in initialOrOptions || "disablePooling" in initialOrOptions || "debug" in initialOrOptions)) {
|
|
10
14
|
return initialOrOptions;
|
|
11
15
|
}
|
|
12
16
|
return { initial: initialOrOptions };
|
|
@@ -19,14 +23,19 @@ function useViraState(channel, initialOrOptions) {
|
|
|
19
23
|
onError,
|
|
20
24
|
deepMerge: useDeepMerge = true,
|
|
21
25
|
apiUrl: apiUrlOption,
|
|
22
|
-
authToken: authTokenOption
|
|
26
|
+
authToken: authTokenOption,
|
|
27
|
+
disablePooling = false,
|
|
28
|
+
debug: debugOption
|
|
23
29
|
} = options;
|
|
24
30
|
const [data, setData] = useState(initial);
|
|
25
31
|
const [isConnected, setIsConnected] = useState(false);
|
|
26
32
|
const [error, setError] = useState(null);
|
|
33
|
+
const transportRef = useRef(null);
|
|
27
34
|
const clientRef = useRef(null);
|
|
28
35
|
const sessionRef = useRef(null);
|
|
29
36
|
const msgIdCounterRef = useRef(0);
|
|
37
|
+
const wasConnectedRef = useRef(false);
|
|
38
|
+
const lastErrorRef = useRef(null);
|
|
30
39
|
const apiUrl = useMemo(() => {
|
|
31
40
|
if (apiUrlOption) return apiUrlOption;
|
|
32
41
|
try {
|
|
@@ -45,6 +54,19 @@ function useViraState(channel, initialOrOptions) {
|
|
|
45
54
|
return "";
|
|
46
55
|
}
|
|
47
56
|
}, [authTokenOption]);
|
|
57
|
+
const debug = useMemo(() => {
|
|
58
|
+
if (debugOption !== void 0) return Boolean(debugOption);
|
|
59
|
+
try {
|
|
60
|
+
const env = globalThis.import?.meta?.env || globalThis.process?.env;
|
|
61
|
+
return String(env?.VITE_VRP_DEBUG || "").toLowerCase() === "true";
|
|
62
|
+
} catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}, [debugOption]);
|
|
66
|
+
const pool = useMemo(() => {
|
|
67
|
+
if (disablePooling) return null;
|
|
68
|
+
return getViraConnectionPool({ url: apiUrl, authToken, debug });
|
|
69
|
+
}, [disablePooling, apiUrl, authToken, debug]);
|
|
48
70
|
useEffect(() => {
|
|
49
71
|
if (!channel) return;
|
|
50
72
|
const handleMessage = (msg) => {
|
|
@@ -74,6 +96,42 @@ function useViraState(channel, initialOrOptions) {
|
|
|
74
96
|
break;
|
|
75
97
|
}
|
|
76
98
|
};
|
|
99
|
+
if (pool) {
|
|
100
|
+
const unsubChannel = pool.subscribe(channel, handleMessage);
|
|
101
|
+
const unsubStatus = pool.onStatus((status) => {
|
|
102
|
+
setError(status.error);
|
|
103
|
+
setIsConnected(status.connected);
|
|
104
|
+
if (status.connected && !wasConnectedRef.current) {
|
|
105
|
+
wasConnectedRef.current = true;
|
|
106
|
+
onOpen?.();
|
|
107
|
+
}
|
|
108
|
+
if (!status.connected && wasConnectedRef.current) {
|
|
109
|
+
wasConnectedRef.current = false;
|
|
110
|
+
const synthetic = typeof CloseEvent !== "undefined" ? new CloseEvent("close", { code: 1001, reason: "pooled disconnect" }) : { code: 1001, reason: "pooled disconnect" };
|
|
111
|
+
onClose?.(synthetic);
|
|
112
|
+
}
|
|
113
|
+
if (status.error) {
|
|
114
|
+
if (lastErrorRef.current !== status.error) {
|
|
115
|
+
lastErrorRef.current = status.error;
|
|
116
|
+
onError?.(status.error);
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
lastErrorRef.current = null;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
transportRef.current = {
|
|
123
|
+
sendEvent: (name, payload, msgId) => pool.sendEvent(channel, name, payload, msgId),
|
|
124
|
+
sendUpdate: (payload, msgId) => pool.sendUpdate(channel, payload, msgId),
|
|
125
|
+
sendDiff: (patch, msgId) => pool.sendDiff(channel, patch, msgId)
|
|
126
|
+
};
|
|
127
|
+
return () => {
|
|
128
|
+
unsubStatus();
|
|
129
|
+
unsubChannel();
|
|
130
|
+
transportRef.current = null;
|
|
131
|
+
setIsConnected(false);
|
|
132
|
+
setError(null);
|
|
133
|
+
};
|
|
134
|
+
}
|
|
77
135
|
const handleConnect = () => {
|
|
78
136
|
setIsConnected(true);
|
|
79
137
|
setError(null);
|
|
@@ -101,13 +159,19 @@ function useViraState(channel, initialOrOptions) {
|
|
|
101
159
|
}
|
|
102
160
|
});
|
|
103
161
|
clientRef.current = client;
|
|
162
|
+
transportRef.current = {
|
|
163
|
+
sendEvent: (name, payload, msgId) => client.sendEvent(name, payload, msgId),
|
|
164
|
+
sendUpdate: (payload, msgId) => client.sendUpdate(payload, msgId),
|
|
165
|
+
sendDiff: (patch, msgId) => client.sendDiff(patch, msgId)
|
|
166
|
+
};
|
|
104
167
|
return () => {
|
|
105
168
|
client.close();
|
|
106
169
|
clientRef.current = null;
|
|
170
|
+
transportRef.current = null;
|
|
107
171
|
setIsConnected(false);
|
|
108
172
|
setError(null);
|
|
109
173
|
};
|
|
110
|
-
}, [channel, apiUrl, authToken, onOpen, onClose, onError, useDeepMerge]);
|
|
174
|
+
}, [channel, apiUrl, authToken, onOpen, onClose, onError, useDeepMerge, pool]);
|
|
111
175
|
const generateMsgId = useCallback(() => {
|
|
112
176
|
if (!enableMsgId) return void 0;
|
|
113
177
|
msgIdCounterRef.current++;
|
|
@@ -115,19 +179,19 @@ function useViraState(channel, initialOrOptions) {
|
|
|
115
179
|
}, [channel, enableMsgId]);
|
|
116
180
|
const sendEvent = useCallback(
|
|
117
181
|
(name, payload, msgId) => {
|
|
118
|
-
|
|
182
|
+
transportRef.current?.sendEvent(name, payload, msgId ?? generateMsgId());
|
|
119
183
|
},
|
|
120
184
|
[generateMsgId]
|
|
121
185
|
);
|
|
122
186
|
const sendUpdate = useCallback(
|
|
123
187
|
(payload, msgId) => {
|
|
124
|
-
|
|
188
|
+
transportRef.current?.sendUpdate(payload, msgId ?? generateMsgId());
|
|
125
189
|
},
|
|
126
190
|
[generateMsgId]
|
|
127
191
|
);
|
|
128
192
|
const sendDiff = useCallback(
|
|
129
193
|
(patch, msgId) => {
|
|
130
|
-
|
|
194
|
+
transportRef.current?.sendDiff(patch, msgId ?? generateMsgId());
|
|
131
195
|
},
|
|
132
196
|
[generateMsgId]
|
|
133
197
|
);
|
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 { createViraClient, deepMerge, type ViraClient, type Message } 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}\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 ) {\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 } = 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 const clientRef = useRef<ViraClient | null>(null);\r\n const sessionRef = useRef<string | null>(null);\r\n const msgIdCounterRef = useRef(0);\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 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 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\r\n return () => {\r\n client.close();\r\n clientRef.current = null;\r\n setIsConnected(false);\r\n setError(null);\r\n };\r\n }, [channel, apiUrl, authToken, onOpen, onClose, onError, useDeepMerge]);\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 clientRef.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 clientRef.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 clientRef.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';\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":[]}
|