@nurama/sdk 0.0.0-stage → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.md +1080 -2
- package/dist/BotClient.d.ts +66 -0
- package/dist/BotClient.d.ts.map +1 -0
- package/dist/BotClient.js +68 -0
- package/dist/BotClient.js.map +1 -0
- package/dist/NuramaClient.d.ts +480 -0
- package/dist/NuramaClient.d.ts.map +1 -0
- package/dist/NuramaClient.js +902 -0
- package/dist/NuramaClient.js.map +1 -0
- package/dist/browser/nurama-bot-sdk.js +12051 -0
- package/dist/browser/nurama-bot-sdk.min.js +1 -0
- package/dist/browser/nurama-sdk.js +12003 -0
- package/dist/browser/nurama-sdk.min.js +1 -0
- package/dist/routes/ai.d.ts +280 -0
- package/dist/routes/ai.d.ts.map +1 -0
- package/dist/routes/ai.js +173 -0
- package/dist/routes/ai.js.map +1 -0
- package/dist/routes/asset.d.ts +493 -0
- package/dist/routes/asset.d.ts.map +1 -0
- package/dist/routes/asset.js +848 -0
- package/dist/routes/asset.js.map +1 -0
- package/dist/routes/auth.d.ts +218 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +454 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/blogPosts.d.ts +17 -0
- package/dist/routes/blogPosts.d.ts.map +1 -0
- package/dist/routes/blogPosts.js +29 -0
- package/dist/routes/blogPosts.js.map +1 -0
- package/dist/routes/board.d.ts +187 -0
- package/dist/routes/board.d.ts.map +1 -0
- package/dist/routes/board.js +270 -0
- package/dist/routes/board.js.map +1 -0
- package/dist/routes/bot.d.ts +202 -0
- package/dist/routes/bot.d.ts.map +1 -0
- package/dist/routes/bot.js +229 -0
- package/dist/routes/bot.js.map +1 -0
- package/dist/routes/chat.d.ts +842 -0
- package/dist/routes/chat.d.ts.map +1 -0
- package/dist/routes/chat.js +863 -0
- package/dist/routes/chat.js.map +1 -0
- package/dist/routes/chatAi.d.ts +51 -0
- package/dist/routes/chatAi.d.ts.map +1 -0
- package/dist/routes/chatAi.js +109 -0
- package/dist/routes/chatAi.js.map +1 -0
- package/dist/routes/config.d.ts +11 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +24 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/convo.d.ts +169 -0
- package/dist/routes/convo.d.ts.map +1 -0
- package/dist/routes/convo.js +284 -0
- package/dist/routes/convo.js.map +1 -0
- package/dist/routes/credits.d.ts +82 -0
- package/dist/routes/credits.d.ts.map +1 -0
- package/dist/routes/credits.js +49 -0
- package/dist/routes/credits.js.map +1 -0
- package/dist/routes/device.d.ts +74 -0
- package/dist/routes/device.d.ts.map +1 -0
- package/dist/routes/device.js +122 -0
- package/dist/routes/device.js.map +1 -0
- package/dist/routes/folder.d.ts +75 -0
- package/dist/routes/folder.d.ts.map +1 -0
- package/dist/routes/folder.js +99 -0
- package/dist/routes/folder.js.map +1 -0
- package/dist/routes/invite.d.ts +61 -0
- package/dist/routes/invite.d.ts.map +1 -0
- package/dist/routes/invite.js +86 -0
- package/dist/routes/invite.js.map +1 -0
- package/dist/routes/joinLink.d.ts +88 -0
- package/dist/routes/joinLink.d.ts.map +1 -0
- package/dist/routes/joinLink.js +205 -0
- package/dist/routes/joinLink.js.map +1 -0
- package/dist/routes/membership.d.ts +116 -0
- package/dist/routes/membership.d.ts.map +1 -0
- package/dist/routes/membership.js +183 -0
- package/dist/routes/membership.js.map +1 -0
- package/dist/routes/notification.d.ts +103 -0
- package/dist/routes/notification.d.ts.map +1 -0
- package/dist/routes/notification.js +89 -0
- package/dist/routes/notification.js.map +1 -0
- package/dist/routes/oauthGrant.d.ts +45 -0
- package/dist/routes/oauthGrant.d.ts.map +1 -0
- package/dist/routes/oauthGrant.js +32 -0
- package/dist/routes/oauthGrant.js.map +1 -0
- package/dist/routes/payment.d.ts +56 -0
- package/dist/routes/payment.d.ts.map +1 -0
- package/dist/routes/payment.js +78 -0
- package/dist/routes/payment.js.map +1 -0
- package/dist/routes/product.d.ts +43 -0
- package/dist/routes/product.d.ts.map +1 -0
- package/dist/routes/product.js +53 -0
- package/dist/routes/product.js.map +1 -0
- package/dist/routes/project.d.ts +821 -0
- package/dist/routes/project.d.ts.map +1 -0
- package/dist/routes/project.js +1153 -0
- package/dist/routes/project.js.map +1 -0
- package/dist/routes/public.d.ts +269 -0
- package/dist/routes/public.d.ts.map +1 -0
- package/dist/routes/public.js +412 -0
- package/dist/routes/public.js.map +1 -0
- package/dist/routes/scratch.d.ts +70 -0
- package/dist/routes/scratch.d.ts.map +1 -0
- package/dist/routes/scratch.js +67 -0
- package/dist/routes/scratch.js.map +1 -0
- package/dist/routes/settings.d.ts +102 -0
- package/dist/routes/settings.d.ts.map +1 -0
- package/dist/routes/settings.js +94 -0
- package/dist/routes/settings.js.map +1 -0
- package/dist/routes/shortlink.d.ts +79 -0
- package/dist/routes/shortlink.d.ts.map +1 -0
- package/dist/routes/shortlink.js +25 -0
- package/dist/routes/shortlink.js.map +1 -0
- package/dist/routes/socket.d.ts +108 -0
- package/dist/routes/socket.d.ts.map +1 -0
- package/dist/routes/socket.js +573 -0
- package/dist/routes/socket.js.map +1 -0
- package/dist/routes/storage.d.ts +44 -0
- package/dist/routes/storage.d.ts.map +1 -0
- package/dist/routes/storage.js +49 -0
- package/dist/routes/storage.js.map +1 -0
- package/dist/routes/subscription.d.ts +184 -0
- package/dist/routes/subscription.d.ts.map +1 -0
- package/dist/routes/subscription.js +219 -0
- package/dist/routes/subscription.js.map +1 -0
- package/dist/routes/supportChat.d.ts +40 -0
- package/dist/routes/supportChat.d.ts.map +1 -0
- package/dist/routes/supportChat.js +53 -0
- package/dist/routes/supportChat.js.map +1 -0
- package/dist/routes/supportTicket.d.ts +89 -0
- package/dist/routes/supportTicket.d.ts.map +1 -0
- package/dist/routes/supportTicket.js +54 -0
- package/dist/routes/supportTicket.js.map +1 -0
- package/dist/routes/tag.d.ts +72 -0
- package/dist/routes/tag.d.ts.map +1 -0
- package/dist/routes/tag.js +81 -0
- package/dist/routes/tag.js.map +1 -0
- package/dist/routes/task.d.ts +252 -0
- package/dist/routes/task.d.ts.map +1 -0
- package/dist/routes/task.js +284 -0
- package/dist/routes/task.js.map +1 -0
- package/dist/routes/taskRelation.d.ts +80 -0
- package/dist/routes/taskRelation.d.ts.map +1 -0
- package/dist/routes/taskRelation.js +71 -0
- package/dist/routes/taskRelation.js.map +1 -0
- package/dist/routes/token.d.ts +97 -0
- package/dist/routes/token.d.ts.map +1 -0
- package/dist/routes/token.js +73 -0
- package/dist/routes/token.js.map +1 -0
- package/dist/routes/user.d.ts +112 -0
- package/dist/routes/user.d.ts.map +1 -0
- package/dist/routes/user.js +151 -0
- package/dist/routes/user.js.map +1 -0
- package/dist/routes/version.d.ts +42 -0
- package/dist/routes/version.d.ts.map +1 -0
- package/dist/routes/version.js +38 -0
- package/dist/routes/version.js.map +1 -0
- package/dist/routes/webhook.d.ts +170 -0
- package/dist/routes/webhook.d.ts.map +1 -0
- package/dist/routes/webhook.js +173 -0
- package/dist/routes/webhook.js.map +1 -0
- package/dist/routes/workspace.d.ts +120 -0
- package/dist/routes/workspace.d.ts.map +1 -0
- package/dist/routes/workspace.js +199 -0
- package/dist/routes/workspace.js.map +1 -0
- package/dist/utils/uploadSessionManager.d.ts +133 -0
- package/dist/utils/uploadSessionManager.d.ts.map +1 -0
- package/dist/utils/uploadSessionManager.js +321 -0
- package/dist/utils/uploadSessionManager.js.map +1 -0
- package/dist/utils/urlParams.d.ts +35 -0
- package/dist/utils/urlParams.d.ts.map +1 -0
- package/dist/utils/urlParams.js +146 -0
- package/dist/utils/urlParams.js.map +1 -0
- package/dist/version.d.ts +15 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +12 -0
- package/dist/version.js.map +1 -0
- package/package.json +87 -3
- package/src/BotClient.ts +113 -0
- package/src/NuramaClient.ts +1253 -0
- package/src/bot-browser-entry.js +15 -0
- package/src/browser-entry.js +20 -0
- package/src/routes/ai.ts +378 -0
- package/src/routes/asset.ts +1104 -0
- package/src/routes/auth.ts +587 -0
- package/src/routes/blogPosts.ts +29 -0
- package/src/routes/board.ts +403 -0
- package/src/routes/bot.ts +356 -0
- package/src/routes/chat.ts +1292 -0
- package/src/routes/chatAi.ts +125 -0
- package/src/routes/config.ts +31 -0
- package/src/routes/convo.ts +321 -0
- package/src/routes/credits.ts +112 -0
- package/src/routes/device.ts +133 -0
- package/src/routes/folder.ts +154 -0
- package/src/routes/invite.ts +133 -0
- package/src/routes/joinLink.ts +233 -0
- package/src/routes/membership.ts +237 -0
- package/src/routes/notification.ts +166 -0
- package/src/routes/oauthGrant.ts +64 -0
- package/src/routes/payment.ts +104 -0
- package/src/routes/product.ts +67 -0
- package/src/routes/project.ts +1528 -0
- package/src/routes/public.ts +496 -0
- package/src/routes/scratch.ts +94 -0
- package/src/routes/settings.ts +152 -0
- package/src/routes/shortlink.ts +90 -0
- package/src/routes/socket.ts +757 -0
- package/src/routes/storage.ts +83 -0
- package/src/routes/subscription.ts +307 -0
- package/src/routes/supportChat.ts +62 -0
- package/src/routes/supportTicket.ts +114 -0
- package/src/routes/tag.ts +131 -0
- package/src/routes/task.ts +431 -0
- package/src/routes/taskRelation.ts +125 -0
- package/src/routes/token.ts +152 -0
- package/src/routes/user.ts +214 -0
- package/src/routes/version.ts +62 -0
- package/src/routes/webhook.ts +295 -0
- package/src/routes/workspace.ts +223 -0
- package/src/utils/uploadSessionManager.ts +407 -0
- package/src/utils/urlParams.ts +181 -0
- package/src/version.ts +22 -0
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
|
|
3
|
+
import NuramaClient from '../NuramaClient.js';
|
|
4
|
+
|
|
5
|
+
// Define a generic Socket interface to avoid tight coupling to the socket.io-client types
|
|
6
|
+
export interface SocketInterface {
|
|
7
|
+
connected: boolean;
|
|
8
|
+
on: (event: string, listener: (...args: any[]) => void) => void;
|
|
9
|
+
off: (event: string) => void;
|
|
10
|
+
disconnect: () => void;
|
|
11
|
+
emit: (event: string, ...args: any[]) => void;
|
|
12
|
+
// socket.io v4 timeout-bounded ack pattern: `.timeout(ms).emit(ev, ...args, cb)`.
|
|
13
|
+
// Returns a thin chainable whose `.emit` invokes `cb(Error | null, ...ackArgs)`
|
|
14
|
+
// — Error on timeout, null on server ack. Used by `emitWithAck`.
|
|
15
|
+
timeout: (ms: number) => { emit: (event: string, ...args: any[]) => void };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Interfaces for socket events
|
|
19
|
+
export interface BaseNotificationEvent {
|
|
20
|
+
type: string;
|
|
21
|
+
initiatorId: string | null;
|
|
22
|
+
initiatorType: 'user' | 'system';
|
|
23
|
+
initiator: any | null; // User object if initiatorType is 'user'
|
|
24
|
+
resourceId: string;
|
|
25
|
+
resourceType: string;
|
|
26
|
+
channels: string[];
|
|
27
|
+
createdAt: string;
|
|
28
|
+
tokens?: Record<string, any>;
|
|
29
|
+
changes?: {
|
|
30
|
+
create?: Array<{ resourceId: string; resourceType: string; resource: any }>;
|
|
31
|
+
update?: Array<{ resourceId: string; resourceType: string; resource: any; oldResource?: any; newResource?: any }>;
|
|
32
|
+
delete?: Array<{ resourceId: string; resourceType: string }>;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SocketOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Automatically handle token refresh
|
|
39
|
+
* @default true
|
|
40
|
+
*/
|
|
41
|
+
autoRefresh?: boolean;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Automatically reconnect if connection is lost
|
|
45
|
+
* @default true
|
|
46
|
+
*/
|
|
47
|
+
autoReconnect?: boolean;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Automatically attempt to reconnect if token expires
|
|
51
|
+
* @default true
|
|
52
|
+
*/
|
|
53
|
+
autoReconnectOnTokenExpiry?: boolean;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Debug logging
|
|
57
|
+
* @default false
|
|
58
|
+
*/
|
|
59
|
+
debug?: boolean;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Override the WebSocket URL for this specific connection
|
|
63
|
+
* Takes precedence over the client's websocketURL option
|
|
64
|
+
*/
|
|
65
|
+
websocketURL?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface PublicSocketOptions {
|
|
69
|
+
/**
|
|
70
|
+
* Automatically reconnect if connection is lost
|
|
71
|
+
* @default true
|
|
72
|
+
*/
|
|
73
|
+
autoReconnect?: boolean;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Debug logging
|
|
77
|
+
* @default false
|
|
78
|
+
*/
|
|
79
|
+
debug?: boolean;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Override the WebSocket URL for this specific connection
|
|
83
|
+
* Takes precedence over the client's websocketURL option
|
|
84
|
+
*/
|
|
85
|
+
websocketURL?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface SocketChannel {
|
|
89
|
+
/** Socket.IO instance */
|
|
90
|
+
socket: SocketInterface;
|
|
91
|
+
/** Channel path */
|
|
92
|
+
channel: string;
|
|
93
|
+
/** Event listeners */
|
|
94
|
+
listeners: Record<string, Set<(event: any) => void>>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Listener-set keys that the built-in socket handlers bound in connect()/
|
|
99
|
+
* connectPublic() dispatch to directly (rather than via a subscribe()-bound
|
|
100
|
+
* `socket.on`). When re-establishing subscriptions after an in-place reconnect
|
|
101
|
+
* these must be repopulated on the new connection's listener set, NOT re-run
|
|
102
|
+
* through subscribe() — doing so would bind a second `socket.on` dispatcher and
|
|
103
|
+
* fire each callback twice.
|
|
104
|
+
*/
|
|
105
|
+
const LIFECYCLE_LISTENER_EVENTS = new Set([
|
|
106
|
+
'disconnect',
|
|
107
|
+
'tokenExpired',
|
|
108
|
+
'tokenBlacklisted',
|
|
109
|
+
'reconnect_failed',
|
|
110
|
+
'reconnect',
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
/** First reconnect delay; socket.io doubles it per failed attempt, up to RECONNECTION_DELAY_MAX, ±50% jitter. */
|
|
114
|
+
const RECONNECTION_DELAY = 1000;
|
|
115
|
+
const RECONNECTION_DELAY_MAX = 30000;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Extra first-retry delay, up to this, after the SERVER closed the connection ("transport close"): a ws deploy
|
|
119
|
+
* or restart drops every client of that server at once, and without it they would all reconnect within the
|
|
120
|
+
* same second. Network drops ("ping timeout", "transport error") keep the normal 1s retry.
|
|
121
|
+
*/
|
|
122
|
+
const SERVER_CLOSE_RECONNECT_SPREAD = 10000;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Spread reconnects after a server-side close (see SERVER_CLOSE_RECONNECT_SPREAD). Sets the manager's next
|
|
126
|
+
* delay from the socket's 'disconnect', which socket.io emits before it schedules the retry, and restores the
|
|
127
|
+
* normal delay on 'connect'. Channels share one manager, so the last channel's draw wins, which is fine.
|
|
128
|
+
*/
|
|
129
|
+
const spreadReconnectAfterServerClose = (socket: any): void => {
|
|
130
|
+
const manager = socket.io;
|
|
131
|
+
if (typeof manager?.reconnectionDelay !== 'function') return;
|
|
132
|
+
socket.on('disconnect', (reason: string) => {
|
|
133
|
+
if (reason === 'transport close') {
|
|
134
|
+
manager.reconnectionDelay(RECONNECTION_DELAY + Math.random() * SERVER_CLOSE_RECONNECT_SPREAD);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
socket.on('connect', () => manager.reconnectionDelay(RECONNECTION_DELAY));
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export default function createSocketMethods(client: NuramaClient) {
|
|
141
|
+
// Track active socket connections
|
|
142
|
+
const activeConnections: Record<string, SocketChannel> = {};
|
|
143
|
+
|
|
144
|
+
// Extract configuration from client
|
|
145
|
+
const debug = client.debug;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Log helper for socket-related messages
|
|
149
|
+
*/
|
|
150
|
+
const _log = (...args: any[]): void => {
|
|
151
|
+
if (debug) {
|
|
152
|
+
console.log('[SOCKET]', ...args);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// We'll need to dynamically import socket.io-client since it's not available in all environments
|
|
157
|
+
const getSocketIO = async (): Promise<any> => {
|
|
158
|
+
try {
|
|
159
|
+
// Handle different versions of socket.io-client
|
|
160
|
+
const socketModule = await import('socket.io-client');
|
|
161
|
+
// Socket.io 4.x exports the io function directly
|
|
162
|
+
return socketModule.default || socketModule;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
throw new Error('socket.io-client is not installed. Please install it with: npm install socket.io-client');
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get an authentication token for WebSocket connection.
|
|
170
|
+
* Returns the long-lived API key when the client is in bot mode, otherwise the JWT.
|
|
171
|
+
*/
|
|
172
|
+
const getAuthToken = (): string | null => {
|
|
173
|
+
return (client as any)._apiKey || (client as any)._getAccessToken();
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/** True when the client is authenticating with a bot API key. */
|
|
177
|
+
const isApiKeyMode = (): boolean => Boolean((client as any)._apiKey);
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get the WebSocket server URL
|
|
181
|
+
*
|
|
182
|
+
* Priority:
|
|
183
|
+
* 1. Connection-specific websocketURL (from options)
|
|
184
|
+
* 2. Client-level websocketURL (from client options)
|
|
185
|
+
* 3. Use the baseURL directly - Socket.IO handles protocol conversion
|
|
186
|
+
*/
|
|
187
|
+
const getWebSocketURL = (connectionOptions?: SocketOptions): string => {
|
|
188
|
+
// 1. Check connection-specific override
|
|
189
|
+
if (connectionOptions?.websocketURL) {
|
|
190
|
+
return connectionOptions.websocketURL;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 2. Check client-level websocketURL
|
|
194
|
+
if (client.websocketURL) {
|
|
195
|
+
return client.websocketURL;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 3. Use baseURL directly - Socket.IO handles protocol conversion
|
|
199
|
+
return client.baseURL;
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Connect to a socket channel
|
|
204
|
+
* @param channel Channel path to connect to (e.g., '/user/123')
|
|
205
|
+
* @param options Socket connection options
|
|
206
|
+
* @returns Promise that resolves with the socket channel
|
|
207
|
+
*/
|
|
208
|
+
const connect = async (channel: string, options: SocketOptions = {}): Promise<SocketChannel> => {
|
|
209
|
+
_log(`Connecting to channel: ${channel}`);
|
|
210
|
+
|
|
211
|
+
// Default options
|
|
212
|
+
const socketOptions = {
|
|
213
|
+
autoRefresh: true,
|
|
214
|
+
autoReconnect: true,
|
|
215
|
+
autoReconnectOnTokenExpiry: true,
|
|
216
|
+
debug: client.debug,
|
|
217
|
+
...options
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// Check if already connected
|
|
221
|
+
if (activeConnections[channel]) {
|
|
222
|
+
_log(`Already connected to channel: ${channel}`);
|
|
223
|
+
return activeConnections[channel];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Get access token
|
|
227
|
+
const token = getAuthToken();
|
|
228
|
+
if (!token) {
|
|
229
|
+
throw new Error('Authentication required. Please login first.');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Get WebSocket URL using the helper function - no protocol conversion needed
|
|
233
|
+
const wsUrl = getWebSocketURL(options);
|
|
234
|
+
_log(`Using Socket.IO URL: ${wsUrl}`);
|
|
235
|
+
|
|
236
|
+
// Dynamically import socket.io-client
|
|
237
|
+
const io = await getSocketIO();
|
|
238
|
+
|
|
239
|
+
// Create socket connection with enhanced reconnection configuration
|
|
240
|
+
const socket = io(`${wsUrl}${channel}`, {
|
|
241
|
+
query: { token },
|
|
242
|
+
|
|
243
|
+
// Reconnection configuration for resilience
|
|
244
|
+
reconnection: socketOptions.autoReconnect,
|
|
245
|
+
// Never give up: the server may refuse connections for a while (busy, or a deploy draining), and after a
|
|
246
|
+
// final failure socket.io stops for good without telling the app (the manager's 'reconnect_failed' never
|
|
247
|
+
// reaches socket listeners). Retries settle at one every ~30s.
|
|
248
|
+
reconnectionAttempts: Infinity,
|
|
249
|
+
reconnectionDelay: RECONNECTION_DELAY,
|
|
250
|
+
reconnectionDelayMax: RECONNECTION_DELAY_MAX,
|
|
251
|
+
randomizationFactor: 0.5, // Add jitter to avoid thundering herd problem
|
|
252
|
+
|
|
253
|
+
// Connection timeout
|
|
254
|
+
timeout: 20000, // 20 seconds to establish initial connection
|
|
255
|
+
|
|
256
|
+
// Transport configuration
|
|
257
|
+
transports: ['websocket', 'polling'], // Prefer WebSocket, fallback to polling
|
|
258
|
+
upgrade: true, // Allow transport upgrade
|
|
259
|
+
rememberUpgrade: true, // Remember successful upgrade
|
|
260
|
+
|
|
261
|
+
// Ping/pong for connection health
|
|
262
|
+
forceNew: false, // Reuse existing connection if available
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// Create channel object
|
|
266
|
+
const socketChannel: SocketChannel = {
|
|
267
|
+
socket,
|
|
268
|
+
channel,
|
|
269
|
+
listeners: {},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Store connection immediately so subscribers can register socket.on handlers
|
|
273
|
+
// even before the connection is fully established. Socket.io preserves
|
|
274
|
+
// handlers across reconnections, so they will fire once connected.
|
|
275
|
+
activeConnections[channel] = socketChannel;
|
|
276
|
+
|
|
277
|
+
spreadReconnectAfterServerClose(socket);
|
|
278
|
+
|
|
279
|
+
// Setup event listeners
|
|
280
|
+
return new Promise((resolve, reject) => {
|
|
281
|
+
let settled = false;
|
|
282
|
+
let hasConnected = false;
|
|
283
|
+
|
|
284
|
+
// Handle connection (initial or after reconnection)
|
|
285
|
+
socket.on('connect', () => {
|
|
286
|
+
_log(`Connected to channel: ${channel}`);
|
|
287
|
+
activeConnections[channel] = socketChannel;
|
|
288
|
+
if (hasConnected) {
|
|
289
|
+
// A second 'connect' on the same socket is socket.io reconnecting it (the manager emits 'reconnect',
|
|
290
|
+
// but socket listeners never see that). Notify app-level onReconnect listeners so they can
|
|
291
|
+
// REST-refetch any gap of server->client messages missed while the transport was down (socket.io has
|
|
292
|
+
// no replay across a reconnect).
|
|
293
|
+
_log(`Reconnected to channel ${channel}`);
|
|
294
|
+
const reconnectListeners = socketChannel.listeners['reconnect'] || new Set();
|
|
295
|
+
reconnectListeners.forEach(listener => listener({ type: 'reconnect' }));
|
|
296
|
+
}
|
|
297
|
+
hasConnected = true;
|
|
298
|
+
if (!settled) {
|
|
299
|
+
settled = true;
|
|
300
|
+
resolve(socketChannel);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Handle connection error — log but let socket.io retry
|
|
305
|
+
socket.on('connect_error', (error: unknown) => {
|
|
306
|
+
_log(`Connection error for channel ${channel}:`, error);
|
|
307
|
+
// Don't reject — socket.io's auto-reconnect will keep trying
|
|
308
|
+
// Resolve the promise so callers aren't blocked — subscriptions
|
|
309
|
+
// registered via socket.on will fire once the connection succeeds
|
|
310
|
+
if (!settled) {
|
|
311
|
+
settled = true;
|
|
312
|
+
resolve(socketChannel);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Handle reconnection failure — only reject when socket.io gives up entirely
|
|
317
|
+
socket.on('reconnect_failed', () => {
|
|
318
|
+
_log(`Reconnection failed for channel ${channel} after maximum attempts`);
|
|
319
|
+
// Emit to listeners for UI feedback
|
|
320
|
+
const eventListeners = socketChannel.listeners['reconnect_failed'] || new Set();
|
|
321
|
+
eventListeners.forEach(listener => listener({ type: 'reconnect_failed' }));
|
|
322
|
+
if (!settled) {
|
|
323
|
+
settled = true;
|
|
324
|
+
reject(new Error(`Connection to ${channel} failed after maximum reconnection attempts`));
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Handle token events
|
|
329
|
+
socket.on('tokenEvent', (event: string) => {
|
|
330
|
+
_log(`Token event on channel ${channel}:`, event);
|
|
331
|
+
|
|
332
|
+
if (event === 'TokenExpired' && socketOptions.autoReconnectOnTokenExpiry) {
|
|
333
|
+
// API keys don't expire client-side; if the server emits TokenExpired
|
|
334
|
+
// for a bot, treat it as terminal — there's nothing to refresh.
|
|
335
|
+
if (isApiKeyMode()) {
|
|
336
|
+
_log('Token expired event in API key mode — disconnecting (no refresh path)');
|
|
337
|
+
disconnect(channel);
|
|
338
|
+
const eventListeners = socketChannel.listeners['tokenExpired'] || new Set();
|
|
339
|
+
eventListeners.forEach(listener => listener({ type: 'tokenExpired' }));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Refresh token and reconnect
|
|
344
|
+
_log('Token expired, attempting to refresh and reconnect');
|
|
345
|
+
|
|
346
|
+
// Call the client's _refreshToken method (using 'as any' for TypeScript)
|
|
347
|
+
(client as any)._refreshToken()
|
|
348
|
+
.then(() => {
|
|
349
|
+
_log('Token refreshed, reconnecting');
|
|
350
|
+
// Preserve event subscriptions across the reconnect. connect()
|
|
351
|
+
// builds a brand-new socket whose listeners map starts empty, so
|
|
352
|
+
// without re-binding these the channel reconnects but silently
|
|
353
|
+
// stops delivering 'notification' events — the app-level
|
|
354
|
+
// subscription is lost until a full page reload. This fires on the
|
|
355
|
+
// server's token-expiry sweep (~every 30 min for a long-lived
|
|
356
|
+
// session), so the reconnect must carry the subscriptions over.
|
|
357
|
+
const preservedListeners = socketChannel.listeners;
|
|
358
|
+
disconnect(channel)
|
|
359
|
+
.then(() => connect(channel, options))
|
|
360
|
+
.then((newConnection) => {
|
|
361
|
+
Object.entries(preservedListeners).forEach(([listenerEvent, callbacks]) => {
|
|
362
|
+
callbacks.forEach((callback) => {
|
|
363
|
+
if (LIFECYCLE_LISTENER_EVENTS.has(listenerEvent)) {
|
|
364
|
+
// Built-in handlers on the fresh socket already dispatch
|
|
365
|
+
// these; just repopulate the set they read from.
|
|
366
|
+
if (!newConnection.listeners[listenerEvent]) {
|
|
367
|
+
newConnection.listeners[listenerEvent] = new Set();
|
|
368
|
+
}
|
|
369
|
+
newConnection.listeners[listenerEvent].add(callback);
|
|
370
|
+
} else {
|
|
371
|
+
subscribe(channel, listenerEvent, callback as (e: any) => void);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
// The fresh socket fired 'connect', not 'reconnect', so notify
|
|
376
|
+
// onReconnect listeners here too — a token-refresh reconnect is
|
|
377
|
+
// still a gap that needs the same REST catch-up.
|
|
378
|
+
(newConnection.listeners['reconnect'] || new Set()).forEach(listener =>
|
|
379
|
+
listener({ type: 'reconnect', reason: 'tokenRefresh' }));
|
|
380
|
+
})
|
|
381
|
+
.catch((error: unknown) => {
|
|
382
|
+
_log(`Failed to re-establish subscriptions after token refresh on ${channel}:`, error);
|
|
383
|
+
});
|
|
384
|
+
})
|
|
385
|
+
.catch((error: unknown) => {
|
|
386
|
+
_log('Failed to refresh token', error);
|
|
387
|
+
// Emit disconnect event to listeners
|
|
388
|
+
const eventListeners = socketChannel.listeners['tokenExpired'] || new Set();
|
|
389
|
+
eventListeners.forEach(listener => listener({ type: 'tokenExpired', error }));
|
|
390
|
+
});
|
|
391
|
+
} else if (event === 'tokenBlacklisted') {
|
|
392
|
+
// Force disconnect
|
|
393
|
+
_log('Token blacklisted, disconnecting');
|
|
394
|
+
disconnect(channel);
|
|
395
|
+
|
|
396
|
+
// Emit disconnect event to listeners
|
|
397
|
+
const eventListeners = socketChannel.listeners['tokenBlacklisted'] || new Set();
|
|
398
|
+
eventListeners.forEach(listener => listener({ type: 'tokenBlacklisted' }));
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Handle disconnect
|
|
403
|
+
socket.on('disconnect', (reason: string) => {
|
|
404
|
+
_log(`Disconnected from channel ${channel}:`, reason);
|
|
405
|
+
|
|
406
|
+
// Emit disconnect event to listeners
|
|
407
|
+
const eventListeners = socketChannel.listeners['disconnect'] || new Set();
|
|
408
|
+
eventListeners.forEach(listener => listener({ type: 'disconnect', reason }));
|
|
409
|
+
|
|
410
|
+
// Remove from active connections
|
|
411
|
+
delete activeConnections[channel];
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Connect to a public socket channel without authentication
|
|
418
|
+
* @param publicToken Public file system token (10-character alphanumeric)
|
|
419
|
+
* @param options Socket connection options
|
|
420
|
+
* @returns Promise that resolves with the socket channel
|
|
421
|
+
*/
|
|
422
|
+
const connectPublic = async (publicToken: string, options: PublicSocketOptions = {}): Promise<SocketChannel> => {
|
|
423
|
+
const channel = `public/${publicToken}`;
|
|
424
|
+
_log(`Connecting to public channel: ${channel}`);
|
|
425
|
+
|
|
426
|
+
// Default options
|
|
427
|
+
const socketOptions = {
|
|
428
|
+
autoReconnect: true,
|
|
429
|
+
debug: client.debug,
|
|
430
|
+
...options
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// Check if already connected
|
|
434
|
+
if (activeConnections[channel]) {
|
|
435
|
+
_log(`Already connected to public channel: ${channel}`);
|
|
436
|
+
return activeConnections[channel];
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Get WebSocket URL using the helper function
|
|
440
|
+
const wsUrl = getWebSocketURL(options);
|
|
441
|
+
_log(`Using Socket.IO URL for public connection: ${wsUrl}`);
|
|
442
|
+
|
|
443
|
+
// Dynamically import socket.io-client
|
|
444
|
+
const io = await getSocketIO();
|
|
445
|
+
|
|
446
|
+
// Create socket connection - no token needed for public channels
|
|
447
|
+
const socket = io(`${wsUrl}${channel}`, {
|
|
448
|
+
query: { publicToken },
|
|
449
|
+
|
|
450
|
+
// Reconnection configuration for resilience
|
|
451
|
+
reconnection: socketOptions.autoReconnect,
|
|
452
|
+
reconnectionAttempts: Infinity, // as connect(): never give up silently
|
|
453
|
+
reconnectionDelay: RECONNECTION_DELAY,
|
|
454
|
+
reconnectionDelayMax: RECONNECTION_DELAY_MAX,
|
|
455
|
+
randomizationFactor: 0.5,
|
|
456
|
+
|
|
457
|
+
// Connection timeout
|
|
458
|
+
timeout: 20000,
|
|
459
|
+
|
|
460
|
+
// Transport configuration
|
|
461
|
+
transports: ['websocket', 'polling'],
|
|
462
|
+
upgrade: true,
|
|
463
|
+
rememberUpgrade: true,
|
|
464
|
+
|
|
465
|
+
forceNew: false,
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Create channel object
|
|
469
|
+
const socketChannel: SocketChannel = {
|
|
470
|
+
socket,
|
|
471
|
+
channel,
|
|
472
|
+
listeners: {},
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
spreadReconnectAfterServerClose(socket);
|
|
476
|
+
|
|
477
|
+
// Setup event listeners
|
|
478
|
+
return new Promise((resolve, reject) => {
|
|
479
|
+
// Handle connection
|
|
480
|
+
socket.on('connect', () => {
|
|
481
|
+
_log(`Connected to public channel: ${channel}`);
|
|
482
|
+
activeConnections[channel] = socketChannel;
|
|
483
|
+
resolve(socketChannel);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// Handle connection error
|
|
487
|
+
socket.on('connect_error', (error: unknown) => {
|
|
488
|
+
_log(`Connection error for public channel ${channel}:`, error);
|
|
489
|
+
reject(error);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// Handle reconnection failure
|
|
493
|
+
socket.on('reconnect_failed', () => {
|
|
494
|
+
_log(`Reconnection failed for public channel ${channel} after maximum attempts`);
|
|
495
|
+
const eventListeners = socketChannel.listeners['reconnect_failed'] || new Set();
|
|
496
|
+
eventListeners.forEach(listener => listener({ type: 'reconnect_failed' }));
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// Handle disconnect
|
|
500
|
+
socket.on('disconnect', (reason: string) => {
|
|
501
|
+
_log(`Disconnected from public channel ${channel}:`, reason);
|
|
502
|
+
|
|
503
|
+
// Emit disconnect event to listeners
|
|
504
|
+
const eventListeners = socketChannel.listeners['disconnect'] || new Set();
|
|
505
|
+
eventListeners.forEach(listener => listener({ type: 'disconnect', reason }));
|
|
506
|
+
|
|
507
|
+
// Remove from active connections
|
|
508
|
+
delete activeConnections[channel];
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Disconnect from a socket channel
|
|
515
|
+
* @param channel Channel to disconnect from
|
|
516
|
+
*/
|
|
517
|
+
const disconnect = async (channel: string): Promise<void> => {
|
|
518
|
+
const connection = activeConnections[channel];
|
|
519
|
+
if (connection) {
|
|
520
|
+
_log(`Disconnecting from channel: ${channel}`);
|
|
521
|
+
connection.socket.disconnect();
|
|
522
|
+
delete activeConnections[channel];
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Subscribe to an event on a channel
|
|
528
|
+
* @param channel Channel to subscribe to
|
|
529
|
+
* @param event Event name to listen for
|
|
530
|
+
* @param callback Callback function to handle events
|
|
531
|
+
*/
|
|
532
|
+
const subscribe = async <T = BaseNotificationEvent>(
|
|
533
|
+
channel: string,
|
|
534
|
+
event: string,
|
|
535
|
+
callback: (event: T) => void
|
|
536
|
+
): Promise<void> => {
|
|
537
|
+
// Get or create connection
|
|
538
|
+
let connection = activeConnections[channel];
|
|
539
|
+
if (!connection) {
|
|
540
|
+
connection = await connect(channel);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Initialize listeners set for this event if needed
|
|
544
|
+
if (!connection.listeners[event]) {
|
|
545
|
+
connection.listeners[event] = new Set();
|
|
546
|
+
|
|
547
|
+
// Add socket listener for this event
|
|
548
|
+
connection.socket.on(event, (eventData: T) => {
|
|
549
|
+
_log(`Received event '${event}' on channel ${channel}:`, eventData);
|
|
550
|
+
|
|
551
|
+
// Call all registered callbacks
|
|
552
|
+
const eventListeners = connection.listeners[event] || new Set();
|
|
553
|
+
eventListeners.forEach(listener => listener(eventData));
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Add callback to listeners
|
|
558
|
+
connection.listeners[event].add(callback as any);
|
|
559
|
+
_log(`Subscribed to event '${event}' on channel ${channel}`);
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Subscribe to an event on a public channel
|
|
564
|
+
* Automatically connects to the public channel if not already connected
|
|
565
|
+
* @param publicToken Public file system token (10-character alphanumeric)
|
|
566
|
+
* @param event Event name to listen for
|
|
567
|
+
* @param callback Callback function to handle events
|
|
568
|
+
*/
|
|
569
|
+
const subscribePublic = async <T = BaseNotificationEvent>(
|
|
570
|
+
publicToken: string,
|
|
571
|
+
event: string,
|
|
572
|
+
callback: (event: T) => void
|
|
573
|
+
): Promise<void> => {
|
|
574
|
+
const channel = `public/${publicToken}`;
|
|
575
|
+
|
|
576
|
+
// Get or create public connection
|
|
577
|
+
let connection = activeConnections[channel];
|
|
578
|
+
if (!connection) {
|
|
579
|
+
connection = await connectPublic(publicToken);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Initialize listeners set for this event if needed
|
|
583
|
+
if (!connection.listeners[event]) {
|
|
584
|
+
connection.listeners[event] = new Set();
|
|
585
|
+
|
|
586
|
+
// Add socket listener for this event
|
|
587
|
+
connection.socket.on(event, (eventData: T) => {
|
|
588
|
+
_log(`Received event '${event}' on public channel ${channel}:`, eventData);
|
|
589
|
+
|
|
590
|
+
// Call all registered callbacks
|
|
591
|
+
const eventListeners = connection.listeners[event] || new Set();
|
|
592
|
+
eventListeners.forEach(listener => listener(eventData));
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Add callback to listeners
|
|
597
|
+
connection.listeners[event].add(callback as any);
|
|
598
|
+
_log(`Subscribed to event '${event}' on public channel ${channel}`);
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Unsubscribe from an event on a channel
|
|
603
|
+
* @param channel Channel to unsubscribe from
|
|
604
|
+
* @param event Event name to stop listening for
|
|
605
|
+
* @param callback Callback function to remove (if not provided, all callbacks will be removed)
|
|
606
|
+
*/
|
|
607
|
+
/**
|
|
608
|
+
* Stop listening for an event on a channel. If `callback` is provided,
|
|
609
|
+
* only that specific listener is removed; otherwise every listener for
|
|
610
|
+
* that event is cleared.
|
|
611
|
+
*
|
|
612
|
+
* @param channel Channel to unsubscribe from
|
|
613
|
+
* @param event Event name to stop listening for
|
|
614
|
+
* @param callback Specific callback to remove (optional)
|
|
615
|
+
*/
|
|
616
|
+
const unsubscribe = (
|
|
617
|
+
channel: string,
|
|
618
|
+
event: string,
|
|
619
|
+
callback?: (event: any) => void
|
|
620
|
+
): void => {
|
|
621
|
+
const connection = activeConnections[channel];
|
|
622
|
+
if (!connection || !connection.listeners[event]) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (callback) {
|
|
627
|
+
// Remove specific callback
|
|
628
|
+
connection.listeners[event].delete(callback);
|
|
629
|
+
_log(`Unsubscribed specific callback from event '${event}' on channel ${channel}`);
|
|
630
|
+
} else {
|
|
631
|
+
// Remove all callbacks
|
|
632
|
+
connection.listeners[event].clear();
|
|
633
|
+
connection.socket.off(event);
|
|
634
|
+
_log(`Unsubscribed all callbacks from event '${event}' on channel ${channel}`);
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Check if connected to a channel
|
|
640
|
+
* @param channel Channel to check
|
|
641
|
+
* @returns True if connected, false otherwise
|
|
642
|
+
*/
|
|
643
|
+
const isConnected = (channel: string): boolean => {
|
|
644
|
+
return !!activeConnections[channel]?.socket.connected;
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Emit an event to a connected channel
|
|
649
|
+
* @param channel Channel to emit to
|
|
650
|
+
* @param event Event name
|
|
651
|
+
* @param data Event data
|
|
652
|
+
*/
|
|
653
|
+
const emit = (channel: string, event: string, data?: unknown): void => {
|
|
654
|
+
const connection = activeConnections[channel];
|
|
655
|
+
if (!connection?.socket.connected) {
|
|
656
|
+
_log(`Cannot emit to channel ${channel}: not connected`);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
_log(`Emitting '${event}' on channel ${channel}`);
|
|
660
|
+
connection.socket.emit(event, data);
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Emit an event with a timeout-bounded server acknowledgement. Resolves
|
|
665
|
+
* `true` if the server acks within `timeoutMs`, `false` on timeout or
|
|
666
|
+
* transport error. Used by the FE WS layer (`WebSocketManager.probeChannel`)
|
|
667
|
+
* to actively verify a channel's liveness when `socket.connected` may be
|
|
668
|
+
* stale — most notably after a backgrounded tab returns to focus, where
|
|
669
|
+
* the flag can remain `true` for up to socket.io's own heartbeat window
|
|
670
|
+
* (~25–45s) even after the underlying TCP transport has died.
|
|
671
|
+
*
|
|
672
|
+
* Relies on socket.io v4's `socket.timeout(ms).emit(ev, data, cb)`
|
|
673
|
+
* pattern: the BE handler invokes `ack()` (its trailing callback arg);
|
|
674
|
+
* if no ack arrives within `timeoutMs` the cb is invoked with an Error.
|
|
675
|
+
*/
|
|
676
|
+
const emitWithAck = (
|
|
677
|
+
channel: string,
|
|
678
|
+
event: string,
|
|
679
|
+
data: unknown,
|
|
680
|
+
timeoutMs: number,
|
|
681
|
+
): Promise<boolean> => {
|
|
682
|
+
const connection = activeConnections[channel];
|
|
683
|
+
if (!connection) return Promise.resolve(false);
|
|
684
|
+
return new Promise<boolean>((resolve) => {
|
|
685
|
+
try {
|
|
686
|
+
connection.socket.timeout(timeoutMs).emit(event, data, (err: Error | null) => {
|
|
687
|
+
resolve(!err);
|
|
688
|
+
});
|
|
689
|
+
} catch (error) {
|
|
690
|
+
_log(`emitWithAck error on channel ${channel}:`, error);
|
|
691
|
+
resolve(false);
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Disconnect from all channels
|
|
698
|
+
*/
|
|
699
|
+
const disconnectAll = async (): Promise<void> => {
|
|
700
|
+
_log('Disconnecting from all channels');
|
|
701
|
+
const channels = Object.keys(activeConnections);
|
|
702
|
+
for (const channel of channels) {
|
|
703
|
+
await disconnect(channel);
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Register a callback for when Socket.IO exhausts all reconnection attempts
|
|
709
|
+
* @param channel Channel to monitor
|
|
710
|
+
* @param callback Callback invoked when reconnection fails permanently
|
|
711
|
+
*/
|
|
712
|
+
const onReconnectFailed = (channel: string, callback: () => void): void => {
|
|
713
|
+
const connection = activeConnections[channel];
|
|
714
|
+
if (connection) {
|
|
715
|
+
// Add to the channel's listener set so the existing reconnect_failed
|
|
716
|
+
// handler in connect()/connectPublic() dispatches to it
|
|
717
|
+
if (!connection.listeners['reconnect_failed']) {
|
|
718
|
+
connection.listeners['reconnect_failed'] = new Set();
|
|
719
|
+
}
|
|
720
|
+
connection.listeners['reconnect_failed'].add(callback);
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Register a callback for when the channel reconnects — a socket.io
|
|
726
|
+
* transport-level reconnect, or the token-refresh reconnect. Use it to recover
|
|
727
|
+
* any gap of server->client messages missed while the connection was down;
|
|
728
|
+
* socket.io does not replay those. Dispatched from the 'reconnect' handler in
|
|
729
|
+
* connect() and from the token-refresh path.
|
|
730
|
+
*/
|
|
731
|
+
const onReconnect = (channel: string, callback: (event?: unknown) => void): void => {
|
|
732
|
+
const connection = activeConnections[channel];
|
|
733
|
+
if (connection) {
|
|
734
|
+
if (!connection.listeners['reconnect']) {
|
|
735
|
+
connection.listeners['reconnect'] = new Set();
|
|
736
|
+
}
|
|
737
|
+
connection.listeners['reconnect'].add(callback);
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
return {
|
|
742
|
+
connect,
|
|
743
|
+
connectPublic,
|
|
744
|
+
disconnect,
|
|
745
|
+
disconnectAll,
|
|
746
|
+
subscribe,
|
|
747
|
+
subscribePublic,
|
|
748
|
+
unsubscribe,
|
|
749
|
+
isConnected,
|
|
750
|
+
emit,
|
|
751
|
+
emitWithAck,
|
|
752
|
+
onReconnect,
|
|
753
|
+
onReconnectFailed,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export type SocketMethods = ReturnType<typeof createSocketMethods>;
|