@djangocfg/centrifugo 2.1.101 → 2.1.102
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/components.cjs +871 -0
- package/dist/components.cjs.map +1 -0
- package/dist/components.d.mts +129 -0
- package/dist/components.d.ts +129 -0
- package/dist/components.mjs +857 -0
- package/dist/components.mjs.map +1 -0
- package/dist/hooks.cjs +379 -0
- package/dist/hooks.cjs.map +1 -0
- package/dist/hooks.d.mts +128 -0
- package/dist/hooks.d.ts +128 -0
- package/dist/hooks.mjs +374 -0
- package/dist/hooks.mjs.map +1 -0
- package/dist/index.cjs +3007 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +838 -0
- package/dist/index.d.ts +838 -0
- package/dist/index.mjs +2948 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +35 -13
- package/src/components/CentrifugoMonitor/CentrifugoMonitorDialog.tsx +2 -1
- package/src/events.ts +1 -1
package/dist/hooks.cjs
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var consola = require('consola');
|
|
5
|
+
require('@djangocfg/api/auth');
|
|
6
|
+
require('lucide-react');
|
|
7
|
+
require('@djangocfg/ui-core/hooks');
|
|
8
|
+
require('@djangocfg/ui-nextjs');
|
|
9
|
+
require('moment');
|
|
10
|
+
require('react/jsx-runtime');
|
|
11
|
+
require('centrifuge');
|
|
12
|
+
|
|
13
|
+
var __defProp = Object.defineProperty;
|
|
14
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
15
|
+
var consolaLogger = consola.createConsola({
|
|
16
|
+
level: 4 ,
|
|
17
|
+
formatOptions: {
|
|
18
|
+
colors: true,
|
|
19
|
+
date: false,
|
|
20
|
+
compact: false
|
|
21
|
+
}
|
|
22
|
+
}).withTag("[Centrifugo]");
|
|
23
|
+
function getConsolaLogger(tag) {
|
|
24
|
+
const consola = consolaLogger.withTag(`[${tag}]`);
|
|
25
|
+
return {
|
|
26
|
+
debug: /* @__PURE__ */ __name((message, data) => consola.debug(message, data || ""), "debug"),
|
|
27
|
+
info: /* @__PURE__ */ __name((message, data) => consola.info(message, data || ""), "info"),
|
|
28
|
+
success: /* @__PURE__ */ __name((message, data) => consola.success(message, data || ""), "success"),
|
|
29
|
+
warning: /* @__PURE__ */ __name((message, data) => consola.warn(message, data || ""), "warning"),
|
|
30
|
+
error: /* @__PURE__ */ __name((message, error) => consola.error(message, error || ""), "error")
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
__name(getConsolaLogger, "getConsolaLogger");
|
|
34
|
+
|
|
35
|
+
// src/core/utils/channelValidator.ts
|
|
36
|
+
function validateChannelName(channel) {
|
|
37
|
+
const hashCount = (channel.match(/#/g) || []).length;
|
|
38
|
+
if (hashCount >= 2) {
|
|
39
|
+
const parts = channel.split("#");
|
|
40
|
+
const [namespace, possibleUserId, ...rest] = parts;
|
|
41
|
+
const isNumericUserId = /^\d+$/.test(possibleUserId);
|
|
42
|
+
if (!isNumericUserId && possibleUserId) {
|
|
43
|
+
const suggestion = channel.replace(/#/g, ":");
|
|
44
|
+
return {
|
|
45
|
+
valid: false,
|
|
46
|
+
warning: `Channel "${channel}" uses '#' separator which Centrifugo interprets as user-limited channel boundary. The part "${possibleUserId}" will be treated as user_id, which may cause permission errors if not in JWT token.`,
|
|
47
|
+
suggestion: `Use ':' for namespace separation: "${suggestion}"`
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (isNumericUserId) {
|
|
51
|
+
return {
|
|
52
|
+
valid: true,
|
|
53
|
+
warning: `Channel "${channel}" appears to be a user-limited channel (user_id: ${possibleUserId}). Make sure your JWT token's "sub" field matches "${possibleUserId}".`
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (hashCount === 1) {
|
|
58
|
+
const [namespace, userId] = channel.split("#");
|
|
59
|
+
if (userId && !/^\d+$/.test(userId) && userId !== "*") {
|
|
60
|
+
const suggestion = channel.replace("#", ":");
|
|
61
|
+
return {
|
|
62
|
+
valid: false,
|
|
63
|
+
warning: `Channel "${channel}" uses '#' but "${userId}" doesn't look like a user_id. This might cause permission issues.`,
|
|
64
|
+
suggestion: `Consider using ':' instead: "${suggestion}"`
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { valid: true };
|
|
69
|
+
}
|
|
70
|
+
__name(validateChannelName, "validateChannelName");
|
|
71
|
+
function logChannelWarnings(channel, logger3) {
|
|
72
|
+
const result = validateChannelName(channel);
|
|
73
|
+
if (!result.valid && result.warning) {
|
|
74
|
+
const message = `[Centrifugo Channel Warning]
|
|
75
|
+
${result.warning}${result.suggestion ? `
|
|
76
|
+
\u{1F4A1} Suggestion: ${result.suggestion}` : ""}`;
|
|
77
|
+
if (logger3?.warning) {
|
|
78
|
+
logger3.warning(message);
|
|
79
|
+
} else {
|
|
80
|
+
console.warn(message);
|
|
81
|
+
}
|
|
82
|
+
} else if (result.warning) {
|
|
83
|
+
if (logger3?.warning) {
|
|
84
|
+
logger3.warning(`[Centrifugo] ${result.warning}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
__name(logChannelWarnings, "logChannelWarnings");
|
|
89
|
+
getConsolaLogger("ConnectionStatus");
|
|
90
|
+
getConsolaLogger("SubscriptionsList");
|
|
91
|
+
|
|
92
|
+
// src/config.ts
|
|
93
|
+
process.env.NEXT_PUBLIC_STATIC_BUILD === "true";
|
|
94
|
+
function getVisibilityState() {
|
|
95
|
+
if (typeof document === "undefined") return true;
|
|
96
|
+
return document.visibilityState === "visible";
|
|
97
|
+
}
|
|
98
|
+
__name(getVisibilityState, "getVisibilityState");
|
|
99
|
+
function usePageVisibility(options = {}) {
|
|
100
|
+
const { onVisible, onHidden, onChange } = options;
|
|
101
|
+
const [state, setState] = react.useState(() => ({
|
|
102
|
+
isVisible: getVisibilityState(),
|
|
103
|
+
wasHidden: false,
|
|
104
|
+
visibleSince: getVisibilityState() ? Date.now() : null,
|
|
105
|
+
hiddenDuration: 0
|
|
106
|
+
}));
|
|
107
|
+
const onVisibleRef = react.useRef(onVisible);
|
|
108
|
+
const onHiddenRef = react.useRef(onHidden);
|
|
109
|
+
const onChangeRef = react.useRef(onChange);
|
|
110
|
+
const hiddenAtRef = react.useRef(null);
|
|
111
|
+
onVisibleRef.current = onVisible;
|
|
112
|
+
onHiddenRef.current = onHidden;
|
|
113
|
+
onChangeRef.current = onChange;
|
|
114
|
+
const checkVisibility = react.useCallback(() => {
|
|
115
|
+
return getVisibilityState();
|
|
116
|
+
}, []);
|
|
117
|
+
react.useEffect(() => {
|
|
118
|
+
if (typeof document === "undefined") return;
|
|
119
|
+
const handleVisibilityChange = /* @__PURE__ */ __name(() => {
|
|
120
|
+
const isNowVisible = getVisibilityState();
|
|
121
|
+
setState((prev) => {
|
|
122
|
+
let hiddenDuration = prev.hiddenDuration;
|
|
123
|
+
if (isNowVisible && hiddenAtRef.current !== null) {
|
|
124
|
+
hiddenDuration = Date.now() - hiddenAtRef.current;
|
|
125
|
+
hiddenAtRef.current = null;
|
|
126
|
+
}
|
|
127
|
+
if (!isNowVisible) {
|
|
128
|
+
hiddenAtRef.current = Date.now();
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
isVisible: isNowVisible,
|
|
132
|
+
wasHidden: prev.wasHidden || !isNowVisible,
|
|
133
|
+
visibleSince: isNowVisible ? prev.visibleSince ?? Date.now() : null,
|
|
134
|
+
hiddenDuration
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
onChangeRef.current?.(isNowVisible);
|
|
138
|
+
if (isNowVisible) {
|
|
139
|
+
onVisibleRef.current?.();
|
|
140
|
+
} else {
|
|
141
|
+
onHiddenRef.current?.();
|
|
142
|
+
}
|
|
143
|
+
}, "handleVisibilityChange");
|
|
144
|
+
document.addEventListener("visibilitychange", handleVisibilityChange);
|
|
145
|
+
return () => {
|
|
146
|
+
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
147
|
+
};
|
|
148
|
+
}, []);
|
|
149
|
+
return {
|
|
150
|
+
...state,
|
|
151
|
+
checkVisibility
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
__name(usePageVisibility, "usePageVisibility");
|
|
155
|
+
react.createContext(void 0);
|
|
156
|
+
var CentrifugoContext = react.createContext(void 0);
|
|
157
|
+
function useCentrifugo() {
|
|
158
|
+
const context = react.useContext(CentrifugoContext);
|
|
159
|
+
if (context === void 0) {
|
|
160
|
+
throw new Error("useCentrifugo must be used within a CentrifugoProvider");
|
|
161
|
+
}
|
|
162
|
+
return context;
|
|
163
|
+
}
|
|
164
|
+
__name(useCentrifugo, "useCentrifugo");
|
|
165
|
+
|
|
166
|
+
// src/hooks/useSubscription.ts
|
|
167
|
+
function useSubscription(options) {
|
|
168
|
+
const { client, isConnected } = useCentrifugo();
|
|
169
|
+
const { channel, enabled = true, onPublication, onError } = options;
|
|
170
|
+
const [data, setData] = react.useState(null);
|
|
171
|
+
const [error, setError] = react.useState(null);
|
|
172
|
+
const [isSubscribed, setIsSubscribed] = react.useState(false);
|
|
173
|
+
const unsubscribeRef = react.useRef(null);
|
|
174
|
+
const logger3 = react.useRef(getConsolaLogger("useSubscription")).current;
|
|
175
|
+
const onPublicationRef = react.useRef(onPublication);
|
|
176
|
+
const onErrorRef = react.useRef(onError);
|
|
177
|
+
react.useEffect(() => {
|
|
178
|
+
onPublicationRef.current = onPublication;
|
|
179
|
+
onErrorRef.current = onError;
|
|
180
|
+
}, [onPublication, onError]);
|
|
181
|
+
const unsubscribe2 = react.useCallback(() => {
|
|
182
|
+
if (unsubscribeRef.current) {
|
|
183
|
+
try {
|
|
184
|
+
unsubscribeRef.current();
|
|
185
|
+
unsubscribeRef.current = null;
|
|
186
|
+
setIsSubscribed(false);
|
|
187
|
+
logger3.info(`Unsubscribed from channel: ${channel}`);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
logger3.error(`Error during unsubscribe from ${channel}`, err);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}, [channel, logger3]);
|
|
193
|
+
react.useEffect(() => {
|
|
194
|
+
if (!client || !isConnected || !enabled) {
|
|
195
|
+
if (!isConnected && isSubscribed) {
|
|
196
|
+
setIsSubscribed(false);
|
|
197
|
+
}
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
logChannelWarnings(channel, logger3);
|
|
201
|
+
logger3.info(`Subscribing to channel: ${channel}`);
|
|
202
|
+
try {
|
|
203
|
+
const unsub = client.subscribe(channel, (receivedData) => {
|
|
204
|
+
try {
|
|
205
|
+
setData(receivedData);
|
|
206
|
+
setError(null);
|
|
207
|
+
onPublicationRef.current?.(receivedData);
|
|
208
|
+
} catch (callbackError) {
|
|
209
|
+
logger3.error(`Error in onPublication callback for ${channel}`, callbackError);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
unsubscribeRef.current = unsub;
|
|
213
|
+
setIsSubscribed(true);
|
|
214
|
+
logger3.success(`Subscribed to channel: ${channel}`);
|
|
215
|
+
} catch (err) {
|
|
216
|
+
const subscriptionError = err instanceof Error ? err : new Error("Subscription failed");
|
|
217
|
+
setError(subscriptionError);
|
|
218
|
+
try {
|
|
219
|
+
onErrorRef.current?.(subscriptionError);
|
|
220
|
+
} catch (callbackError) {
|
|
221
|
+
logger3.error(`Error in onError callback for ${channel}`, callbackError);
|
|
222
|
+
}
|
|
223
|
+
logger3.error(`Subscription failed: ${channel}`, subscriptionError);
|
|
224
|
+
}
|
|
225
|
+
return () => {
|
|
226
|
+
unsubscribe2();
|
|
227
|
+
};
|
|
228
|
+
}, [client, isConnected, enabled, channel, unsubscribe2, logger3, isSubscribed]);
|
|
229
|
+
return {
|
|
230
|
+
data,
|
|
231
|
+
error,
|
|
232
|
+
isSubscribed,
|
|
233
|
+
unsubscribe: unsubscribe2
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
__name(useSubscription, "useSubscription");
|
|
237
|
+
function useRPC(defaultOptions = {}) {
|
|
238
|
+
const { client, isConnected } = useCentrifugo();
|
|
239
|
+
const [isLoading, setIsLoading] = react.useState(false);
|
|
240
|
+
const [error, setError] = react.useState(null);
|
|
241
|
+
const logger3 = react.useRef(getConsolaLogger("useRPC")).current;
|
|
242
|
+
const abortControllerRef = react.useRef(null);
|
|
243
|
+
const reset = react.useCallback(() => {
|
|
244
|
+
setIsLoading(false);
|
|
245
|
+
setError(null);
|
|
246
|
+
if (abortControllerRef.current) {
|
|
247
|
+
abortControllerRef.current.abort();
|
|
248
|
+
abortControllerRef.current = null;
|
|
249
|
+
}
|
|
250
|
+
}, []);
|
|
251
|
+
const call2 = react.useCallback(
|
|
252
|
+
async (method, params, options = {}) => {
|
|
253
|
+
if (!client) {
|
|
254
|
+
const error2 = new Error("Centrifugo client not available");
|
|
255
|
+
setError(error2);
|
|
256
|
+
throw error2;
|
|
257
|
+
}
|
|
258
|
+
if (!isConnected) {
|
|
259
|
+
const error2 = new Error("Not connected to Centrifugo");
|
|
260
|
+
setError(error2);
|
|
261
|
+
throw error2;
|
|
262
|
+
}
|
|
263
|
+
setError(null);
|
|
264
|
+
setIsLoading(true);
|
|
265
|
+
const abortController = new AbortController();
|
|
266
|
+
abortControllerRef.current = abortController;
|
|
267
|
+
try {
|
|
268
|
+
const mergedOptions = {
|
|
269
|
+
...defaultOptions,
|
|
270
|
+
...options
|
|
271
|
+
};
|
|
272
|
+
logger3.info(`RPC call: ${method}`, { params });
|
|
273
|
+
const result = await client.rpc(
|
|
274
|
+
method,
|
|
275
|
+
params,
|
|
276
|
+
{
|
|
277
|
+
timeout: mergedOptions.timeout,
|
|
278
|
+
replyChannel: mergedOptions.replyChannel
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
if (abortController.signal.aborted) {
|
|
282
|
+
throw new Error("RPC call aborted");
|
|
283
|
+
}
|
|
284
|
+
logger3.success(`RPC success: ${method}`);
|
|
285
|
+
setIsLoading(false);
|
|
286
|
+
return result;
|
|
287
|
+
} catch (err) {
|
|
288
|
+
const rpcError = err instanceof Error ? err : new Error("RPC call failed");
|
|
289
|
+
if (!abortController.signal.aborted) {
|
|
290
|
+
setError(rpcError);
|
|
291
|
+
logger3.error(`RPC failed: ${method}`, rpcError);
|
|
292
|
+
const onError = options.onError || defaultOptions.onError;
|
|
293
|
+
if (onError) {
|
|
294
|
+
try {
|
|
295
|
+
onError(rpcError);
|
|
296
|
+
} catch (callbackError) {
|
|
297
|
+
logger3.error("Error in onError callback", callbackError);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
setIsLoading(false);
|
|
302
|
+
throw rpcError;
|
|
303
|
+
} finally {
|
|
304
|
+
if (abortControllerRef.current === abortController) {
|
|
305
|
+
abortControllerRef.current = null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
[client, isConnected, defaultOptions, logger3]
|
|
310
|
+
);
|
|
311
|
+
return {
|
|
312
|
+
call: call2,
|
|
313
|
+
isLoading,
|
|
314
|
+
error,
|
|
315
|
+
reset
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
__name(useRPC, "useRPC");
|
|
319
|
+
function useNamedRPC(defaultOptions = {}) {
|
|
320
|
+
const { client, isConnected } = useCentrifugo();
|
|
321
|
+
const [isLoading, setIsLoading] = react.useState(false);
|
|
322
|
+
const [error, setError] = react.useState(null);
|
|
323
|
+
const logger3 = react.useRef(getConsolaLogger("useNamedRPC")).current;
|
|
324
|
+
const reset = react.useCallback(() => {
|
|
325
|
+
setIsLoading(false);
|
|
326
|
+
setError(null);
|
|
327
|
+
}, []);
|
|
328
|
+
const call2 = react.useCallback(
|
|
329
|
+
async (method, data) => {
|
|
330
|
+
if (!client) {
|
|
331
|
+
const error2 = new Error("Centrifugo client not available");
|
|
332
|
+
setError(error2);
|
|
333
|
+
throw error2;
|
|
334
|
+
}
|
|
335
|
+
if (!isConnected) {
|
|
336
|
+
const error2 = new Error("Not connected to Centrifugo");
|
|
337
|
+
setError(error2);
|
|
338
|
+
throw error2;
|
|
339
|
+
}
|
|
340
|
+
setError(null);
|
|
341
|
+
setIsLoading(true);
|
|
342
|
+
try {
|
|
343
|
+
logger3.info(`Native RPC call: ${method}`, { data });
|
|
344
|
+
const result = await client.namedRPC(method, data);
|
|
345
|
+
logger3.success(`Native RPC success: ${method}`);
|
|
346
|
+
setIsLoading(false);
|
|
347
|
+
return result;
|
|
348
|
+
} catch (err) {
|
|
349
|
+
const rpcError = err instanceof Error ? err : new Error("Native RPC call failed");
|
|
350
|
+
setError(rpcError);
|
|
351
|
+
logger3.error(`Native RPC failed: ${method}`, rpcError);
|
|
352
|
+
if (defaultOptions.onError) {
|
|
353
|
+
try {
|
|
354
|
+
defaultOptions.onError(rpcError);
|
|
355
|
+
} catch (callbackError) {
|
|
356
|
+
logger3.error("Error in onError callback", callbackError);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
setIsLoading(false);
|
|
360
|
+
throw rpcError;
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
[client, isConnected, defaultOptions, logger3]
|
|
364
|
+
);
|
|
365
|
+
return {
|
|
366
|
+
call: call2,
|
|
367
|
+
isLoading,
|
|
368
|
+
error,
|
|
369
|
+
reset
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
__name(useNamedRPC, "useNamedRPC");
|
|
373
|
+
|
|
374
|
+
exports.useNamedRPC = useNamedRPC;
|
|
375
|
+
exports.usePageVisibility = usePageVisibility;
|
|
376
|
+
exports.useRPC = useRPC;
|
|
377
|
+
exports.useSubscription = useSubscription;
|
|
378
|
+
//# sourceMappingURL=hooks.cjs.map
|
|
379
|
+
//# sourceMappingURL=hooks.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/logger/consolaLogger.ts","../src/core/utils/channelValidator.ts","../src/components/ConnectionStatus/ConnectionStatus.tsx","../src/components/SubscriptionsList/SubscriptionsList.tsx","../src/config.ts","../src/hooks/usePageVisibility.ts","../src/providers/LogsProvider/LogsProvider.tsx","../src/providers/CentrifugoProvider/CentrifugoProvider.tsx","../src/hooks/useSubscription.ts","../src/hooks/useRPC.ts","../src/hooks/useNamedRPC.ts"],"names":["createConsola","logger","useState","useRef","useCallback","useEffect","createContext","useContext","unsubscribe","call","error"],"mappings":";;;;;;;;;;;;;;AAeO,IAAM,gBAAgBA,qBAAA,CAAc;AAAA,EACzC,KAAA,EAAuB,CAAA,CAAI;AAAA,EAC3B,aAAA,EAAe;AAAA,IACb,MAAA,EAAQ,IAAA;AAAA,IACR,IAAA,EAAM,KAAA;AAAA,IACN,SAAS;AAAC;AAEd,CAAC,CAAA,CAAE,QAAQ,cAAc,CAAA;AAKlB,SAAS,iBAAiB,GAAA,EAAqB;AACpD,EAAA,MAAM,OAAA,GAAU,aAAA,CAAc,OAAA,CAAQ,CAAA,CAAA,EAAI,GAAG,CAAA,CAAA,CAAG,CAAA;AAEhD,EAAA,OAAO;AAAA,IACL,KAAA,0BAAQ,OAAA,EAAiB,IAAA,KAAmB,QAAQ,KAAA,CAAM,OAAA,EAAS,IAAA,IAAQ,EAAE,CAAA,EAAtE,OAAA,CAAA;AAAA,IACP,IAAA,0BAAO,OAAA,EAAiB,IAAA,KAAmB,QAAQ,IAAA,CAAK,OAAA,EAAS,IAAA,IAAQ,EAAE,CAAA,EAArE,MAAA,CAAA;AAAA,IACN,OAAA,0BAAU,OAAA,EAAiB,IAAA,KAAmB,QAAQ,OAAA,CAAQ,OAAA,EAAS,IAAA,IAAQ,EAAE,CAAA,EAAxE,SAAA,CAAA;AAAA,IACT,OAAA,0BAAU,OAAA,EAAiB,IAAA,KAAmB,QAAQ,IAAA,CAAK,OAAA,EAAS,IAAA,IAAQ,EAAE,CAAA,EAArE,SAAA,CAAA;AAAA,IACT,KAAA,0BAAQ,OAAA,EAAiB,KAAA,KAA4B,QAAQ,KAAA,CAAM,OAAA,EAAS,KAAA,IAAS,EAAE,CAAA,EAAhF,OAAA;AAAA,GACT;AACF;AAVgB,MAAA,CAAA,gBAAA,EAAA,kBAAA,CAAA;;;ACMT,SAAS,oBAAoB,OAAA,EAA0C;AAE5E,EAAA,MAAM,aAAa,OAAA,CAAQ,KAAA,CAAM,IAAI,CAAA,IAAK,EAAC,EAAG,MAAA;AAE9C,EAAA,IAAI,aAAa,CAAA,EAAG;AAGlB,IAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,KAAA,CAAM,GAAG,CAAA;AAC/B,IAAA,MAAM,CAAC,SAAA,EAAW,cAAA,EAAgB,GAAG,IAAI,CAAA,GAAI,KAAA;AAG7C,IAAA,MAAM,eAAA,GAAkB,OAAA,CAAQ,IAAA,CAAK,cAAc,CAAA;AAEnD,IAAA,IAAI,CAAC,mBAAmB,cAAA,EAAgB;AAEtC,MAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,OAAA,CAAQ,IAAA,EAAM,GAAG,CAAA;AAE5C,MAAA,OAAO;AAAA,QACL,KAAA,EAAO,KAAA;AAAA,QACP,OAAA,EAAS,CAAA,SAAA,EAAY,OAAO,CAAA,6FAAA,EACN,cAAc,CAAA,oFAAA,CAAA;AAAA,QACpC,UAAA,EAAY,sCAAsC,UAAU,CAAA,CAAA;AAAA,OAC9D;AAAA,IACF;AAEA,IAAA,IAAI,eAAA,EAAiB;AACnB,MAAA,OAAO;AAAA,QACL,KAAA,EAAO,IAAA;AAAA,QACP,SAAS,CAAA,SAAA,EAAY,OAAO,CAAA,iDAAA,EAAoD,cAAc,sDAClC,cAAc,CAAA,EAAA;AAAA,OAC5E;AAAA,IACF;AAAA,EACF;AAGA,EAAA,IAAI,cAAc,CAAA,EAAG;AACnB,IAAA,MAAM,CAAC,SAAA,EAAW,MAAM,CAAA,GAAI,OAAA,CAAQ,MAAM,GAAG,CAAA;AAC7C,IAAA,IAAI,UAAU,CAAC,OAAA,CAAQ,KAAK,MAAM,CAAA,IAAK,WAAW,GAAA,EAAK;AAErD,MAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,OAAA,CAAQ,GAAA,EAAK,GAAG,CAAA;AAC3C,MAAA,OAAO;AAAA,QACL,KAAA,EAAO,KAAA;AAAA,QACP,OAAA,EAAS,CAAA,SAAA,EAAY,OAAO,CAAA,gBAAA,EAAmB,MAAM,CAAA,kEAAA,CAAA;AAAA,QAErD,UAAA,EAAY,gCAAgC,UAAU,CAAA,CAAA;AAAA,OACxD;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,EAAE,OAAO,IAAA,EAAK;AACvB;AAlDgB,MAAA,CAAA,mBAAA,EAAA,qBAAA,CAAA;AA0DT,SAAS,kBAAA,CACd,SACAC,OAAAA,EACM;AAKN,EAAA,MAAM,MAAA,GAAS,oBAAoB,OAAO,CAAA;AAE1C,EAAA,IAAI,CAAC,MAAA,CAAO,KAAA,IAAS,MAAA,CAAO,OAAA,EAAS;AACnC,IAAA,MAAM,OAAA,GAAU,CAAA;AAAA,EAAiC,MAAA,CAAO,OAAO,CAAA,EAC7D,MAAA,CAAO,UAAA,GAAa;AAAA,sBAAA,EAAoB,MAAA,CAAO,UAAU,CAAA,CAAA,GAAK,EAChE,CAAA,CAAA;AAEA,IAAA,IAAIA,SAAQ,OAAA,EAAS;AACnB,MAAAA,OAAAA,CAAO,QAAQ,OAAO,CAAA;AAAA,IACxB,CAAA,MAAO;AACL,MAAA,OAAA,CAAQ,KAAK,OAAO,CAAA;AAAA,IACtB;AAAA,EACF,CAAA,MAAA,IAAW,OAAO,OAAA,EAAS;AAEzB,IAAA,IAAIA,SAAQ,OAAA,EAAS;AACnB,MAAAA,OAAAA,CAAO,OAAA,CAAQ,CAAA,aAAA,EAAgB,MAAA,CAAO,OAAO,CAAA,CAAE,CAAA;AAAA,IACjD;AAAA,EACF;AACF;AA1BgB,MAAA,CAAA,kBAAA,EAAA,oBAAA,CAAA;ACzED,iBAAiB,kBAAkB;ACCnC,iBAAiB,mBAAmB;;;ACbtB,OAAA,CAAQ,GAAA,CAAI,wBAAA,KAA6B;AC+CtE,SAAS,kBAAA,GAA8B;AACrC,EAAA,IAAI,OAAO,QAAA,KAAa,WAAA,EAAa,OAAO,IAAA;AAC5C,EAAA,OAAO,SAAS,eAAA,KAAoB,SAAA;AACtC;AAHS,MAAA,CAAA,kBAAA,EAAA,oBAAA,CAAA;AASF,SAAS,iBAAA,CACd,OAAA,GAAoC,EAAC,EACZ;AACzB,EAAA,MAAM,EAAE,SAAA,EAAW,QAAA,EAAU,QAAA,EAAS,GAAI,OAAA;AAE1C,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAIC,eAA8B,OAAO;AAAA,IAC7D,WAAW,kBAAA,EAAmB;AAAA,IAC9B,SAAA,EAAW,KAAA;AAAA,IACX,YAAA,EAAc,kBAAA,EAAmB,GAAI,IAAA,CAAK,KAAI,GAAI,IAAA;AAAA,IAClD,cAAA,EAAgB;AAAA,GAClB,CAAE,CAAA;AAGF,EAAA,MAAM,YAAA,GAAeC,aAAO,SAAS,CAAA;AACrC,EAAA,MAAM,WAAA,GAAcA,aAAO,QAAQ,CAAA;AACnC,EAAA,MAAM,WAAA,GAAcA,aAAO,QAAQ,CAAA;AACnC,EAAA,MAAM,WAAA,GAAcA,aAAsB,IAAI,CAAA;AAG9C,EAAA,YAAA,CAAa,OAAA,GAAU,SAAA;AACvB,EAAA,WAAA,CAAY,OAAA,GAAU,QAAA;AACtB,EAAA,WAAA,CAAY,OAAA,GAAU,QAAA;AAEtB,EAAA,MAAM,eAAA,GAAkBC,kBAAY,MAAe;AACjD,IAAA,OAAO,kBAAA,EAAmB;AAAA,EAC5B,CAAA,EAAG,EAAE,CAAA;AAEL,EAAAC,gBAAU,MAAM;AACd,IAAA,IAAI,OAAO,aAAa,WAAA,EAAa;AAErC,IAAA,MAAM,yCAAyB,MAAA,CAAA,MAAM;AACnC,MAAA,MAAM,eAAe,kBAAA,EAAmB;AAExC,MAAA,QAAA,CAAS,CAAC,IAAA,KAAS;AAEjB,QAAA,IAAI,iBAAiB,IAAA,CAAK,cAAA;AAC1B,QAAA,IAAI,YAAA,IAAgB,WAAA,CAAY,OAAA,KAAY,IAAA,EAAM;AAChD,UAAA,cAAA,GAAiB,IAAA,CAAK,GAAA,EAAI,GAAI,WAAA,CAAY,OAAA;AAC1C,UAAA,WAAA,CAAY,OAAA,GAAU,IAAA;AAAA,QACxB;AAGA,QAAA,IAAI,CAAC,YAAA,EAAc;AACjB,UAAA,WAAA,CAAY,OAAA,GAAU,KAAK,GAAA,EAAI;AAAA,QACjC;AAEA,QAAA,OAAO;AAAA,UACL,SAAA,EAAW,YAAA;AAAA,UACX,SAAA,EAAW,IAAA,CAAK,SAAA,IAAa,CAAC,YAAA;AAAA,UAC9B,cAAc,YAAA,GAAgB,IAAA,CAAK,YAAA,IAAgB,IAAA,CAAK,KAAI,GAAK,IAAA;AAAA,UACjE;AAAA,SACF;AAAA,MACF,CAAC,CAAA;AAGD,MAAA,WAAA,CAAY,UAAU,YAAY,CAAA;AAElC,MAAA,IAAI,YAAA,EAAc;AAChB,QAAA,YAAA,CAAa,OAAA,IAAU;AAAA,MACzB,CAAA,MAAO;AACL,QAAA,WAAA,CAAY,OAAA,IAAU;AAAA,MACxB;AAAA,IACF,CAAA,EAhC+B,wBAAA,CAAA;AAkC/B,IAAA,QAAA,CAAS,gBAAA,CAAiB,oBAAoB,sBAAsB,CAAA;AAEpE,IAAA,OAAO,MAAM;AACX,MAAA,QAAA,CAAS,mBAAA,CAAoB,oBAAoB,sBAAsB,CAAA;AAAA,IACzE,CAAA;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,OAAO;AAAA,IACL,GAAG,KAAA;AAAA,IACH;AAAA,GACF;AACF;AA3EgB,MAAA,CAAA,iBAAA,EAAA,mBAAA,CAAA;AC3BIC,oBAA4C,MAAS;ACoBzE,IAAM,iBAAA,GAAoBA,oBAAkD,MAAS,CAAA;AA8e9E,SAAS,aAAA,GAAwC;AACtD,EAAA,MAAM,OAAA,GAAUC,iBAAW,iBAAiB,CAAA;AAE5C,EAAA,IAAI,YAAY,MAAA,EAAW;AACzB,IAAA,MAAM,IAAI,MAAM,wDAAwD,CAAA;AAAA,EAC1E;AAEA,EAAA,OAAO,OAAA;AACT;AARgB,MAAA,CAAA,aAAA,EAAA,eAAA,CAAA;;;ACngBT,SAAS,gBACd,OAAA,EAC0B;AAC1B,EAAA,MAAM,EAAE,MAAA,EAAQ,WAAA,EAAY,GAAI,aAAA,EAAc;AAC9C,EAAA,MAAM,EAAE,OAAA,EAAS,OAAA,GAAU,IAAA,EAAM,aAAA,EAAe,SAAQ,GAAI,OAAA;AAE5D,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,CAAA,GAAIL,eAAmB,IAAI,CAAA;AAC/C,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAIA,eAAuB,IAAI,CAAA;AACrD,EAAA,MAAM,CAAC,YAAA,EAAc,eAAe,CAAA,GAAIA,eAAS,KAAK,CAAA;AAEtD,EAAA,MAAM,cAAA,GAAiBC,aAA4B,IAAI,CAAA;AACvD,EAAA,MAAMF,OAAAA,GAASE,YAAAA,CAAO,gBAAA,CAAiB,iBAAiB,CAAC,CAAA,CAAE,OAAA;AAG3D,EAAA,MAAM,gBAAA,GAAmBA,aAAO,aAAa,CAAA;AAC7C,EAAA,MAAM,UAAA,GAAaA,aAAO,OAAO,CAAA;AAEjC,EAAAE,gBAAU,MAAM;AACd,IAAA,gBAAA,CAAiB,OAAA,GAAU,aAAA;AAC3B,IAAA,UAAA,CAAW,OAAA,GAAU,OAAA;AAAA,EACvB,CAAA,EAAG,CAAC,aAAA,EAAe,OAAO,CAAC,CAAA;AAG3B,EAAA,MAAMG,YAAAA,GAAcJ,kBAAY,MAAM;AACpC,IAAA,IAAI,eAAe,OAAA,EAAS;AAC1B,MAAA,IAAI;AACF,QAAA,cAAA,CAAe,OAAA,EAAQ;AACvB,QAAA,cAAA,CAAe,OAAA,GAAU,IAAA;AACzB,QAAA,eAAA,CAAgB,KAAK,CAAA;AACrB,QAAAH,OAAAA,CAAO,IAAA,CAAK,CAAA,2BAAA,EAA8B,OAAO,CAAA,CAAE,CAAA;AAAA,MACrD,SAAS,GAAA,EAAK;AACZ,QAAAA,OAAAA,CAAO,KAAA,CAAM,CAAA,8BAAA,EAAiC,OAAO,IAAI,GAAG,CAAA;AAAA,MAC9D;AAAA,IACF;AAAA,EACF,CAAA,EAAG,CAAC,OAAA,EAASA,OAAM,CAAC,CAAA;AAGpB,EAAAI,gBAAU,MAAM;AACd,IAAA,IAAI,CAAC,MAAA,IAAU,CAAC,WAAA,IAAe,CAAC,OAAA,EAAS;AAEvC,MAAA,IAAI,CAAC,eAAe,YAAA,EAAc;AAChC,QAAA,eAAA,CAAgB,KAAK,CAAA;AAAA,MACvB;AACA,MAAA;AAAA,IACF;AAGA,IAAA,kBAAA,CAAmB,SAASJ,OAAM,CAAA;AAElC,IAAAA,OAAAA,CAAO,IAAA,CAAK,CAAA,wBAAA,EAA2B,OAAO,CAAA,CAAE,CAAA;AAEhD,IAAA,IAAI;AACF,MAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,SAAA,CAAU,OAAA,EAAS,CAAC,YAAA,KAAoB;AAC3D,QAAA,IAAI;AAEF,UAAA,OAAA,CAAQ,YAAY,CAAA;AACpB,UAAA,QAAA,CAAS,IAAI,CAAA;AACb,UAAA,gBAAA,CAAiB,UAAU,YAAY,CAAA;AAAA,QACzC,SAAS,aAAA,EAAe;AACtB,UAAAA,OAAAA,CAAO,KAAA,CAAM,CAAA,oCAAA,EAAuC,OAAO,IAAI,aAAa,CAAA;AAAA,QAC9E;AAAA,MACF,CAAC,CAAA;AAED,MAAA,cAAA,CAAe,OAAA,GAAU,KAAA;AACzB,MAAA,eAAA,CAAgB,IAAI,CAAA;AACpB,MAAAA,OAAAA,CAAO,OAAA,CAAQ,CAAA,uBAAA,EAA0B,OAAO,CAAA,CAAE,CAAA;AAAA,IACpD,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,oBAAoB,GAAA,YAAe,KAAA,GAAQ,GAAA,GAAM,IAAI,MAAM,qBAAqB,CAAA;AACtF,MAAA,QAAA,CAAS,iBAAiB,CAAA;AAE1B,MAAA,IAAI;AACF,QAAA,UAAA,CAAW,UAAU,iBAAiB,CAAA;AAAA,MACxC,SAAS,aAAA,EAAe;AACtB,QAAAA,OAAAA,CAAO,KAAA,CAAM,CAAA,8BAAA,EAAiC,OAAO,IAAI,aAAa,CAAA;AAAA,MACxE;AAEA,MAAAA,OAAAA,CAAO,KAAA,CAAM,CAAA,qBAAA,EAAwB,OAAO,IAAI,iBAAiB,CAAA;AAAA,IACnE;AAGA,IAAA,OAAO,MAAM;AACX,MAAAO,YAAAA,EAAY;AAAA,IACd,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,MAAA,EAAQ,WAAA,EAAa,SAAS,OAAA,EAASA,YAAAA,EAAaP,OAAAA,EAAQ,YAAY,CAAC,CAAA;AAE7E,EAAA,OAAO;AAAA,IACL,IAAA;AAAA,IACA,KAAA;AAAA,IACA,YAAA;AAAA,IACA,WAAA,EAAAO;AAAA,GACF;AACF;AA3FgB,MAAA,CAAA,eAAA,EAAA,iBAAA,CAAA;ACUT,SAAS,MAAA,CAAO,cAAA,GAAgC,EAAC,EAAiB;AACvE,EAAA,MAAM,EAAE,MAAA,EAAQ,WAAA,EAAY,GAAI,aAAA,EAAc;AAC9C,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAIN,eAAS,KAAK,CAAA;AAChD,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAIA,eAAuB,IAAI,CAAA;AAErD,EAAA,MAAMD,OAAAA,GAASE,YAAAA,CAAO,gBAAA,CAAiB,QAAQ,CAAC,CAAA,CAAE,OAAA;AAClD,EAAA,MAAM,kBAAA,GAAqBA,aAA+B,IAAI,CAAA;AAE9D,EAAA,MAAM,KAAA,GAAQC,kBAAY,MAAM;AAC9B,IAAA,YAAA,CAAa,KAAK,CAAA;AAClB,IAAA,QAAA,CAAS,IAAI,CAAA;AACb,IAAA,IAAI,mBAAmB,OAAA,EAAS;AAC9B,MAAA,kBAAA,CAAmB,QAAQ,KAAA,EAAM;AACjC,MAAA,kBAAA,CAAmB,OAAA,GAAU,IAAA;AAAA,IAC/B;AAAA,EACF,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAMK,KAAAA,GAAOL,iBAAAA;AAAA,IACX,OACE,MAAA,EACA,MAAA,EACA,OAAA,GAAyB,EAAC,KACH;AACvB,MAAA,IAAI,CAAC,MAAA,EAAQ;AACX,QAAA,MAAMM,MAAAA,GAAQ,IAAI,KAAA,CAAM,iCAAiC,CAAA;AACzD,QAAA,QAAA,CAASA,MAAK,CAAA;AACd,QAAA,MAAMA,MAAAA;AAAA,MACR;AAEA,MAAA,IAAI,CAAC,WAAA,EAAa;AAChB,QAAA,MAAMA,MAAAA,GAAQ,IAAI,KAAA,CAAM,6BAA6B,CAAA;AACrD,QAAA,QAAA,CAASA,MAAK,CAAA;AACd,QAAA,MAAMA,MAAAA;AAAA,MACR;AAGA,MAAA,QAAA,CAAS,IAAI,CAAA;AACb,MAAA,YAAA,CAAa,IAAI,CAAA;AAGjB,MAAA,MAAM,eAAA,GAAkB,IAAI,eAAA,EAAgB;AAC5C,MAAA,kBAAA,CAAmB,OAAA,GAAU,eAAA;AAE7B,MAAA,IAAI;AACF,QAAA,MAAM,aAAA,GAAgB;AAAA,UACpB,GAAG,cAAA;AAAA,UACH,GAAG;AAAA,SACL;AAEA,QAAAT,QAAO,IAAA,CAAK,CAAA,UAAA,EAAa,MAAM,CAAA,CAAA,EAAI,EAAE,QAAQ,CAAA;AAE7C,QAAA,MAAM,MAAA,GAAS,MAAM,MAAA,CAAO,GAAA;AAAA,UAC1B,MAAA;AAAA,UACA,MAAA;AAAA,UACA;AAAA,YACE,SAAS,aAAA,CAAc,OAAA;AAAA,YACvB,cAAc,aAAA,CAAc;AAAA;AAC9B,SACF;AAGA,QAAA,IAAI,eAAA,CAAgB,OAAO,OAAA,EAAS;AAClC,UAAA,MAAM,IAAI,MAAM,kBAAkB,CAAA;AAAA,QACpC;AAEA,QAAAA,OAAAA,CAAO,OAAA,CAAQ,CAAA,aAAA,EAAgB,MAAM,CAAA,CAAE,CAAA;AACvC,QAAA,YAAA,CAAa,KAAK,CAAA;AAClB,QAAA,OAAO,MAAA;AAAA,MACT,SAAS,GAAA,EAAK;AACZ,QAAA,MAAM,WAAW,GAAA,YAAe,KAAA,GAAQ,GAAA,GAAM,IAAI,MAAM,iBAAiB,CAAA;AAGzE,QAAA,IAAI,CAAC,eAAA,CAAgB,MAAA,CAAO,OAAA,EAAS;AACnC,UAAA,QAAA,CAAS,QAAQ,CAAA;AACjB,UAAAA,OAAAA,CAAO,KAAA,CAAM,CAAA,YAAA,EAAe,MAAM,IAAI,QAAQ,CAAA;AAG9C,UAAA,MAAM,OAAA,GAAU,OAAA,CAAQ,OAAA,IAAW,cAAA,CAAe,OAAA;AAClD,UAAA,IAAI,OAAA,EAAS;AACX,YAAA,IAAI;AACF,cAAA,OAAA,CAAQ,QAAQ,CAAA;AAAA,YAClB,SAAS,aAAA,EAAe;AACtB,cAAAA,OAAAA,CAAO,KAAA,CAAM,2BAAA,EAA6B,aAAa,CAAA;AAAA,YACzD;AAAA,UACF;AAAA,QACF;AAEA,QAAA,YAAA,CAAa,KAAK,CAAA;AAClB,QAAA,MAAM,QAAA;AAAA,MACR,CAAA,SAAE;AACA,QAAA,IAAI,kBAAA,CAAmB,YAAY,eAAA,EAAiB;AAClD,UAAA,kBAAA,CAAmB,OAAA,GAAU,IAAA;AAAA,QAC/B;AAAA,MACF;AAAA,IACF,CAAA;AAAA,IACA,CAAC,MAAA,EAAQ,WAAA,EAAa,cAAA,EAAgBA,OAAM;AAAA,GAC9C;AAEA,EAAA,OAAO;AAAA,IACL,IAAA,EAAAQ,KAAAA;AAAA,IACA,SAAA;AAAA,IACA,KAAA;AAAA,IACA;AAAA,GACF;AACF;AAxGgB,MAAA,CAAA,MAAA,EAAA,QAAA,CAAA;ACET,SAAS,WAAA,CACd,cAAA,GAAqC,EAAC,EACnB;AACnB,EAAA,MAAM,EAAE,MAAA,EAAQ,WAAA,EAAY,GAAI,aAAA,EAAc;AAC9C,EAAA,MAAM,CAAC,SAAA,EAAW,YAAY,CAAA,GAAIP,eAAS,KAAK,CAAA;AAChD,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAIA,eAAuB,IAAI,CAAA;AAErD,EAAA,MAAMD,OAAAA,GAASE,YAAAA,CAAO,gBAAA,CAAiB,aAAa,CAAC,CAAA,CAAE,OAAA;AAEvD,EAAA,MAAM,KAAA,GAAQC,kBAAY,MAAM;AAC9B,IAAA,YAAA,CAAa,KAAK,CAAA;AAClB,IAAA,QAAA,CAAS,IAAI,CAAA;AAAA,EACf,CAAA,EAAG,EAAE,CAAA;AAEL,EAAA,MAAMK,KAAAA,GAAOL,iBAAAA;AAAA,IACX,OACE,QACA,IAAA,KACuB;AACvB,MAAA,IAAI,CAAC,MAAA,EAAQ;AACX,QAAA,MAAMM,MAAAA,GAAQ,IAAI,KAAA,CAAM,iCAAiC,CAAA;AACzD,QAAA,QAAA,CAASA,MAAK,CAAA;AACd,QAAA,MAAMA,MAAAA;AAAA,MACR;AAEA,MAAA,IAAI,CAAC,WAAA,EAAa;AAChB,QAAA,MAAMA,MAAAA,GAAQ,IAAI,KAAA,CAAM,6BAA6B,CAAA;AACrD,QAAA,QAAA,CAASA,MAAK,CAAA;AACd,QAAA,MAAMA,MAAAA;AAAA,MACR;AAGA,MAAA,QAAA,CAAS,IAAI,CAAA;AACb,MAAA,YAAA,CAAa,IAAI,CAAA;AAEjB,MAAA,IAAI;AACF,QAAAT,QAAO,IAAA,CAAK,CAAA,iBAAA,EAAoB,MAAM,CAAA,CAAA,EAAI,EAAE,MAAM,CAAA;AAElD,QAAA,MAAM,MAAA,GAAS,MAAM,MAAA,CAAO,QAAA,CAA8B,QAAQ,IAAI,CAAA;AAEtE,QAAAA,OAAAA,CAAO,OAAA,CAAQ,CAAA,oBAAA,EAAuB,MAAM,CAAA,CAAE,CAAA;AAC9C,QAAA,YAAA,CAAa,KAAK,CAAA;AAClB,QAAA,OAAO,MAAA;AAAA,MACT,SAAS,GAAA,EAAK;AACZ,QAAA,MAAM,WACJ,GAAA,YAAe,KAAA,GAAQ,GAAA,GAAM,IAAI,MAAM,wBAAwB,CAAA;AAEjE,QAAA,QAAA,CAAS,QAAQ,CAAA;AACjB,QAAAA,OAAAA,CAAO,KAAA,CAAM,CAAA,mBAAA,EAAsB,MAAM,IAAI,QAAQ,CAAA;AAGrD,QAAA,IAAI,eAAe,OAAA,EAAS;AAC1B,UAAA,IAAI;AACF,YAAA,cAAA,CAAe,QAAQ,QAAQ,CAAA;AAAA,UACjC,SAAS,aAAA,EAAe;AACtB,YAAAA,OAAAA,CAAO,KAAA,CAAM,2BAAA,EAA6B,aAAa,CAAA;AAAA,UACzD;AAAA,QACF;AAEA,QAAA,YAAA,CAAa,KAAK,CAAA;AAClB,QAAA,MAAM,QAAA;AAAA,MACR;AAAA,IACF,CAAA;AAAA,IACA,CAAC,MAAA,EAAQ,WAAA,EAAa,cAAA,EAAgBA,OAAM;AAAA,GAC9C;AAEA,EAAA,OAAO;AAAA,IACL,IAAA,EAAAQ,KAAAA;AAAA,IACA,SAAA;AAAA,IACA,KAAA;AAAA,IACA;AAAA,GACF;AACF;AAxEgB,MAAA,CAAA,WAAA,EAAA,aAAA,CAAA","file":"hooks.cjs","sourcesContent":["/**\n * Shared Consola Logger\n *\n * Single consola instance used across the package for development logging.\n */\n\nimport { createConsola } from 'consola';\n\nimport type { Logger } from './createLogger';\n\nconst isDevelopment = process.env.NODE_ENV === 'development';\n\n/**\n * Shared consola logger for Centrifugo package\n */\nexport const consolaLogger = createConsola({\n level: isDevelopment ? 4 : 3,\n formatOptions: {\n colors: true,\n date: false,\n compact: !isDevelopment,\n },\n}).withTag('[Centrifugo]');\n\n/**\n * Get a consola logger with custom tag wrapped to match Logger interface\n */\nexport function getConsolaLogger(tag: string): Logger {\n const consola = consolaLogger.withTag(`[${tag}]`);\n\n return {\n debug: (message: string, data?: unknown) => consola.debug(message, data || ''),\n info: (message: string, data?: unknown) => consola.info(message, data || ''),\n success: (message: string, data?: unknown) => consola.success(message, data || ''),\n warning: (message: string, data?: unknown) => consola.warn(message, data || ''),\n error: (message: string, error?: Error | unknown) => consola.error(message, error || ''),\n };\n}\n","/**\n * Channel name validation utilities for Centrifugo\n *\n * Helps detect common mistakes with channel naming that can lead to permission errors.\n */\n\nexport interface ChannelValidationResult {\n valid: boolean;\n warning?: string;\n suggestion?: string;\n}\n\n/**\n * Validates Centrifugo channel name and detects potential issues.\n *\n * Common issues:\n * - Using `#` for namespace separator (should use `:`)\n * - User-limited channels without proper JWT token setup\n *\n * @param channel - Channel name to validate\n * @returns Validation result with warnings and suggestions\n *\n * @example\n * ```ts\n * // ❌ Bad: might be interpreted as user-limited channel\n * validateChannelName('terminal#session#abc123')\n * // Returns: { valid: false, warning: \"...\", suggestion: \"Use terminal:session:abc123\" }\n *\n * // ✅ Good: proper namespace separator\n * validateChannelName('terminal:session:abc123')\n * // Returns: { valid: true }\n * ```\n */\nexport function validateChannelName(channel: string): ChannelValidationResult {\n // Check for multiple # symbols (potential user-limited channel misuse)\n const hashCount = (channel.match(/#/g) || []).length;\n\n if (hashCount >= 2) {\n // Pattern: namespace#something#something\n // This might be interpreted as user-limited channel: namespace#user_id#channel\n const parts = channel.split('#');\n const [namespace, possibleUserId, ...rest] = parts;\n\n // Check if second part looks like a user ID (numeric)\n const isNumericUserId = /^\\d+$/.test(possibleUserId);\n\n if (!isNumericUserId && possibleUserId) {\n // Non-numeric second part after # - likely a mistake\n const suggestion = channel.replace(/#/g, ':');\n\n return {\n valid: false,\n warning: `Channel \"${channel}\" uses '#' separator which Centrifugo interprets as user-limited channel boundary. ` +\n `The part \"${possibleUserId}\" will be treated as user_id, which may cause permission errors if not in JWT token.`,\n suggestion: `Use ':' for namespace separation: \"${suggestion}\"`\n };\n }\n\n if (isNumericUserId) {\n return {\n valid: true,\n warning: `Channel \"${channel}\" appears to be a user-limited channel (user_id: ${possibleUserId}). ` +\n `Make sure your JWT token's \"sub\" field matches \"${possibleUserId}\".`,\n };\n }\n }\n\n // Single # is okay for user-limited channels like \"user#123\"\n if (hashCount === 1) {\n const [namespace, userId] = channel.split('#');\n if (userId && !/^\\d+$/.test(userId) && userId !== '*') {\n // Non-numeric user_id (not a wildcard) - might be a mistake\n const suggestion = channel.replace('#', ':');\n return {\n valid: false,\n warning: `Channel \"${channel}\" uses '#' but \"${userId}\" doesn't look like a user_id. ` +\n `This might cause permission issues.`,\n suggestion: `Consider using ':' instead: \"${suggestion}\"`\n };\n }\n }\n\n return { valid: true };\n}\n\n/**\n * Log channel validation warnings to console (development only).\n *\n * @param channel - Channel name to validate\n * @param logger - Logger instance (optional)\n */\nexport function logChannelWarnings(\n channel: string,\n logger?: { warning: (msg: string) => void }\n): void {\n if (process.env.NODE_ENV === 'production') {\n return; // Skip in production\n }\n\n const result = validateChannelName(channel);\n\n if (!result.valid && result.warning) {\n const message = `[Centrifugo Channel Warning]\\n${result.warning}${\n result.suggestion ? `\\n💡 Suggestion: ${result.suggestion}` : ''\n }`;\n\n if (logger?.warning) {\n logger.warning(message);\n } else {\n console.warn(message);\n }\n } else if (result.warning) {\n // Valid but has informational warning\n if (logger?.warning) {\n logger.warning(`[Centrifugo] ${result.warning}`);\n }\n }\n}\n","/**\n * Connection Status Component\n *\n * Universal component for displaying Centrifugo connection status\n * Supports multiple variants: badge, inline, card, detailed\n */\n\n'use client';\n\nimport { Clock, Radio, Wifi, WifiOff } from 'lucide-react';\nimport moment from 'moment';\nimport React, { useEffect, useState } from 'react';\n\nimport { Badge } from '@djangocfg/ui-nextjs';\n\nimport { getConsolaLogger } from '../../core/logger/consolaLogger';\nimport { useCentrifugo } from '../../providers/CentrifugoProvider';\n\nconst logger = getConsolaLogger('ConnectionStatus');\n\n// ─────────────────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────────────────\n\nexport interface ConnectionStatusProps {\n variant?: 'badge' | 'inline' | 'detailed';\n showUptime?: boolean;\n showSubscriptions?: boolean;\n className?: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Component\n// ─────────────────────────────────────────────────────────────────────────\n\nexport function ConnectionStatus({\n variant = 'badge',\n showUptime = false,\n showSubscriptions = false,\n className = '',\n}: ConnectionStatusProps) {\n const { isConnected, client } = useCentrifugo();\n const [connectionTime, setConnectionTime] = useState<moment.Moment | null>(null);\n const [uptime, setUptime] = useState<string>('');\n const [activeSubscriptions, setActiveSubscriptions] = useState<number>(0);\n\n // Track connection time\n useEffect(() => {\n if (isConnected && !connectionTime) {\n setConnectionTime(moment.utc());\n } else if (!isConnected) {\n setConnectionTime(null);\n }\n }, [isConnected, connectionTime]);\n\n // Update uptime every second\n useEffect(() => {\n if (!isConnected || !connectionTime) {\n setUptime('');\n return;\n }\n\n const updateUptime = () => {\n const now = moment.utc();\n const duration = moment.duration(now.diff(connectionTime));\n\n const hours = Math.floor(duration.asHours());\n const minutes = duration.minutes();\n const seconds = duration.seconds();\n\n if (hours > 0) {\n setUptime(`${hours}h ${minutes}m`);\n } else if (minutes > 0) {\n setUptime(`${minutes}m ${seconds}s`);\n } else {\n setUptime(`${seconds}s`);\n }\n };\n\n updateUptime();\n const interval = setInterval(updateUptime, 1000);\n\n return () => clearInterval(interval);\n }, [isConnected, connectionTime]);\n\n // Update active subscriptions count\n useEffect(() => {\n if (!client || !isConnected) {\n setActiveSubscriptions(0);\n return;\n }\n\n const updateCount = () => {\n try {\n const centrifuge = client.getCentrifuge();\n const subs = centrifuge.subscriptions();\n setActiveSubscriptions(Object.keys(subs).length);\n } catch (error) {\n logger.error('Failed to get active subscriptions', error);\n }\n };\n\n updateCount();\n const interval = setInterval(updateCount, 2000);\n\n return () => clearInterval(interval);\n }, [client, isConnected]);\n\n // Badge variant\n if (variant === 'badge') {\n return (\n <Badge\n variant={isConnected ? 'default' : 'destructive'}\n className={`flex items-center gap-1 ${isConnected ? 'animate-pulse' : ''} ${className}`}\n >\n <span className={`h-2 w-2 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`} />\n {isConnected ? 'Connected' : 'Disconnected'}\n </Badge>\n );\n }\n\n // Inline variant\n if (variant === 'inline') {\n return (\n <div className={`flex items-center gap-2 ${className}`}>\n {isConnected ? (\n <Wifi className=\"h-4 w-4 text-green-600\" />\n ) : (\n <WifiOff className=\"h-4 w-4 text-red-600\" />\n )}\n <span className=\"text-sm font-medium\">\n {isConnected ? 'Connected' : 'Disconnected'}\n </span>\n {showUptime && uptime && (\n <span className=\"text-xs text-muted-foreground\">({uptime})</span>\n )}\n {showSubscriptions && (\n <span className=\"text-xs text-muted-foreground flex items-center gap-1\">\n <Radio className=\"h-3 w-3\" />\n {activeSubscriptions}\n </span>\n )}\n </div>\n );\n }\n\n // Detailed variant\n return (\n <div className={`space-y-3 ${className}`}>\n {/* Status Badge */}\n <div className=\"flex items-center gap-2\">\n <Badge\n variant={isConnected ? 'default' : 'destructive'}\n className={`flex items-center gap-1 ${isConnected ? 'animate-pulse' : ''}`}\n >\n <span className={`h-2 w-2 rounded-full ${isConnected ? 'bg-green-500' : 'bg-red-500'}`} />\n {isConnected ? 'Connected' : 'Disconnected'}\n </Badge>\n </div>\n\n {/* Connection Info */}\n {isConnected ? (\n <div className=\"space-y-2\">\n {/* Uptime */}\n {showUptime && uptime && (\n <div className=\"flex items-center justify-between text-xs\">\n <span className=\"text-muted-foreground flex items-center gap-1\">\n <Clock className=\"h-3 w-3\" />\n Uptime:\n </span>\n <span className=\"font-mono font-medium\">{uptime}</span>\n </div>\n )}\n\n {/* Active Subscriptions */}\n {showSubscriptions && (\n <div className=\"flex items-center justify-between text-xs\">\n <span className=\"text-muted-foreground flex items-center gap-1\">\n <Radio className=\"h-3 w-3\" />\n Subscriptions:\n </span>\n <span className=\"font-mono font-medium\">{activeSubscriptions}</span>\n </div>\n )}\n </div>\n ) : (\n <div className=\"text-xs text-muted-foreground p-2 rounded bg-red-50 dark:bg-red-950/20\">\n Real-time features unavailable\n </div>\n )}\n </div>\n );\n}\n\n","/**\n * Subscriptions List Component\n *\n * Displays active Centrifugo subscriptions with status and controls\n */\n\n'use client';\n\nimport { Subscription, SubscriptionState } from 'centrifuge';\nimport { Radio, RefreshCw, Trash2 } from 'lucide-react';\nimport React, { useEffect, useState } from 'react';\n\nimport {\n Badge, Button, Card, CardContent, CardHeader, CardTitle, ScrollArea\n} from '@djangocfg/ui-nextjs';\n\nimport { getConsolaLogger } from '../../core/logger/consolaLogger';\nimport { useCentrifugo } from '../../providers/CentrifugoProvider';\n\nconst logger = getConsolaLogger('SubscriptionsList');\n\n// ─────────────────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────────────────\n\ninterface SubscriptionItem {\n channel: string;\n state: SubscriptionState;\n}\n\nexport interface SubscriptionsListProps {\n showControls?: boolean;\n onSubscriptionClick?: (channel: string) => void;\n className?: string;\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Component\n// ─────────────────────────────────────────────────────────────────────────\n\nexport function SubscriptionsList({\n showControls = true,\n onSubscriptionClick,\n className = '',\n}: SubscriptionsListProps) {\n const { isConnected, client } = useCentrifugo();\n const [subscriptions, setSubscriptions] = useState<SubscriptionItem[]>([]);\n\n // Update subscriptions list\n const updateSubscriptions = () => {\n if (!client || !isConnected) {\n setSubscriptions([]);\n return;\n }\n\n try {\n const centrifuge = client.getCentrifuge();\n const subs = centrifuge.subscriptions();\n const subscriptionsList: SubscriptionItem[] = [];\n \n for (const [channel, sub] of Object.entries(subs)) {\n subscriptionsList.push({\n channel,\n state: sub.state,\n });\n }\n \n setSubscriptions(subscriptionsList);\n } catch (error) {\n logger.error('Failed to get subscriptions', error);\n setSubscriptions([]);\n }\n };\n\n // Auto-update subscriptions\n useEffect(() => {\n updateSubscriptions();\n\n if (!client) return;\n\n const centrifuge = client.getCentrifuge();\n\n const handleSubscribed = () => updateSubscriptions();\n const handleUnsubscribed = () => updateSubscriptions();\n\n centrifuge.on('subscribed', handleSubscribed);\n centrifuge.on('unsubscribed', handleUnsubscribed);\n\n const interval = setInterval(updateSubscriptions, 3000);\n\n return () => {\n centrifuge.off('subscribed', handleSubscribed);\n centrifuge.off('unsubscribed', handleUnsubscribed);\n clearInterval(interval);\n };\n }, [client, isConnected]);\n\n // Unsubscribe from channel\n const handleUnsubscribe = async (channel: string) => {\n if (!client) return;\n\n try {\n await client.unsubscribe(channel);\n updateSubscriptions();\n } catch (error) {\n logger.error('Failed to unsubscribe', error);\n }\n };\n\n return (\n <Card className={className}>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <CardTitle className=\"flex items-center gap-2\">\n <Radio className=\"h-5 w-5\" />\n Active Subscriptions\n <Badge variant=\"outline\">{subscriptions.length}</Badge>\n </CardTitle>\n\n {showControls && (\n <Button size=\"sm\" variant=\"outline\" onClick={updateSubscriptions}>\n <RefreshCw className=\"h-4 w-4\" />\n </Button>\n )}\n </div>\n </CardHeader>\n\n <CardContent>\n {!isConnected ? (\n <div className=\"text-center py-8 text-sm text-muted-foreground\">\n Not connected to Centrifugo\n </div>\n ) : subscriptions.length === 0 ? (\n <div className=\"text-center py-8 text-sm text-muted-foreground\">\n No active subscriptions\n </div>\n ) : (\n <ScrollArea className=\"h-[300px]\">\n <div className=\"space-y-2\">\n {subscriptions.map((sub) => (\n <div\n key={sub.channel}\n className=\"flex items-center justify-between p-3 rounded border hover:bg-muted/50 transition-colors\"\n >\n <div\n className=\"flex-1 min-w-0 cursor-pointer\"\n onClick={() => onSubscriptionClick?.(sub.channel)}\n >\n <div className=\"flex items-center gap-2\">\n <Badge variant=\"outline\" className=\"text-xs\">\n {sub.state}\n </Badge>\n <span className=\"text-sm font-mono truncate\">{sub.channel}</span>\n </div>\n </div>\n\n {showControls && (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => handleUnsubscribe(sub.channel)}\n >\n <Trash2 className=\"h-4 w-4 text-destructive\" />\n </Button>\n )}\n </div>\n ))}\n </div>\n </ScrollArea>\n )}\n </CardContent>\n </Card>\n );\n}\n\n","/**\n * Centrifugo Package Configuration\n */\n\nexport const isDevelopment = process.env.NODE_ENV === 'development';\nexport const isProduction = !isDevelopment;\nexport const isStaticBuild = process.env.NEXT_PUBLIC_STATIC_BUILD === 'true';\n\nconst showDebugPanel = isDevelopment && !isStaticBuild;\n\n/**\n * Reconnect configuration with exponential backoff\n */\nexport const reconnectConfig = {\n // Initial delay before first reconnect attempt (ms)\n initialDelay: isDevelopment ? 2000 : 1000,\n // Maximum delay between reconnect attempts (ms)\n maxDelay: isDevelopment ? 30000 : 60000,\n // Multiplier for exponential backoff\n multiplier: 1.5,\n // Maximum number of reconnect attempts\n // Dev: 3 attempts then stop (server probably not running)\n // Prod: 10 attempts then stop (avoid infinite reconnection spam)\n maxAttempts: isDevelopment ? 3 : 10,\n // Jitter factor to randomize delays (0-1)\n jitter: 0.1,\n} as const;\n\nexport const centrifugoConfig = {\n // Show debug panel only in development and not in static builds\n showDebugPanel,\n // Reconnect settings\n reconnect: reconnectConfig,\n} as const;\n\nexport type CentrifugoConfig = typeof centrifugoConfig;\nexport type ReconnectConfig = typeof reconnectConfig;\n","/**\n * usePageVisibility Hook\n *\n * Tracks browser tab visibility state using the Page Visibility API.\n * Returns true when the page is visible, false when hidden.\n *\n * Use cases:\n * - Pause/resume WebSocket connections\n * - Pause expensive operations when tab is inactive\n * - Trigger data sync when tab becomes visible\n */\n\n'use client';\n\nimport { useEffect, useState, useCallback, useRef } from 'react';\n\n// =============================================================================\n// TYPES\n// =============================================================================\n\nexport interface PageVisibilityState {\n /** Whether the page is currently visible */\n isVisible: boolean;\n /** Whether the page was ever hidden during this session */\n wasHidden: boolean;\n /** Timestamp when page became visible (for uptime tracking) */\n visibleSince: number | null;\n /** How long the page was hidden (ms), reset when visible */\n hiddenDuration: number;\n}\n\nexport interface UsePageVisibilityOptions {\n /** Callback when page becomes visible */\n onVisible?: () => void;\n /** Callback when page becomes hidden */\n onHidden?: () => void;\n /** Callback with visibility state change */\n onChange?: (isVisible: boolean) => void;\n}\n\nexport interface UsePageVisibilityResult extends PageVisibilityState {\n /** Force check visibility state */\n checkVisibility: () => boolean;\n}\n\n// =============================================================================\n// HELPERS\n// =============================================================================\n\n/**\n * Get current visibility state\n * SSR-safe: returns true on server\n */\nfunction getVisibilityState(): boolean {\n if (typeof document === 'undefined') return true;\n return document.visibilityState === 'visible';\n}\n\n// =============================================================================\n// HOOK\n// =============================================================================\n\nexport function usePageVisibility(\n options: UsePageVisibilityOptions = {}\n): UsePageVisibilityResult {\n const { onVisible, onHidden, onChange } = options;\n\n const [state, setState] = useState<PageVisibilityState>(() => ({\n isVisible: getVisibilityState(),\n wasHidden: false,\n visibleSince: getVisibilityState() ? Date.now() : null,\n hiddenDuration: 0,\n }));\n\n // Refs to avoid stale closures in callbacks\n const onVisibleRef = useRef(onVisible);\n const onHiddenRef = useRef(onHidden);\n const onChangeRef = useRef(onChange);\n const hiddenAtRef = useRef<number | null>(null);\n\n // Keep refs updated\n onVisibleRef.current = onVisible;\n onHiddenRef.current = onHidden;\n onChangeRef.current = onChange;\n\n const checkVisibility = useCallback((): boolean => {\n return getVisibilityState();\n }, []);\n\n useEffect(() => {\n if (typeof document === 'undefined') return;\n\n const handleVisibilityChange = () => {\n const isNowVisible = getVisibilityState();\n\n setState((prev) => {\n // Calculate hidden duration if becoming visible\n let hiddenDuration = prev.hiddenDuration;\n if (isNowVisible && hiddenAtRef.current !== null) {\n hiddenDuration = Date.now() - hiddenAtRef.current;\n hiddenAtRef.current = null;\n }\n\n // Track when we became hidden\n if (!isNowVisible) {\n hiddenAtRef.current = Date.now();\n }\n\n return {\n isVisible: isNowVisible,\n wasHidden: prev.wasHidden || !isNowVisible,\n visibleSince: isNowVisible ? (prev.visibleSince ?? Date.now()) : null,\n hiddenDuration,\n };\n });\n\n // Trigger callbacks\n onChangeRef.current?.(isNowVisible);\n\n if (isNowVisible) {\n onVisibleRef.current?.();\n } else {\n onHiddenRef.current?.();\n }\n };\n\n document.addEventListener('visibilitychange', handleVisibilityChange);\n\n return () => {\n document.removeEventListener('visibilitychange', handleVisibilityChange);\n };\n }, []);\n\n return {\n ...state,\n checkVisibility,\n };\n}\n\nexport default usePageVisibility;\n","/**\n * Logs Provider\n *\n * Provides access to accumulated logs via React Context.\n * Wraps LogsStore and exposes logs + controls.\n */\n\n'use client';\n\nimport {\n createContext, ReactNode, useCallback, useContext, useEffect, useState\n} from 'react';\n\nimport { getGlobalLogsStore } from '../../core/logger';\n\nimport type { LogEntry, LogLevel } from '../../core/types';\n// ─────────────────────────────────────────────────────────────────────────\n// Context\n// ─────────────────────────────────────────────────────────────────────────\n\nexport interface LogsContextValue {\n logs: LogEntry[];\n filteredLogs: LogEntry[];\n filter: LogsFilter;\n setFilter: (filter: Partial<LogsFilter>) => void;\n clearLogs: () => void;\n count: number;\n}\n\nexport interface LogsFilter {\n level?: LogLevel;\n source?: LogEntry['source'];\n search?: string;\n}\n\nconst LogsContext = createContext<LogsContextValue | undefined>(undefined);\n\n// ─────────────────────────────────────────────────────────────────────────\n// Provider\n// ─────────────────────────────────────────────────────────────────────────\n\nexport interface LogsProviderProps {\n children: ReactNode;\n}\n\nexport function LogsProvider({ children }: LogsProviderProps) {\n const [logs, setLogs] = useState<LogEntry[]>([]);\n const [filter, setFilterState] = useState<LogsFilter>({});\n\n const logsStore = getGlobalLogsStore();\n\n // Subscribe to log changes\n useEffect(() => {\n // Initial load\n setLogs(logsStore.getAll());\n\n // Subscribe to updates\n const unsubscribe = logsStore.subscribe((updatedLogs) => {\n setLogs(updatedLogs);\n });\n\n return unsubscribe;\n }, [logsStore]);\n\n // Filter logs\n const filteredLogs = logs.filter((log) => {\n if (filter.level && log.level !== filter.level) return false;\n if (filter.source && log.source !== filter.source) return false;\n if (filter.search) {\n const searchLower = filter.search.toLowerCase();\n return log.message.toLowerCase().includes(searchLower);\n }\n return true;\n });\n\n // Set filter (merge with existing)\n const setFilter = useCallback((partialFilter: Partial<LogsFilter>) => {\n setFilterState((prev) => ({ ...prev, ...partialFilter }));\n }, []);\n\n // Clear all logs\n const clearLogs = useCallback(() => {\n logsStore.clear();\n }, [logsStore]);\n\n const value: LogsContextValue = {\n logs,\n filteredLogs,\n filter,\n setFilter,\n clearLogs,\n count: logs.length,\n };\n\n return <LogsContext.Provider value={value}>{children}</LogsContext.Provider>;\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Hook\n// ─────────────────────────────────────────────────────────────────────────\n\nexport function useLogs(): LogsContextValue {\n const context = useContext(LogsContext);\n\n if (context === undefined) {\n throw new Error('useLogs must be used within a LogsProvider');\n }\n\n return context;\n}\n","/**\n * Centrifugo Provider\n *\n * Main provider that manages WebSocket connection.\n * Wraps LogsProvider to provide logs accumulation.\n */\n\n'use client';\n\nimport {\n createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState\n} from 'react';\n\nimport { useAuth } from '@djangocfg/api/auth';\n\nimport {\n CentrifugoMonitorDialog\n} from '../../components/CentrifugoMonitor/CentrifugoMonitorDialog';\nimport { isDevelopment, isStaticBuild, reconnectConfig } from '../../config';\nimport { CentrifugoRPCClient } from '../../core/client';\nimport { getConsolaLogger } from '../../core/logger/consolaLogger';\nimport { useCodegenTip } from '../../hooks/useCodegenTip';\nimport { usePageVisibility } from '../../hooks/usePageVisibility';\nimport { LogsProvider } from '../LogsProvider';\n\nimport type { ConnectionState, CentrifugoToken, ActiveSubscription } from '../../core/types';\n// ─────────────────────────────────────────────────────────────────────────\n// Context\n// ─────────────────────────────────────────────────────────────────────────\n\nexport interface CentrifugoContextValue {\n // Client\n client: CentrifugoRPCClient | null;\n\n // Connection State\n isConnected: boolean;\n isConnecting: boolean;\n error: Error | null;\n connectionState: ConnectionState;\n\n // Connection Info\n uptime: number; // seconds\n subscriptions: string[];\n activeSubscriptions: ActiveSubscription[];\n\n // Controls\n connect: () => Promise<void>;\n disconnect: () => void;\n reconnect: () => Promise<void>;\n unsubscribe: (channel: string) => void;\n\n // Config\n enabled: boolean;\n}\n\nconst CentrifugoContext = createContext<CentrifugoContextValue | undefined>(undefined);\n\n// ─────────────────────────────────────────────────────────────────────────\n// Provider Props\n// ─────────────────────────────────────────────────────────────────────────\n\nexport interface CentrifugoProviderProps {\n children: ReactNode;\n enabled?: boolean;\n url?: string;\n autoConnect?: boolean;\n /**\n * Callback to refresh the Centrifugo token when it expires.\n * If provided, centrifuge-js will automatically call this when token expires.\n * Should return a fresh JWT token string.\n *\n * @example\n * onTokenRefresh={async () => {\n * const response = await getCentrifugoAuthTokenRetrieve();\n * return response.token;\n * }}\n */\n onTokenRefresh?: () => Promise<string>;\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Inner Provider (has access to LogsProvider)\n// ─────────────────────────────────────────────────────────────────────────\n\nfunction CentrifugoProviderInner({\n children,\n enabled = false,\n url,\n autoConnect: autoConnectProp = true,\n onTokenRefresh,\n}: CentrifugoProviderProps) {\n // useAuth is SSR-safe - returns default state when outside AuthProvider\n const { isAuthenticated, isLoading, user } = useAuth();\n\n const [client, setClient] = useState<CentrifugoRPCClient | null>(null);\n const [isConnected, setIsConnected] = useState(false);\n const [isConnecting, setIsConnecting] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n const [connectionTime, setConnectionTime] = useState<Date | null>(null);\n const [uptime, setUptime] = useState<number>(0);\n const [subscriptions, setSubscriptions] = useState<string[]>([]);\n const [activeSubscriptions, setActiveSubscriptions] = useState<ActiveSubscription[]>([]);\n\n const logger = useMemo(() => getConsolaLogger('provider'), []);\n\n // Show dev tip about client generation\n useCodegenTip();\n\n const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);\n const hasConnectedRef = useRef(false);\n const isConnectingRef = useRef(false);\n const isMountedRef = useRef(true);\n const reconnectAttemptRef = useRef(0);\n const reconnectStoppedRef = useRef(false); // Track if we should stop reconnecting\n const devWarningShownRef = useRef(false); // Track if server unavailable warning was shown\n const connectRef = useRef<(() => Promise<void>) | null>(null);\n const disconnectRef = useRef<(() => void) | null>(null);\n const wasConnectedBeforeHiddenRef = useRef(false); // Track connection state before page hidden\n const clientInstanceRef = useRef<CentrifugoRPCClient | null>(null); // Stable client reference for reconnection\n\n const centrifugoToken: CentrifugoToken | undefined = user?.centrifugo;\n const hasCentrifugoToken = !!centrifugoToken?.token;\n\n // Calculate reconnect delay with exponential backoff\n const getReconnectDelay = useCallback((attempt: number): number => {\n const { initialDelay, maxDelay, multiplier, jitter } = reconnectConfig;\n\n // Exponential backoff: initialDelay * multiplier^attempt\n let delay = initialDelay * Math.pow(multiplier, attempt);\n\n // Cap at maxDelay\n delay = Math.min(delay, maxDelay);\n\n // Add jitter to prevent thundering herd\n const jitterAmount = delay * jitter * (Math.random() * 2 - 1);\n delay = Math.round(delay + jitterAmount);\n\n return delay;\n }, []);\n\n const wsUrl = useMemo(() => {\n if (url) return url;\n if (centrifugoToken?.centrifugo_url) return centrifugoToken.centrifugo_url;\n return '';\n }, [url, centrifugoToken?.centrifugo_url]);\n\n const autoConnect = autoConnectProp &&\n (isAuthenticated && !isLoading) &&\n enabled &&\n hasCentrifugoToken;\n\n // Log connection decision\n useEffect(() => {\n if (!isLoading) {\n logger.info(`Auto-connect: ${autoConnect ? 'YES' : 'NO'}`, {\n authenticated: isAuthenticated,\n loading: isLoading,\n enabled,\n hasToken: hasCentrifugoToken,\n url: wsUrl,\n });\n }\n }, [autoConnect, isAuthenticated, isLoading, enabled, hasCentrifugoToken, logger, wsUrl]);\n\n // Update uptime every second\n useEffect(() => {\n if (!isConnected || !connectionTime) {\n setUptime(0);\n return;\n }\n\n const updateUptime = () => {\n const now = new Date();\n const diff = Math.floor((now.getTime() - connectionTime.getTime()) / 1000);\n setUptime(diff);\n };\n\n updateUptime();\n const interval = setInterval(updateUptime, 1000);\n\n return () => clearInterval(interval);\n }, [isConnected, connectionTime]);\n\n // Update subscriptions periodically\n useEffect(() => {\n if (!client || !isConnected) {\n setSubscriptions([]);\n setActiveSubscriptions([]);\n return;\n }\n\n const updateSubs = () => {\n try {\n const subs = client.getAllSubscriptions?.() || [];\n setSubscriptions(subs);\n\n // Convert to ActiveSubscription format\n const activeSubs: ActiveSubscription[] = subs.map((channel) => ({\n channel,\n type: 'client' as const,\n subscribedAt: Date.now(),\n }));\n setActiveSubscriptions(activeSubs);\n } catch (error) {\n logger.error('Failed to get subscriptions', error);\n }\n };\n\n updateSubs();\n const interval = setInterval(updateSubs, 2000);\n\n return () => clearInterval(interval);\n }, [client, isConnected, logger]);\n\n // Connect function\n const connect = useCallback(async () => {\n // Don't reconnect if we've decided to stop (dev mode hit max attempts)\n if (reconnectStoppedRef.current) return;\n if (hasConnectedRef.current || isConnectingRef.current) return;\n if (isConnecting || isConnected) return;\n\n isConnectingRef.current = true;\n setIsConnecting(true);\n setError(null);\n\n try {\n // Check if we can reuse existing client (reconnection scenario)\n if (clientInstanceRef.current) {\n logger.info('Reconnecting to WebSocket server (reusing client)...');\n await clientInstanceRef.current.connect();\n\n if (!isMountedRef.current) {\n isConnectingRef.current = false;\n return;\n }\n\n hasConnectedRef.current = true;\n isConnectingRef.current = false;\n\n // Clear any pending reconnect timeout\n if (reconnectTimeoutRef.current) {\n clearTimeout(reconnectTimeoutRef.current);\n reconnectTimeoutRef.current = null;\n }\n\n // Use existing client reference - NO setClient() call to keep reference stable\n setIsConnected(true);\n setConnectionTime(new Date());\n setError(null);\n\n logger.success('WebSocket reconnected');\n\n // Reset reconnect state on successful connection\n reconnectAttemptRef.current = 0;\n devWarningShownRef.current = false;\n reconnectStoppedRef.current = false;\n return;\n }\n\n // First connection - create new client\n logger.info('Connecting to WebSocket server...');\n\n if (!centrifugoToken?.token) {\n throw new Error('No Centrifugo token available');\n }\n\n const token = centrifugoToken.token;\n let userId = user?.id?.toString() || '1';\n\n if (!user?.id) {\n try {\n const tokenPayload = JSON.parse(atob(token.split('.')[1]));\n userId = tokenPayload.user_id?.toString() || tokenPayload.sub?.toString() || '1';\n } catch (err) {\n // Fallback\n }\n }\n\n const rpcClient = new CentrifugoRPCClient({\n url: wsUrl,\n token,\n userId,\n timeout: 30000,\n logger,\n getToken: onTokenRefresh,\n });\n await rpcClient.connect();\n\n if (!isMountedRef.current) {\n rpcClient.disconnect();\n isConnectingRef.current = false;\n return;\n }\n\n hasConnectedRef.current = true;\n isConnectingRef.current = false;\n\n // Clear any pending reconnect timeout\n if (reconnectTimeoutRef.current) {\n clearTimeout(reconnectTimeoutRef.current);\n reconnectTimeoutRef.current = null;\n }\n\n // Store client in ref for future reconnections\n clientInstanceRef.current = rpcClient;\n setClient(rpcClient);\n setIsConnected(true);\n setConnectionTime(new Date());\n setError(null);\n\n logger.success('WebSocket connected');\n\n // Reset reconnect state on successful connection\n reconnectAttemptRef.current = 0;\n devWarningShownRef.current = false;\n reconnectStoppedRef.current = false;\n } catch (err) {\n const error = err instanceof Error ? err : new Error('Connection failed');\n setError(error);\n setClient(null);\n setIsConnected(false);\n setConnectionTime(null);\n hasConnectedRef.current = false;\n isConnectingRef.current = false;\n\n const isAuthError = error.message.includes('token') ||\n error.message.includes('auth') ||\n error.message.includes('expired');\n\n if (isAuthError) {\n logger.error('Authentication failed', error);\n } else {\n // Check if we should attempt reconnect\n const { maxAttempts } = reconnectConfig;\n const currentAttempt = reconnectAttemptRef.current;\n\n // In dev mode: show warning once and stop after maxAttempts\n if (isDevelopment) {\n if (!devWarningShownRef.current) {\n devWarningShownRef.current = true;\n logger.warning(\n '🔌 Centrifugo server is not running. ' +\n 'Start it with: docker compose -f docker-compose-local-services.yml up centrifugo'\n );\n }\n\n // Stop reconnecting after maxAttempts in dev mode\n if (maxAttempts > 0 && currentAttempt >= maxAttempts) {\n reconnectStoppedRef.current = true; // Mark as stopped permanently\n logger.info(`Stopped reconnecting after ${maxAttempts} attempts (dev mode)`);\n return;\n }\n }\n\n // Try to reconnect with exponential backoff (respects maxAttempts)\n if (currentAttempt < maxAttempts) {\n const delay = getReconnectDelay(currentAttempt);\n reconnectAttemptRef.current = currentAttempt + 1;\n\n if (!isDevelopment || currentAttempt < 2) {\n // Only log in prod, or first 2 attempts in dev\n logger.info(`Reconnecting in ${Math.round(delay / 1000)}s (attempt ${currentAttempt + 1}/${maxAttempts})...`);\n }\n\n reconnectTimeoutRef.current = setTimeout(() => {\n connectRef.current?.();\n }, delay);\n } else {\n // Max attempts reached - stop reconnecting\n reconnectStoppedRef.current = true;\n logger.warning(`Stopped reconnecting after ${maxAttempts} attempts. WebSocket server may be unavailable.`);\n }\n }\n } finally {\n setIsConnecting(false);\n }\n }, [wsUrl, centrifugoToken, user, logger, isConnecting, isConnected, getReconnectDelay, onTokenRefresh]);\n\n // Disconnect function\n const disconnect = useCallback(() => {\n if (isConnectingRef.current) return;\n\n if (reconnectTimeoutRef.current) {\n clearTimeout(reconnectTimeoutRef.current);\n reconnectTimeoutRef.current = null;\n }\n\n if (client) {\n logger.info('Disconnecting from WebSocket server...');\n client.disconnect();\n setClient(null);\n setIsConnected(false);\n setConnectionTime(null);\n setError(null);\n setSubscriptions([]);\n }\n\n hasConnectedRef.current = false;\n isConnectingRef.current = false;\n reconnectAttemptRef.current = 0;\n devWarningShownRef.current = false;\n reconnectStoppedRef.current = false; // Reset so manual reconnect works\n }, [client, logger]);\n\n // Reconnect function\n const reconnect = useCallback(async () => {\n disconnect();\n await connect();\n }, [connect, disconnect]);\n\n // Unsubscribe function\n const unsubscribe = useCallback((channel: string) => {\n if (!client) {\n logger.warning('Cannot unsubscribe: client not connected');\n return;\n }\n\n try {\n client.unsubscribe?.(channel);\n logger.info(`Unsubscribed from channel: ${channel}`);\n\n // Update state immediately\n setSubscriptions((prev) => prev.filter((ch) => ch !== channel));\n setActiveSubscriptions((prev) => prev.filter((sub) => sub.channel !== channel));\n } catch (error) {\n logger.error(`Failed to unsubscribe from ${channel}`, error);\n }\n }, [client, logger]);\n\n // Keep refs up-to-date\n connectRef.current = connect;\n disconnectRef.current = disconnect;\n\n // Auto-connect on mount - uses refs to avoid recreation issues\n useEffect(() => {\n isMountedRef.current = true;\n\n if (autoConnect && !hasConnectedRef.current && !reconnectStoppedRef.current) {\n connectRef.current?.();\n }\n\n return () => {\n if (isConnectingRef.current && !hasConnectedRef.current) {\n return;\n }\n\n if (!hasConnectedRef.current) {\n return;\n }\n\n isMountedRef.current = false;\n disconnectRef.current?.();\n };\n }, [autoConnect]); // Only depend on autoConnect, not on connect/disconnect\n\n // ==========================================================================\n // PAGE VISIBILITY HANDLING\n // ==========================================================================\n // When tab becomes hidden: pause reconnect attempts (save battery)\n // When tab becomes visible: trigger reconnect if needed\n\n usePageVisibility({\n onHidden: () => {\n // Save connection state before hiding\n wasConnectedBeforeHiddenRef.current = isConnected;\n\n // Pause reconnect attempts while tab is hidden (save battery)\n if (reconnectTimeoutRef.current) {\n clearTimeout(reconnectTimeoutRef.current);\n reconnectTimeoutRef.current = null;\n logger.debug('Paused reconnect attempts (tab hidden)');\n }\n },\n onVisible: () => {\n // Only attempt reconnect if:\n // 1. We should auto-connect\n // 2. We're not already connected or connecting\n // 3. We haven't stopped reconnecting\n // 4. We were previously connected (or never connected yet)\n const shouldReconnect =\n autoConnect &&\n !isConnected &&\n !isConnectingRef.current &&\n !reconnectStoppedRef.current &&\n (wasConnectedBeforeHiddenRef.current || !hasConnectedRef.current);\n\n if (shouldReconnect) {\n // Reset reconnect attempts for fresh start\n reconnectAttemptRef.current = 0;\n devWarningShownRef.current = false;\n\n logger.info('Tab visible - attempting reconnect...');\n connectRef.current?.();\n } else if (isConnected) {\n logger.debug('Tab visible - connection still active');\n }\n },\n });\n\n const connectionState: ConnectionState = isConnected\n ? 'connected'\n : isConnecting\n ? 'connecting'\n : error\n ? 'error'\n : 'disconnected';\n\n const value: CentrifugoContextValue = {\n client,\n isConnected,\n isConnecting,\n error,\n connectionState,\n uptime,\n subscriptions,\n activeSubscriptions,\n connect,\n disconnect,\n reconnect,\n unsubscribe,\n enabled,\n };\n\n return (\n <CentrifugoContext.Provider value={value}>\n {children}\n </CentrifugoContext.Provider>\n );\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Main Provider (wraps LogsProvider and includes Dialog)\n// ─────────────────────────────────────────────────────────────────────────\n\nexport function CentrifugoProvider(props: CentrifugoProviderProps) {\n return (\n <LogsProvider>\n <CentrifugoProviderInner {...props}>\n {props.children}\n <CentrifugoMonitorDialog />\n </CentrifugoProviderInner>\n </LogsProvider>\n );\n}\n\n// ─────────────────────────────────────────────────────────────────────────\n// Hook\n// ─────────────────────────────────────────────────────────────────────────\n\nexport function useCentrifugo(): CentrifugoContextValue {\n const context = useContext(CentrifugoContext);\n\n if (context === undefined) {\n throw new Error('useCentrifugo must be used within a CentrifugoProvider');\n }\n\n return context;\n}\n","/**\n * useSubscription Hook\n *\n * Subscribe to Centrifugo channel with auto-cleanup.\n * \n * Best practices:\n * - Callbacks are stored in refs to avoid re-subscriptions\n * - Proper error handling in callbacks (as recommended by centrifuge-js)\n * - Cleanup on unmount\n * - Stable unsubscribe function\n */\n\n'use client';\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { getConsolaLogger } from '../core/logger/consolaLogger';\nimport { logChannelWarnings } from '../core/utils/channelValidator';\nimport { useCentrifugo } from '../providers/CentrifugoProvider';\n\nexport interface UseSubscriptionOptions<T = any> {\n channel: string;\n enabled?: boolean;\n onPublication?: (data: T) => void;\n onError?: (error: Error) => void;\n}\n\nexport interface UseSubscriptionResult<T = any> {\n data: T | null;\n error: Error | null;\n isSubscribed: boolean;\n unsubscribe: () => void;\n}\n\nexport function useSubscription<T = any>(\n options: UseSubscriptionOptions<T>\n): UseSubscriptionResult<T> {\n const { client, isConnected } = useCentrifugo();\n const { channel, enabled = true, onPublication, onError } = options;\n\n const [data, setData] = useState<T | null>(null);\n const [error, setError] = useState<Error | null>(null);\n const [isSubscribed, setIsSubscribed] = useState(false);\n\n const unsubscribeRef = useRef<(() => void) | null>(null);\n const logger = useRef(getConsolaLogger('useSubscription')).current;\n \n // Store callbacks in refs to avoid re-subscriptions when they change\n const onPublicationRef = useRef(onPublication);\n const onErrorRef = useRef(onError);\n \n useEffect(() => {\n onPublicationRef.current = onPublication;\n onErrorRef.current = onError;\n }, [onPublication, onError]);\n\n // Unsubscribe function\n const unsubscribe = useCallback(() => {\n if (unsubscribeRef.current) {\n try {\n unsubscribeRef.current();\n unsubscribeRef.current = null;\n setIsSubscribed(false);\n logger.info(`Unsubscribed from channel: ${channel}`);\n } catch (err) {\n logger.error(`Error during unsubscribe from ${channel}`, err);\n }\n }\n }, [channel, logger]);\n\n // Subscribe effect\n useEffect(() => {\n if (!client || !isConnected || !enabled) {\n // Reset state when not connected\n if (!isConnected && isSubscribed) {\n setIsSubscribed(false);\n }\n return;\n }\n\n // Validate channel name and log warnings (dev only)\n logChannelWarnings(channel, logger);\n\n logger.info(`Subscribing to channel: ${channel}`);\n\n try {\n const unsub = client.subscribe(channel, (receivedData: T) => {\n try {\n // Error handling in callback as recommended by centrifuge-js docs\n setData(receivedData);\n setError(null);\n onPublicationRef.current?.(receivedData);\n } catch (callbackError) {\n logger.error(`Error in onPublication callback for ${channel}`, callbackError);\n }\n });\n\n unsubscribeRef.current = unsub;\n setIsSubscribed(true);\n logger.success(`Subscribed to channel: ${channel}`);\n } catch (err) {\n const subscriptionError = err instanceof Error ? err : new Error('Subscription failed');\n setError(subscriptionError);\n \n try {\n onErrorRef.current?.(subscriptionError);\n } catch (callbackError) {\n logger.error(`Error in onError callback for ${channel}`, callbackError);\n }\n \n logger.error(`Subscription failed: ${channel}`, subscriptionError);\n }\n\n // Cleanup on unmount or deps change\n return () => {\n unsubscribe();\n };\n }, [client, isConnected, enabled, channel, unsubscribe, logger, isSubscribed]);\n\n return {\n data,\n error,\n isSubscribed,\n unsubscribe,\n };\n}\n","/**\n * useRPC Hook\n *\n * React hook for making RPC calls via Centrifugo using correlation ID pattern.\n * Provides type-safe request-response communication over WebSocket.\n * \n * Pattern:\n * 1. Client sends request with correlation_id to rpc#{method}\n * 2. Backend processes and sends response to user#{userId} with same correlation_id\n * 3. Client matches response by correlation_id and resolves Promise\n * \n * @example\n * const { call, isLoading, error } = useRPC();\n * \n * const handleGetStats = async () => {\n * const result = await call('tasks.get_stats', { bot_id: '123' });\n * console.log('Stats:', result);\n * };\n */\n\n'use client';\n\nimport { useCallback, useRef, useState } from 'react';\n\nimport { getConsolaLogger } from '../core/logger/consolaLogger';\nimport { useCentrifugo } from '../providers/CentrifugoProvider';\n\nexport interface UseRPCOptions {\n timeout?: number;\n replyChannel?: string;\n onError?: (error: Error) => void;\n}\n\nexport interface UseRPCResult {\n call: <TRequest = any, TResponse = any>(\n method: string,\n params: TRequest,\n options?: UseRPCOptions\n ) => Promise<TResponse>;\n isLoading: boolean;\n error: Error | null;\n reset: () => void;\n}\n\nexport function useRPC(defaultOptions: UseRPCOptions = {}): UseRPCResult {\n const { client, isConnected } = useCentrifugo();\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const logger = useRef(getConsolaLogger('useRPC')).current;\n const abortControllerRef = useRef<AbortController | null>(null);\n\n const reset = useCallback(() => {\n setIsLoading(false);\n setError(null);\n if (abortControllerRef.current) {\n abortControllerRef.current.abort();\n abortControllerRef.current = null;\n }\n }, []);\n\n const call = useCallback(\n async <TRequest = any, TResponse = any>(\n method: string,\n params: TRequest,\n options: UseRPCOptions = {}\n ): Promise<TResponse> => {\n if (!client) {\n const error = new Error('Centrifugo client not available');\n setError(error);\n throw error;\n }\n\n if (!isConnected) {\n const error = new Error('Not connected to Centrifugo');\n setError(error);\n throw error;\n }\n\n // Reset previous state\n setError(null);\n setIsLoading(true);\n\n // Create abort controller for this call\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n\n try {\n const mergedOptions = {\n ...defaultOptions,\n ...options,\n };\n\n logger.info(`RPC call: ${method}`, { params });\n\n const result = await client.rpc<TRequest, TResponse>(\n method,\n params,\n {\n timeout: mergedOptions.timeout,\n replyChannel: mergedOptions.replyChannel,\n }\n );\n\n // Check if aborted\n if (abortController.signal.aborted) {\n throw new Error('RPC call aborted');\n }\n\n logger.success(`RPC success: ${method}`);\n setIsLoading(false);\n return result;\n } catch (err) {\n const rpcError = err instanceof Error ? err : new Error('RPC call failed');\n \n // Don't set error if aborted\n if (!abortController.signal.aborted) {\n setError(rpcError);\n logger.error(`RPC failed: ${method}`, rpcError);\n \n // Call error callback if provided\n const onError = options.onError || defaultOptions.onError;\n if (onError) {\n try {\n onError(rpcError);\n } catch (callbackError) {\n logger.error('Error in onError callback', callbackError);\n }\n }\n }\n \n setIsLoading(false);\n throw rpcError;\n } finally {\n if (abortControllerRef.current === abortController) {\n abortControllerRef.current = null;\n }\n }\n },\n [client, isConnected, defaultOptions, logger]\n );\n\n return {\n call,\n isLoading,\n error,\n reset,\n };\n}\n\n","/**\n * useNamedRPC Hook\n *\n * React hook for making native Centrifugo RPC calls via RPC proxy.\n * Uses Centrifugo's built-in RPC mechanism which proxies to Django.\n *\n * Flow:\n * 1. Client calls namedRPC('terminal.input', data)\n * 2. Centrifuge.js sends RPC over WebSocket\n * 3. Centrifugo proxies to Django: POST /centrifugo/rpc/\n * 4. Django routes to @websocket_rpc handler\n * 5. Response returned to client\n *\n * @example\n * const { call, isLoading, error } = useNamedRPC();\n *\n * const handleSendInput = async () => {\n * const result = await call('terminal.input', {\n * session_id: 'abc-123',\n * data: btoa('ls -la')\n * });\n * console.log('Result:', result);\n * };\n */\n\n'use client';\n\nimport { useCallback, useRef, useState } from 'react';\n\nimport { getConsolaLogger } from '../core/logger/consolaLogger';\nimport { useCentrifugo } from '../providers/CentrifugoProvider';\n\nexport interface UseNamedRPCOptions {\n onError?: (error: Error) => void;\n}\n\nexport interface UseNamedRPCResult {\n call: <TRequest = any, TResponse = any>(\n method: string,\n data: TRequest\n ) => Promise<TResponse>;\n isLoading: boolean;\n error: Error | null;\n reset: () => void;\n}\n\nexport function useNamedRPC(\n defaultOptions: UseNamedRPCOptions = {}\n): UseNamedRPCResult {\n const { client, isConnected } = useCentrifugo();\n const [isLoading, setIsLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const logger = useRef(getConsolaLogger('useNamedRPC')).current;\n\n const reset = useCallback(() => {\n setIsLoading(false);\n setError(null);\n }, []);\n\n const call = useCallback(\n async <TRequest = any, TResponse = any>(\n method: string,\n data: TRequest\n ): Promise<TResponse> => {\n if (!client) {\n const error = new Error('Centrifugo client not available');\n setError(error);\n throw error;\n }\n\n if (!isConnected) {\n const error = new Error('Not connected to Centrifugo');\n setError(error);\n throw error;\n }\n\n // Reset previous state\n setError(null);\n setIsLoading(true);\n\n try {\n logger.info(`Native RPC call: ${method}`, { data });\n\n const result = await client.namedRPC<TRequest, TResponse>(method, data);\n\n logger.success(`Native RPC success: ${method}`);\n setIsLoading(false);\n return result;\n } catch (err) {\n const rpcError =\n err instanceof Error ? err : new Error('Native RPC call failed');\n\n setError(rpcError);\n logger.error(`Native RPC failed: ${method}`, rpcError);\n\n // Call error callback if provided\n if (defaultOptions.onError) {\n try {\n defaultOptions.onError(rpcError);\n } catch (callbackError) {\n logger.error('Error in onError callback', callbackError);\n }\n }\n\n setIsLoading(false);\n throw rpcError;\n }\n },\n [client, isConnected, defaultOptions, logger]\n );\n\n return {\n call,\n isLoading,\n error,\n reset,\n };\n}\n"]}
|