@hpkv/websocket-client
Version:
HPKV WebSocket client for Node.js
8 lines (7 loc) • 19.7 kB
JavaScript
/**
* @hpkv/websocket-client v1.4.1
* HPKV WebSocket client for Node.js
* @license MIT
*/
;var e=require("ws");function t(e){return e&&e.__esModule?e:{default:e}}var n=t(require("cross-fetch"));const s={CONNECTING:0,OPEN:1,CLOSING:2,CLOSED:3};var o,i;exports.ConnectionState=void 0,(o=exports.ConnectionState||(exports.ConnectionState={})).DISCONNECTED="DISCONNECTED",o.CONNECTING="CONNECTING",o.CONNECTED="CONNECTED",o.DISCONNECTING="DISCONNECTING",o.RECONNECTING="RECONNECTING",exports.HPKVOperation=void 0,(i=exports.HPKVOperation||(exports.HPKVOperation={}))[i.GET=1]="GET",i[i.SET=2]="SET",i[i.PATCH=3]="PATCH",i[i.DELETE=4]="DELETE",i[i.RANGE=5]="RANGE",i[i.ATOMIC=6]="ATOMIC";class r extends Error{constructor(e,t){super(e),this.code=t,this.name="HPKVError"}}class c extends r{constructor(e){super(e),this.name="ConnectionError"}}class a extends r{constructor(e){super(e),this.name="TimeoutError"}}class h extends r{constructor(e){super(e),this.name="AuthenticationError"}}class l{constructor(){this.events={}}on(e,t){return this.events[e]||(this.events[e]=[]),this.events[e].push(t),this}off(e,t){return this.events[e]&&(this.events[e]=this.events[e].filter(e=>e!==t)),this}emit(e,...t){return!!this.events[e]&&(this.events[e].forEach(e=>e(...t)),!0)}}function u(t){let n=null;if("undefined"!=typeof window&&"function"==typeof window.WebSocket?n=window.WebSocket:"undefined"!=typeof self&&"function"==typeof self.WebSocket?n=self.WebSocket:"undefined"!=typeof global&&"function"==typeof global.WebSocket&&(n=global.WebSocket),n)return function(e,t){const n=new t(e),s={};return{get readyState(){return n.readyState},on(e,t){let o;switch(e){case"open":o=e=>t();break;case"message":o=e=>{try{const n="string"==typeof e.data?JSON.parse(e.data):e.data;t(n)}catch(n){n instanceof SyntaxError?t(e.data):console.error('[HPKV Websocket Client] createBrowserWebSocket: Error processing "message" or in listener callback:',n)}};break;case"close":o=e=>t(null==e?void 0:e.code,null==e?void 0:e.reason);break;case"error":o=e=>t(e);break;default:throw new Error(`[HPKV Websocket Client] createBrowserWebSocket: Attaching direct listener for unhandled event type "${e}"`)}return n.addEventListener(e,o),s[e]=s[e]||[],s[e].push({original:t,wrapper:o}),this},removeAllListeners(){return Object.keys(s).forEach(e=>{s[e].forEach(t=>{n.removeEventListener(e,t.wrapper)}),delete s[e]}),this},removeListener(e,t){if(!s[e])return this;const o=s[e].findIndex(e=>e.original===t);if(-1!==o){const{wrapper:t}=s[e][o];n.removeEventListener(e,t),s[e].splice(o,1),0===s[e].length&&delete s[e]}return this},send(e){n.send(e)},close(e,t){n.close(e,t)}}}(t,n);if(void 0!==e.WebSocket)return function(t){try{const n=new e.WebSocket(t),s={};return{get readyState(){return n.readyState},on(e,t){let o;switch(e){case"open":o=()=>t();break;case"message":o=e=>{try{let n;if("object"!=typeof e||Buffer.isBuffer(e)){if(!Buffer.isBuffer(e)&&"string"!=typeof e)return void t(e);{const t=Buffer.isBuffer(e)?e.toString("utf8"):e;n=JSON.parse(t)}}else n=e;"type"in n&&n.type,t(n)}catch(n){if(!(n instanceof SyntaxError))throw n;Buffer.isBuffer(e)?t(e.toString("utf8")):t(e)}};break;case"close":o=(e,n)=>{const s=n?n.toString("utf8"):"";t(e,s)};break;case"error":o=e=>t(e);break;default:throw new r(`Attaching direct listener for unhandled websocket event type "${e}"`)}return n.on(e,o),s[e]=s[e]||[],s[e].push({original:t,wrapper:o}),this},removeAllListeners(){return n.removeAllListeners(),Object.keys(s).forEach(e=>{delete s[e]}),this},removeListener(e,t){if(!s[e])return this;const o=s[e].findIndex(e=>e.original===t);if(-1!==o){const{wrapper:t}=s[e][o];n.removeListener(e,t),s[e].splice(o,1),0===s[e].length&&delete s[e]}return this},send(e){n.send(e)},close(e,t){n.close(e,t)}}}catch(e){throw new c(`Failed to initialize WebSocket for Node.js: ${e instanceof Error?e.message:String(e)}`)}}(t);throw new r("No suitable WebSocket implementation found.")}const m={OPERATION:1e4,CLEANUP:6e4};class p{constructor(e){this.messageId=0,this.messageMap=new Map,this.timeouts={...m},this.cleanupInterval=null,this.onRateLimitExceeded=null,e&&(this.timeouts={...this.timeouts,...e}),this.initCleanupInterval()}initCleanupInterval(){this.clearCleanupInterval(),this.cleanupInterval=setInterval(()=>this.cleanupStaleRequests(),this.timeouts.CLEANUP)}clearCleanupInterval(){null!==this.cleanupInterval&&(clearInterval(this.cleanupInterval),this.cleanupInterval=null)}getNextMessageId(){return this.messageId>=Number.MAX_SAFE_INTEGER-1e3&&(this.messageId=0),++this.messageId}createMessage(e){return{...e,messageId:this.getNextMessageId()}}registerRequest(e,t,n){const s=n||this.timeouts.OPERATION;let o,i;const r=new Promise((e,t)=>{o=e,i=t}),c=setTimeout(()=>{this.messageMap.has(e)&&(this.messageMap.delete(e),i(new a(`Operation timed out after ${s}ms: ${t}`)))},s);this.messageMap.set(e,{resolve:o,reject:i,timer:c,timestamp:Date.now(),operation:t});return{promise:r,cancel:t=>{this.messageMap.has(e)&&(clearTimeout(c),this.messageMap.delete(e),i(new Error(t)))}}}handleMessage(e){const t=e,n=t.messageId;if(!n)return;const s=this.messageMap.get(n);s&&(200===t.code?s.resolve(e):(Promise.resolve().then(()=>{var t;"code"in e&&429===e.code&&(null===(t=this.onRateLimitExceeded)||void 0===t||t.call(this,e))}),s.reject(new r(t.error||t.message||"Unknown error",t.code||500))),clearTimeout(s.timer),this.messageMap.delete(n))}isNotification(e){return"type"in e&&"notification"===e.type}isErrorResponse(e){return"code"in e&&200!==e.code||"error"in e}cancelAllRequests(e){for(const[t,n]of this.messageMap.entries())clearTimeout(n.timer),n.reject(e),this.messageMap.delete(t)}cleanupStaleRequests(){const e=Date.now(),t=3*this.timeouts.OPERATION;for(const[n,s]of this.messageMap.entries()){const o=e-s.timestamp;o>t&&(clearTimeout(s.timer),s.reject(new a(`Request ${n} (${s.operation}) timed out after ${o}ms`)),this.messageMap.delete(n))}}get pendingCount(){return this.messageMap.size}destroy(){this.cancelAllRequests(new Error("Handler destroyed")),this.clearCleanupInterval()}}const C={enabled:!0,rateLimit:10};class d{constructor(e){this.throttleQueue=[],this.processingQueue=!1,this.nextAvailableSlotTime=0,this.backoffUntil=0,this.backoffExponent=0,this.throttlingConfig={...C,...e||{}},this.currentRate=this.throttlingConfig.rateLimit||C.rateLimit}get config(){return{...this.throttlingConfig}}getMetrics(){return{currentRate:this.currentRate,queueLength:this.throttleQueue.length}}updateConfig(e){const t=this.throttlingConfig.enabled;if(this.throttlingConfig={...this.throttlingConfig,...e},this.currentRate=this.throttlingConfig.rateLimit||C.rateLimit,t&&!this.throttlingConfig.enabled)for(;this.throttleQueue.length>0;){const e=this.throttleQueue.shift();e&&e()}}notify429(){if(Date.now()<this.backoffUntil)return;this.currentRate=Math.max(.1*(this.throttlingConfig.rateLimit||C.rateLimit),.5*this.currentRate);const e=1e3*Math.min(60,2**this.backoffExponent);this.backoffUntil=Date.now()+e,this.backoffExponent++}async throttleRequest(){if(!this.throttlingConfig.enabled)return Promise.resolve();const e=Date.now(),t=1e3/this.currentRate;return Math.max(e,this.nextAvailableSlotTime,this.backoffUntil)<=e?(this.nextAvailableSlotTime=e+t,Promise.resolve()):new Promise(e=>{this.throttleQueue.push(e),this.processingQueue||this.processThrottleQueue()})}processThrottleQueue(){if(0===this.throttleQueue.length)return void(this.processingQueue=!1);this.processingQueue=!0;const e=Date.now();if(this.nextAvailableSlotTime=Math.max(e,this.nextAvailableSlotTime),e<this.backoffUntil){this.nextAvailableSlotTime=Math.max(this.nextAvailableSlotTime,this.backoffUntil);const t=this.nextAvailableSlotTime-e;return void setTimeout(()=>this.processThrottleQueue(),t)}const t=1e3/this.currentRate,n=this.nextAvailableSlotTime-e;setTimeout(()=>{const e=this.throttleQueue.shift();e&&e(),this.throttleQueue.length>0?this.processThrottleQueue():this.processingQueue=!1},n),this.nextAvailableSlotTime+=t}destroy(){for(;this.throttleQueue.length>0;){const e=this.throttleQueue.shift();e&&e()}}}class g{constructor(e,t){this.ws=null,this.connectionPromise=null,this.connectionState=exports.ConnectionState.DISCONNECTED,this.reconnectAttempts=0,this.connectionTimeout=null,this.emitter=new l,this.isGracefulDisconnect=!1;let n=e.replace(/^http:\/\//,"ws://").replace(/^https:\/\//,"wss://");n.endsWith("/ws")||(n+="/ws"),this.baseUrl=n,this.retry={maxReconnectAttempts:(null==t?void 0:t.maxReconnectAttempts)||3,initialDelayBetweenReconnects:(null==t?void 0:t.initialDelayBetweenReconnects)||1e3,maxDelayBetweenReconnects:(null==t?void 0:t.maxDelayBetweenReconnects)||3e4,jitterMs:500},this.connectionTimeoutDuration=(null==t?void 0:t.connectionTimeout)||5e3,this.messageHandler=new p,this.throttlingManager=new d(null==t?void 0:t.throttling),this.messageHandler.onRateLimitExceeded=e=>{this.throttlingManager.notify429()}}async get(e,t){return this.sendMessage({op:exports.HPKVOperation.GET,key:e},t)}async set(e,t,n=!1,s){const o="string"==typeof t?t:JSON.stringify(t),i=n?exports.HPKVOperation.PATCH:exports.HPKVOperation.SET;return this.sendMessage({op:i,key:e,value:o},s)}async delete(e,t){return this.sendMessage({op:exports.HPKVOperation.DELETE,key:e},t)}async range(e,t,n,s){return this.sendMessage({op:exports.HPKVOperation.RANGE,key:e,endKey:t,limit:null==n?void 0:n.limit},s)}async atomicIncrement(e,t,n){return this.sendMessage({op:exports.HPKVOperation.ATOMIC,key:e,value:t},n)}async connect(){if(this.isGracefulDisconnect=!1,this.syncConnectionState(),this.connectionState!==exports.ConnectionState.CONNECTED||!this.isWebSocketOpen())return this.connectionState!==exports.ConnectionState.CONNECTING&&this.connectionState!==exports.ConnectionState.RECONNECTING||!this.connectionPromise?(this.connectionState=exports.ConnectionState.CONNECTING,this.connectionPromise=new Promise((e,t)=>this._initiateConnectionAttempt(e,t)),this.connectionPromise):this.connectionPromise}_initiateConnectionAttempt(e,t){let n;this.connectionTimeout&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=null),this.connectionTimeout=setTimeout(()=>{if(this.connectionState!==exports.ConnectionState.CONNECTED){if(this.ws){const e=this.ws;e.removeListener("open",o),e.removeListener("error",i),e.removeListener("close",r),this.ws===e&&(e.removeAllListeners(),this.ws=null)}this.connectionState=exports.ConnectionState.DISCONNECTED;const e=new a(`Connection timeout after ${this.connectionTimeoutDuration}ms (client-side)`);this.emitter.emit("error",e),t(e)}},this.connectionTimeoutDuration);try{const e=this.buildConnectionUrl();n=u(e),this.ws=n}catch(e){this.connectionTimeout&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=null),this.connectionState=exports.ConnectionState.DISCONNECTED;const n=e instanceof Error?e.message:"Unknown error creating WebSocket",s=new c(n);return this.emitter.emit("error",s),void t(s)}const s=()=>{n&&(n.removeListener("open",o),n.removeListener("error",i),n.removeListener("close",r))},o=()=>{s(),this.connectionTimeout&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=null),this.connectionState=exports.ConnectionState.CONNECTED,this.emitter.emit("connected"),this.reconnectAttempts=0,this.ws===n&&(this.ws.on("message",e=>this.handleMessage(e)),this.ws.on("error",e=>this.handleWebSocketError(e)),this.ws.on("close",(e,t)=>this.handleWebSocketClose(e,t))),e()},i=e=>{if(this.connectionState!==exports.ConnectionState.CONNECTING)return void s();s(),this.connectionTimeout&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=null),this.connectionState=exports.ConnectionState.DISCONNECTED,this.ws===n&&(this.ws=null);const o=new c(`WebSocket connection error during connect: ${e.message||"Unknown WebSocket Error"}`);this.emitter.emit("error",o),t(o)},r=(e,o)=>{if(this.connectionState!==exports.ConnectionState.CONNECTING)return void s();s(),this.connectionTimeout&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=null),this.connectionState=exports.ConnectionState.DISCONNECTED,this.ws===n&&(this.ws=null);const i=new c(`WebSocket closed before opening (code: ${null!=e?e:"N/A"}, reason: ${null!=o?o:"N/A"})`);this.emitter.emit("error",i),t(i)};n.on("open",o),n.on("error",i),n.on("close",r)}async disconnect(e=!0){return this.isGracefulDisconnect=!0,this.reconnectAttempts=0,this.syncConnectionState(),this.connectionState!==exports.ConnectionState.DISCONNECTED||this.ws?(this.connectionState=exports.ConnectionState.DISCONNECTING,e&&this.messageHandler.cancelAllRequests(new c("Connection closed by client")),this.cleanup(1e3,"Normal closure by client").finally(()=>{this.connectionState=exports.ConnectionState.DISCONNECTED,this.connectionPromise=null,this.isGracefulDisconnect&&(this.isGracefulDisconnect=!1)})):(this.isGracefulDisconnect=!1,Promise.resolve())}async reconnect(){this.reconnectAttempts++,this.connectionState=exports.ConnectionState.RECONNECTING;const e=Math.min(this.retry.initialDelayBetweenReconnects*Math.pow(2,this.reconnectAttempts-1),this.retry.maxDelayBetweenReconnects)+(this.retry.jitterMs?Math.floor(Math.random()*this.retry.jitterMs):0);this.emitter.emit("reconnecting",{attempt:this.reconnectAttempts,maxAttempts:this.retry.maxReconnectAttempts,delay:e}),await new Promise(t=>setTimeout(t,e));try{await this.connect(),this.emitter.emit("connected")}catch(e){if(this.reconnectAttempts<this.retry.maxReconnectAttempts)return this.reconnect();{this.connectionState=exports.ConnectionState.DISCONNECTED;const t=new c(e instanceof Error?`Reconnection failed after ${this.retry.maxReconnectAttempts} attempts: ${e.message}`:`Reconnection failed after ${this.retry.maxReconnectAttempts} attempts`);throw this.emitter.emit("reconnectFailed",t),this.messageHandler.cancelAllRequests(t),t}}}getConnectionState(){return this.syncConnectionState(),this.connectionState}getConnectionStats(){this.syncConnectionState();const e=this.throttlingManager.getMetrics();return{isConnected:this.connectionState===exports.ConnectionState.CONNECTED,reconnectAttempts:this.reconnectAttempts,messagesPending:this.messageHandler.pendingCount,connectionState:this.connectionState,throttling:this.throttlingManager.config.enabled?{currentRate:e.currentRate,queueLength:e.queueLength}:null}}isWebSocketOpen(){return null!==this.ws&&this.ws.readyState===s.OPEN}syncConnectionState(){if(this.ws)switch(this.ws.readyState){case s.CONNECTING:this.connectionState=exports.ConnectionState.CONNECTING;break;case s.OPEN:this.connectionState=exports.ConnectionState.CONNECTED;break;case s.CLOSING:this.connectionState=exports.ConnectionState.DISCONNECTING;break;case s.CLOSED:this.connectionState=exports.ConnectionState.DISCONNECTED}else this.connectionState=exports.ConnectionState.DISCONNECTED}on(e,t){this.emitter.on(e,t)}off(e,t){this.emitter.off(e,t)}getThrottlingStatus(){return{enabled:this.throttlingManager.config.enabled,config:this.throttlingManager.config,metrics:this.throttlingManager.getMetrics()}}updateThrottlingConfig(e){this.throttlingManager.updateConfig(e)}async sendMessage(e,t){await this.throttlingManager.throttleRequest(),this.syncConnectionState();const n=this.messageHandler.createMessage(e),{promise:s,cancel:o}=this.messageHandler.registerRequest(n.messageId,e.op.toString(),t||void 0);try{this.ws&&this.isWebSocketOpen()?this.ws.send(JSON.stringify(n)):o("WebSocket is not open")}catch(e){o(`Failed to send message: ${e instanceof Error?e.message:"Unknown error"}`)}return s}handleMessage(e){this.messageHandler.handleMessage(e)}handleWebSocketError(e){const t=new c(e.message||"Unknown WebSocket error occurred");this.emitter.emit("error",t)}handleWebSocketClose(e,t){const n=this.connectionState===exports.ConnectionState.CONNECTED,s=this.connectionState;this.ws&&(this.ws.removeAllListeners(),this.ws=null),this.connectionState=exports.ConnectionState.DISCONNECTED,this.connectionPromise=null,this.emitter.emit("disconnected",{code:e,reason:t,previousState:s,gracefully:this.isGracefulDisconnect}),this.isGracefulDisconnect?this.isGracefulDisconnect=!1:(this.messageHandler.cancelAllRequests(new c(`Connection closed unexpectedly (code: ${null!=e?e:"N/A"}, reason: ${null!=t?t:"N/A"})`)),(1e3!==e||1e3===e&&n)&&this.initiateReconnectionCycle())}async cleanup(e,t){return this.connectionTimeout&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=null),this.ws?new Promise(n=>{const o=this.ws;let i=null,r=!1;const c=()=>{r||(r=!0,i&&(clearTimeout(i),i=null),o.removeListener("close",a),this.ws===o&&(o.readyState!==s.CLOSED&&o.removeAllListeners(),this.ws=null),this.connectionState=exports.ConnectionState.DISCONNECTED,this.connectionPromise=null,n())},a=()=>{c()};o.readyState!==s.CLOSED?(o.on("close",a),o.readyState!==s.OPEN&&o.readyState!==s.CONNECTING||o.close(e,t),i=setTimeout(()=>{c()},200)):c()}):(this.connectionState=exports.ConnectionState.DISCONNECTED,this.connectionPromise=null,Promise.resolve())}initiateReconnectionCycle(){this.isGracefulDisconnect?this.connectionState!==exports.ConnectionState.DISCONNECTED&&(this.connectionState=exports.ConnectionState.DISCONNECTED):this.connectionState!==exports.ConnectionState.RECONNECTING&&this.connectionState!==exports.ConnectionState.CONNECTING&&(this.connectionState=exports.ConnectionState.RECONNECTING,this.reconnectAttempts=0,this.reconnect().catch(e=>{this.connectionState!==exports.ConnectionState.DISCONNECTED&&(this.connectionState=exports.ConnectionState.DISCONNECTED)}))}destroy(){this.messageHandler.cancelAllRequests(new c("Client destroyed")),this.messageHandler.destroy(),this.throttlingManager.destroy()}}class S extends g{constructor(e,t,n){super(t,n),this.apiKey=e}buildConnectionUrl(){return`${this.baseUrl}?apiKey=${this.apiKey}`}}class f extends g{constructor(e,t,n){super(t,n),this.subscriptions=new Map,this.token=e}buildConnectionUrl(){return`${this.baseUrl}?token=${this.token}`}subscribe(e){const t=Math.random().toString(36).substring(2,15);return this.subscriptions.set(t,e),t}unsubscribe(e){this.subscriptions.delete(e)}handleMessage(e){if(!e||!("type"in e)||"notification"!==e.type)return super.handleMessage(e);{const t=e;this.subscriptions.size>0&&this.subscriptions.forEach(e=>{Promise.resolve().then(()=>{e(t)})})}}}exports.AuthenticationError=h,exports.BaseWebSocketClient=g,exports.ConnectionError=c,exports.HPKVApiClient=S,exports.HPKVClientFactory=class{static createApiClient(e,t,n){return new S(e,t,n)}static createSubscriptionClient(e,t,n){return new f(e,t,n)}},exports.HPKVError=r,exports.HPKVSubscriptionClient=f,exports.MessageHandler=p,exports.ThrottlingManager=d,exports.TimeoutError=a,exports.WS_CONSTANTS=s,exports.WebsocketTokenManager=class{constructor(e,t){if(!e)throw new h("API key is required to generate a token");if(!t)throw new r("Base URL is required to generate a token");this.apiKey=e,this.baseUrl=t.replace(/^wss?:\/\//,"https://").replace(/\/ws$/,""),this.fetchFn="undefined"!=typeof globalThis&&globalThis.fetch?globalThis.fetch:n.default}async generateToken(e){try{const t=await this.fetchFn(`${this.baseUrl}/token/websocket`,{method:"POST",headers:{"Content-Type":"application/json","x-api-key":this.apiKey},body:JSON.stringify(e)});if(!t.ok){const e=t.status,n=t.statusText||"Unknown error";if(401===e||403===e)throw new h(`Failed to generate token: ${e} ${n}`);throw new r(`Failed to generate token: ${e} ${n}`)}return(await t.json()).token}catch(e){if(e instanceof h||e instanceof r)throw e;if(e instanceof TypeError)throw new r("Failed to generate token: No response from server");throw new r(`Failed to generate token: ${e instanceof Error?e.message:"Unknown error"}`)}}},exports.createWebSocket=u;
//# sourceMappingURL=index.min.js.map