@pingagent/sdk 0.1.7 → 0.1.9
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/bin/pingagent.js +24 -3
- package/dist/chunk-3OEFISNL.js +2433 -0
- package/dist/chunk-5Z6HZWDA.js +2603 -0
- package/dist/chunk-BSDY6AKB.js +2918 -0
- package/dist/chunk-PFABO4C7.js +2961 -0
- package/dist/chunk-QK2GMSWC.js +2959 -0
- package/dist/chunk-TCYDOFRQ.js +2085 -0
- package/dist/chunk-V7HHUQT6.js +1962 -0
- package/dist/index.d.ts +403 -5
- package/dist/index.js +41 -3
- package/dist/web-server.js +1151 -16
- package/package.json +11 -3
- package/__tests__/cli.test.ts +0 -225
- package/__tests__/identity.test.ts +0 -47
- package/__tests__/store.test.ts +0 -332
- package/src/a2a-adapter.ts +0 -159
- package/src/auth.ts +0 -50
- package/src/client.ts +0 -582
- package/src/contacts.ts +0 -210
- package/src/history.ts +0 -269
- package/src/identity.ts +0 -86
- package/src/index.ts +0 -25
- package/src/paths.ts +0 -52
- package/src/store.ts +0 -62
- package/src/transport.ts +0 -141
- package/src/web-server.ts +0 -1148
- package/src/ws-subscription.ts +0 -428
- package/tsconfig.json +0 -8
package/src/ws-subscription.ts
DELETED
|
@@ -1,428 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WebSocket subscription for real-time inbox messages.
|
|
3
|
-
* Connects to Gateway /v1/ws per conversation and receives ws_message pushes.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import WebSocket from 'ws';
|
|
7
|
-
import type { ConversationEntry } from './client.js';
|
|
8
|
-
|
|
9
|
-
export interface WsControlPayload {
|
|
10
|
-
action: string;
|
|
11
|
-
target?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface WsSubscriptionOptions {
|
|
15
|
-
serverUrl: string;
|
|
16
|
-
getAccessToken: () => string | Promise<string>;
|
|
17
|
-
myDid: string;
|
|
18
|
-
listConversations: () => Promise<ConversationEntry[]>;
|
|
19
|
-
onMessage: (envelope: any, conversationId: string) => void;
|
|
20
|
-
onControl?: (control: WsControlPayload, conversationId: string) => void;
|
|
21
|
-
onError?: (err: Error) => void;
|
|
22
|
-
/** Optional debug hook for observability (non-fatal events). */
|
|
23
|
-
onDebug?: (info: { event: string; conversationId?: string; detail?: Record<string, unknown> }) => void;
|
|
24
|
-
/** Called when a WebSocket opens or reopens; e.g. Skill can run catchUp to fill gaps. */
|
|
25
|
-
onOpen?: (conversationId: string) => void;
|
|
26
|
-
/**
|
|
27
|
-
* When true (default), drop ws_message envelopes sent by myself (sender_did === myDid).
|
|
28
|
-
* Set to false when you intentionally run multiple devices/processes under the same DID and want cross-device delivery.
|
|
29
|
-
*/
|
|
30
|
-
ignoreSelfMessages?: boolean;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Keepalive heartbeat for idle connections to reduce "WS silently dropped" periods.
|
|
34
|
-
* This is client-side only (WS ping/pong control frames) and does not add inbox HTTP fetch cost.
|
|
35
|
-
*/
|
|
36
|
-
heartbeat?: {
|
|
37
|
-
enable?: boolean;
|
|
38
|
-
/** If idle time >= this value, start pinging. */
|
|
39
|
-
idleThresholdMs?: number;
|
|
40
|
-
/** Minimum interval between pings while connection is idle. */
|
|
41
|
-
pingIntervalMs?: number;
|
|
42
|
-
/** How long to wait for pong after sending ping. If timeout, close the socket. */
|
|
43
|
-
pongTimeoutMs?: number;
|
|
44
|
-
/** Max consecutive missed pongs before closing. */
|
|
45
|
-
maxMissedPongs?: number;
|
|
46
|
-
/** Heartbeat scheduler tick frequency. */
|
|
47
|
-
tickMs?: number;
|
|
48
|
-
/** Jitter for ping interval to avoid synchronized bursts across many connections. */
|
|
49
|
-
jitter?: number;
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const RECONNECT_BASE_MS = 1000;
|
|
54
|
-
const RECONNECT_MAX_MS = 30_000;
|
|
55
|
-
const RECONNECT_JITTER = 0.2;
|
|
56
|
-
const LIST_CONVERSATIONS_INTERVAL_MS = 60_000;
|
|
57
|
-
|
|
58
|
-
const DEFAULT_HEARTBEAT = {
|
|
59
|
-
enable: true,
|
|
60
|
-
idleThresholdMs: 10_000,
|
|
61
|
-
pingIntervalMs: 15_000,
|
|
62
|
-
pongTimeoutMs: 10_000,
|
|
63
|
-
maxMissedPongs: 2,
|
|
64
|
-
tickMs: 5_000,
|
|
65
|
-
jitter: 0.2,
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export class WsSubscription {
|
|
69
|
-
private opts: WsSubscriptionOptions;
|
|
70
|
-
private connections = new Map<string, WebSocket>();
|
|
71
|
-
private reconnectTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
72
|
-
private reconnectAttempts = new Map<string, number>();
|
|
73
|
-
private listInterval: ReturnType<typeof setInterval> | null = null;
|
|
74
|
-
private stopped = false;
|
|
75
|
-
/** Conversation IDs that were explicitly stopped (e.g. revoke); do not reconnect. */
|
|
76
|
-
private stoppedConversations = new Set<string>();
|
|
77
|
-
private lastParseErrorAtByConv = new Map<string, number>();
|
|
78
|
-
private lastFrameAtByConv = new Map<string, number>();
|
|
79
|
-
|
|
80
|
-
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
81
|
-
private heartbeatStates = new Map<
|
|
82
|
-
string,
|
|
83
|
-
{
|
|
84
|
-
lastRxAt: number;
|
|
85
|
-
lastPingAt: number;
|
|
86
|
-
nextPingAt: number;
|
|
87
|
-
missedPongs: number;
|
|
88
|
-
pongTimeout: ReturnType<typeof setTimeout> | null;
|
|
89
|
-
}
|
|
90
|
-
>();
|
|
91
|
-
|
|
92
|
-
constructor(opts: WsSubscriptionOptions) {
|
|
93
|
-
this.opts = opts;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
start(): void {
|
|
97
|
-
this.stopped = false;
|
|
98
|
-
this.connectAll();
|
|
99
|
-
this.listInterval = setInterval(() => this.syncConnections(), LIST_CONVERSATIONS_INTERVAL_MS);
|
|
100
|
-
|
|
101
|
-
// Start heartbeat scheduler (idle-triggered) once per process.
|
|
102
|
-
const hb = this.opts.heartbeat ?? {};
|
|
103
|
-
if (hb.enable ?? DEFAULT_HEARTBEAT.enable) {
|
|
104
|
-
this.heartbeatTimer = setInterval(() => this.heartbeatTick(), hb.tickMs ?? DEFAULT_HEARTBEAT.tickMs);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
stop(): void {
|
|
109
|
-
this.stopped = true;
|
|
110
|
-
if (this.listInterval) {
|
|
111
|
-
clearInterval(this.listInterval);
|
|
112
|
-
this.listInterval = null;
|
|
113
|
-
}
|
|
114
|
-
if (this.heartbeatTimer) {
|
|
115
|
-
clearInterval(this.heartbeatTimer);
|
|
116
|
-
this.heartbeatTimer = null;
|
|
117
|
-
}
|
|
118
|
-
for (const timer of this.reconnectTimers.values()) {
|
|
119
|
-
clearTimeout(timer);
|
|
120
|
-
}
|
|
121
|
-
this.reconnectTimers.clear();
|
|
122
|
-
for (const [convId, ws] of this.connections) {
|
|
123
|
-
ws.removeAllListeners();
|
|
124
|
-
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
125
|
-
ws.close();
|
|
126
|
-
}
|
|
127
|
-
this.connections.delete(convId);
|
|
128
|
-
}
|
|
129
|
-
this.reconnectAttempts.clear();
|
|
130
|
-
this.stoppedConversations.clear();
|
|
131
|
-
this.lastParseErrorAtByConv.clear();
|
|
132
|
-
this.lastFrameAtByConv.clear();
|
|
133
|
-
|
|
134
|
-
for (const state of this.heartbeatStates.values()) {
|
|
135
|
-
if (state.pongTimeout) clearTimeout(state.pongTimeout);
|
|
136
|
-
}
|
|
137
|
-
this.heartbeatStates.clear();
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Stop a single conversation's WebSocket and do not reconnect.
|
|
142
|
-
* Used when the conversation is revoked or the client no longer wants to subscribe.
|
|
143
|
-
*/
|
|
144
|
-
stopConversation(conversationId: string): void {
|
|
145
|
-
this.stoppedConversations.add(conversationId);
|
|
146
|
-
const timer = this.reconnectTimers.get(conversationId);
|
|
147
|
-
if (timer) {
|
|
148
|
-
clearTimeout(timer);
|
|
149
|
-
this.reconnectTimers.delete(conversationId);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const hbState = this.heartbeatStates.get(conversationId);
|
|
153
|
-
if (hbState?.pongTimeout) clearTimeout(hbState.pongTimeout);
|
|
154
|
-
this.heartbeatStates.delete(conversationId);
|
|
155
|
-
|
|
156
|
-
const ws = this.connections.get(conversationId);
|
|
157
|
-
if (ws) {
|
|
158
|
-
ws.removeAllListeners();
|
|
159
|
-
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
|
|
160
|
-
ws.close();
|
|
161
|
-
}
|
|
162
|
-
this.connections.delete(conversationId);
|
|
163
|
-
}
|
|
164
|
-
this.lastParseErrorAtByConv.delete(conversationId);
|
|
165
|
-
this.lastFrameAtByConv.delete(conversationId);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
private wsUrl(conversationId: string): string {
|
|
169
|
-
const base = this.opts.serverUrl.replace(/^http/, 'ws').replace(/\/$/, '');
|
|
170
|
-
return `${base}/v1/ws?conversation_id=${encodeURIComponent(conversationId)}`;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
private async connectAsync(conversationId: string): Promise<void> {
|
|
174
|
-
if (this.stopped || this.stoppedConversations.has(conversationId) || this.connections.has(conversationId)) return;
|
|
175
|
-
|
|
176
|
-
const token = await Promise.resolve(this.opts.getAccessToken());
|
|
177
|
-
const url = this.wsUrl(conversationId);
|
|
178
|
-
const ws = new WebSocket(url, {
|
|
179
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
this.connections.set(conversationId, ws);
|
|
183
|
-
|
|
184
|
-
const hb = this.opts.heartbeat ?? {};
|
|
185
|
-
const hbEnabled = hb.enable ?? DEFAULT_HEARTBEAT.enable;
|
|
186
|
-
if (hbEnabled) {
|
|
187
|
-
const now = Date.now();
|
|
188
|
-
this.heartbeatStates.set(conversationId, {
|
|
189
|
-
lastRxAt: now,
|
|
190
|
-
lastPingAt: 0,
|
|
191
|
-
nextPingAt: now + (hb.idleThresholdMs ?? DEFAULT_HEARTBEAT.idleThresholdMs),
|
|
192
|
-
missedPongs: 0,
|
|
193
|
-
pongTimeout: null,
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
ws.on('open', () => {
|
|
198
|
-
this.reconnectAttempts.set(conversationId, 0);
|
|
199
|
-
this.opts.onOpen?.(conversationId);
|
|
200
|
-
// ws_connected will arrive as first message
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
ws.on('message', (data: any) => {
|
|
204
|
-
try {
|
|
205
|
-
// Any server frame counts as activity; keep connection stable and avoid unnecessary pings.
|
|
206
|
-
const state = this.heartbeatStates.get(conversationId);
|
|
207
|
-
if (state) {
|
|
208
|
-
state.lastRxAt = Date.now();
|
|
209
|
-
state.missedPongs = 0;
|
|
210
|
-
if (state.pongTimeout) {
|
|
211
|
-
clearTimeout(state.pongTimeout);
|
|
212
|
-
state.pongTimeout = null;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const rawText =
|
|
217
|
-
typeof data === 'string'
|
|
218
|
-
? data
|
|
219
|
-
: Buffer.isBuffer(data)
|
|
220
|
-
? data.toString()
|
|
221
|
-
: Array.isArray(data)
|
|
222
|
-
? Buffer.concat(data).toString()
|
|
223
|
-
: data instanceof ArrayBuffer
|
|
224
|
-
? Buffer.from(new Uint8Array(data)).toString()
|
|
225
|
-
: // Fallback: try best-effort stringification
|
|
226
|
-
String(data);
|
|
227
|
-
|
|
228
|
-
this.lastFrameAtByConv.set(conversationId, Date.now());
|
|
229
|
-
|
|
230
|
-
const msg = JSON.parse(rawText);
|
|
231
|
-
|
|
232
|
-
if (msg.type === 'ws_connected') {
|
|
233
|
-
this.opts.onDebug?.({
|
|
234
|
-
event: 'ws_connected',
|
|
235
|
-
conversationId,
|
|
236
|
-
detail: {
|
|
237
|
-
your_did: msg.your_did,
|
|
238
|
-
server_ts_ms: msg.server_ts_ms,
|
|
239
|
-
conversation_id: msg.conversation_id,
|
|
240
|
-
},
|
|
241
|
-
});
|
|
242
|
-
} else if (msg.type === 'ws_message' && msg.envelope) {
|
|
243
|
-
const env = msg.envelope;
|
|
244
|
-
const ignoreSelf = this.opts.ignoreSelfMessages ?? true;
|
|
245
|
-
if (!ignoreSelf || env.sender_did !== this.opts.myDid) {
|
|
246
|
-
this.opts.onMessage(env, conversationId);
|
|
247
|
-
} else {
|
|
248
|
-
this.opts.onDebug?.({
|
|
249
|
-
event: 'ws_message_ignored_self',
|
|
250
|
-
conversationId,
|
|
251
|
-
detail: { sender_did: env.sender_did, message_id: env.message_id, seq: env.seq },
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
} else if (msg.type === 'ws_control' && msg.control) {
|
|
255
|
-
this.opts.onControl?.(msg.control, conversationId);
|
|
256
|
-
this.opts.onDebug?.({ event: 'ws_control', conversationId, detail: msg.control });
|
|
257
|
-
} else if (msg.type === 'ws_receipt' && msg.receipt) {
|
|
258
|
-
this.opts.onDebug?.({ event: 'ws_receipt', conversationId, detail: msg.receipt });
|
|
259
|
-
} else if (msg?.type) {
|
|
260
|
-
this.opts.onDebug?.({ event: 'ws_unknown_type', conversationId, detail: { type: msg.type } });
|
|
261
|
-
}
|
|
262
|
-
} catch (e: any) {
|
|
263
|
-
// Observability: parse failures are otherwise silent and look like "server never pushes".
|
|
264
|
-
const now = Date.now();
|
|
265
|
-
const last = this.lastParseErrorAtByConv.get(conversationId) ?? 0;
|
|
266
|
-
if (now - last > 30_000) {
|
|
267
|
-
this.lastParseErrorAtByConv.set(conversationId, now);
|
|
268
|
-
this.opts.onDebug?.({
|
|
269
|
-
event: 'ws_parse_error',
|
|
270
|
-
conversationId,
|
|
271
|
-
detail: {
|
|
272
|
-
message: e?.message ?? String(e),
|
|
273
|
-
last_frame_at_ms: this.lastFrameAtByConv.get(conversationId) ?? null,
|
|
274
|
-
},
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
ws.on('pong', () => {
|
|
281
|
-
const state = this.heartbeatStates.get(conversationId);
|
|
282
|
-
if (!state) return;
|
|
283
|
-
state.lastRxAt = Date.now();
|
|
284
|
-
state.missedPongs = 0;
|
|
285
|
-
if (state.pongTimeout) {
|
|
286
|
-
clearTimeout(state.pongTimeout);
|
|
287
|
-
state.pongTimeout = null;
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
ws.on('close', () => {
|
|
292
|
-
this.connections.delete(conversationId);
|
|
293
|
-
const state = this.heartbeatStates.get(conversationId);
|
|
294
|
-
if (state?.pongTimeout) clearTimeout(state.pongTimeout);
|
|
295
|
-
this.heartbeatStates.delete(conversationId);
|
|
296
|
-
if (!this.stopped && !this.stoppedConversations.has(conversationId)) {
|
|
297
|
-
this.scheduleReconnect(conversationId);
|
|
298
|
-
}
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
ws.on('error', (err) => {
|
|
302
|
-
this.opts.onError?.(err);
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
private heartbeatTick(): void {
|
|
307
|
-
const hb = this.opts.heartbeat ?? {};
|
|
308
|
-
const hbEnabled = hb.enable ?? DEFAULT_HEARTBEAT.enable;
|
|
309
|
-
if (!hbEnabled) return;
|
|
310
|
-
|
|
311
|
-
const idleThresholdMs = hb.idleThresholdMs ?? DEFAULT_HEARTBEAT.idleThresholdMs;
|
|
312
|
-
const pingIntervalMs = hb.pingIntervalMs ?? DEFAULT_HEARTBEAT.pingIntervalMs;
|
|
313
|
-
const pongTimeoutMs = hb.pongTimeoutMs ?? DEFAULT_HEARTBEAT.pongTimeoutMs;
|
|
314
|
-
const maxMissedPongs = hb.maxMissedPongs ?? DEFAULT_HEARTBEAT.maxMissedPongs;
|
|
315
|
-
const jitter = hb.jitter ?? DEFAULT_HEARTBEAT.jitter;
|
|
316
|
-
|
|
317
|
-
const now = Date.now();
|
|
318
|
-
|
|
319
|
-
for (const [conversationId, ws] of this.connections) {
|
|
320
|
-
if (ws.readyState !== WebSocket.OPEN) continue;
|
|
321
|
-
const state = this.heartbeatStates.get(conversationId);
|
|
322
|
-
if (!state) continue;
|
|
323
|
-
|
|
324
|
-
// If we are waiting for a pong, do not send another ping.
|
|
325
|
-
if (state.pongTimeout) continue;
|
|
326
|
-
|
|
327
|
-
const idleFor = now - state.lastRxAt;
|
|
328
|
-
if (idleFor < idleThresholdMs) continue;
|
|
329
|
-
|
|
330
|
-
if (now < state.nextPingAt) continue;
|
|
331
|
-
if (state.lastPingAt !== 0 && now - state.lastPingAt < pingIntervalMs * 0.5) {
|
|
332
|
-
// Extra guard against edge cases (should be rare).
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Application-level keepalive for CDN/NAT idle timeouts (more reliable than ping/pong alone).
|
|
337
|
-
// Receiver can ignore this message.
|
|
338
|
-
try {
|
|
339
|
-
ws.send(JSON.stringify({ type: 'hb' }));
|
|
340
|
-
} catch {
|
|
341
|
-
// ignore; ping/pong + reconnect will handle dead sockets
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
ws.ping();
|
|
345
|
-
state.lastPingAt = now;
|
|
346
|
-
const jitterFactor = 1 + (Math.random() - 0.5) * 2 * jitter;
|
|
347
|
-
state.nextPingAt = now + Math.max(1000, pingIntervalMs * jitterFactor);
|
|
348
|
-
|
|
349
|
-
state.pongTimeout = setTimeout(() => {
|
|
350
|
-
state.missedPongs += 1;
|
|
351
|
-
state.pongTimeout = null;
|
|
352
|
-
|
|
353
|
-
if (state.missedPongs >= maxMissedPongs) {
|
|
354
|
-
try {
|
|
355
|
-
// Trigger close handler -> scheduleReconnect
|
|
356
|
-
ws.close(1001, 'pong timeout');
|
|
357
|
-
} catch {
|
|
358
|
-
// ignore
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}, pongTimeoutMs);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
private scheduleReconnect(conversationId: string): void {
|
|
366
|
-
if (this.reconnectTimers.has(conversationId)) return;
|
|
367
|
-
|
|
368
|
-
const attempt = this.reconnectAttempts.get(conversationId) ?? 0;
|
|
369
|
-
this.reconnectAttempts.set(conversationId, attempt + 1);
|
|
370
|
-
|
|
371
|
-
const tryConnect = () => {
|
|
372
|
-
this.reconnectTimers.delete(conversationId);
|
|
373
|
-
if (this.stopped) return;
|
|
374
|
-
void this.connectAsync(conversationId);
|
|
375
|
-
};
|
|
376
|
-
|
|
377
|
-
const delay = Math.min(
|
|
378
|
-
RECONNECT_BASE_MS * Math.pow(2, attempt) + (Math.random() - 0.5) * RECONNECT_JITTER * RECONNECT_BASE_MS,
|
|
379
|
-
RECONNECT_MAX_MS,
|
|
380
|
-
);
|
|
381
|
-
const timer = setTimeout(tryConnect, delay);
|
|
382
|
-
this.reconnectTimers.set(conversationId, timer);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
private isSubscribableConversationType(type: string): boolean {
|
|
386
|
-
// Server may return pending_dm alongside dm listings. WS upgrade is supported for any conversation_id.
|
|
387
|
-
return type === 'dm' || type === 'pending_dm' || type === 'channel' || type === 'group';
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
private async connectAll(): Promise<void> {
|
|
391
|
-
try {
|
|
392
|
-
const convos = await this.opts.listConversations();
|
|
393
|
-
const subscribable = convos.filter((c) => this.isSubscribableConversationType(c.type));
|
|
394
|
-
for (const c of subscribable) {
|
|
395
|
-
void this.connectAsync(c.conversation_id);
|
|
396
|
-
}
|
|
397
|
-
} catch (err) {
|
|
398
|
-
this.opts.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
private async syncConnections(): Promise<void> {
|
|
403
|
-
if (this.stopped) return;
|
|
404
|
-
try {
|
|
405
|
-
const convos = await this.opts.listConversations();
|
|
406
|
-
const subscribableIds = new Set(
|
|
407
|
-
convos.filter((c) => this.isSubscribableConversationType(c.type)).map((c) => c.conversation_id),
|
|
408
|
-
);
|
|
409
|
-
for (const convId of subscribableIds) {
|
|
410
|
-
if (!this.stoppedConversations.has(convId) && !this.connections.has(convId)) {
|
|
411
|
-
void this.connectAsync(convId);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
for (const convId of this.connections.keys()) {
|
|
415
|
-
if (!subscribableIds.has(convId) || this.stoppedConversations.has(convId)) {
|
|
416
|
-
const ws = this.connections.get(convId);
|
|
417
|
-
if (ws) {
|
|
418
|
-
ws.removeAllListeners();
|
|
419
|
-
ws.close();
|
|
420
|
-
this.connections.delete(convId);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
} catch {
|
|
425
|
-
// ignore
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|