verani 0.1.7 → 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}setState(e){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.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()}connect(){try{this.connectionManager.setState(`connecting`),this.emitLifecycleEvent(`connecting`),this.ws=new WebSocket(this.url);let e=setTimeout(()=>{this.connectionManager.getState()===`connecting`&&(this.ws?.close(),this.handleConnectionError(Error(`Connection timeout`)))},this.options.connectionTimeout);this.ws.addEventListener(`open`,()=>{clearTimeout(e),this.handleOpen()}),this.ws.addEventListener(`message`,e=>{this.handleMessage(e)}),this.ws.addEventListener(`close`,t=>{clearTimeout(e),this.handleClose(t)}),this.ws.addEventListener(`error`,t=>{clearTimeout(e),this.handleError(t)})}catch(e){this.handleConnectionError(e)}}handleOpen(){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.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.emitLifecycleEvent(`error`,e),this.onErrorCallback?.(e)}handleConnectionError(e){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}waitForConnection(){return this.isConnected()?Promise.resolve():(this.connectionPromise||=new Promise((e,t)=>{this.connectionResolve=e,this.connectionReject=t}),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.disconnect(),this.connect()}disconnect(){this.connectionManager.cancelReconnect(),this.ws&&=(this.ws.close(1e3,`Client disconnect`),void 0)}close(){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
@@ -33,6 +33,10 @@ declare class ConnectionManager {
33
33
  * Gets the current connection state
34
34
  */
35
35
  getState(): ConnectionState;
36
+ /**
37
+ * Validates if a state transition is valid
38
+ */
39
+ private isValidStateTransition;
36
40
  /**
37
41
  * Updates the connection state and notifies listeners
38
42
  */
@@ -78,6 +82,10 @@ interface VeraniClientOptions {
78
82
  maxQueueSize?: number;
79
83
  /** Connection timeout in milliseconds */
80
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;
81
89
  }
82
90
  /**
83
91
  * Verani WebSocket client with automatic reconnection and lifecycle management
@@ -96,12 +104,30 @@ declare class VeraniClient {
96
104
  private connectionPromise?;
97
105
  private connectionResolve?;
98
106
  private connectionReject?;
107
+ private connectionTimeout?;
108
+ private isConnecting;
109
+ private connectionId;
110
+ private pingInterval?;
111
+ private pongTimeout?;
112
+ private lastPongReceived;
99
113
  /**
100
114
  * Creates a new Verani client
101
115
  * @param url - WebSocket URL to connect to
102
116
  * @param options - Client configuration options
103
117
  */
104
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;
127
+ /**
128
+ * Cleans up existing WebSocket connection and resources
129
+ */
130
+ private cleanupWebSocket;
105
131
  /**
106
132
  * Establishes WebSocket connection
107
133
  */
@@ -142,6 +168,16 @@ declare class VeraniClient {
142
168
  * Checks if the client is currently connected
143
169
  */
144
170
  isConnected(): boolean;
171
+ /**
172
+ * Gets detailed connection information
173
+ */
174
+ getConnectionState(): {
175
+ state: ConnectionState;
176
+ isConnected: boolean;
177
+ isConnecting: boolean;
178
+ reconnectAttempts: number;
179
+ connectionId: number;
180
+ };
145
181
  /**
146
182
  * Waits for the connection to be established
147
183
  * @returns Promise that resolves when connected
package/dist/client.d.mts CHANGED
@@ -33,6 +33,10 @@ declare class ConnectionManager {
33
33
  * Gets the current connection state
34
34
  */
35
35
  getState(): ConnectionState;
36
+ /**
37
+ * Validates if a state transition is valid
38
+ */
39
+ private isValidStateTransition;
36
40
  /**
37
41
  * Updates the connection state and notifies listeners
38
42
  */
@@ -78,6 +82,10 @@ interface VeraniClientOptions {
78
82
  maxQueueSize?: number;
79
83
  /** Connection timeout in milliseconds */
80
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;
81
89
  }
82
90
  /**
83
91
  * Verani WebSocket client with automatic reconnection and lifecycle management
@@ -96,12 +104,30 @@ declare class VeraniClient {
96
104
  private connectionPromise?;
97
105
  private connectionResolve?;
98
106
  private connectionReject?;
107
+ private connectionTimeout?;
108
+ private isConnecting;
109
+ private connectionId;
110
+ private pingInterval?;
111
+ private pongTimeout?;
112
+ private lastPongReceived;
99
113
  /**
100
114
  * Creates a new Verani client
101
115
  * @param url - WebSocket URL to connect to
102
116
  * @param options - Client configuration options
103
117
  */
104
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;
127
+ /**
128
+ * Cleans up existing WebSocket connection and resources
129
+ */
130
+ private cleanupWebSocket;
105
131
  /**
106
132
  * Establishes WebSocket connection
107
133
  */
@@ -142,6 +168,16 @@ declare class VeraniClient {
142
168
  * Checks if the client is currently connected
143
169
  */
144
170
  isConnected(): boolean;
171
+ /**
172
+ * Gets detailed connection information
173
+ */
174
+ getConnectionState(): {
175
+ state: ConnectionState;
176
+ isConnected: boolean;
177
+ isConnecting: boolean;
178
+ reconnectAttempts: number;
179
+ connectionId: number;
180
+ };
145
181
  /**
146
182
  * Waits for the connection to be established
147
183
  * @returns Promise that resolves when connected
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}setState(e){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.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()}connect(){try{this.connectionManager.setState(`connecting`),this.emitLifecycleEvent(`connecting`),this.ws=new WebSocket(this.url);let e=setTimeout(()=>{this.connectionManager.getState()===`connecting`&&(this.ws?.close(),this.handleConnectionError(Error(`Connection timeout`)))},this.options.connectionTimeout);this.ws.addEventListener(`open`,()=>{clearTimeout(e),this.handleOpen()}),this.ws.addEventListener(`message`,e=>{this.handleMessage(e)}),this.ws.addEventListener(`close`,c=>{clearTimeout(e),this.handleClose(c)}),this.ws.addEventListener(`error`,c=>{clearTimeout(e),this.handleError(c)})}catch(e){this.handleConnectionError(e)}}handleOpen(){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.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.emitLifecycleEvent(`error`,e),this.onErrorCallback?.(e)}handleConnectionError(e){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}waitForConnection(){return this.isConnected()?Promise.resolve():(this.connectionPromise||=new Promise((e,c)=>{this.connectionResolve=e,this.connectionReject=c}),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.disconnect(),this.connect()}disconnect(){this.connectionManager.cancelReconnect(),this.ws&&=(this.ws.close(1e3,`Client disconnect`),void 0)}close(){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.7",
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": [