verani 0.1.8 → 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/dist/client.cjs CHANGED
@@ -1 +1 @@
1
- const require_types=require(`./types-083oWz55.cjs`);function encodeClientMessage$1(t){return require_types.n(t)}function decodeServerMessage$1(t){return require_types.s(t)}const DEFAULT_RECONNECTION_CONFIG={enabled:!0,maxAttempts:10,initialDelay:1e3,maxDelay:3e4,backoffMultiplier:1.5};var ConnectionManager=class{constructor(e=DEFAULT_RECONNECTION_CONFIG,t){this.config=e,this.onStateChange=t,this.state=`disconnected`,this.reconnectAttempts=0,this.currentDelay=e.initialDelay}getState(){return this.state}isValidStateTransition(e,t){return{disconnected:[`connecting`,`reconnecting`],connecting:[`connected`,`disconnected`,`error`,`reconnecting`],connected:[`disconnected`,`reconnecting`],reconnecting:[`connecting`,`disconnected`,`error`],error:[`reconnecting`,`disconnected`,`connecting`]}[e]?.includes(t)??!1}setState(e){this.state!==e&&(this.isValidStateTransition(this.state,e),this.state=e,this.onStateChange?.(e))}resetReconnection(){this.reconnectAttempts=0,this.currentDelay=this.config.initialDelay,this.clearReconnectTimer()}scheduleReconnect(e){return this.config.enabled?this.config.maxAttempts>0&&this.reconnectAttempts>=this.config.maxAttempts?(this.setState(`error`),!1):(this.clearReconnectTimer(),this.setState(`reconnecting`),this.reconnectAttempts++,this.reconnectTimer=setTimeout(()=>{e(),this.currentDelay=Math.min(this.currentDelay*this.config.backoffMultiplier,this.config.maxDelay)},this.currentDelay),!0):!1}cancelReconnect(){this.clearReconnectTimer(),this.state===`reconnecting`&&this.setState(`disconnected`)}clearReconnectTimer(){this.reconnectTimer!==void 0&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=void 0)}getReconnectAttempts(){return this.reconnectAttempts}getNextDelay(){return this.currentDelay}destroy(){this.clearReconnectTimer()}},VeraniClient=class{constructor(e,t={}){this.url=e,this.listeners=new Map,this.messageQueue=[],this.isConnecting=!1,this.connectionId=0,this.options={reconnection:{enabled:t.reconnection?.enabled??DEFAULT_RECONNECTION_CONFIG.enabled,maxAttempts:t.reconnection?.maxAttempts??DEFAULT_RECONNECTION_CONFIG.maxAttempts,initialDelay:t.reconnection?.initialDelay??DEFAULT_RECONNECTION_CONFIG.initialDelay,maxDelay:t.reconnection?.maxDelay??DEFAULT_RECONNECTION_CONFIG.maxDelay,backoffMultiplier:t.reconnection?.backoffMultiplier??DEFAULT_RECONNECTION_CONFIG.backoffMultiplier},maxQueueSize:t.maxQueueSize??100,connectionTimeout:t.connectionTimeout??1e4},this.connectionManager=new ConnectionManager(this.options.reconnection,e=>{this.onStateChangeCallback?.(e)}),this.connect()}cleanupWebSocket(){if(this.connectionTimeout!==void 0&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=void 0),this.ws){let e=this.ws;if(this.ws=void 0,e.readyState===WebSocket.OPEN||e.readyState===WebSocket.CONNECTING)try{e.close(1e3,`Cleanup`)}catch{}}}connect(){if(!this.isConnecting&&!this.isConnected()){this.cleanupWebSocket();try{this.isConnecting=!0,this.connectionId++;let e=this.connectionId;this.connectionManager.setState(`connecting`),this.emitLifecycleEvent(`connecting`),this.ws=new WebSocket(this.url),this.connectionTimeout=setTimeout(()=>{this.isConnecting&&this.connectionId===e&&(this.ws?.close(),this.handleConnectionError(Error(`Connection timeout`)))},this.options.connectionTimeout),this.ws.addEventListener(`open`,()=>{this.connectionId===e&&this.handleOpen()}),this.ws.addEventListener(`message`,t=>{this.connectionId===e&&this.handleMessage(t)}),this.ws.addEventListener(`close`,t=>{this.connectionId===e&&this.handleClose(t)}),this.ws.addEventListener(`error`,t=>{this.connectionId===e&&this.handleError(t)})}catch(e){this.isConnecting=!1,this.handleConnectionError(e)}}}handleOpen(){this.isConnecting=!1,this.connectionTimeout!==void 0&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=void 0),this.connectionManager.setState(`connected`),this.connectionManager.resetReconnection(),this.flushMessageQueue(),this.connectionResolve&&(this.connectionResolve(),this.connectionPromise=void 0,this.connectionResolve=void 0,this.connectionReject=void 0),this.emitLifecycleEvent(`open`),this.emitLifecycleEvent(`connected`),this.onOpenCallback?.()}handleMessage(e){let t=decodeServerMessage$1(e.data);if(!t)return;let r=t.type,i=t.data;t.type===`event`&&t.data&&typeof t.data==`object`&&`type`in t.data&&(r=t.data.type,i=t.data);let a=this.listeners.get(r);if(a)for(let e of a)try{e(i)}catch{}}handleClose(e){this.isConnecting=!1,this.connectionTimeout!==void 0&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=void 0),this.connectionManager.setState(`disconnected`),this.connectionReject&&=(this.connectionReject(Error(`Connection closed: ${e.reason||`Unknown reason`}`)),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0),this.emitLifecycleEvent(`close`,e),this.emitLifecycleEvent(`disconnected`,e),this.onCloseCallback?.(e),e.code!==1e3&&e.code!==1001&&this.connectionManager.scheduleReconnect(()=>this.connect())&&this.emitLifecycleEvent(`reconnecting`)}handleError(e){this.isConnecting=!1,this.connectionTimeout!==void 0&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=void 0),this.emitLifecycleEvent(`error`,e),this.onErrorCallback?.(e),this.handleConnectionError(Error(`WebSocket error`))}handleConnectionError(e){this.isConnecting=!1,this.connectionTimeout!==void 0&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=void 0),this.connectionReject&&=(this.connectionReject(e),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0),this.emitLifecycleEvent(`error`,e),this.connectionManager.scheduleReconnect(()=>this.connect())&&this.emitLifecycleEvent(`reconnecting`)}emitLifecycleEvent(e,t){let n=this.listeners.get(e);if(n)for(let e of n)try{e(t)}catch{}}flushMessageQueue(){if(!(!this.ws||this.ws.readyState!==WebSocket.OPEN))for(;this.messageQueue.length>0;){let e=this.messageQueue.shift();try{this.ws.send(encodeClientMessage$1(e))}catch{}}}getState(){return this.connectionManager.getState()}isConnected(){return this.ws?.readyState===WebSocket.OPEN&&this.connectionManager.getState()===`connected`}getConnectionState(){return{state:this.connectionManager.getState(),isConnected:this.isConnected(),isConnecting:this.isConnecting,reconnectAttempts:this.connectionManager.getReconnectAttempts(),connectionId:this.connectionId}}waitForConnection(){return this.isConnected()?Promise.resolve():(this.connectionPromise||=new Promise((e,t)=>{this.connectionResolve=e,this.connectionReject=t;let n=setTimeout(()=>{this.connectionReject&&=(this.connectionReject(Error(`Connection wait timeout`)),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0)},this.options.connectionTimeout*2);this.connectionPromise&&this.connectionPromise.finally(()=>{clearTimeout(n)})}),this.connectionPromise)}on(e,t){this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t)}off(e,t){let n=this.listeners.get(e);n&&(n.delete(t),n.size===0&&this.listeners.delete(e))}once(e,t){let n=r=>{this.off(e,n),t(r)};this.on(e,n)}emit(e,n){let r={type:e,data:n};if(this.isConnected())try{this.ws.send(encodeClientMessage$1(r))}catch{this.queueMessage(r)}else this.queueMessage(r)}queueMessage(e){this.messageQueue.length>=this.options.maxQueueSize&&this.messageQueue.shift(),this.messageQueue.push(e)}onOpen(e){this.onOpenCallback=e}onClose(e){this.onCloseCallback=e}onError(e){this.onErrorCallback=e}onStateChange(e){this.onStateChangeCallback=e}reconnect(){this.connectionManager.resetReconnection(),this.connectionManager.cancelReconnect(),this.cleanupWebSocket(),this.isConnecting=!1,this.connectionManager.setState(`disconnected`),this.connect()}disconnect(){this.connectionManager.cancelReconnect(),this.isConnecting=!1,this.connectionReject&&=(this.connectionReject(Error(`Connection disconnected`)),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0),this.cleanupWebSocket(),this.connectionManager.setState(`disconnected`)}close(){this.connectionReject&&=(this.connectionReject(Error(`Client closed`)),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0),this.disconnect(),this.listeners.clear(),this.messageQueue=[],this.connectionManager.destroy()}};exports.ConnectionManager=ConnectionManager,exports.DEFAULT_RECONNECTION_CONFIG=DEFAULT_RECONNECTION_CONFIG,exports.PROTOCOL_VERSION=require_types.t,exports.VeraniClient=VeraniClient,exports.decodeClientMessage=require_types.a,exports.decodeFrame=require_types.o,exports.decodeServerMessage=require_types.s,exports.encodeClientMessage=require_types.n,exports.encodeFrame=require_types.r,exports.encodeServerMessage=require_types.i;
1
+ const require_types=require(`./types-083oWz55.cjs`);function encodeClientMessage$1(t){return require_types.n(t)}function decodeServerMessage$1(t){return require_types.s(t)}const DEFAULT_RECONNECTION_CONFIG={enabled:!0,maxAttempts:10,initialDelay:1e3,maxDelay:3e4,backoffMultiplier:1.5};var ConnectionManager=class{constructor(e=DEFAULT_RECONNECTION_CONFIG,t){this.config=e,this.onStateChange=t,this.state=`disconnected`,this.reconnectAttempts=0,this.currentDelay=e.initialDelay}getState(){return this.state}isValidStateTransition(e,t){return{disconnected:[`connecting`,`reconnecting`],connecting:[`connected`,`disconnected`,`error`,`reconnecting`],connected:[`disconnected`,`reconnecting`],reconnecting:[`connecting`,`disconnected`,`error`],error:[`reconnecting`,`disconnected`,`connecting`]}[e]?.includes(t)??!1}setState(e){this.state!==e&&(this.isValidStateTransition(this.state,e),this.state=e,this.onStateChange?.(e))}resetReconnection(){this.reconnectAttempts=0,this.currentDelay=this.config.initialDelay,this.clearReconnectTimer()}scheduleReconnect(e){return this.config.enabled?this.config.maxAttempts>0&&this.reconnectAttempts>=this.config.maxAttempts?(this.setState(`error`),!1):(this.clearReconnectTimer(),this.setState(`reconnecting`),this.reconnectAttempts++,this.reconnectTimer=setTimeout(()=>{e(),this.currentDelay=Math.min(this.currentDelay*this.config.backoffMultiplier,this.config.maxDelay)},this.currentDelay),!0):!1}cancelReconnect(){this.clearReconnectTimer(),this.state===`reconnecting`&&this.setState(`disconnected`)}clearReconnectTimer(){this.reconnectTimer!==void 0&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=void 0)}getReconnectAttempts(){return this.reconnectAttempts}getNextDelay(){return this.currentDelay}destroy(){this.clearReconnectTimer()}},VeraniClient=class{constructor(e,t={}){this.url=e,this.listeners=new Map,this.messageQueue=[],this.isConnecting=!1,this.connectionId=0,this.lastPongReceived=0,this.options={reconnection:{enabled:t.reconnection?.enabled??DEFAULT_RECONNECTION_CONFIG.enabled,maxAttempts:t.reconnection?.maxAttempts??DEFAULT_RECONNECTION_CONFIG.maxAttempts,initialDelay:t.reconnection?.initialDelay??DEFAULT_RECONNECTION_CONFIG.initialDelay,maxDelay:t.reconnection?.maxDelay??DEFAULT_RECONNECTION_CONFIG.maxDelay,backoffMultiplier:t.reconnection?.backoffMultiplier??DEFAULT_RECONNECTION_CONFIG.backoffMultiplier},maxQueueSize:t.maxQueueSize??100,connectionTimeout:t.connectionTimeout??1e4,pingInterval:t.pingInterval??3e4,pongTimeout:t.pongTimeout??1e4},this.connectionManager=new ConnectionManager(this.options.reconnection,e=>{this.onStateChangeCallback?.(e)}),this.connect()}startPingInterval(){this.options.pingInterval===0||this.pingInterval!==void 0||(this.lastPongReceived=Date.now(),this.pingInterval=setInterval(()=>{if(!this.ws||this.ws.readyState!==WebSocket.OPEN){this.stopPingInterval();return}if(Date.now()-this.lastPongReceived>this.options.pongTimeout+this.options.pingInterval){this.stopPingInterval(),this.ws.close(1006,`Pong timeout`);return}try{this.emit(`ping`,{timestamp:Date.now()})}catch{}},this.options.pingInterval))}stopPingInterval(){this.pingInterval!==void 0&&(clearInterval(this.pingInterval),this.pingInterval=void 0),this.pongTimeout!==void 0&&(clearTimeout(this.pongTimeout),this.pongTimeout=void 0)}cleanupWebSocket(){if(this.stopPingInterval(),this.connectionTimeout!==void 0&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=void 0),this.ws){let e=this.ws;if(this.ws=void 0,e.readyState===WebSocket.OPEN||e.readyState===WebSocket.CONNECTING)try{e.close(1e3,`Cleanup`)}catch{}}}connect(){if(!this.isConnecting&&!this.isConnected()){this.cleanupWebSocket();try{this.isConnecting=!0,this.connectionId++;let e=this.connectionId;this.connectionManager.setState(`connecting`),this.emitLifecycleEvent(`connecting`),this.ws=new WebSocket(this.url),this.connectionTimeout=setTimeout(()=>{this.isConnecting&&this.connectionId===e&&(this.ws?.close(),this.handleConnectionError(Error(`Connection timeout`)))},this.options.connectionTimeout),this.ws.addEventListener(`open`,()=>{this.connectionId===e&&this.handleOpen()}),this.ws.addEventListener(`message`,t=>{this.connectionId===e&&this.handleMessage(t)}),this.ws.addEventListener(`close`,t=>{this.connectionId===e&&this.handleClose(t)}),this.ws.addEventListener(`error`,t=>{this.connectionId===e&&this.handleError(t)})}catch(e){this.isConnecting=!1,this.handleConnectionError(e)}}}handleOpen(){this.isConnecting=!1,this.connectionTimeout!==void 0&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=void 0),this.connectionManager.setState(`connected`),this.connectionManager.resetReconnection(),this.startPingInterval(),this.flushMessageQueue(),this.connectionResolve&&(this.connectionResolve(),this.connectionPromise=void 0,this.connectionResolve=void 0,this.connectionReject=void 0),this.emitLifecycleEvent(`open`),this.emitLifecycleEvent(`connected`),this.onOpenCallback?.()}handleMessage(e){let t=decodeServerMessage$1(e.data);if(!t)return;if(t.type===`event`&&t.channel===`pong`){this.lastPongReceived=Date.now();return}let r=t.type,i=t.data;t.type===`event`&&t.data&&typeof t.data==`object`&&`type`in t.data&&(r=t.data.type,i=t.data);let a=this.listeners.get(r);if(a)for(let e of a)try{e(i)}catch{}}handleClose(e){this.isConnecting=!1,this.connectionTimeout!==void 0&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=void 0),this.connectionManager.setState(`disconnected`),this.connectionReject&&=(this.connectionReject(Error(`Connection closed: ${e.reason||`Unknown reason`}`)),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0),this.emitLifecycleEvent(`close`,e),this.emitLifecycleEvent(`disconnected`,e),this.onCloseCallback?.(e),e.code!==1e3&&e.code!==1001&&this.connectionManager.scheduleReconnect(()=>this.connect())&&this.emitLifecycleEvent(`reconnecting`)}handleError(e){this.isConnecting=!1,this.connectionTimeout!==void 0&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=void 0),this.emitLifecycleEvent(`error`,e),this.onErrorCallback?.(e),this.handleConnectionError(Error(`WebSocket error`))}handleConnectionError(e){this.isConnecting=!1,this.connectionTimeout!==void 0&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=void 0),this.connectionReject&&=(this.connectionReject(e),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0),this.emitLifecycleEvent(`error`,e),this.connectionManager.scheduleReconnect(()=>this.connect())&&this.emitLifecycleEvent(`reconnecting`)}emitLifecycleEvent(e,t){let n=this.listeners.get(e);if(n)for(let e of n)try{e(t)}catch{}}flushMessageQueue(){if(!(!this.ws||this.ws.readyState!==WebSocket.OPEN))for(;this.messageQueue.length>0;){let e=this.messageQueue.shift();try{this.ws.send(encodeClientMessage$1(e))}catch{}}}getState(){return this.connectionManager.getState()}isConnected(){return this.ws?.readyState===WebSocket.OPEN&&this.connectionManager.getState()===`connected`}getConnectionState(){return{state:this.connectionManager.getState(),isConnected:this.isConnected(),isConnecting:this.isConnecting,reconnectAttempts:this.connectionManager.getReconnectAttempts(),connectionId:this.connectionId}}waitForConnection(){return this.isConnected()?Promise.resolve():(this.connectionPromise||=new Promise((e,t)=>{this.connectionResolve=e,this.connectionReject=t;let n=setTimeout(()=>{this.connectionReject&&=(this.connectionReject(Error(`Connection wait timeout`)),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0)},this.options.connectionTimeout*2);this.connectionPromise&&this.connectionPromise.finally(()=>{clearTimeout(n)})}),this.connectionPromise)}on(e,t){this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(t)}off(e,t){let n=this.listeners.get(e);n&&(n.delete(t),n.size===0&&this.listeners.delete(e))}once(e,t){let n=r=>{this.off(e,n),t(r)};this.on(e,n)}emit(e,n){let r={type:e,data:n};if(this.isConnected())try{this.ws.send(encodeClientMessage$1(r))}catch{this.queueMessage(r)}else this.queueMessage(r)}queueMessage(e){this.messageQueue.length>=this.options.maxQueueSize&&this.messageQueue.shift(),this.messageQueue.push(e)}onOpen(e){this.onOpenCallback=e}onClose(e){this.onCloseCallback=e}onError(e){this.onErrorCallback=e}onStateChange(e){this.onStateChangeCallback=e}reconnect(){this.connectionManager.resetReconnection(),this.connectionManager.cancelReconnect(),this.cleanupWebSocket(),this.isConnecting=!1,this.connectionManager.setState(`disconnected`),this.connect()}disconnect(){this.connectionManager.cancelReconnect(),this.isConnecting=!1,this.connectionReject&&=(this.connectionReject(Error(`Connection disconnected`)),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0),this.cleanupWebSocket(),this.connectionManager.setState(`disconnected`)}close(){this.connectionReject&&=(this.connectionReject(Error(`Client closed`)),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0),this.disconnect(),this.listeners.clear(),this.messageQueue=[],this.connectionManager.destroy()}};exports.ConnectionManager=ConnectionManager,exports.DEFAULT_RECONNECTION_CONFIG=DEFAULT_RECONNECTION_CONFIG,exports.PROTOCOL_VERSION=require_types.t,exports.VeraniClient=VeraniClient,exports.decodeClientMessage=require_types.a,exports.decodeFrame=require_types.o,exports.decodeServerMessage=require_types.s,exports.encodeClientMessage=require_types.n,exports.encodeFrame=require_types.r,exports.encodeServerMessage=require_types.i;
package/dist/client.d.cts CHANGED
@@ -82,6 +82,10 @@ interface VeraniClientOptions {
82
82
  maxQueueSize?: number;
83
83
  /** Connection timeout in milliseconds */
84
84
  connectionTimeout?: number;
85
+ /** Ping interval in milliseconds (0 = disabled, default: 30000) */
86
+ pingInterval?: number;
87
+ /** Pong timeout in milliseconds (default: 10000) */
88
+ pongTimeout?: number;
85
89
  }
86
90
  /**
87
91
  * Verani WebSocket client with automatic reconnection and lifecycle management
@@ -103,12 +107,23 @@ declare class VeraniClient {
103
107
  private connectionTimeout?;
104
108
  private isConnecting;
105
109
  private connectionId;
110
+ private pingInterval?;
111
+ private pongTimeout?;
112
+ private lastPongReceived;
106
113
  /**
107
114
  * Creates a new Verani client
108
115
  * @param url - WebSocket URL to connect to
109
116
  * @param options - Client configuration options
110
117
  */
111
118
  constructor(url: string, options?: VeraniClientOptions);
119
+ /**
120
+ * Starts the ping interval to keep the connection alive
121
+ */
122
+ private startPingInterval;
123
+ /**
124
+ * Stops the ping interval
125
+ */
126
+ private stopPingInterval;
112
127
  /**
113
128
  * Cleans up existing WebSocket connection and resources
114
129
  */
package/dist/client.d.mts CHANGED
@@ -82,6 +82,10 @@ interface VeraniClientOptions {
82
82
  maxQueueSize?: number;
83
83
  /** Connection timeout in milliseconds */
84
84
  connectionTimeout?: number;
85
+ /** Ping interval in milliseconds (0 = disabled, default: 30000) */
86
+ pingInterval?: number;
87
+ /** Pong timeout in milliseconds (default: 10000) */
88
+ pongTimeout?: number;
85
89
  }
86
90
  /**
87
91
  * Verani WebSocket client with automatic reconnection and lifecycle management
@@ -103,12 +107,23 @@ declare class VeraniClient {
103
107
  private connectionTimeout?;
104
108
  private isConnecting;
105
109
  private connectionId;
110
+ private pingInterval?;
111
+ private pongTimeout?;
112
+ private lastPongReceived;
106
113
  /**
107
114
  * Creates a new Verani client
108
115
  * @param url - WebSocket URL to connect to
109
116
  * @param options - Client configuration options
110
117
  */
111
118
  constructor(url: string, options?: VeraniClientOptions);
119
+ /**
120
+ * Starts the ping interval to keep the connection alive
121
+ */
122
+ private startPingInterval;
123
+ /**
124
+ * Stops the ping interval
125
+ */
126
+ private stopPingInterval;
112
127
  /**
113
128
  * Cleans up existing WebSocket connection and resources
114
129
  */
package/dist/client.mjs CHANGED
@@ -1 +1 @@
1
- import{a as decodeClientMessage,i as encodeServerMessage,n as encodeClientMessage,o as decodeFrame,r as encodeFrame,s as decodeServerMessage,t as PROTOCOL_VERSION}from"./types-CJLnZrA8.mjs";function encodeClientMessage$1(e){return encodeClientMessage(e)}function decodeServerMessage$1(e){return decodeServerMessage(e)}const DEFAULT_RECONNECTION_CONFIG={enabled:!0,maxAttempts:10,initialDelay:1e3,maxDelay:3e4,backoffMultiplier:1.5};var ConnectionManager=class{constructor(e=DEFAULT_RECONNECTION_CONFIG,c){this.config=e,this.onStateChange=c,this.state=`disconnected`,this.reconnectAttempts=0,this.currentDelay=e.initialDelay}getState(){return this.state}isValidStateTransition(e,c){return{disconnected:[`connecting`,`reconnecting`],connecting:[`connected`,`disconnected`,`error`,`reconnecting`],connected:[`disconnected`,`reconnecting`],reconnecting:[`connecting`,`disconnected`,`error`],error:[`reconnecting`,`disconnected`,`connecting`]}[e]?.includes(c)??!1}setState(e){this.state!==e&&(this.isValidStateTransition(this.state,e),this.state=e,this.onStateChange?.(e))}resetReconnection(){this.reconnectAttempts=0,this.currentDelay=this.config.initialDelay,this.clearReconnectTimer()}scheduleReconnect(e){return this.config.enabled?this.config.maxAttempts>0&&this.reconnectAttempts>=this.config.maxAttempts?(this.setState(`error`),!1):(this.clearReconnectTimer(),this.setState(`reconnecting`),this.reconnectAttempts++,this.reconnectTimer=setTimeout(()=>{e(),this.currentDelay=Math.min(this.currentDelay*this.config.backoffMultiplier,this.config.maxDelay)},this.currentDelay),!0):!1}cancelReconnect(){this.clearReconnectTimer(),this.state===`reconnecting`&&this.setState(`disconnected`)}clearReconnectTimer(){this.reconnectTimer!==void 0&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=void 0)}getReconnectAttempts(){return this.reconnectAttempts}getNextDelay(){return this.currentDelay}destroy(){this.clearReconnectTimer()}},VeraniClient=class{constructor(e,c={}){this.url=e,this.listeners=new Map,this.messageQueue=[],this.isConnecting=!1,this.connectionId=0,this.options={reconnection:{enabled:c.reconnection?.enabled??DEFAULT_RECONNECTION_CONFIG.enabled,maxAttempts:c.reconnection?.maxAttempts??DEFAULT_RECONNECTION_CONFIG.maxAttempts,initialDelay:c.reconnection?.initialDelay??DEFAULT_RECONNECTION_CONFIG.initialDelay,maxDelay:c.reconnection?.maxDelay??DEFAULT_RECONNECTION_CONFIG.maxDelay,backoffMultiplier:c.reconnection?.backoffMultiplier??DEFAULT_RECONNECTION_CONFIG.backoffMultiplier},maxQueueSize:c.maxQueueSize??100,connectionTimeout:c.connectionTimeout??1e4},this.connectionManager=new ConnectionManager(this.options.reconnection,e=>{this.onStateChangeCallback?.(e)}),this.connect()}cleanupWebSocket(){if(this.connectionTimeout!==void 0&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=void 0),this.ws){let e=this.ws;if(this.ws=void 0,e.readyState===WebSocket.OPEN||e.readyState===WebSocket.CONNECTING)try{e.close(1e3,`Cleanup`)}catch{}}}connect(){if(!this.isConnecting&&!this.isConnected()){this.cleanupWebSocket();try{this.isConnecting=!0,this.connectionId++;let e=this.connectionId;this.connectionManager.setState(`connecting`),this.emitLifecycleEvent(`connecting`),this.ws=new WebSocket(this.url),this.connectionTimeout=setTimeout(()=>{this.isConnecting&&this.connectionId===e&&(this.ws?.close(),this.handleConnectionError(Error(`Connection timeout`)))},this.options.connectionTimeout),this.ws.addEventListener(`open`,()=>{this.connectionId===e&&this.handleOpen()}),this.ws.addEventListener(`message`,c=>{this.connectionId===e&&this.handleMessage(c)}),this.ws.addEventListener(`close`,c=>{this.connectionId===e&&this.handleClose(c)}),this.ws.addEventListener(`error`,c=>{this.connectionId===e&&this.handleError(c)})}catch(e){this.isConnecting=!1,this.handleConnectionError(e)}}}handleOpen(){this.isConnecting=!1,this.connectionTimeout!==void 0&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=void 0),this.connectionManager.setState(`connected`),this.connectionManager.resetReconnection(),this.flushMessageQueue(),this.connectionResolve&&(this.connectionResolve(),this.connectionPromise=void 0,this.connectionResolve=void 0,this.connectionReject=void 0),this.emitLifecycleEvent(`open`),this.emitLifecycleEvent(`connected`),this.onOpenCallback?.()}handleMessage(e){let c=decodeServerMessage$1(e.data);if(!c)return;let l=c.type,u=c.data;c.type===`event`&&c.data&&typeof c.data==`object`&&`type`in c.data&&(l=c.data.type,u=c.data);let d=this.listeners.get(l);if(d)for(let e of d)try{e(u)}catch{}}handleClose(e){this.isConnecting=!1,this.connectionTimeout!==void 0&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=void 0),this.connectionManager.setState(`disconnected`),this.connectionReject&&=(this.connectionReject(Error(`Connection closed: ${e.reason||`Unknown reason`}`)),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0),this.emitLifecycleEvent(`close`,e),this.emitLifecycleEvent(`disconnected`,e),this.onCloseCallback?.(e),e.code!==1e3&&e.code!==1001&&this.connectionManager.scheduleReconnect(()=>this.connect())&&this.emitLifecycleEvent(`reconnecting`)}handleError(e){this.isConnecting=!1,this.connectionTimeout!==void 0&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=void 0),this.emitLifecycleEvent(`error`,e),this.onErrorCallback?.(e),this.handleConnectionError(Error(`WebSocket error`))}handleConnectionError(e){this.isConnecting=!1,this.connectionTimeout!==void 0&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=void 0),this.connectionReject&&=(this.connectionReject(e),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0),this.emitLifecycleEvent(`error`,e),this.connectionManager.scheduleReconnect(()=>this.connect())&&this.emitLifecycleEvent(`reconnecting`)}emitLifecycleEvent(e,c){let l=this.listeners.get(e);if(l)for(let e of l)try{e(c)}catch{}}flushMessageQueue(){if(!(!this.ws||this.ws.readyState!==WebSocket.OPEN))for(;this.messageQueue.length>0;){let e=this.messageQueue.shift();try{this.ws.send(encodeClientMessage$1(e))}catch{}}}getState(){return this.connectionManager.getState()}isConnected(){return this.ws?.readyState===WebSocket.OPEN&&this.connectionManager.getState()===`connected`}getConnectionState(){return{state:this.connectionManager.getState(),isConnected:this.isConnected(),isConnecting:this.isConnecting,reconnectAttempts:this.connectionManager.getReconnectAttempts(),connectionId:this.connectionId}}waitForConnection(){return this.isConnected()?Promise.resolve():(this.connectionPromise||=new Promise((e,c)=>{this.connectionResolve=e,this.connectionReject=c;let l=setTimeout(()=>{this.connectionReject&&=(this.connectionReject(Error(`Connection wait timeout`)),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0)},this.options.connectionTimeout*2);this.connectionPromise&&this.connectionPromise.finally(()=>{clearTimeout(l)})}),this.connectionPromise)}on(e,c){this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(c)}off(e,c){let l=this.listeners.get(e);l&&(l.delete(c),l.size===0&&this.listeners.delete(e))}once(e,c){let l=u=>{this.off(e,l),c(u)};this.on(e,l)}emit(e,c){let l={type:e,data:c};if(this.isConnected())try{this.ws.send(encodeClientMessage$1(l))}catch{this.queueMessage(l)}else this.queueMessage(l)}queueMessage(e){this.messageQueue.length>=this.options.maxQueueSize&&this.messageQueue.shift(),this.messageQueue.push(e)}onOpen(e){this.onOpenCallback=e}onClose(e){this.onCloseCallback=e}onError(e){this.onErrorCallback=e}onStateChange(e){this.onStateChangeCallback=e}reconnect(){this.connectionManager.resetReconnection(),this.connectionManager.cancelReconnect(),this.cleanupWebSocket(),this.isConnecting=!1,this.connectionManager.setState(`disconnected`),this.connect()}disconnect(){this.connectionManager.cancelReconnect(),this.isConnecting=!1,this.connectionReject&&=(this.connectionReject(Error(`Connection disconnected`)),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0),this.cleanupWebSocket(),this.connectionManager.setState(`disconnected`)}close(){this.connectionReject&&=(this.connectionReject(Error(`Client closed`)),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0),this.disconnect(),this.listeners.clear(),this.messageQueue=[],this.connectionManager.destroy()}};export{ConnectionManager,DEFAULT_RECONNECTION_CONFIG,PROTOCOL_VERSION,VeraniClient,decodeClientMessage,decodeFrame,decodeServerMessage,encodeClientMessage,encodeFrame,encodeServerMessage};
1
+ import{a as decodeClientMessage,i as encodeServerMessage,n as encodeClientMessage,o as decodeFrame,r as encodeFrame,s as decodeServerMessage,t as PROTOCOL_VERSION}from"./types-CJLnZrA8.mjs";function encodeClientMessage$1(e){return encodeClientMessage(e)}function decodeServerMessage$1(e){return decodeServerMessage(e)}const DEFAULT_RECONNECTION_CONFIG={enabled:!0,maxAttempts:10,initialDelay:1e3,maxDelay:3e4,backoffMultiplier:1.5};var ConnectionManager=class{constructor(e=DEFAULT_RECONNECTION_CONFIG,c){this.config=e,this.onStateChange=c,this.state=`disconnected`,this.reconnectAttempts=0,this.currentDelay=e.initialDelay}getState(){return this.state}isValidStateTransition(e,c){return{disconnected:[`connecting`,`reconnecting`],connecting:[`connected`,`disconnected`,`error`,`reconnecting`],connected:[`disconnected`,`reconnecting`],reconnecting:[`connecting`,`disconnected`,`error`],error:[`reconnecting`,`disconnected`,`connecting`]}[e]?.includes(c)??!1}setState(e){this.state!==e&&(this.isValidStateTransition(this.state,e),this.state=e,this.onStateChange?.(e))}resetReconnection(){this.reconnectAttempts=0,this.currentDelay=this.config.initialDelay,this.clearReconnectTimer()}scheduleReconnect(e){return this.config.enabled?this.config.maxAttempts>0&&this.reconnectAttempts>=this.config.maxAttempts?(this.setState(`error`),!1):(this.clearReconnectTimer(),this.setState(`reconnecting`),this.reconnectAttempts++,this.reconnectTimer=setTimeout(()=>{e(),this.currentDelay=Math.min(this.currentDelay*this.config.backoffMultiplier,this.config.maxDelay)},this.currentDelay),!0):!1}cancelReconnect(){this.clearReconnectTimer(),this.state===`reconnecting`&&this.setState(`disconnected`)}clearReconnectTimer(){this.reconnectTimer!==void 0&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=void 0)}getReconnectAttempts(){return this.reconnectAttempts}getNextDelay(){return this.currentDelay}destroy(){this.clearReconnectTimer()}},VeraniClient=class{constructor(e,c={}){this.url=e,this.listeners=new Map,this.messageQueue=[],this.isConnecting=!1,this.connectionId=0,this.lastPongReceived=0,this.options={reconnection:{enabled:c.reconnection?.enabled??DEFAULT_RECONNECTION_CONFIG.enabled,maxAttempts:c.reconnection?.maxAttempts??DEFAULT_RECONNECTION_CONFIG.maxAttempts,initialDelay:c.reconnection?.initialDelay??DEFAULT_RECONNECTION_CONFIG.initialDelay,maxDelay:c.reconnection?.maxDelay??DEFAULT_RECONNECTION_CONFIG.maxDelay,backoffMultiplier:c.reconnection?.backoffMultiplier??DEFAULT_RECONNECTION_CONFIG.backoffMultiplier},maxQueueSize:c.maxQueueSize??100,connectionTimeout:c.connectionTimeout??1e4,pingInterval:c.pingInterval??3e4,pongTimeout:c.pongTimeout??1e4},this.connectionManager=new ConnectionManager(this.options.reconnection,e=>{this.onStateChangeCallback?.(e)}),this.connect()}startPingInterval(){this.options.pingInterval===0||this.pingInterval!==void 0||(this.lastPongReceived=Date.now(),this.pingInterval=setInterval(()=>{if(!this.ws||this.ws.readyState!==WebSocket.OPEN){this.stopPingInterval();return}if(Date.now()-this.lastPongReceived>this.options.pongTimeout+this.options.pingInterval){this.stopPingInterval(),this.ws.close(1006,`Pong timeout`);return}try{this.emit(`ping`,{timestamp:Date.now()})}catch{}},this.options.pingInterval))}stopPingInterval(){this.pingInterval!==void 0&&(clearInterval(this.pingInterval),this.pingInterval=void 0),this.pongTimeout!==void 0&&(clearTimeout(this.pongTimeout),this.pongTimeout=void 0)}cleanupWebSocket(){if(this.stopPingInterval(),this.connectionTimeout!==void 0&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=void 0),this.ws){let e=this.ws;if(this.ws=void 0,e.readyState===WebSocket.OPEN||e.readyState===WebSocket.CONNECTING)try{e.close(1e3,`Cleanup`)}catch{}}}connect(){if(!this.isConnecting&&!this.isConnected()){this.cleanupWebSocket();try{this.isConnecting=!0,this.connectionId++;let e=this.connectionId;this.connectionManager.setState(`connecting`),this.emitLifecycleEvent(`connecting`),this.ws=new WebSocket(this.url),this.connectionTimeout=setTimeout(()=>{this.isConnecting&&this.connectionId===e&&(this.ws?.close(),this.handleConnectionError(Error(`Connection timeout`)))},this.options.connectionTimeout),this.ws.addEventListener(`open`,()=>{this.connectionId===e&&this.handleOpen()}),this.ws.addEventListener(`message`,c=>{this.connectionId===e&&this.handleMessage(c)}),this.ws.addEventListener(`close`,c=>{this.connectionId===e&&this.handleClose(c)}),this.ws.addEventListener(`error`,c=>{this.connectionId===e&&this.handleError(c)})}catch(e){this.isConnecting=!1,this.handleConnectionError(e)}}}handleOpen(){this.isConnecting=!1,this.connectionTimeout!==void 0&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=void 0),this.connectionManager.setState(`connected`),this.connectionManager.resetReconnection(),this.startPingInterval(),this.flushMessageQueue(),this.connectionResolve&&(this.connectionResolve(),this.connectionPromise=void 0,this.connectionResolve=void 0,this.connectionReject=void 0),this.emitLifecycleEvent(`open`),this.emitLifecycleEvent(`connected`),this.onOpenCallback?.()}handleMessage(e){let c=decodeServerMessage$1(e.data);if(!c)return;if(c.type===`event`&&c.channel===`pong`){this.lastPongReceived=Date.now();return}let l=c.type,u=c.data;c.type===`event`&&c.data&&typeof c.data==`object`&&`type`in c.data&&(l=c.data.type,u=c.data);let d=this.listeners.get(l);if(d)for(let e of d)try{e(u)}catch{}}handleClose(e){this.isConnecting=!1,this.connectionTimeout!==void 0&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=void 0),this.connectionManager.setState(`disconnected`),this.connectionReject&&=(this.connectionReject(Error(`Connection closed: ${e.reason||`Unknown reason`}`)),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0),this.emitLifecycleEvent(`close`,e),this.emitLifecycleEvent(`disconnected`,e),this.onCloseCallback?.(e),e.code!==1e3&&e.code!==1001&&this.connectionManager.scheduleReconnect(()=>this.connect())&&this.emitLifecycleEvent(`reconnecting`)}handleError(e){this.isConnecting=!1,this.connectionTimeout!==void 0&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=void 0),this.emitLifecycleEvent(`error`,e),this.onErrorCallback?.(e),this.handleConnectionError(Error(`WebSocket error`))}handleConnectionError(e){this.isConnecting=!1,this.connectionTimeout!==void 0&&(clearTimeout(this.connectionTimeout),this.connectionTimeout=void 0),this.connectionReject&&=(this.connectionReject(e),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0),this.emitLifecycleEvent(`error`,e),this.connectionManager.scheduleReconnect(()=>this.connect())&&this.emitLifecycleEvent(`reconnecting`)}emitLifecycleEvent(e,c){let l=this.listeners.get(e);if(l)for(let e of l)try{e(c)}catch{}}flushMessageQueue(){if(!(!this.ws||this.ws.readyState!==WebSocket.OPEN))for(;this.messageQueue.length>0;){let e=this.messageQueue.shift();try{this.ws.send(encodeClientMessage$1(e))}catch{}}}getState(){return this.connectionManager.getState()}isConnected(){return this.ws?.readyState===WebSocket.OPEN&&this.connectionManager.getState()===`connected`}getConnectionState(){return{state:this.connectionManager.getState(),isConnected:this.isConnected(),isConnecting:this.isConnecting,reconnectAttempts:this.connectionManager.getReconnectAttempts(),connectionId:this.connectionId}}waitForConnection(){return this.isConnected()?Promise.resolve():(this.connectionPromise||=new Promise((e,c)=>{this.connectionResolve=e,this.connectionReject=c;let l=setTimeout(()=>{this.connectionReject&&=(this.connectionReject(Error(`Connection wait timeout`)),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0)},this.options.connectionTimeout*2);this.connectionPromise&&this.connectionPromise.finally(()=>{clearTimeout(l)})}),this.connectionPromise)}on(e,c){this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(c)}off(e,c){let l=this.listeners.get(e);l&&(l.delete(c),l.size===0&&this.listeners.delete(e))}once(e,c){let l=u=>{this.off(e,l),c(u)};this.on(e,l)}emit(e,c){let l={type:e,data:c};if(this.isConnected())try{this.ws.send(encodeClientMessage$1(l))}catch{this.queueMessage(l)}else this.queueMessage(l)}queueMessage(e){this.messageQueue.length>=this.options.maxQueueSize&&this.messageQueue.shift(),this.messageQueue.push(e)}onOpen(e){this.onOpenCallback=e}onClose(e){this.onCloseCallback=e}onError(e){this.onErrorCallback=e}onStateChange(e){this.onStateChangeCallback=e}reconnect(){this.connectionManager.resetReconnection(),this.connectionManager.cancelReconnect(),this.cleanupWebSocket(),this.isConnecting=!1,this.connectionManager.setState(`disconnected`),this.connect()}disconnect(){this.connectionManager.cancelReconnect(),this.isConnecting=!1,this.connectionReject&&=(this.connectionReject(Error(`Connection disconnected`)),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0),this.cleanupWebSocket(),this.connectionManager.setState(`disconnected`)}close(){this.connectionReject&&=(this.connectionReject(Error(`Client closed`)),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0),this.disconnect(),this.listeners.clear(),this.messageQueue=[],this.connectionManager.destroy()}};export{ConnectionManager,DEFAULT_RECONNECTION_CONFIG,PROTOCOL_VERSION,VeraniClient,decodeClientMessage,decodeFrame,decodeServerMessage,encodeClientMessage,encodeFrame,encodeServerMessage};
package/dist/verani.cjs CHANGED
@@ -1 +1 @@
1
- const require_types=require(`./types-083oWz55.cjs`);let __cloudflare_actors=require(`@cloudflare/actors`);function defaultExtractMeta(e){let a=crypto.randomUUID(),o=crypto.randomUUID(),s=new URL(e.url).searchParams.get(`channels`);return{userId:a,clientId:o,channels:s?s.split(`,`).map(e=>e.trim()).filter(Boolean):[`default`]}}function defineRoom(e){return{name:e.name,websocketPath:e.websocketPath,extractMeta:e.extractMeta||defaultExtractMeta,onConnect:e.onConnect,onDisconnect:e.onDisconnect,onMessage:e.onMessage,onError:e.onError}}function storeAttachment(e,a){e.serializeAttachment(a)}function restoreSessions(e){let a=0;for(let o of e.ctx.getWebSockets()){let s=o.deserializeAttachment();s&&(e.sessions.set(o,{ws:o,meta:s}),a++)}}function decodeFrame$1(a){return require_types.o(a)??{type:`invalid`}}function encodeFrame$1(a){return require_types.r(a)}function sanitizeToClassName(e){return e.replace(/^\/+/,``).split(/[-_\/\s]+/).map(e=>e.replace(/[^a-zA-Z0-9]/g,``)).filter(e=>e.length>0).map(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()).join(``)||`VeraniActor`}function createActorHandler(e){let o=sanitizeToClassName(e.name||e.websocketPath||`VeraniActor`);class s extends __cloudflare_actors.Actor{constructor(...e){super(...e),this.sessions=new Map}static configuration(a){return{locationHint:`me`,sockets:{upgradePath:e.websocketPath,autoResponse:{ping:`ping`,pong:`pong`}}}}async shouldUpgradeWebSocket(e){return!0}async fetch(a){let o=new URL(a.url),s=a.headers.get(`Upgrade`);return o.pathname===e.websocketPath&&s===`websocket`&&await this.shouldUpgradeWebSocket(a)?this.onWebSocketUpgrade(a):this.onRequest(a)}async onInit(){try{restoreSessions(this),e.onHibernationRestore&&this.sessions.size>0&&await e.onHibernationRestore(this)}catch{}}onWebSocketConnect(a,o){try{let s;if(s=e.extractMeta?e.extractMeta(o):{userId:`anonymous`,clientId:crypto.randomUUID(),channels:[`default`]},storeAttachment(a,s),this.sessions.set(a,{ws:a,meta:s}),e.onConnect){let o={actor:this,ws:a,meta:s};e.onConnect(o)}}catch(o){if(e.onError)try{e.onError(o,{actor:this,ws:a,meta:{userId:`unknown`,clientId:`unknown`,channels:[]}})}catch{}a.close(1011,`Internal server error`)}}onWebSocketMessage(a,o){let s;try{let c=decodeFrame$1(o);if(s=this.sessions.get(a),!s)return;if(e.onMessage){let o={actor:this,ws:a,meta:s.meta,frame:c};e.onMessage(o,c)}}catch(o){if(e.onError&&s)try{e.onError(o,{actor:this,ws:a,meta:s.meta})}catch{}}}onWebSocketDisconnect(a){try{let o=this.sessions.get(a);if(this.sessions.delete(a),o&&e.onDisconnect){let s={actor:this,ws:a,meta:o.meta};e.onDisconnect(s)}}catch{}}broadcast(e,a,o){let s=0,c=encodeFrame$1({type:`event`,channel:e,data:a});for(let{ws:a,meta:l}of this.sessions.values())if(l.channels.includes(e)&&!(o?.except&&a===o.except)&&!(o?.userIds&&!o.userIds.includes(l.userId))&&!(o?.clientIds&&!o.clientIds.includes(l.clientId)))try{a.send(c),s++}catch{}return s}getSessionCount(){return this.sessions.size}getConnectedUserIds(){let e=new Set;for(let{meta:a}of this.sessions.values())e.add(a.userId);return Array.from(e)}getUserSessions(e){let a=[];for(let{ws:o,meta:s}of this.sessions.values())s.userId===e&&a.push(o);return a}sendToUser(e,a,o){let s=0,c=encodeFrame$1({type:`event`,channel:a,data:o});for(let{ws:o,meta:l}of this.sessions.values())if(l.userId===e&&l.channels.includes(a))try{o.send(c),s++}catch{}return s}getStorage(){return this.ctx.storage}}return Object.defineProperty(s,`name`,{value:o,writable:!1,configurable:!0}),s}exports.PROTOCOL_VERSION=require_types.t,exports.createActorHandler=createActorHandler,exports.decodeClientMessage=require_types.a,exports.decodeFrame=require_types.o,exports.decodeServerMessage=require_types.s,exports.defineRoom=defineRoom,exports.encodeClientMessage=require_types.n,exports.encodeFrame=require_types.r,exports.encodeServerMessage=require_types.i,exports.restoreSessions=restoreSessions,exports.storeAttachment=storeAttachment;
1
+ const require_types=require(`./types-083oWz55.cjs`);let __cloudflare_actors=require(`@cloudflare/actors`);function defaultExtractMeta(e){let a=crypto.randomUUID(),o=crypto.randomUUID(),s=new URL(e.url).searchParams.get(`channels`);return{userId:a,clientId:o,channels:s?s.split(`,`).map(e=>e.trim()).filter(Boolean):[`default`]}}function defineRoom(e){return{name:e.name,websocketPath:e.websocketPath,extractMeta:e.extractMeta||defaultExtractMeta,onConnect:e.onConnect,onDisconnect:e.onDisconnect,onMessage:e.onMessage,onError:e.onError}}function isValidConnectionMeta(e){return!(!e||typeof e!=`object`||typeof e.userId!=`string`||!e.userId||typeof e.clientId!=`string`||!e.clientId||!Array.isArray(e.channels)||!e.channels.every(e=>typeof e==`string`))}function storeAttachment(e,a){e.serializeAttachment(a)}function restoreSessions(e){let a=0,o=0;for(let s of e.ctx.getWebSockets()){if(s.readyState!==WebSocket.OPEN){o++;continue}let l=s.deserializeAttachment();if(!l){o++;continue}if(!isValidConnectionMeta(l)){o++;continue}e.sessions.set(s,{ws:s,meta:l}),a++}}function decodeFrame$1(a){return require_types.o(a)??{type:`invalid`}}function encodeFrame$1(a){return require_types.r(a)}function sanitizeToClassName(e){return e.replace(/^\/+/,``).split(/[-_\/\s]+/).map(e=>e.replace(/[^a-zA-Z0-9]/g,``)).filter(e=>e.length>0).map(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()).join(``)||`VeraniActor`}function createActorHandler(e){let o=sanitizeToClassName(e.name||e.websocketPath||`VeraniActor`);class s extends __cloudflare_actors.Actor{constructor(...e){super(...e),this.sessions=new Map}static configuration(a){return{locationHint:`me`,sockets:{upgradePath:e.websocketPath,autoResponse:{ping:`ping`,pong:`pong`}}}}async shouldUpgradeWebSocket(e){return!0}async fetch(a){let o=new URL(a.url),s=a.headers.get(`Upgrade`);return o.pathname===e.websocketPath&&s===`websocket`&&await this.shouldUpgradeWebSocket(a)?this.onWebSocketUpgrade(a):this.onRequest(a)}async onInit(){try{restoreSessions(this)}catch{}if(e.onHibernationRestore&&this.sessions.size>0)try{await e.onHibernationRestore(this)}catch{}else e.onHibernationRestore&&this.sessions.size}async onWebSocketConnect(a,o){let s;try{if(s=e.extractMeta?await e.extractMeta(o):{userId:`anonymous`,clientId:crypto.randomUUID(),channels:[`default`]},storeAttachment(a,s),e.onConnect){let o={actor:this,ws:a,meta:s};await e.onConnect(o)}this.sessions.set(a,{ws:a,meta:s})}catch(o){if(e.onError&&s)try{await e.onError(o,{actor:this,ws:a,meta:s})}catch{}a.close(1011,`Internal server error`)}}async onWebSocketMessage(a,o){let s;try{let c=decodeFrame$1(o);if(s=this.sessions.get(a),!s)return;if(e.onMessage){let o={actor:this,ws:a,meta:s.meta,frame:c};await e.onMessage(o,c)}}catch(o){if(e.onError&&s)try{await e.onError(o,{actor:this,ws:a,meta:s.meta})}catch{}}}async onWebSocketDisconnect(a){try{let o=this.sessions.get(a);if(this.sessions.delete(a),o&&e.onDisconnect){let s={actor:this,ws:a,meta:o.meta};await e.onDisconnect(s)}}catch{}}cleanupStaleSessions(){let e=0,a=[];for(let[e,o]of this.sessions.entries())e.readyState!==WebSocket.OPEN&&a.push(e);for(let o of a)this.sessions.delete(o),e++;return e}broadcast(e,a,o){let s=0,c=encodeFrame$1({type:`event`,channel:e,data:a}),l=[];for(let{ws:a,meta:u}of this.sessions.values())if(u.channels.includes(e)&&!(o?.except&&a===o.except)&&!(o?.userIds&&!o.userIds.includes(u.userId))&&!(o?.clientIds&&!o.clientIds.includes(u.clientId))){if(a.readyState!==WebSocket.OPEN){l.push(a);continue}try{a.send(c),s++}catch{l.push(a)}}for(let e of l)this.sessions.delete(e);return l.length,s}getSessionCount(){return this.sessions.size}getConnectedUserIds(){let e=new Set;for(let{meta:a}of this.sessions.values())e.add(a.userId);return Array.from(e)}getUserSessions(e){let a=[];for(let{ws:o,meta:s}of this.sessions.values())s.userId===e&&a.push(o);return a}sendToUser(e,a,o){let s=0,c=encodeFrame$1({type:`event`,channel:a,data:o}),l=[];for(let{ws:o,meta:u}of this.sessions.values())if(u.userId===e&&u.channels.includes(a)){if(o.readyState!==WebSocket.OPEN){l.push(o);continue}try{o.send(c),s++}catch{l.push(o)}}for(let e of l)this.sessions.delete(e);return l.length,s}getStorage(){return this.ctx.storage}}return Object.defineProperty(s,`name`,{value:o,writable:!1,configurable:!0}),s}exports.PROTOCOL_VERSION=require_types.t,exports.createActorHandler=createActorHandler,exports.decodeClientMessage=require_types.a,exports.decodeFrame=require_types.o,exports.decodeServerMessage=require_types.s,exports.defineRoom=defineRoom,exports.encodeClientMessage=require_types.n,exports.encodeFrame=require_types.r,exports.encodeServerMessage=require_types.i,exports.restoreSessions=restoreSessions,exports.storeAttachment=storeAttachment;
package/dist/verani.d.cts CHANGED
@@ -57,6 +57,13 @@ interface VeraniActor<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown
57
57
  * @see @src/actor/actor-runtime.ts sendToUser()
58
58
  */
59
59
  sendToUser(userId: string, channel: string, data?: any): number;
60
+ /**
61
+ * Validates and removes stale WebSocket sessions.
62
+ * Called automatically during broadcast/send operations, but can be called manually.
63
+ * Returns the number of stale sessions removed.
64
+ * @see @src/actor/actor-runtime.ts cleanupStaleSessions()
65
+ */
66
+ cleanupStaleSessions(): number;
60
67
  /**
61
68
  * Access the Durable Object storage API for this actor instance.
62
69
  * @see @src/actor/actor-runtime.ts getStorage()
@@ -83,23 +90,50 @@ interface MessageContext<TMeta extends ConnectionMeta = ConnectionMeta, E = unkn
83
90
  }
84
91
  /**
85
92
  * Room definition with lifecycle hooks
93
+ *
94
+ * **Important:** All lifecycle hooks are properly awaited if they return a Promise.
95
+ * This ensures async operations complete before the actor proceeds to the next step
96
+ * or potentially enters hibernation.
86
97
  */
87
98
  interface RoomDefinition<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> {
88
99
  /** Optional room name for debugging */
89
100
  name?: string;
90
101
  /** WebSocket upgrade path (default: "/ws") */
91
102
  websocketPath: string;
92
- /** Extract metadata from the connection request */
103
+ /**
104
+ * Extract metadata from the connection request.
105
+ * This function is awaited if it returns a Promise.
106
+ */
93
107
  extractMeta?(req: Request): TMeta | Promise<TMeta>;
94
- /** Called when a new WebSocket connection is established */
108
+ /**
109
+ * Called when a new WebSocket connection is established.
110
+ * This hook is awaited if it returns a Promise. The session is only added to the
111
+ * sessions map after this hook completes successfully. If this hook throws, the
112
+ * connection is closed and no orphaned session is created.
113
+ */
95
114
  onConnect?(ctx: RoomContext<TMeta, E>): void | Promise<void>;
96
- /** Called when a WebSocket connection is closed */
115
+ /**
116
+ * Called when a WebSocket connection is closed.
117
+ * This hook is awaited if it returns a Promise. The session is removed from the
118
+ * sessions map before this hook is called.
119
+ */
97
120
  onDisconnect?(ctx: RoomContext<TMeta, E>): void | Promise<void>;
98
- /** Called when a message is received from a connection */
121
+ /**
122
+ * Called when a message is received from a connection.
123
+ * This hook is awaited if it returns a Promise. The actor will not process
124
+ * other messages from this connection until this hook completes.
125
+ */
99
126
  onMessage?(ctx: MessageContext<TMeta, E>, frame: MessageFrame): void | Promise<void>;
100
- /** Called when an error occurs in a lifecycle hook */
127
+ /**
128
+ * Called when an error occurs in a lifecycle hook.
129
+ * This hook is also awaited if it returns a Promise.
130
+ */
101
131
  onError?(error: Error, ctx: RoomContext<TMeta, E>): void | Promise<void>;
102
- /** Called after actor wakes from hibernation and sessions are restored */
132
+ /**
133
+ * Called after actor wakes from hibernation and sessions are restored.
134
+ * This hook is awaited if it returns a Promise. It is called even if some
135
+ * sessions failed to restore, allowing you to handle partial restoration scenarios.
136
+ */
103
137
  onHibernationRestore?(actor: VeraniActor<TMeta, E>): void | Promise<void>;
104
138
  }
105
139
  //#endregion
package/dist/verani.d.mts CHANGED
@@ -57,6 +57,13 @@ interface VeraniActor<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown
57
57
  * @see @src/actor/actor-runtime.ts sendToUser()
58
58
  */
59
59
  sendToUser(userId: string, channel: string, data?: any): number;
60
+ /**
61
+ * Validates and removes stale WebSocket sessions.
62
+ * Called automatically during broadcast/send operations, but can be called manually.
63
+ * Returns the number of stale sessions removed.
64
+ * @see @src/actor/actor-runtime.ts cleanupStaleSessions()
65
+ */
66
+ cleanupStaleSessions(): number;
60
67
  /**
61
68
  * Access the Durable Object storage API for this actor instance.
62
69
  * @see @src/actor/actor-runtime.ts getStorage()
@@ -83,23 +90,50 @@ interface MessageContext<TMeta extends ConnectionMeta = ConnectionMeta, E = unkn
83
90
  }
84
91
  /**
85
92
  * Room definition with lifecycle hooks
93
+ *
94
+ * **Important:** All lifecycle hooks are properly awaited if they return a Promise.
95
+ * This ensures async operations complete before the actor proceeds to the next step
96
+ * or potentially enters hibernation.
86
97
  */
87
98
  interface RoomDefinition<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> {
88
99
  /** Optional room name for debugging */
89
100
  name?: string;
90
101
  /** WebSocket upgrade path (default: "/ws") */
91
102
  websocketPath: string;
92
- /** Extract metadata from the connection request */
103
+ /**
104
+ * Extract metadata from the connection request.
105
+ * This function is awaited if it returns a Promise.
106
+ */
93
107
  extractMeta?(req: Request): TMeta | Promise<TMeta>;
94
- /** Called when a new WebSocket connection is established */
108
+ /**
109
+ * Called when a new WebSocket connection is established.
110
+ * This hook is awaited if it returns a Promise. The session is only added to the
111
+ * sessions map after this hook completes successfully. If this hook throws, the
112
+ * connection is closed and no orphaned session is created.
113
+ */
95
114
  onConnect?(ctx: RoomContext<TMeta, E>): void | Promise<void>;
96
- /** Called when a WebSocket connection is closed */
115
+ /**
116
+ * Called when a WebSocket connection is closed.
117
+ * This hook is awaited if it returns a Promise. The session is removed from the
118
+ * sessions map before this hook is called.
119
+ */
97
120
  onDisconnect?(ctx: RoomContext<TMeta, E>): void | Promise<void>;
98
- /** Called when a message is received from a connection */
121
+ /**
122
+ * Called when a message is received from a connection.
123
+ * This hook is awaited if it returns a Promise. The actor will not process
124
+ * other messages from this connection until this hook completes.
125
+ */
99
126
  onMessage?(ctx: MessageContext<TMeta, E>, frame: MessageFrame): void | Promise<void>;
100
- /** Called when an error occurs in a lifecycle hook */
127
+ /**
128
+ * Called when an error occurs in a lifecycle hook.
129
+ * This hook is also awaited if it returns a Promise.
130
+ */
101
131
  onError?(error: Error, ctx: RoomContext<TMeta, E>): void | Promise<void>;
102
- /** Called after actor wakes from hibernation and sessions are restored */
132
+ /**
133
+ * Called after actor wakes from hibernation and sessions are restored.
134
+ * This hook is awaited if it returns a Promise. It is called even if some
135
+ * sessions failed to restore, allowing you to handle partial restoration scenarios.
136
+ */
103
137
  onHibernationRestore?(actor: VeraniActor<TMeta, E>): void | Promise<void>;
104
138
  }
105
139
  //#endregion
package/dist/verani.mjs CHANGED
@@ -1 +1 @@
1
- import{a as decodeClientMessage,i as encodeServerMessage,n as encodeClientMessage,o as decodeFrame,r as encodeFrame,s as decodeServerMessage,t as PROTOCOL_VERSION}from"./types-CJLnZrA8.mjs";import{Actor}from"@cloudflare/actors";function defaultExtractMeta(e){let d=crypto.randomUUID(),f=crypto.randomUUID(),p=new URL(e.url).searchParams.get(`channels`);return{userId:d,clientId:f,channels:p?p.split(`,`).map(e=>e.trim()).filter(Boolean):[`default`]}}function defineRoom(e){return{name:e.name,websocketPath:e.websocketPath,extractMeta:e.extractMeta||defaultExtractMeta,onConnect:e.onConnect,onDisconnect:e.onDisconnect,onMessage:e.onMessage,onError:e.onError}}function storeAttachment(e,d){e.serializeAttachment(d)}function restoreSessions(e){let d=0;for(let f of e.ctx.getWebSockets()){let p=f.deserializeAttachment();p&&(e.sessions.set(f,{ws:f,meta:p}),d++)}}function decodeFrame$1(e){return decodeFrame(e)??{type:`invalid`}}function encodeFrame$1(e){return encodeFrame(e)}function sanitizeToClassName(e){return e.replace(/^\/+/,``).split(/[-_\/\s]+/).map(e=>e.replace(/[^a-zA-Z0-9]/g,``)).filter(e=>e.length>0).map(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()).join(``)||`VeraniActor`}function createActorHandler(e){let d=sanitizeToClassName(e.name||e.websocketPath||`VeraniActor`);class f extends Actor{constructor(...e){super(...e),this.sessions=new Map}static configuration(d){return{locationHint:`me`,sockets:{upgradePath:e.websocketPath,autoResponse:{ping:`ping`,pong:`pong`}}}}async shouldUpgradeWebSocket(e){return!0}async fetch(d){let f=new URL(d.url),p=d.headers.get(`Upgrade`);return f.pathname===e.websocketPath&&p===`websocket`&&await this.shouldUpgradeWebSocket(d)?this.onWebSocketUpgrade(d):this.onRequest(d)}async onInit(){try{restoreSessions(this),e.onHibernationRestore&&this.sessions.size>0&&await e.onHibernationRestore(this)}catch{}}onWebSocketConnect(d,f){try{let p;if(p=e.extractMeta?e.extractMeta(f):{userId:`anonymous`,clientId:crypto.randomUUID(),channels:[`default`]},storeAttachment(d,p),this.sessions.set(d,{ws:d,meta:p}),e.onConnect){let f={actor:this,ws:d,meta:p};e.onConnect(f)}}catch(f){if(e.onError)try{e.onError(f,{actor:this,ws:d,meta:{userId:`unknown`,clientId:`unknown`,channels:[]}})}catch{}d.close(1011,`Internal server error`)}}onWebSocketMessage(d,f){let p;try{let m=decodeFrame$1(f);if(p=this.sessions.get(d),!p)return;if(e.onMessage){let f={actor:this,ws:d,meta:p.meta,frame:m};e.onMessage(f,m)}}catch(f){if(e.onError&&p)try{e.onError(f,{actor:this,ws:d,meta:p.meta})}catch{}}}onWebSocketDisconnect(d){try{let f=this.sessions.get(d);if(this.sessions.delete(d),f&&e.onDisconnect){let p={actor:this,ws:d,meta:f.meta};e.onDisconnect(p)}}catch{}}broadcast(e,d,f){let p=0,m=encodeFrame$1({type:`event`,channel:e,data:d});for(let{ws:d,meta:h}of this.sessions.values())if(h.channels.includes(e)&&!(f?.except&&d===f.except)&&!(f?.userIds&&!f.userIds.includes(h.userId))&&!(f?.clientIds&&!f.clientIds.includes(h.clientId)))try{d.send(m),p++}catch{}return p}getSessionCount(){return this.sessions.size}getConnectedUserIds(){let e=new Set;for(let{meta:d}of this.sessions.values())e.add(d.userId);return Array.from(e)}getUserSessions(e){let d=[];for(let{ws:f,meta:p}of this.sessions.values())p.userId===e&&d.push(f);return d}sendToUser(e,d,f){let p=0,m=encodeFrame$1({type:`event`,channel:d,data:f});for(let{ws:f,meta:h}of this.sessions.values())if(h.userId===e&&h.channels.includes(d))try{f.send(m),p++}catch{}return p}getStorage(){return this.ctx.storage}}return Object.defineProperty(f,`name`,{value:d,writable:!1,configurable:!0}),f}export{PROTOCOL_VERSION,createActorHandler,decodeClientMessage,decodeFrame,decodeServerMessage,defineRoom,encodeClientMessage,encodeFrame,encodeServerMessage,restoreSessions,storeAttachment};
1
+ import{a as decodeClientMessage,i as encodeServerMessage,n as encodeClientMessage,o as decodeFrame,r as encodeFrame,s as decodeServerMessage,t as PROTOCOL_VERSION}from"./types-CJLnZrA8.mjs";import{Actor}from"@cloudflare/actors";function defaultExtractMeta(e){let d=crypto.randomUUID(),f=crypto.randomUUID(),p=new URL(e.url).searchParams.get(`channels`);return{userId:d,clientId:f,channels:p?p.split(`,`).map(e=>e.trim()).filter(Boolean):[`default`]}}function defineRoom(e){return{name:e.name,websocketPath:e.websocketPath,extractMeta:e.extractMeta||defaultExtractMeta,onConnect:e.onConnect,onDisconnect:e.onDisconnect,onMessage:e.onMessage,onError:e.onError}}function isValidConnectionMeta(e){return!(!e||typeof e!=`object`||typeof e.userId!=`string`||!e.userId||typeof e.clientId!=`string`||!e.clientId||!Array.isArray(e.channels)||!e.channels.every(e=>typeof e==`string`))}function storeAttachment(e,d){e.serializeAttachment(d)}function restoreSessions(e){let d=0,f=0;for(let p of e.ctx.getWebSockets()){if(p.readyState!==WebSocket.OPEN){f++;continue}let m=p.deserializeAttachment();if(!m){f++;continue}if(!isValidConnectionMeta(m)){f++;continue}e.sessions.set(p,{ws:p,meta:m}),d++}}function decodeFrame$1(e){return decodeFrame(e)??{type:`invalid`}}function encodeFrame$1(e){return encodeFrame(e)}function sanitizeToClassName(e){return e.replace(/^\/+/,``).split(/[-_\/\s]+/).map(e=>e.replace(/[^a-zA-Z0-9]/g,``)).filter(e=>e.length>0).map(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()).join(``)||`VeraniActor`}function createActorHandler(e){let d=sanitizeToClassName(e.name||e.websocketPath||`VeraniActor`);class f extends Actor{constructor(...e){super(...e),this.sessions=new Map}static configuration(d){return{locationHint:`me`,sockets:{upgradePath:e.websocketPath,autoResponse:{ping:`ping`,pong:`pong`}}}}async shouldUpgradeWebSocket(e){return!0}async fetch(d){let f=new URL(d.url),p=d.headers.get(`Upgrade`);return f.pathname===e.websocketPath&&p===`websocket`&&await this.shouldUpgradeWebSocket(d)?this.onWebSocketUpgrade(d):this.onRequest(d)}async onInit(){try{restoreSessions(this)}catch{}if(e.onHibernationRestore&&this.sessions.size>0)try{await e.onHibernationRestore(this)}catch{}else e.onHibernationRestore&&this.sessions.size}async onWebSocketConnect(d,f){let p;try{if(p=e.extractMeta?await e.extractMeta(f):{userId:`anonymous`,clientId:crypto.randomUUID(),channels:[`default`]},storeAttachment(d,p),e.onConnect){let f={actor:this,ws:d,meta:p};await e.onConnect(f)}this.sessions.set(d,{ws:d,meta:p})}catch(f){if(e.onError&&p)try{await e.onError(f,{actor:this,ws:d,meta:p})}catch{}d.close(1011,`Internal server error`)}}async onWebSocketMessage(d,f){let p;try{let m=decodeFrame$1(f);if(p=this.sessions.get(d),!p)return;if(e.onMessage){let f={actor:this,ws:d,meta:p.meta,frame:m};await e.onMessage(f,m)}}catch(f){if(e.onError&&p)try{await e.onError(f,{actor:this,ws:d,meta:p.meta})}catch{}}}async onWebSocketDisconnect(d){try{let f=this.sessions.get(d);if(this.sessions.delete(d),f&&e.onDisconnect){let p={actor:this,ws:d,meta:f.meta};await e.onDisconnect(p)}}catch{}}cleanupStaleSessions(){let e=0,d=[];for(let[e,f]of this.sessions.entries())e.readyState!==WebSocket.OPEN&&d.push(e);for(let f of d)this.sessions.delete(f),e++;return e}broadcast(e,d,f){let p=0,m=encodeFrame$1({type:`event`,channel:e,data:d}),h=[];for(let{ws:d,meta:g}of this.sessions.values())if(g.channels.includes(e)&&!(f?.except&&d===f.except)&&!(f?.userIds&&!f.userIds.includes(g.userId))&&!(f?.clientIds&&!f.clientIds.includes(g.clientId))){if(d.readyState!==WebSocket.OPEN){h.push(d);continue}try{d.send(m),p++}catch{h.push(d)}}for(let e of h)this.sessions.delete(e);return h.length,p}getSessionCount(){return this.sessions.size}getConnectedUserIds(){let e=new Set;for(let{meta:d}of this.sessions.values())e.add(d.userId);return Array.from(e)}getUserSessions(e){let d=[];for(let{ws:f,meta:p}of this.sessions.values())p.userId===e&&d.push(f);return d}sendToUser(e,d,f){let p=0,m=encodeFrame$1({type:`event`,channel:d,data:f}),h=[];for(let{ws:f,meta:g}of this.sessions.values())if(g.userId===e&&g.channels.includes(d)){if(f.readyState!==WebSocket.OPEN){h.push(f);continue}try{f.send(m),p++}catch{h.push(f)}}for(let e of h)this.sessions.delete(e);return h.length,p}getStorage(){return this.ctx.storage}}return Object.defineProperty(f,`name`,{value:d,writable:!1,configurable:!0}),f}export{PROTOCOL_VERSION,createActorHandler,decodeClientMessage,decodeFrame,decodeServerMessage,defineRoom,encodeClientMessage,encodeFrame,encodeServerMessage,restoreSessions,storeAttachment};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verani",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "A simple, focused realtime SDK for Cloudflare Actors with Socket.io-like semantics",
5
5
  "license": "ISC",
6
6
  "keywords": [