@vira-ui/react 1.0.2 → 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/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,37 @@ 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]);
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]);
75
111
  (0, import_react.useEffect)(() => {
76
112
  if (!channel) return;
77
113
  const handleMessage = (msg) => {
@@ -79,35 +115,129 @@ function useViraState(channel, initialOrOptions) {
79
115
  case "update":
80
116
  case "event":
81
117
  if (msg.channel === channel) {
82
- setData(msg.data);
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
+ });
83
165
  }
84
166
  break;
85
167
  case "diff":
86
168
  if (msg.channel === channel) {
87
169
  setData((prev) => {
170
+ if (!prev || !msg.patch || typeof msg.patch === "object" && (msg.patch.type === "diff" || msg.patch.type === "event")) {
171
+ return prev;
172
+ }
88
173
  if (!prev) {
89
- return msg.patch || null;
174
+ return null;
90
175
  }
91
176
  if (typeof prev === "object" && typeof msg.patch === "object") {
92
- if (useDeepMerge) {
93
- return (0, import_core.deepMerge)(prev, msg.patch);
94
- } else {
95
- return { ...prev, ...msg.patch };
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
+ }
96
182
  }
183
+ return newData;
97
184
  }
98
- return msg.patch || prev;
185
+ return prev;
99
186
  });
100
187
  }
101
188
  break;
102
189
  }
190
+ ;
103
191
  };
192
+ if (pool) {
193
+ const unsubChannel = pool.subscribe(channel, handleMessage);
194
+ const unsubStatus = pool.onStatus((status) => {
195
+ setError(status.error);
196
+ setIsConnected(status.connected);
197
+ if (status.connected && !wasConnectedRef.current) {
198
+ wasConnectedRef.current = true;
199
+ loadInitialData();
200
+ onOpen?.();
201
+ }
202
+ if (!status.connected && wasConnectedRef.current) {
203
+ wasConnectedRef.current = false;
204
+ const synthetic = typeof CloseEvent !== "undefined" ? new CloseEvent("close", { code: 1001, reason: "pooled disconnect" }) : { code: 1001, reason: "pooled disconnect" };
205
+ onClose?.(synthetic);
206
+ }
207
+ if (status.error) {
208
+ if (lastErrorRef.current !== status.error) {
209
+ lastErrorRef.current = status.error;
210
+ onError?.(status.error);
211
+ }
212
+ } else {
213
+ lastErrorRef.current = null;
214
+ }
215
+ });
216
+ transportRef.current = {
217
+ sendEvent: (name, payload, msgId) => pool.sendEvent(channel, name, payload, msgId),
218
+ sendUpdate: (payload, msgId) => pool.sendUpdate(channel, payload, msgId),
219
+ sendDiff: (patch, msgId) => pool.sendDiff(channel, patch, msgId)
220
+ };
221
+ return () => {
222
+ unsubStatus();
223
+ unsubChannel();
224
+ transportRef.current = null;
225
+ setIsConnected(false);
226
+ setError(null);
227
+ };
228
+ }
104
229
  const handleConnect = () => {
105
230
  setIsConnected(true);
106
231
  setError(null);
232
+ if (!wasConnectedRef.current) {
233
+ wasConnectedRef.current = true;
234
+ loadInitialData();
235
+ }
107
236
  onOpen?.();
108
237
  };
109
238
  const handleDisconnect = (event) => {
110
239
  setIsConnected(false);
240
+ wasConnectedRef.current = false;
111
241
  onClose?.(event);
112
242
  };
113
243
  const handleError = (err) => {
@@ -128,13 +258,19 @@ function useViraState(channel, initialOrOptions) {
128
258
  }
129
259
  });
130
260
  clientRef.current = client;
261
+ transportRef.current = {
262
+ sendEvent: (name, payload, msgId) => client.sendEvent(name, payload, msgId),
263
+ sendUpdate: (payload, msgId) => client.sendUpdate(payload, msgId),
264
+ sendDiff: (patch, msgId) => client.sendDiff(patch, msgId)
265
+ };
131
266
  return () => {
132
267
  client.close();
133
268
  clientRef.current = null;
269
+ transportRef.current = null;
134
270
  setIsConnected(false);
135
271
  setError(null);
136
272
  };
137
- }, [channel, apiUrl, authToken, onOpen, onClose, onError, useDeepMerge]);
273
+ }, [channel, apiUrl, authToken, onOpen, onClose, onError, useDeepMerge, pool]);
138
274
  const generateMsgId = (0, import_react.useCallback)(() => {
139
275
  if (!enableMsgId) return void 0;
140
276
  msgIdCounterRef.current++;
@@ -142,19 +278,19 @@ function useViraState(channel, initialOrOptions) {
142
278
  }, [channel, enableMsgId]);
143
279
  const sendEvent = (0, import_react.useCallback)(
144
280
  (name, payload, msgId) => {
145
- clientRef.current?.sendEvent(name, payload, msgId ?? generateMsgId());
281
+ transportRef.current?.sendEvent(name, payload, msgId ?? generateMsgId());
146
282
  },
147
283
  [generateMsgId]
148
284
  );
149
285
  const sendUpdate = (0, import_react.useCallback)(
150
286
  (payload, msgId) => {
151
- clientRef.current?.sendUpdate(payload, msgId ?? generateMsgId());
287
+ transportRef.current?.sendUpdate(payload, msgId ?? generateMsgId());
152
288
  },
153
289
  [generateMsgId]
154
290
  );
155
291
  const sendDiff = (0, import_react.useCallback)(
156
292
  (patch, msgId) => {
157
- clientRef.current?.sendDiff(patch, msgId ?? generateMsgId());
293
+ transportRef.current?.sendDiff(patch, msgId ?? generateMsgId());
158
294
  },
159
295
  [generateMsgId]
160
296
  );
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":["/**\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
@@ -1,12 +1,16 @@
1
1
  // src/useViraState.ts
2
2
  import { useEffect, useMemo, useRef, useState, useCallback } from "react";
3
- import { createViraClient, deepMerge } from "@vira-ui/core";
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,37 @@ 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]);
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]);
48
88
  useEffect(() => {
49
89
  if (!channel) return;
50
90
  const handleMessage = (msg) => {
@@ -52,35 +92,129 @@ function useViraState(channel, initialOrOptions) {
52
92
  case "update":
53
93
  case "event":
54
94
  if (msg.channel === channel) {
55
- setData(msg.data);
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
+ });
56
142
  }
57
143
  break;
58
144
  case "diff":
59
145
  if (msg.channel === channel) {
60
146
  setData((prev) => {
147
+ if (!prev || !msg.patch || typeof msg.patch === "object" && (msg.patch.type === "diff" || msg.patch.type === "event")) {
148
+ return prev;
149
+ }
61
150
  if (!prev) {
62
- return msg.patch || null;
151
+ return null;
63
152
  }
64
153
  if (typeof prev === "object" && typeof msg.patch === "object") {
65
- if (useDeepMerge) {
66
- return deepMerge(prev, msg.patch);
67
- } else {
68
- return { ...prev, ...msg.patch };
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
+ }
69
159
  }
160
+ return newData;
70
161
  }
71
- return msg.patch || prev;
162
+ return prev;
72
163
  });
73
164
  }
74
165
  break;
75
166
  }
167
+ ;
76
168
  };
169
+ if (pool) {
170
+ const unsubChannel = pool.subscribe(channel, handleMessage);
171
+ const unsubStatus = pool.onStatus((status) => {
172
+ setError(status.error);
173
+ setIsConnected(status.connected);
174
+ if (status.connected && !wasConnectedRef.current) {
175
+ wasConnectedRef.current = true;
176
+ loadInitialData();
177
+ onOpen?.();
178
+ }
179
+ if (!status.connected && wasConnectedRef.current) {
180
+ wasConnectedRef.current = false;
181
+ const synthetic = typeof CloseEvent !== "undefined" ? new CloseEvent("close", { code: 1001, reason: "pooled disconnect" }) : { code: 1001, reason: "pooled disconnect" };
182
+ onClose?.(synthetic);
183
+ }
184
+ if (status.error) {
185
+ if (lastErrorRef.current !== status.error) {
186
+ lastErrorRef.current = status.error;
187
+ onError?.(status.error);
188
+ }
189
+ } else {
190
+ lastErrorRef.current = null;
191
+ }
192
+ });
193
+ transportRef.current = {
194
+ sendEvent: (name, payload, msgId) => pool.sendEvent(channel, name, payload, msgId),
195
+ sendUpdate: (payload, msgId) => pool.sendUpdate(channel, payload, msgId),
196
+ sendDiff: (patch, msgId) => pool.sendDiff(channel, patch, msgId)
197
+ };
198
+ return () => {
199
+ unsubStatus();
200
+ unsubChannel();
201
+ transportRef.current = null;
202
+ setIsConnected(false);
203
+ setError(null);
204
+ };
205
+ }
77
206
  const handleConnect = () => {
78
207
  setIsConnected(true);
79
208
  setError(null);
209
+ if (!wasConnectedRef.current) {
210
+ wasConnectedRef.current = true;
211
+ loadInitialData();
212
+ }
80
213
  onOpen?.();
81
214
  };
82
215
  const handleDisconnect = (event) => {
83
216
  setIsConnected(false);
217
+ wasConnectedRef.current = false;
84
218
  onClose?.(event);
85
219
  };
86
220
  const handleError = (err) => {
@@ -101,13 +235,19 @@ function useViraState(channel, initialOrOptions) {
101
235
  }
102
236
  });
103
237
  clientRef.current = client;
238
+ transportRef.current = {
239
+ sendEvent: (name, payload, msgId) => client.sendEvent(name, payload, msgId),
240
+ sendUpdate: (payload, msgId) => client.sendUpdate(payload, msgId),
241
+ sendDiff: (patch, msgId) => client.sendDiff(patch, msgId)
242
+ };
104
243
  return () => {
105
244
  client.close();
106
245
  clientRef.current = null;
246
+ transportRef.current = null;
107
247
  setIsConnected(false);
108
248
  setError(null);
109
249
  };
110
- }, [channel, apiUrl, authToken, onOpen, onClose, onError, useDeepMerge]);
250
+ }, [channel, apiUrl, authToken, onOpen, onClose, onError, useDeepMerge, pool]);
111
251
  const generateMsgId = useCallback(() => {
112
252
  if (!enableMsgId) return void 0;
113
253
  msgIdCounterRef.current++;
@@ -115,19 +255,19 @@ function useViraState(channel, initialOrOptions) {
115
255
  }, [channel, enableMsgId]);
116
256
  const sendEvent = useCallback(
117
257
  (name, payload, msgId) => {
118
- clientRef.current?.sendEvent(name, payload, msgId ?? generateMsgId());
258
+ transportRef.current?.sendEvent(name, payload, msgId ?? generateMsgId());
119
259
  },
120
260
  [generateMsgId]
121
261
  );
122
262
  const sendUpdate = useCallback(
123
263
  (payload, msgId) => {
124
- clientRef.current?.sendUpdate(payload, msgId ?? generateMsgId());
264
+ transportRef.current?.sendUpdate(payload, msgId ?? generateMsgId());
125
265
  },
126
266
  [generateMsgId]
127
267
  );
128
268
  const sendDiff = useCallback(
129
269
  (patch, msgId) => {
130
- clientRef.current?.sendDiff(patch, msgId ?? generateMsgId());
270
+ transportRef.current?.sendDiff(patch, msgId ?? generateMsgId());
131
271
  },
132
272
  [generateMsgId]
133
273
  );