diff --git a/src/utils/client.js b/src/utils/client.js index 9f13d68..3c8783b 100644 --- a/src/utils/client.js +++ b/src/utils/client.js @@ -34,6 +34,7 @@ function _checkActivityForHibernation() { return; } if(!client || client.hibernating + || client.userClosedConnection || !client.connectionEstablished // cant hibernate if connection isnt already established/ is being establised || ID_TO_RESOLVE_REJECT_MAP.size > 0){ // if there are any pending responses, we cant hibernate return; @@ -91,8 +92,18 @@ function _setupClientAndWaitForClose(connectedCb) { __receiveMessage(data); }); + let terminated = false; function _connectionTerminated(reason) { + if (terminated) { + return; + } + terminated = true; + console.log(reason); + // not set bufferRequests = true for unexpected failures. + // Real connection failures → reject immediately, don't buffer + // Hibernation → buffer and reconnect transparently + client.connectionEstablished = false; for (let [sequenceNumber, handler] of ID_TO_RESOLVE_REJECT_MAP) { handler.reject(reason); @@ -100,11 +111,20 @@ function _setupClientAndWaitForClose(connectedCb) { } resolve(); } - client.on('close', function () { - // https://websockets.spec.whatwg.org/#eventdef-websocket-error - // https://stackoverflow.com/questions/40084398/is-onclose-always-called-after-onerror-for-websocket - // we do not need to listen for error event as an error event is immediately followed by a close event. - _connectionTerminated('connection closed'); + + client.on('error', function (err) { + // ECONNREFUSED, ENOTFOUND, TLS errors, etc. can land here + _connectionTerminated(err); + }); + + client.on('close', function (code, reasonBuf) { + // reasonBuf may be a Buffer in ws; keep it simple + _connectionTerminated(`connection closed (${code})`); + }); + + // Optional but useful: HTTP-level failures (proxies, 401, 403, 502...) + client.on('unexpected-response', function (req, res) { + _connectionTerminated(`unexpected-response: ${res.statusCode}`); }); }); } @@ -131,13 +151,14 @@ function _cancelBackoffTimer() { } } -async function _setupAndMaintainConnection(firstConnectionCb, neverConnectedCB) { +async function _setupAndMaintainConnection(resolveOnFirstConnect, reject) { backoffTimer = null; function connected() { _resetBackoffTime(); - if(firstConnectionCb){ - firstConnectionCb("connected"); - firstConnectionCb = null; + if(resolveOnFirstConnect){ + resolveOnFirstConnect("connected"); + resolveOnFirstConnect = null; + reject = null; // setup hibernate timer on first connection activityInHibernateInterval = 1; hibernateTimer = setInterval(_checkActivityForHibernation, INACTIVITY_TIME_FOR_HIBERNATE); @@ -160,8 +181,8 @@ async function _setupAndMaintainConnection(firstConnectionCb, neverConnectedCB) client && client.userClosedConnectionCB && client.userClosedConnectionCB(); client = cocoDBEndPointURL = cocoAuthKey = null; id = 0; - if(neverConnectedCB){ - neverConnectedCB(new Error("user Cancelled")); + if(reject){ + reject(new Error("user Cancelled")); } } @@ -219,7 +240,7 @@ export function close() { currentClient.userClosedConnection = true; currentClient.userClosedConnectionCB = function () { for(let entry of pendingSendMessages){ - entry.reject(); + entry.reject(new Error('Connection closed')); } pendingSendMessages = []; resolve(); @@ -239,7 +260,7 @@ export function close() { * @returns {string} A function that increments the id variable and returns the new value as a hexadecimal string. */ function getId() { - id++; + id++; // Will take about 300 years to run out at 1 million sustained tps to db with Number.MAX_SAFE_INTEGER return id.toString(16); } @@ -284,7 +305,7 @@ function _sendPendingMessages() { */ export function sendMessage(message) { // make a copy as the user may start modifying the object while we are sending it. - message = structuredClone(message); + message = JSON.parse(JSON.stringify(message)); // faster than structured clone for most cases return new Promise(function (resolve, reject) { if(bufferRequests){ if(pendingSendMessages.length > MAX_PENDING_SEND_BUFFER_SIZE){