@djangocfg/centrifugo 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +345 -34
- package/package.json +6 -4
- package/src/config.ts +1 -1
- package/src/core/client/CentrifugoRPCClient.ts +281 -0
- package/src/core/client/index.ts +5 -0
- package/src/core/index.ts +15 -0
- package/src/core/logger/LogsStore.ts +101 -0
- package/src/core/logger/createLogger.ts +79 -0
- package/src/core/logger/index.ts +9 -0
- package/src/core/types/index.ts +68 -0
- package/src/debug/ConnectionTab/ConnectionTab.tsx +160 -0
- package/src/debug/ConnectionTab/index.ts +5 -0
- package/src/debug/DebugPanel/DebugPanel.tsx +88 -0
- package/src/debug/DebugPanel/index.ts +5 -0
- package/src/debug/LogsTab/LogsTab.tsx +236 -0
- package/src/debug/LogsTab/index.ts +5 -0
- package/src/debug/SubscriptionsTab/SubscriptionsTab.tsx +135 -0
- package/src/debug/SubscriptionsTab/index.ts +5 -0
- package/src/debug/index.ts +11 -0
- package/src/hooks/index.ts +2 -5
- package/src/hooks/useSubscription.ts +66 -65
- package/src/index.ts +94 -13
- package/src/providers/CentrifugoProvider/CentrifugoProvider.tsx +380 -0
- package/src/providers/CentrifugoProvider/index.ts +6 -0
- package/src/providers/LogsProvider/LogsProvider.tsx +107 -0
- package/src/providers/LogsProvider/index.ts +6 -0
- package/src/providers/index.ts +9 -0
- package/API_GENERATOR.md +0 -253
- package/src/components/CentrifugoDebug.tsx +0 -182
- package/src/components/index.ts +0 -5
- package/src/context/CentrifugoProvider.tsx +0 -228
- package/src/context/index.ts +0 -5
- package/src/hooks/useLogger.ts +0 -69
- package/src/types/index.ts +0 -45
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Centrifugo RPC Client
|
|
3
|
+
*
|
|
4
|
+
* Handles WebSocket connection and RPC call correlation.
|
|
5
|
+
* Provides both RPC (request/response) and Subscription (pub/sub) patterns.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Centrifuge } from 'centrifuge';
|
|
9
|
+
import type { Logger } from '../logger';
|
|
10
|
+
import { createLogger } from '../logger';
|
|
11
|
+
|
|
12
|
+
export class CentrifugoRPCClient {
|
|
13
|
+
private centrifuge: Centrifuge;
|
|
14
|
+
private subscription: any;
|
|
15
|
+
private channelSubscriptions: Map<string, any> = new Map();
|
|
16
|
+
private pendingRequests: Map<string, { resolve: Function; reject: Function }> = new Map();
|
|
17
|
+
private readonly replyChannel: string;
|
|
18
|
+
private readonly timeout: number;
|
|
19
|
+
private readonly logger: Logger;
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
url: string,
|
|
23
|
+
token: string,
|
|
24
|
+
userId: string,
|
|
25
|
+
timeout: number = 30000,
|
|
26
|
+
logger?: Logger
|
|
27
|
+
) {
|
|
28
|
+
this.replyChannel = `user#${userId}`;
|
|
29
|
+
this.timeout = timeout;
|
|
30
|
+
this.logger = logger || createLogger({ source: 'client' });
|
|
31
|
+
|
|
32
|
+
this.centrifuge = new Centrifuge(url, {
|
|
33
|
+
token,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
this.centrifuge.on('disconnected', (ctx) => {
|
|
37
|
+
// Reject all pending requests
|
|
38
|
+
this.pendingRequests.forEach(({ reject }) => {
|
|
39
|
+
reject(new Error('Disconnected from Centrifugo'));
|
|
40
|
+
});
|
|
41
|
+
this.pendingRequests.clear();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async connect(): Promise<void> {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
let resolved = false;
|
|
48
|
+
|
|
49
|
+
// Listen to Centrifuge connection events
|
|
50
|
+
const onConnected = () => {
|
|
51
|
+
if (!resolved) {
|
|
52
|
+
resolved = true;
|
|
53
|
+
resolve();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const onError = (ctx: any) => {
|
|
58
|
+
if (!resolved) {
|
|
59
|
+
resolved = true;
|
|
60
|
+
reject(new Error(ctx.message || 'Connection error'));
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
this.centrifuge.on('connected', onConnected);
|
|
65
|
+
this.centrifuge.on('error', onError);
|
|
66
|
+
|
|
67
|
+
// Start connection
|
|
68
|
+
this.centrifuge.connect();
|
|
69
|
+
|
|
70
|
+
// Subscribe to reply channel
|
|
71
|
+
this.subscription = this.centrifuge.newSubscription(this.replyChannel);
|
|
72
|
+
|
|
73
|
+
this.subscription.on('publication', (ctx: any) => {
|
|
74
|
+
this.handleResponse(ctx.data);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
this.subscription.on('subscribed', () => {
|
|
78
|
+
// Subscription successful (optional, we already resolved on 'connected')
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
this.subscription.on('error', (ctx: any) => {
|
|
82
|
+
// Error code 105 = "already subscribed" (server-side subscription from JWT)
|
|
83
|
+
// This is not an error - the channel is already active via server-side subscription
|
|
84
|
+
if (ctx.error?.code === 105) {
|
|
85
|
+
// This is fine, server-side subscription exists
|
|
86
|
+
} else {
|
|
87
|
+
this.logger.error(`Subscription error for ${this.replyChannel}:`, ctx.error);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
this.subscription.subscribe();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async disconnect(): Promise<void> {
|
|
96
|
+
// Unsubscribe from all event channels
|
|
97
|
+
this.unsubscribeAll();
|
|
98
|
+
|
|
99
|
+
// Unsubscribe from RPC reply channel
|
|
100
|
+
if (this.subscription) {
|
|
101
|
+
this.subscription.unsubscribe();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.centrifuge.disconnect();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async call<T = any>(method: string, params: any): Promise<T> {
|
|
108
|
+
const correlationId = this.generateCorrelationId();
|
|
109
|
+
|
|
110
|
+
const message = {
|
|
111
|
+
method,
|
|
112
|
+
params,
|
|
113
|
+
correlation_id: correlationId,
|
|
114
|
+
reply_to: this.replyChannel,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Create promise for response
|
|
118
|
+
const promise = new Promise<T>((resolve, reject) => {
|
|
119
|
+
const timeoutId = setTimeout(() => {
|
|
120
|
+
this.pendingRequests.delete(correlationId);
|
|
121
|
+
reject(new Error(`RPC timeout: ${method}`));
|
|
122
|
+
}, this.timeout);
|
|
123
|
+
|
|
124
|
+
this.pendingRequests.set(correlationId, {
|
|
125
|
+
resolve: (result: T) => {
|
|
126
|
+
clearTimeout(timeoutId);
|
|
127
|
+
resolve(result);
|
|
128
|
+
},
|
|
129
|
+
reject: (error: Error) => {
|
|
130
|
+
clearTimeout(timeoutId);
|
|
131
|
+
reject(error);
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Publish request
|
|
137
|
+
await this.centrifuge.publish('rpc.requests', message);
|
|
138
|
+
|
|
139
|
+
return promise;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private handleResponse(data: any): void {
|
|
143
|
+
const correlationId = data.correlation_id;
|
|
144
|
+
if (!correlationId) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const pending = this.pendingRequests.get(correlationId);
|
|
149
|
+
if (!pending) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.pendingRequests.delete(correlationId);
|
|
154
|
+
|
|
155
|
+
if (data.error) {
|
|
156
|
+
pending.reject(new Error(data.error.message || 'RPC error'));
|
|
157
|
+
} else {
|
|
158
|
+
pending.resolve(data.result);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private generateCorrelationId(): string {
|
|
163
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
167
|
+
// Channel Subscription API (for gRPC events)
|
|
168
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Subscribe to a Centrifugo channel for real-time events.
|
|
172
|
+
*
|
|
173
|
+
* @param channel - Channel name (e.g., 'bot#bot-123#heartbeat')
|
|
174
|
+
* @param callback - Callback for received messages
|
|
175
|
+
* @returns Unsubscribe function
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* const unsubscribe = client.subscribe('bot#bot-123#heartbeat', (data) => {
|
|
179
|
+
* console.log('Heartbeat:', data);
|
|
180
|
+
* });
|
|
181
|
+
*
|
|
182
|
+
* // Later: unsubscribe when done
|
|
183
|
+
* unsubscribe();
|
|
184
|
+
*/
|
|
185
|
+
subscribe(channel: string, callback: (data: any) => void): () => void {
|
|
186
|
+
// Check if already subscribed
|
|
187
|
+
if (this.channelSubscriptions.has(channel)) {
|
|
188
|
+
return () => {}; // Return no-op unsubscribe
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Create new subscription
|
|
192
|
+
const sub = this.centrifuge.newSubscription(channel);
|
|
193
|
+
|
|
194
|
+
// Handle publications
|
|
195
|
+
sub.on('publication', (ctx: any) => {
|
|
196
|
+
callback(ctx.data);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Handle subscription lifecycle
|
|
200
|
+
sub.on('subscribed', () => {
|
|
201
|
+
// Subscription successful
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
sub.on('error', (ctx: any) => {
|
|
205
|
+
this.logger.error(`Subscription error for ${channel}:`, ctx.error);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Start subscription
|
|
209
|
+
sub.subscribe();
|
|
210
|
+
|
|
211
|
+
// Store subscription
|
|
212
|
+
this.channelSubscriptions.set(channel, sub);
|
|
213
|
+
|
|
214
|
+
// Return unsubscribe function
|
|
215
|
+
return () => this.unsubscribe(channel);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Unsubscribe from a channel.
|
|
220
|
+
*
|
|
221
|
+
* @param channel - Channel name
|
|
222
|
+
*/
|
|
223
|
+
unsubscribe(channel: string): void {
|
|
224
|
+
const sub = this.channelSubscriptions.get(channel);
|
|
225
|
+
if (!sub) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
sub.unsubscribe();
|
|
230
|
+
this.channelSubscriptions.delete(channel);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Unsubscribe from all channels.
|
|
235
|
+
*/
|
|
236
|
+
unsubscribeAll(): void {
|
|
237
|
+
if (this.channelSubscriptions.size === 0) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
this.channelSubscriptions.forEach((sub, channel) => {
|
|
242
|
+
sub.unsubscribe();
|
|
243
|
+
});
|
|
244
|
+
this.channelSubscriptions.clear();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get list of active client-side subscriptions.
|
|
249
|
+
*/
|
|
250
|
+
getActiveSubscriptions(): string[] {
|
|
251
|
+
return Array.from(this.channelSubscriptions.keys());
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get list of server-side subscriptions (from JWT token).
|
|
256
|
+
*
|
|
257
|
+
* These are channels automatically subscribed by Centrifugo server
|
|
258
|
+
* based on the 'channels' claim in the JWT token.
|
|
259
|
+
*/
|
|
260
|
+
getServerSideSubscriptions(): string[] {
|
|
261
|
+
try {
|
|
262
|
+
// Access Centrifuge.js internal state for server-side subs
|
|
263
|
+
// @ts-ignore - accessing internal property
|
|
264
|
+
const serverSubs = this.centrifuge._serverSubs || {};
|
|
265
|
+
return Object.keys(serverSubs);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Get all active subscriptions (both client-side and server-side).
|
|
273
|
+
*/
|
|
274
|
+
getAllSubscriptions(): string[] {
|
|
275
|
+
const clientSubs = this.getActiveSubscriptions();
|
|
276
|
+
const serverSubs = this.getServerSideSubscriptions();
|
|
277
|
+
|
|
278
|
+
// Combine and deduplicate
|
|
279
|
+
return Array.from(new Set([...clientSubs, ...serverSubs]));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Module
|
|
3
|
+
*
|
|
4
|
+
* Platform-agnostic core functionality (no React)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Client
|
|
8
|
+
export { CentrifugoRPCClient } from './client';
|
|
9
|
+
|
|
10
|
+
// Logger
|
|
11
|
+
export { createLogger, LogsStore, getGlobalLogsStore } from './logger';
|
|
12
|
+
export type { Logger, LoggerConfig } from './logger';
|
|
13
|
+
|
|
14
|
+
// Types
|
|
15
|
+
export type * from './types';
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logs Store
|
|
3
|
+
*
|
|
4
|
+
* In-memory store for accumulated logs with circular buffer.
|
|
5
|
+
* Thread-safe, max capacity, auto-cleanup.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { LogEntry, LogLevel } from '../types';
|
|
9
|
+
|
|
10
|
+
export class LogsStore {
|
|
11
|
+
private logs: LogEntry[] = [];
|
|
12
|
+
private listeners: Set<(logs: LogEntry[]) => void> = new Set();
|
|
13
|
+
private maxLogs: number;
|
|
14
|
+
|
|
15
|
+
constructor(maxLogs: number = 500) {
|
|
16
|
+
this.maxLogs = maxLogs;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Add log entry
|
|
21
|
+
*/
|
|
22
|
+
add(entry: Omit<LogEntry, 'id' | 'timestamp'>): void {
|
|
23
|
+
const logEntry: LogEntry = {
|
|
24
|
+
...entry,
|
|
25
|
+
id: `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
26
|
+
timestamp: new Date(),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
this.logs.push(logEntry);
|
|
30
|
+
|
|
31
|
+
// Keep only last N logs (circular buffer)
|
|
32
|
+
if (this.logs.length > this.maxLogs) {
|
|
33
|
+
this.logs = this.logs.slice(-this.maxLogs);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Notify listeners
|
|
37
|
+
this.notify();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get all logs
|
|
42
|
+
*/
|
|
43
|
+
getAll(): LogEntry[] {
|
|
44
|
+
return [...this.logs];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get logs by level
|
|
49
|
+
*/
|
|
50
|
+
getByLevel(level: LogLevel): LogEntry[] {
|
|
51
|
+
return this.logs.filter((log) => log.level === level);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get logs by source
|
|
56
|
+
*/
|
|
57
|
+
getBySource(source: LogEntry['source']): LogEntry[] {
|
|
58
|
+
return this.logs.filter((log) => log.source === source);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Clear all logs
|
|
63
|
+
*/
|
|
64
|
+
clear(): void {
|
|
65
|
+
this.logs = [];
|
|
66
|
+
this.notify();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Subscribe to log changes
|
|
71
|
+
*/
|
|
72
|
+
subscribe(listener: (logs: LogEntry[]) => void): () => void {
|
|
73
|
+
this.listeners.add(listener);
|
|
74
|
+
return () => this.listeners.delete(listener);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Notify all listeners
|
|
79
|
+
*/
|
|
80
|
+
private notify(): void {
|
|
81
|
+
const logs = this.getAll();
|
|
82
|
+
this.listeners.forEach((listener) => listener(logs));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get logs count
|
|
87
|
+
*/
|
|
88
|
+
get count(): number {
|
|
89
|
+
return this.logs.length;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Singleton instance
|
|
94
|
+
let globalStore: LogsStore | null = null;
|
|
95
|
+
|
|
96
|
+
export function getGlobalLogsStore(): LogsStore {
|
|
97
|
+
if (!globalStore) {
|
|
98
|
+
globalStore = new LogsStore();
|
|
99
|
+
}
|
|
100
|
+
return globalStore;
|
|
101
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create Logger
|
|
3
|
+
*
|
|
4
|
+
* Creates logger that writes to both console (consola) and LogsStore.
|
|
5
|
+
* Dual output: developer console + UI logs viewer.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createConsola } from 'consola';
|
|
9
|
+
import type { LogEntry, LogLevel } from '../types';
|
|
10
|
+
import { getGlobalLogsStore } from './LogsStore';
|
|
11
|
+
|
|
12
|
+
export interface Logger {
|
|
13
|
+
debug: (message: string, data?: unknown) => void;
|
|
14
|
+
info: (message: string, data?: unknown) => void;
|
|
15
|
+
success: (message: string, data?: unknown) => void;
|
|
16
|
+
warning: (message: string, data?: unknown) => void;
|
|
17
|
+
error: (message: string, error?: Error | unknown) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface LoggerConfig {
|
|
21
|
+
source: LogEntry['source'];
|
|
22
|
+
isDevelopment?: boolean;
|
|
23
|
+
tag?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createLogger(config: LoggerConfig): Logger {
|
|
27
|
+
const { source, isDevelopment = process.env.NODE_ENV === 'development', tag = 'Centrifugo' } = config;
|
|
28
|
+
|
|
29
|
+
const logsStore = getGlobalLogsStore();
|
|
30
|
+
|
|
31
|
+
// Console logger (consola)
|
|
32
|
+
const consola = createConsola({
|
|
33
|
+
level: isDevelopment ? 4 : 3,
|
|
34
|
+
formatOptions: {
|
|
35
|
+
colors: true,
|
|
36
|
+
date: false,
|
|
37
|
+
compact: !isDevelopment,
|
|
38
|
+
},
|
|
39
|
+
}).withTag(tag);
|
|
40
|
+
|
|
41
|
+
const log = (level: LogLevel, message: string, data?: unknown) => {
|
|
42
|
+
// Add to LogsStore (always)
|
|
43
|
+
logsStore.add({
|
|
44
|
+
level,
|
|
45
|
+
source,
|
|
46
|
+
message,
|
|
47
|
+
data,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Write to console (only in development)
|
|
51
|
+
if (!isDevelopment) return;
|
|
52
|
+
|
|
53
|
+
switch (level) {
|
|
54
|
+
case 'debug':
|
|
55
|
+
consola.debug(message, data || '');
|
|
56
|
+
break;
|
|
57
|
+
case 'info':
|
|
58
|
+
consola.info(message, data || '');
|
|
59
|
+
break;
|
|
60
|
+
case 'success':
|
|
61
|
+
consola.success(message, data || '');
|
|
62
|
+
break;
|
|
63
|
+
case 'warning':
|
|
64
|
+
consola.warn(message, data || '');
|
|
65
|
+
break;
|
|
66
|
+
case 'error':
|
|
67
|
+
consola.error(message, data || '');
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
debug: (message, data) => log('debug', message, data),
|
|
74
|
+
info: (message, data) => log('info', message, data),
|
|
75
|
+
success: (message, data) => log('success', message, data),
|
|
76
|
+
warning: (message, data) => log('warning', message, data),
|
|
77
|
+
error: (message, error) => log('error', message, error),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Types for Centrifugo Package
|
|
3
|
+
*
|
|
4
|
+
* Type-only exports, no runtime dependencies
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
8
|
+
// Log Types
|
|
9
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export type LogLevel = 'debug' | 'info' | 'success' | 'warning' | 'error';
|
|
12
|
+
|
|
13
|
+
export interface LogEntry {
|
|
14
|
+
id: string;
|
|
15
|
+
timestamp: Date;
|
|
16
|
+
level: LogLevel;
|
|
17
|
+
source: 'client' | 'provider' | 'subscription' | 'system';
|
|
18
|
+
message: string;
|
|
19
|
+
data?: unknown;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
23
|
+
// Connection Types
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'error';
|
|
27
|
+
|
|
28
|
+
export interface CentrifugoToken {
|
|
29
|
+
token: string;
|
|
30
|
+
centrifugo_url: string;
|
|
31
|
+
channels: string[];
|
|
32
|
+
expires_at: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface User {
|
|
36
|
+
id: number;
|
|
37
|
+
centrifugo?: CentrifugoToken;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
41
|
+
// Subscription Types
|
|
42
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
export interface ActiveSubscription {
|
|
45
|
+
channel: string;
|
|
46
|
+
type: 'client' | 'server';
|
|
47
|
+
subscribedAt: number; // timestamp
|
|
48
|
+
data?: unknown;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
52
|
+
// Client Types
|
|
53
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
export interface CentrifugoClientConfig {
|
|
56
|
+
url: string;
|
|
57
|
+
token: string;
|
|
58
|
+
userId: string;
|
|
59
|
+
timeout?: number;
|
|
60
|
+
onLog?: (entry: Omit<LogEntry, 'id' | 'timestamp'>) => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface CentrifugoClientState {
|
|
64
|
+
isConnected: boolean;
|
|
65
|
+
isConnecting: boolean;
|
|
66
|
+
error: Error | null;
|
|
67
|
+
connectionState: ConnectionState;
|
|
68
|
+
}
|