@product7/product7-js 0.6.5 → 0.6.7
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/product7-js.js
CHANGED
|
@@ -8216,11 +8216,28 @@
|
|
|
8216
8216
|
|
|
8217
8217
|
this.ws = null;
|
|
8218
8218
|
this.reconnectAttempts = 0;
|
|
8219
|
-
|
|
8220
|
-
|
|
8219
|
+
// Reconnect indefinitely with capped exponential backoff. The
|
|
8220
|
+
// previous 5-attempt hard cap meant ~31s of churn on a flaky network
|
|
8221
|
+
// permanently killed the live-chat connection until page refresh —
|
|
8222
|
+
// the dominant "messages don't arrive" symptom in production.
|
|
8223
|
+
this.reconnectBaseDelay = 1000;
|
|
8224
|
+
this.reconnectMaxDelay = 30_000;
|
|
8221
8225
|
this.pingInterval = null;
|
|
8222
8226
|
this.isConnected = false;
|
|
8223
8227
|
|
|
8228
|
+
// Heartbeat watchdog. The browser's onclose/onerror only fires when
|
|
8229
|
+
// it notices the TCP socket is dead — on stalled-but-not-closed
|
|
8230
|
+
// connections (NAT timeouts, sleeping mobile, some intermediaries)
|
|
8231
|
+
// that can take many minutes. We track the wall-clock time of the
|
|
8232
|
+
// last received frame and force a reconnect if nothing arrives
|
|
8233
|
+
// within the timeout. Server's protocol-level pings come every 54s
|
|
8234
|
+
// (pkg/websocket/client.go pingPeriod) and our app-level pings/pongs
|
|
8235
|
+
// every 30s, so 90s catches a dead pipe with one cycle of headroom.
|
|
8236
|
+
this.lastFrameAt = 0;
|
|
8237
|
+
this.heartbeatTimeoutMs = 90_000;
|
|
8238
|
+
this.heartbeatCheckIntervalMs = 15_000;
|
|
8239
|
+
this.heartbeatInterval = null;
|
|
8240
|
+
|
|
8224
8241
|
// Event listeners
|
|
8225
8242
|
this._listeners = new Map();
|
|
8226
8243
|
|
|
@@ -8244,6 +8261,8 @@
|
|
|
8244
8261
|
return;
|
|
8245
8262
|
}
|
|
8246
8263
|
|
|
8264
|
+
this._intentionallyClosed = false;
|
|
8265
|
+
|
|
8247
8266
|
// Mock mode - simulate connection
|
|
8248
8267
|
if (this.mock) {
|
|
8249
8268
|
this.isConnected = true;
|
|
@@ -8275,13 +8294,15 @@
|
|
|
8275
8294
|
*/
|
|
8276
8295
|
disconnect() {
|
|
8277
8296
|
this.isConnected = false;
|
|
8278
|
-
this.
|
|
8297
|
+
this._intentionallyClosed = true; // Prevent reconnection
|
|
8279
8298
|
|
|
8280
8299
|
if (this.pingInterval) {
|
|
8281
8300
|
clearInterval(this.pingInterval);
|
|
8282
8301
|
this.pingInterval = null;
|
|
8283
8302
|
}
|
|
8284
8303
|
|
|
8304
|
+
this._stopHeartbeat();
|
|
8305
|
+
|
|
8285
8306
|
if (this.ws) {
|
|
8286
8307
|
this.ws.close();
|
|
8287
8308
|
this.ws = null;
|
|
@@ -8293,6 +8314,35 @@
|
|
|
8293
8314
|
}
|
|
8294
8315
|
}
|
|
8295
8316
|
|
|
8317
|
+
_startHeartbeat() {
|
|
8318
|
+
this._stopHeartbeat();
|
|
8319
|
+
this.lastFrameAt = Date.now();
|
|
8320
|
+
this.heartbeatInterval = setInterval(() => {
|
|
8321
|
+
if (Date.now() - this.lastFrameAt > this.heartbeatTimeoutMs) {
|
|
8322
|
+
console.warn(
|
|
8323
|
+
`[WebSocket] No frames in ${this.heartbeatTimeoutMs}ms, forcing reconnect`
|
|
8324
|
+
);
|
|
8325
|
+
// Closing the socket fires onclose, which schedules a
|
|
8326
|
+
// reconnect through the normal path.
|
|
8327
|
+
this._stopHeartbeat();
|
|
8328
|
+
if (this.ws) {
|
|
8329
|
+
try {
|
|
8330
|
+
this.ws.close();
|
|
8331
|
+
} catch (_) {
|
|
8332
|
+
// ignore
|
|
8333
|
+
}
|
|
8334
|
+
}
|
|
8335
|
+
}
|
|
8336
|
+
}, this.heartbeatCheckIntervalMs);
|
|
8337
|
+
}
|
|
8338
|
+
|
|
8339
|
+
_stopHeartbeat() {
|
|
8340
|
+
if (this.heartbeatInterval) {
|
|
8341
|
+
clearInterval(this.heartbeatInterval);
|
|
8342
|
+
this.heartbeatInterval = null;
|
|
8343
|
+
}
|
|
8344
|
+
}
|
|
8345
|
+
|
|
8296
8346
|
/**
|
|
8297
8347
|
* Subscribe to events
|
|
8298
8348
|
* @param {string} event - Event name
|
|
@@ -8342,6 +8392,8 @@
|
|
|
8342
8392
|
console.log('[WebSocket] Connected');
|
|
8343
8393
|
this.isConnected = true;
|
|
8344
8394
|
this.reconnectAttempts = 0;
|
|
8395
|
+
this._intentionallyClosed = false;
|
|
8396
|
+
this._startHeartbeat();
|
|
8345
8397
|
this._emit('connected', {});
|
|
8346
8398
|
|
|
8347
8399
|
// Start ping interval to keep connection alive
|
|
@@ -8351,6 +8403,9 @@
|
|
|
8351
8403
|
}
|
|
8352
8404
|
|
|
8353
8405
|
_onMessage(event) {
|
|
8406
|
+
// Any inbound frame counts as a sign of life for the watchdog —
|
|
8407
|
+
// including pongs, message:new, typing, etc.
|
|
8408
|
+
this.lastFrameAt = Date.now();
|
|
8354
8409
|
try {
|
|
8355
8410
|
const data = JSON.parse(event.data);
|
|
8356
8411
|
const { type, payload } = data;
|
|
@@ -8395,8 +8450,13 @@
|
|
|
8395
8450
|
this.pingInterval = null;
|
|
8396
8451
|
}
|
|
8397
8452
|
|
|
8453
|
+
this._stopHeartbeat();
|
|
8454
|
+
|
|
8398
8455
|
this._emit('disconnected', { code: event.code, reason: event.reason });
|
|
8399
|
-
|
|
8456
|
+
// Skip reconnect if disconnect() was called intentionally.
|
|
8457
|
+
if (!this._intentionallyClosed) {
|
|
8458
|
+
this._scheduleReconnect();
|
|
8459
|
+
}
|
|
8400
8460
|
}
|
|
8401
8461
|
|
|
8402
8462
|
_onError(error) {
|
|
@@ -8405,14 +8465,13 @@
|
|
|
8405
8465
|
}
|
|
8406
8466
|
|
|
8407
8467
|
_scheduleReconnect() {
|
|
8408
|
-
if (this.
|
|
8409
|
-
console.log('[WebSocket] Max reconnect attempts reached');
|
|
8410
|
-
this._emit('reconnect_failed', {});
|
|
8411
|
-
return;
|
|
8412
|
-
}
|
|
8468
|
+
if (this._intentionallyClosed) return;
|
|
8413
8469
|
|
|
8414
8470
|
this.reconnectAttempts++;
|
|
8415
|
-
const delay =
|
|
8471
|
+
const delay = Math.min(
|
|
8472
|
+
this.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts - 1),
|
|
8473
|
+
this.reconnectMaxDelay
|
|
8474
|
+
);
|
|
8416
8475
|
console.log(
|
|
8417
8476
|
`[WebSocket] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`
|
|
8418
8477
|
);
|
|
@@ -12081,13 +12140,34 @@
|
|
|
12081
12140
|
this._wsUnsubscribers.push(
|
|
12082
12141
|
this.wsService.on('conversation_closed', this._handleConversationClosed)
|
|
12083
12142
|
);
|
|
12143
|
+
// Track first vs reconnect locally so we only backfill on reconnects.
|
|
12144
|
+
// First connect is right after _initWebSocket() and the surrounding
|
|
12145
|
+
// flow has already loaded fresh data — re-fetching would be wasted.
|
|
12146
|
+
let wsHasConnectedBefore = false;
|
|
12084
12147
|
this._wsUnsubscribers.push(
|
|
12085
12148
|
this.wsService.on('connected', () => {
|
|
12086
12149
|
console.log('[LiveChatWidget] WebSocket connected');
|
|
12150
|
+
const isReconnect = wsHasConnectedBefore;
|
|
12151
|
+
wsHasConnectedBefore = true;
|
|
12087
12152
|
if (this.LiveChatState.activeConversationId) {
|
|
12088
12153
|
this.wsService.send('conversation:subscribe', {
|
|
12089
12154
|
conversation_id: this.LiveChatState.activeConversationId,
|
|
12090
12155
|
});
|
|
12156
|
+
// On reconnect, refetch the active conversation's messages.
|
|
12157
|
+
// The server doesn't replay events that fired during the WS
|
|
12158
|
+
// disconnect, so anything broadcast in the gap is otherwise
|
|
12159
|
+
// permanently lost from the customer's view until they
|
|
12160
|
+
// refresh the page.
|
|
12161
|
+
if (isReconnect) {
|
|
12162
|
+
this.fetchMessages(this.LiveChatState.activeConversationId).catch(
|
|
12163
|
+
(err) => {
|
|
12164
|
+
console.error(
|
|
12165
|
+
'[LiveChatWidget] Failed to backfill messages on reconnect:',
|
|
12166
|
+
err
|
|
12167
|
+
);
|
|
12168
|
+
}
|
|
12169
|
+
);
|
|
12170
|
+
}
|
|
12091
12171
|
}
|
|
12092
12172
|
})
|
|
12093
12173
|
);
|