@wu529778790/open-im 1.9.4-beta.4 → 1.9.4-beta.5
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.
|
@@ -32,6 +32,8 @@ export interface CentrifugeCallbacks {
|
|
|
32
32
|
onDisconnected?: (reason?: string) => void;
|
|
33
33
|
onError?: (error: Error) => void;
|
|
34
34
|
onMessage?: (chatId: string, msgId: string, content: string) => void;
|
|
35
|
+
/** Called when reconnection has failed too many times — caller should do a full re-registration */
|
|
36
|
+
onPersistentFailure?: () => void;
|
|
35
37
|
}
|
|
36
38
|
export declare class WorkBuddyCentrifugeClient {
|
|
37
39
|
private config;
|
|
@@ -41,6 +43,9 @@ export declare class WorkBuddyCentrifugeClient {
|
|
|
41
43
|
private extraSubs;
|
|
42
44
|
private state;
|
|
43
45
|
private processedMsgIds;
|
|
46
|
+
private consecutiveErrors;
|
|
47
|
+
private pendingReplies;
|
|
48
|
+
private flushing;
|
|
44
49
|
private static readonly MAX_MSG_ID_CACHE;
|
|
45
50
|
constructor(config: CentrifugeClientConfig, callbacks?: CentrifugeCallbacks);
|
|
46
51
|
get logPrefix(): string;
|
|
@@ -83,4 +88,12 @@ export declare class WorkBuddyCentrifugeClient {
|
|
|
83
88
|
* Clean up old message IDs from cache
|
|
84
89
|
*/
|
|
85
90
|
private cleanMsgIdCache;
|
|
91
|
+
/**
|
|
92
|
+
* Enqueue a failed reply for later delivery
|
|
93
|
+
*/
|
|
94
|
+
private enqueuePendingReply;
|
|
95
|
+
/**
|
|
96
|
+
* Retry all pending replies after a successful reconnection
|
|
97
|
+
*/
|
|
98
|
+
private flushPendingReplies;
|
|
86
99
|
}
|
|
@@ -6,6 +6,12 @@ import { WebSocket } from 'ws';
|
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
7
|
import { createLogger } from '../logger.js';
|
|
8
8
|
const log = createLogger('WorkBuddyCentrifuge');
|
|
9
|
+
/** Max consecutive errors before triggering full re-registration */
|
|
10
|
+
const PERSISTENT_FAILURE_THRESHOLD = 5;
|
|
11
|
+
/** Max queued replies awaiting delivery */
|
|
12
|
+
const MAX_PENDING_REPLIES = 20;
|
|
13
|
+
/** Max age (ms) of a queued reply before it's discarded */
|
|
14
|
+
const PENDING_REPLY_TTL_MS = 5 * 60_000;
|
|
9
15
|
export class WorkBuddyCentrifugeClient {
|
|
10
16
|
config;
|
|
11
17
|
callbacks;
|
|
@@ -14,6 +20,9 @@ export class WorkBuddyCentrifugeClient {
|
|
|
14
20
|
extraSubs = [];
|
|
15
21
|
state = 'disconnected';
|
|
16
22
|
processedMsgIds = new Set();
|
|
23
|
+
consecutiveErrors = 0;
|
|
24
|
+
pendingReplies = [];
|
|
25
|
+
flushing = false;
|
|
17
26
|
static MAX_MSG_ID_CACHE = 1000;
|
|
18
27
|
constructor(config, callbacks = {}) {
|
|
19
28
|
this.config = config;
|
|
@@ -39,7 +48,9 @@ export class WorkBuddyCentrifugeClient {
|
|
|
39
48
|
this.client.on('connected', (ctx) => {
|
|
40
49
|
log.info(`${this.logPrefix} Connected (transport=${ctx.transport})`);
|
|
41
50
|
this.state = 'connected';
|
|
51
|
+
this.consecutiveErrors = 0;
|
|
42
52
|
this.callbacks.onConnected?.();
|
|
53
|
+
this.flushPendingReplies();
|
|
43
54
|
});
|
|
44
55
|
this.client.on('disconnected', (ctx) => {
|
|
45
56
|
log.info(`${this.logPrefix} Disconnected: code=${ctx.code}, reason=${ctx.reason}`);
|
|
@@ -56,7 +67,15 @@ export class WorkBuddyCentrifugeClient {
|
|
|
56
67
|
});
|
|
57
68
|
this.client.on('error', (ctx) => {
|
|
58
69
|
log.error(`${this.logPrefix} Error: ${ctx.error.message}`);
|
|
70
|
+
this.consecutiveErrors++;
|
|
59
71
|
this.callbacks.onError?.(new Error(ctx.error.message));
|
|
72
|
+
if (this.consecutiveErrors >= PERSISTENT_FAILURE_THRESHOLD) {
|
|
73
|
+
log.warn(`${this.logPrefix} ${this.consecutiveErrors} consecutive errors — triggering full re-registration`);
|
|
74
|
+
this.consecutiveErrors = 0;
|
|
75
|
+
// Stop the current Centrifuge instance so client.ts can create a fresh one
|
|
76
|
+
this.stop();
|
|
77
|
+
this.callbacks.onPersistentFailure?.();
|
|
78
|
+
}
|
|
60
79
|
});
|
|
61
80
|
// Create channel subscription
|
|
62
81
|
this.sub = this.client.newSubscription(this.config.channel, {
|
|
@@ -216,7 +235,7 @@ export class WorkBuddyCentrifugeClient {
|
|
|
216
235
|
}
|
|
217
236
|
}
|
|
218
237
|
if (!sent) {
|
|
219
|
-
|
|
238
|
+
this.enqueuePendingReply(url, httpPayload, this.config.httpAccessToken);
|
|
220
239
|
}
|
|
221
240
|
// Release the heartbeat lock so the periodic registration can resume
|
|
222
241
|
this.config.releaseChannelLockFn?.();
|
|
@@ -325,4 +344,57 @@ export class WorkBuddyCentrifugeClient {
|
|
|
325
344
|
});
|
|
326
345
|
}
|
|
327
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* Enqueue a failed reply for later delivery
|
|
349
|
+
*/
|
|
350
|
+
enqueuePendingReply(url, payload, accessToken) {
|
|
351
|
+
// Evict expired entries
|
|
352
|
+
const now = Date.now();
|
|
353
|
+
this.pendingReplies = this.pendingReplies.filter((r) => now - r.addedAt < PENDING_REPLY_TTL_MS);
|
|
354
|
+
if (this.pendingReplies.length >= MAX_PENDING_REPLIES) {
|
|
355
|
+
const evicted = this.pendingReplies.shift();
|
|
356
|
+
log.warn(`${this.logPrefix} Pending replies full, evicting oldest (msgId=${evicted?.payload.msgId})`);
|
|
357
|
+
}
|
|
358
|
+
this.pendingReplies.push({ url, payload, accessToken, addedAt: now });
|
|
359
|
+
log.warn(`${this.logPrefix} Queued pending reply (queue=${this.pendingReplies.length}, msgId=${payload.msgId})`);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Retry all pending replies after a successful reconnection
|
|
363
|
+
*/
|
|
364
|
+
async flushPendingReplies() {
|
|
365
|
+
if (this.flushing || this.pendingReplies.length === 0)
|
|
366
|
+
return;
|
|
367
|
+
this.flushing = true;
|
|
368
|
+
const now = Date.now();
|
|
369
|
+
// Take only non-expired replies
|
|
370
|
+
const toSend = this.pendingReplies.filter((r) => now - r.addedAt < PENDING_REPLY_TTL_MS);
|
|
371
|
+
this.pendingReplies = [];
|
|
372
|
+
if (toSend.length > 0) {
|
|
373
|
+
log.info(`${this.logPrefix} Flushing ${toSend.length} pending reply(ies)`);
|
|
374
|
+
}
|
|
375
|
+
for (const reply of toSend) {
|
|
376
|
+
try {
|
|
377
|
+
const res = await fetch(reply.url, {
|
|
378
|
+
method: 'POST',
|
|
379
|
+
headers: {
|
|
380
|
+
'Content-Type': 'application/json',
|
|
381
|
+
Authorization: `Bearer ${reply.accessToken}`,
|
|
382
|
+
},
|
|
383
|
+
body: JSON.stringify(reply.payload),
|
|
384
|
+
signal: AbortSignal.timeout(30_000),
|
|
385
|
+
});
|
|
386
|
+
const body = await res.text().catch(() => '');
|
|
387
|
+
if (res.ok) {
|
|
388
|
+
log.info(`${this.logPrefix} Flushed pending reply ok: msgId=${reply.payload.msgId}`);
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
log.error(`${this.logPrefix} Flushed pending reply failed: ${res.status} ${body.substring(0, 200)}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
log.error(`${this.logPrefix} Flushed pending reply error: msgId=${reply.payload.msgId}`, err);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
this.flushing = false;
|
|
399
|
+
}
|
|
328
400
|
}
|
package/dist/workbuddy/client.js
CHANGED
|
@@ -198,6 +198,15 @@ async function connect() {
|
|
|
198
198
|
log.error(`WorkBuddy Centrifuge error: ${error instanceof Error ? error.message : JSON.stringify(error)}`);
|
|
199
199
|
updateState('error');
|
|
200
200
|
},
|
|
201
|
+
onPersistentFailure: () => {
|
|
202
|
+
log.warn('WorkBuddy Centrifuge persistent failure detected — doing full re-registration');
|
|
203
|
+
if (heartbeatTimer) {
|
|
204
|
+
clearInterval(heartbeatTimer);
|
|
205
|
+
heartbeatTimer = null;
|
|
206
|
+
}
|
|
207
|
+
updateState('disconnected');
|
|
208
|
+
scheduleReconnect();
|
|
209
|
+
},
|
|
201
210
|
onMessage: async (chatId, msgId, content) => {
|
|
202
211
|
if (messageHandler) {
|
|
203
212
|
try {
|