verani 0.2.0 → 0.3.1

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/README.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Verani
2
2
 
3
+ <div align="center">
4
+
5
+ [![MADE BY #V0ID](https://img.shields.io/badge/MADE%20BY%20%23V0ID-F3EEE1.svg?style=for-the-badge)](https://github.com/v0id-user)
6
+
7
+ </div>
8
+
3
9
  > A simple, focused realtime SDK for Cloudflare Actors with Socket.io-like semantics
4
10
 
5
11
  Verani brings the familiar developer experience of Socket.io to Cloudflare's Durable Objects (Actors), with proper hibernation support and minimal overhead.
@@ -161,6 +167,7 @@ Verani handles Cloudflare's hibernation automatically:
161
167
  - WebSocket attachment management for hibernation
162
168
  - Selective broadcasting with filters
163
169
  - User and client ID tracking
170
+ - **RPC methods** - Call Actor methods remotely from Workers or other Actors
164
171
  - Error boundaries and logging
165
172
  - Flexible metadata extraction from requests
166
173
 
@@ -172,6 +179,14 @@ Verani handles Cloudflare's hibernation automatically:
172
179
  - Event-based API (on/off/once/emit)
173
180
  - Promise-based connection waiting
174
181
  - Lifecycle callbacks
182
+ - **Ping/pong keepalive** with automatic Page Visibility API resync
183
+
184
+ ### RPC Support
185
+
186
+ - Send messages to users from HTTP endpoints
187
+ - Query actor state remotely
188
+ - Broadcast from external events or scheduled tasks
189
+ - Coordinate between multiple Actors
175
190
 
176
191
  ## Live Examples
177
192
 
@@ -179,7 +194,7 @@ Try out Verani with working examples:
179
194
 
180
195
  ```bash
181
196
  # Clone and run
182
- git clone https://github.com/your-org/verani
197
+ git clone https://github.com/v0id-user/verani
183
198
  cd verani
184
199
  bun install # or npm install
185
200
  bun run dev # or npm run dev
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.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??5e3,pongTimeout:t.pongTimeout??5e3},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.ws.send(encodeClientMessage$1({type:`ping`}))}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===`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;
1
+ const require_types=require(`./types-083oWz55.cjs`);function encodeClientMessage$1(l){return require_types.n(l)}function decodeServerMessage$1(l){return require_types.s(l)}const DEFAULT_RECONNECTION_CONFIG={enabled:!0,maxAttempts:10,initialDelay:1e3,maxDelay:3e4,backoffMultiplier:1.5};var ConnectionManager=class{constructor(e=DEFAULT_RECONNECTION_CONFIG,l){this.config=e,this.onStateChange=l,this.state=`disconnected`,this.reconnectAttempts=0,this.currentDelay=e.initialDelay}getState(){return this.state}isValidStateTransition(e,l){return{disconnected:[`connecting`,`reconnecting`],connecting:[`connected`,`disconnected`,`error`,`reconnecting`],connected:[`disconnected`,`reconnecting`],reconnecting:[`connecting`,`disconnected`,`error`],error:[`reconnecting`,`disconnected`,`connecting`]}[e]?.includes(l)??!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()}};function resolveClientOptions(e){return{reconnection:{enabled:e.reconnection?.enabled??DEFAULT_RECONNECTION_CONFIG.enabled,maxAttempts:e.reconnection?.maxAttempts??DEFAULT_RECONNECTION_CONFIG.maxAttempts,initialDelay:e.reconnection?.initialDelay??DEFAULT_RECONNECTION_CONFIG.initialDelay,maxDelay:e.reconnection?.maxDelay??DEFAULT_RECONNECTION_CONFIG.maxDelay,backoffMultiplier:e.reconnection?.backoffMultiplier??DEFAULT_RECONNECTION_CONFIG.backoffMultiplier},maxQueueSize:e.maxQueueSize??100,connectionTimeout:e.connectionTimeout??1e4,pingInterval:e.pingInterval??5e3,pongTimeout:e.pongTimeout??5e3}}var MessageQueue=class{constructor(e){this.maxQueueSize=e,this.queue=[]}queueMessage(e){this.queue.length>=this.maxQueueSize&&this.queue.shift(),this.queue.push(e)}flushMessageQueue(e){if(!(!e||e.readyState!==WebSocket.OPEN))for(;this.queue.length>0;){let u=this.queue.shift();try{e.send(encodeClientMessage$1(u))}catch{}}}clear(){this.queue=[]}getLength(){return this.queue.length}};function isBrowserEnvironment(){return typeof globalThis<`u`&&`document`in globalThis&&globalThis.document!==void 0&&globalThis.document.hidden!==void 0&&typeof globalThis.document.addEventListener==`function`}function getDocument(){return isBrowserEnvironment()&&`document`in globalThis?globalThis.document:null}function isPageVisible(){let e=getDocument();return e?!e.hidden:!0}function onVisibilityChange(e){let l=getDocument();if(!l)return null;let u=()=>{e(isPageVisible())};return l.addEventListener(`visibilitychange`,u),()=>{l.removeEventListener(`visibilitychange`,u)}}var KeepaliveManager=class{constructor(e,l,u){this.options=e,this.getWebSocket=l,this.onTimeout=u,this.lastPongReceived=0}startPingInterval(){this.options.pingInterval===0||this.pingInterval!==void 0||(this.lastPongReceived=Date.now(),this.visibilityCleanup=onVisibilityChange(e=>{e&&this.resyncPingInterval()}),this.pingInterval=setInterval(()=>{let e=this.getWebSocket();if(!e||e.readyState!==WebSocket.OPEN){this.stopPingInterval();return}if(Date.now()-this.lastPongReceived>this.options.pongTimeout+this.options.pingInterval){this.stopPingInterval(),e.close(1006,`Pong timeout`);return}try{e.send(encodeClientMessage$1({type:`ping`}))}catch{}},this.options.pingInterval))}resyncPingInterval(){let e=this.getWebSocket();if(!(!e||e.readyState!==WebSocket.OPEN)){this.pingInterval!==void 0&&(clearInterval(this.pingInterval),this.pingInterval=void 0);try{e.send(encodeClientMessage$1({type:`ping`}))}catch{}this.lastPongReceived=Date.now(),this.pingInterval=setInterval(()=>{let e=this.getWebSocket();if(!e||e.readyState!==WebSocket.OPEN){this.stopPingInterval();return}if(Date.now()-this.lastPongReceived>this.options.pongTimeout+this.options.pingInterval){this.stopPingInterval(),e.close(1006,`Pong timeout`);return}try{e.send(encodeClientMessage$1({type:`ping`}))}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),this.visibilityCleanup&&=(this.visibilityCleanup(),void 0)}recordPong(){this.lastPongReceived=Date.now()}},EventEmitter=class{constructor(){this.listeners=new Map}on(e,l){this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(l)}off(e,l){let u=this.listeners.get(e);u&&(u.delete(l),u.size===0&&this.listeners.delete(e))}once(e,l){let u=d=>{this.off(e,u),l(d)};this.on(e,u)}emitLifecycleEvent(e,l){let u=this.listeners.get(e);if(u)for(let e of u)try{e(l)}catch{}}dispatch(e,l){let u=this.listeners.get(e);if(u)for(let e of u)try{e(l)}catch{}}clear(){this.listeners.clear()}};function handleWebSocketOpen(e,l,u,d,f,p,m,h){e.clear(),l.setState(`connected`),l.resetReconnection(),u.startPingInterval(),d.flushMessageQueue(f),p.resolve&&(p.resolve(),p.clear()),m.emitLifecycleEvent(`open`),m.emitLifecycleEvent(`connected`),h?.()}function handleWebSocketMessage(e,l,d){let f=decodeServerMessage$1(e.data);if(!f)return;if(f.type===`pong`){l.recordPong();return}let p=f.type,m=f.data;f.type===`event`&&f.data&&typeof f.data==`object`&&`type`in f.data&&(p=f.data.type,m=f.data),d.dispatch(p,m)}function handleWebSocketClose(e,l,u,d,f,p,m,h){m&&(m.value=!1),l.clear(),u.setState(`disconnected`),d.reject&&(d.reject(Error(`Connection closed: ${e.reason||`Unknown reason`}`)),d.clear()),f.emitLifecycleEvent(`close`,e),f.emitLifecycleEvent(`disconnected`,e),h?.(e),e.code!==1e3&&e.code!==1001&&u.scheduleReconnect(p)&&f.emitLifecycleEvent(`reconnecting`)}function handleWebSocketError(e,l,u,d,f,p){f&&(f.value=!1),l.clear(),u.emitLifecycleEvent(`error`,e),p?.(e),d(Error(`WebSocket error`))}function handleConnectionError(e,l,u,d,f,p,m){m&&(m.value=!1),l.clear(),u.reject&&(u.reject(e),u.clear()),f.emitLifecycleEvent(`error`,e),d.scheduleReconnect(p)&&f.emitLifecycleEvent(`reconnecting`)}var ConnectionHandler=class{constructor(e,l,u,d,f,p,m,h,g,_,v,y){this.url=e,this.options=l,this.connectionManager=u,this.keepalive=d,this.eventEmitter=f,this.messageQueue=p,this.connectionPromise=m,this.isConnectingRef=h,this.isConnectedFn=g,this.onOpenCallback=_,this.onCloseCallback=v,this.onErrorCallback=y,this.connectionId=0,this.connectionTimeoutState={value:void 0,clear:()=>{this.connectionTimeoutState.value!==void 0&&(clearTimeout(this.connectionTimeoutState.value),this.connectionTimeoutState.value=void 0)}}}connect(){if(!this.isConnectingRef.value&&!this.isConnectedFn()){this.cleanupWebSocket();try{this.isConnectingRef.value=!0,this.connectionId++;let e=this.connectionId;this.connectionManager.setState(`connecting`),this.eventEmitter.emitLifecycleEvent(`connecting`),this.ws=new WebSocket(this.url),this.connectionTimeoutState.value=setTimeout(()=>{this.isConnectingRef.value&&this.connectionId===e&&(this.ws?.close(),this.handleConnectionErrorInternal(Error(`Connection timeout`)))},this.options.connectionTimeout),this.ws.addEventListener(`open`,()=>{this.connectionId===e&&this.handleOpenInternal()}),this.ws.addEventListener(`message`,l=>{this.connectionId===e&&handleWebSocketMessage(l,this.keepalive,this.eventEmitter)}),this.ws.addEventListener(`close`,l=>{this.connectionId===e&&handleWebSocketClose(l,this.connectionTimeoutState,this.connectionManager,this.connectionPromise,this.eventEmitter,()=>this.connect(),this.isConnectingRef,this.onCloseCallback)}),this.ws.addEventListener(`error`,l=>{this.connectionId===e&&handleWebSocketError(l,this.connectionTimeoutState,this.eventEmitter,e=>this.handleConnectionErrorInternal(e),this.isConnectingRef,this.onErrorCallback)})}catch(e){this.isConnectingRef.value=!1,this.handleConnectionErrorInternal(e)}}}cleanupWebSocket(){if(this.keepalive.stopPingInterval(),this.connectionTimeoutState.clear(),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{}}}getWebSocket(){return this.ws}getConnectionId(){return this.connectionId}handleOpenInternal(){handleWebSocketOpen(this.connectionTimeoutState,this.connectionManager,this.keepalive,this.messageQueue,this.ws,this.connectionPromise,this.eventEmitter,this.onOpenCallback),this.isConnectingRef.value=!1}handleConnectionErrorInternal(e){handleConnectionError(e,this.connectionTimeoutState,this.connectionPromise,this.connectionManager,this.eventEmitter,()=>this.connect(),this.isConnectingRef)}},VeraniClient=class{constructor(e,l={}){this.url=e,this.connectionPromiseState={promise:void 0,resolve:void 0,reject:void 0,clear:()=>{this.connectionPromiseState.promise=void 0,this.connectionPromiseState.resolve=void 0,this.connectionPromiseState.reject=void 0}},this.options=resolveClientOptions(l),this.connectionManager=new ConnectionManager(this.options.reconnection,e=>{this.onStateChangeCallback?.(e)}),this.messageQueue=new MessageQueue(this.options.maxQueueSize),this.eventEmitter=new EventEmitter,this.isConnectingRef={value:!1},this.keepalive=new KeepaliveManager(this.options,()=>this.connectionHandler.getWebSocket(),()=>{let e=this.connectionHandler.getWebSocket();e&&e.close(1006,`Pong timeout`)}),this.connectionHandler=new ConnectionHandler(this.url,this.options,this.connectionManager,this.keepalive,this.eventEmitter,this.messageQueue,this.connectionPromiseState,this.isConnectingRef,()=>this.isConnected(),this.onOpenCallback,this.onCloseCallback,this.onErrorCallback),Object.defineProperty(this,`isConnecting`,{get:()=>this.isConnectingRef.value,enumerable:!0,configurable:!0}),this.connect()}connect(){this.connectionHandler.connect()}cleanupWebSocket(){this.connectionHandler.cleanupWebSocket()}getState(){return this.connectionManager.getState()}isConnected(){return this.connectionHandler.getWebSocket()?.readyState===WebSocket.OPEN&&this.connectionManager.getState()===`connected`}getConnectionState(){return{state:this.connectionManager.getState(),isConnected:this.isConnected(),isConnecting:this.isConnectingRef.value,reconnectAttempts:this.connectionManager.getReconnectAttempts(),connectionId:this.connectionHandler.getConnectionId()}}waitForConnection(){return this.isConnected()?Promise.resolve():(this.connectionPromiseState.promise||(this.connectionPromiseState.promise=new Promise((e,l)=>{this.connectionPromiseState.resolve=e,this.connectionPromiseState.reject=l;let u=setTimeout(()=>{this.connectionPromiseState.reject&&(this.connectionPromiseState.reject(Error(`Connection wait timeout`)),this.connectionPromiseState.clear())},this.options.connectionTimeout*2);this.connectionPromiseState.promise&&this.connectionPromiseState.promise.finally(()=>{clearTimeout(u)})})),this.connectionPromiseState.promise)}on(e,l){this.eventEmitter.on(e,l)}off(e,l){this.eventEmitter.off(e,l)}once(e,l){this.eventEmitter.once(e,l)}emit(e,u){let d={type:e,data:u};if(this.isConnected()){let e=this.connectionHandler.getWebSocket();if(e)try{e.send(encodeClientMessage$1(d))}catch{this.messageQueue.queueMessage(d)}}else this.messageQueue.queueMessage(d)}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.isConnectingRef.value=!1,this.connectionManager.setState(`disconnected`),this.connect()}disconnect(){this.connectionManager.cancelReconnect(),this.isConnectingRef.value=!1,this.connectionPromiseState.reject&&(this.connectionPromiseState.reject(Error(`Connection disconnected`)),this.connectionPromiseState.clear()),this.cleanupWebSocket(),this.connectionManager.setState(`disconnected`)}close(){this.connectionPromiseState.reject&&(this.connectionPromiseState.reject(Error(`Client closed`)),this.connectionPromiseState.clear()),this.disconnect(),this.eventEmitter.clear(),this.messageQueue.clear(),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
@@ -71,7 +71,7 @@ declare class ConnectionManager {
71
71
  destroy(): void;
72
72
  }
73
73
  //#endregion
74
- //#region src/client/client.d.ts
74
+ //#region src/client/runtime/configuration.d.ts
75
75
  /**
76
76
  * Client options for configuring the Verani client
77
77
  */
@@ -87,79 +87,39 @@ interface VeraniClientOptions {
87
87
  /** Pong timeout in milliseconds (default: 5000) */
88
88
  pongTimeout?: number;
89
89
  }
90
+ //#endregion
91
+ //#region src/client/client.d.ts
90
92
  /**
91
93
  * Verani WebSocket client with automatic reconnection and lifecycle management
92
94
  */
93
95
  declare class VeraniClient {
94
96
  private url;
95
- private ws?;
96
- private listeners;
97
+ private connectionHandler;
97
98
  private connectionManager;
98
99
  private messageQueue;
100
+ private keepalive;
101
+ private eventEmitter;
99
102
  private options;
100
103
  private onOpenCallback?;
101
104
  private onCloseCallback?;
102
105
  private onErrorCallback?;
103
106
  private onStateChangeCallback?;
104
- private connectionPromise?;
105
- private connectionResolve?;
106
- private connectionReject?;
107
- private connectionTimeout?;
108
- private isConnecting;
109
- private connectionId;
110
- private pingInterval?;
111
- private pongTimeout?;
112
- private lastPongReceived;
107
+ private connectionPromiseState;
108
+ private isConnectingRef;
113
109
  /**
114
110
  * Creates a new Verani client
115
111
  * @param url - WebSocket URL to connect to
116
112
  * @param options - Client configuration options
117
113
  */
118
114
  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;
131
115
  /**
132
116
  * Establishes WebSocket connection
133
117
  */
134
118
  private connect;
135
119
  /**
136
- * Handles successful WebSocket connection
137
- */
138
- private handleOpen;
139
- /**
140
- * Handles incoming WebSocket messages
141
- */
142
- private handleMessage;
143
- /**
144
- * Handles WebSocket closure
145
- */
146
- private handleClose;
147
- /**
148
- * Handles WebSocket errors
149
- */
150
- private handleError;
151
- /**
152
- * Handles connection errors
153
- */
154
- private handleConnectionError;
155
- /**
156
- * Emits a lifecycle event to registered listeners
157
- */
158
- private emitLifecycleEvent;
159
- /**
160
- * Flushes queued messages when connection is established
120
+ * Cleans up existing WebSocket connection and resources
161
121
  */
162
- private flushMessageQueue;
122
+ private cleanupWebSocket;
163
123
  /**
164
124
  * Gets the current connection state
165
125
  */
@@ -207,10 +167,6 @@ declare class VeraniClient {
207
167
  * @param data - Optional message data
208
168
  */
209
169
  emit(type: string, data?: any): void;
210
- /**
211
- * Queues a message for sending when connected
212
- */
213
- private queueMessage;
214
170
  /**
215
171
  * Registers lifecycle callback for connection open
216
172
  */
package/dist/client.d.mts CHANGED
@@ -71,7 +71,7 @@ declare class ConnectionManager {
71
71
  destroy(): void;
72
72
  }
73
73
  //#endregion
74
- //#region src/client/client.d.ts
74
+ //#region src/client/runtime/configuration.d.ts
75
75
  /**
76
76
  * Client options for configuring the Verani client
77
77
  */
@@ -87,79 +87,39 @@ interface VeraniClientOptions {
87
87
  /** Pong timeout in milliseconds (default: 5000) */
88
88
  pongTimeout?: number;
89
89
  }
90
+ //#endregion
91
+ //#region src/client/client.d.ts
90
92
  /**
91
93
  * Verani WebSocket client with automatic reconnection and lifecycle management
92
94
  */
93
95
  declare class VeraniClient {
94
96
  private url;
95
- private ws?;
96
- private listeners;
97
+ private connectionHandler;
97
98
  private connectionManager;
98
99
  private messageQueue;
100
+ private keepalive;
101
+ private eventEmitter;
99
102
  private options;
100
103
  private onOpenCallback?;
101
104
  private onCloseCallback?;
102
105
  private onErrorCallback?;
103
106
  private onStateChangeCallback?;
104
- private connectionPromise?;
105
- private connectionResolve?;
106
- private connectionReject?;
107
- private connectionTimeout?;
108
- private isConnecting;
109
- private connectionId;
110
- private pingInterval?;
111
- private pongTimeout?;
112
- private lastPongReceived;
107
+ private connectionPromiseState;
108
+ private isConnectingRef;
113
109
  /**
114
110
  * Creates a new Verani client
115
111
  * @param url - WebSocket URL to connect to
116
112
  * @param options - Client configuration options
117
113
  */
118
114
  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;
131
115
  /**
132
116
  * Establishes WebSocket connection
133
117
  */
134
118
  private connect;
135
119
  /**
136
- * Handles successful WebSocket connection
137
- */
138
- private handleOpen;
139
- /**
140
- * Handles incoming WebSocket messages
141
- */
142
- private handleMessage;
143
- /**
144
- * Handles WebSocket closure
145
- */
146
- private handleClose;
147
- /**
148
- * Handles WebSocket errors
149
- */
150
- private handleError;
151
- /**
152
- * Handles connection errors
153
- */
154
- private handleConnectionError;
155
- /**
156
- * Emits a lifecycle event to registered listeners
157
- */
158
- private emitLifecycleEvent;
159
- /**
160
- * Flushes queued messages when connection is established
120
+ * Cleans up existing WebSocket connection and resources
161
121
  */
162
- private flushMessageQueue;
122
+ private cleanupWebSocket;
163
123
  /**
164
124
  * Gets the current connection state
165
125
  */
@@ -207,10 +167,6 @@ declare class VeraniClient {
207
167
  * @param data - Optional message data
208
168
  */
209
169
  emit(type: string, data?: any): void;
210
- /**
211
- * Queues a message for sending when connected
212
- */
213
- private queueMessage;
214
170
  /**
215
171
  * Registers lifecycle callback for connection open
216
172
  */
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.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??5e3,pongTimeout:c.pongTimeout??5e3},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.ws.send(encodeClientMessage$1({type:`ping`}))}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===`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};
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,h){this.config=e,this.onStateChange=h,this.state=`disconnected`,this.reconnectAttempts=0,this.currentDelay=e.initialDelay}getState(){return this.state}isValidStateTransition(e,h){return{disconnected:[`connecting`,`reconnecting`],connecting:[`connected`,`disconnected`,`error`,`reconnecting`],connected:[`disconnected`,`reconnecting`],reconnecting:[`connecting`,`disconnected`,`error`],error:[`reconnecting`,`disconnected`,`connecting`]}[e]?.includes(h)??!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()}};function resolveClientOptions(e){return{reconnection:{enabled:e.reconnection?.enabled??DEFAULT_RECONNECTION_CONFIG.enabled,maxAttempts:e.reconnection?.maxAttempts??DEFAULT_RECONNECTION_CONFIG.maxAttempts,initialDelay:e.reconnection?.initialDelay??DEFAULT_RECONNECTION_CONFIG.initialDelay,maxDelay:e.reconnection?.maxDelay??DEFAULT_RECONNECTION_CONFIG.maxDelay,backoffMultiplier:e.reconnection?.backoffMultiplier??DEFAULT_RECONNECTION_CONFIG.backoffMultiplier},maxQueueSize:e.maxQueueSize??100,connectionTimeout:e.connectionTimeout??1e4,pingInterval:e.pingInterval??5e3,pongTimeout:e.pongTimeout??5e3}}var MessageQueue=class{constructor(e){this.maxQueueSize=e,this.queue=[]}queueMessage(e){this.queue.length>=this.maxQueueSize&&this.queue.shift(),this.queue.push(e)}flushMessageQueue(e){if(!(!e||e.readyState!==WebSocket.OPEN))for(;this.queue.length>0;){let h=this.queue.shift();try{e.send(encodeClientMessage$1(h))}catch{}}}clear(){this.queue=[]}getLength(){return this.queue.length}};function isBrowserEnvironment(){return typeof globalThis<`u`&&`document`in globalThis&&globalThis.document!==void 0&&globalThis.document.hidden!==void 0&&typeof globalThis.document.addEventListener==`function`}function getDocument(){return isBrowserEnvironment()&&`document`in globalThis?globalThis.document:null}function isPageVisible(){let e=getDocument();return e?!e.hidden:!0}function onVisibilityChange(e){let h=getDocument();if(!h)return null;let g=()=>{e(isPageVisible())};return h.addEventListener(`visibilitychange`,g),()=>{h.removeEventListener(`visibilitychange`,g)}}var KeepaliveManager=class{constructor(e,h,g){this.options=e,this.getWebSocket=h,this.onTimeout=g,this.lastPongReceived=0}startPingInterval(){this.options.pingInterval===0||this.pingInterval!==void 0||(this.lastPongReceived=Date.now(),this.visibilityCleanup=onVisibilityChange(e=>{e&&this.resyncPingInterval()}),this.pingInterval=setInterval(()=>{let e=this.getWebSocket();if(!e||e.readyState!==WebSocket.OPEN){this.stopPingInterval();return}if(Date.now()-this.lastPongReceived>this.options.pongTimeout+this.options.pingInterval){this.stopPingInterval(),e.close(1006,`Pong timeout`);return}try{e.send(encodeClientMessage$1({type:`ping`}))}catch{}},this.options.pingInterval))}resyncPingInterval(){let e=this.getWebSocket();if(!(!e||e.readyState!==WebSocket.OPEN)){this.pingInterval!==void 0&&(clearInterval(this.pingInterval),this.pingInterval=void 0);try{e.send(encodeClientMessage$1({type:`ping`}))}catch{}this.lastPongReceived=Date.now(),this.pingInterval=setInterval(()=>{let e=this.getWebSocket();if(!e||e.readyState!==WebSocket.OPEN){this.stopPingInterval();return}if(Date.now()-this.lastPongReceived>this.options.pongTimeout+this.options.pingInterval){this.stopPingInterval(),e.close(1006,`Pong timeout`);return}try{e.send(encodeClientMessage$1({type:`ping`}))}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),this.visibilityCleanup&&=(this.visibilityCleanup(),void 0)}recordPong(){this.lastPongReceived=Date.now()}},EventEmitter=class{constructor(){this.listeners=new Map}on(e,h){this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(h)}off(e,h){let g=this.listeners.get(e);g&&(g.delete(h),g.size===0&&this.listeners.delete(e))}once(e,h){let g=_=>{this.off(e,g),h(_)};this.on(e,g)}emitLifecycleEvent(e,h){let g=this.listeners.get(e);if(g)for(let e of g)try{e(h)}catch{}}dispatch(e,h){let g=this.listeners.get(e);if(g)for(let e of g)try{e(h)}catch{}}clear(){this.listeners.clear()}};function handleWebSocketOpen(e,h,g,_,v,y,b,x){e.clear(),h.setState(`connected`),h.resetReconnection(),g.startPingInterval(),_.flushMessageQueue(v),y.resolve&&(y.resolve(),y.clear()),b.emitLifecycleEvent(`open`),b.emitLifecycleEvent(`connected`),x?.()}function handleWebSocketMessage(e,h,g){let _=decodeServerMessage$1(e.data);if(!_)return;if(_.type===`pong`){h.recordPong();return}let v=_.type,y=_.data;_.type===`event`&&_.data&&typeof _.data==`object`&&`type`in _.data&&(v=_.data.type,y=_.data),g.dispatch(v,y)}function handleWebSocketClose(e,h,g,_,v,y,b,x){b&&(b.value=!1),h.clear(),g.setState(`disconnected`),_.reject&&(_.reject(Error(`Connection closed: ${e.reason||`Unknown reason`}`)),_.clear()),v.emitLifecycleEvent(`close`,e),v.emitLifecycleEvent(`disconnected`,e),x?.(e),e.code!==1e3&&e.code!==1001&&g.scheduleReconnect(y)&&v.emitLifecycleEvent(`reconnecting`)}function handleWebSocketError(e,h,g,_,v,y){v&&(v.value=!1),h.clear(),g.emitLifecycleEvent(`error`,e),y?.(e),_(Error(`WebSocket error`))}function handleConnectionError(e,h,g,_,v,y,b){b&&(b.value=!1),h.clear(),g.reject&&(g.reject(e),g.clear()),v.emitLifecycleEvent(`error`,e),_.scheduleReconnect(y)&&v.emitLifecycleEvent(`reconnecting`)}var ConnectionHandler=class{constructor(e,h,g,_,v,y,b,x,S,C,w,T){this.url=e,this.options=h,this.connectionManager=g,this.keepalive=_,this.eventEmitter=v,this.messageQueue=y,this.connectionPromise=b,this.isConnectingRef=x,this.isConnectedFn=S,this.onOpenCallback=C,this.onCloseCallback=w,this.onErrorCallback=T,this.connectionId=0,this.connectionTimeoutState={value:void 0,clear:()=>{this.connectionTimeoutState.value!==void 0&&(clearTimeout(this.connectionTimeoutState.value),this.connectionTimeoutState.value=void 0)}}}connect(){if(!this.isConnectingRef.value&&!this.isConnectedFn()){this.cleanupWebSocket();try{this.isConnectingRef.value=!0,this.connectionId++;let e=this.connectionId;this.connectionManager.setState(`connecting`),this.eventEmitter.emitLifecycleEvent(`connecting`),this.ws=new WebSocket(this.url),this.connectionTimeoutState.value=setTimeout(()=>{this.isConnectingRef.value&&this.connectionId===e&&(this.ws?.close(),this.handleConnectionErrorInternal(Error(`Connection timeout`)))},this.options.connectionTimeout),this.ws.addEventListener(`open`,()=>{this.connectionId===e&&this.handleOpenInternal()}),this.ws.addEventListener(`message`,h=>{this.connectionId===e&&handleWebSocketMessage(h,this.keepalive,this.eventEmitter)}),this.ws.addEventListener(`close`,h=>{this.connectionId===e&&handleWebSocketClose(h,this.connectionTimeoutState,this.connectionManager,this.connectionPromise,this.eventEmitter,()=>this.connect(),this.isConnectingRef,this.onCloseCallback)}),this.ws.addEventListener(`error`,h=>{this.connectionId===e&&handleWebSocketError(h,this.connectionTimeoutState,this.eventEmitter,e=>this.handleConnectionErrorInternal(e),this.isConnectingRef,this.onErrorCallback)})}catch(e){this.isConnectingRef.value=!1,this.handleConnectionErrorInternal(e)}}}cleanupWebSocket(){if(this.keepalive.stopPingInterval(),this.connectionTimeoutState.clear(),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{}}}getWebSocket(){return this.ws}getConnectionId(){return this.connectionId}handleOpenInternal(){handleWebSocketOpen(this.connectionTimeoutState,this.connectionManager,this.keepalive,this.messageQueue,this.ws,this.connectionPromise,this.eventEmitter,this.onOpenCallback),this.isConnectingRef.value=!1}handleConnectionErrorInternal(e){handleConnectionError(e,this.connectionTimeoutState,this.connectionPromise,this.connectionManager,this.eventEmitter,()=>this.connect(),this.isConnectingRef)}},VeraniClient=class{constructor(e,h={}){this.url=e,this.connectionPromiseState={promise:void 0,resolve:void 0,reject:void 0,clear:()=>{this.connectionPromiseState.promise=void 0,this.connectionPromiseState.resolve=void 0,this.connectionPromiseState.reject=void 0}},this.options=resolveClientOptions(h),this.connectionManager=new ConnectionManager(this.options.reconnection,e=>{this.onStateChangeCallback?.(e)}),this.messageQueue=new MessageQueue(this.options.maxQueueSize),this.eventEmitter=new EventEmitter,this.isConnectingRef={value:!1},this.keepalive=new KeepaliveManager(this.options,()=>this.connectionHandler.getWebSocket(),()=>{let e=this.connectionHandler.getWebSocket();e&&e.close(1006,`Pong timeout`)}),this.connectionHandler=new ConnectionHandler(this.url,this.options,this.connectionManager,this.keepalive,this.eventEmitter,this.messageQueue,this.connectionPromiseState,this.isConnectingRef,()=>this.isConnected(),this.onOpenCallback,this.onCloseCallback,this.onErrorCallback),Object.defineProperty(this,`isConnecting`,{get:()=>this.isConnectingRef.value,enumerable:!0,configurable:!0}),this.connect()}connect(){this.connectionHandler.connect()}cleanupWebSocket(){this.connectionHandler.cleanupWebSocket()}getState(){return this.connectionManager.getState()}isConnected(){return this.connectionHandler.getWebSocket()?.readyState===WebSocket.OPEN&&this.connectionManager.getState()===`connected`}getConnectionState(){return{state:this.connectionManager.getState(),isConnected:this.isConnected(),isConnecting:this.isConnectingRef.value,reconnectAttempts:this.connectionManager.getReconnectAttempts(),connectionId:this.connectionHandler.getConnectionId()}}waitForConnection(){return this.isConnected()?Promise.resolve():(this.connectionPromiseState.promise||(this.connectionPromiseState.promise=new Promise((e,h)=>{this.connectionPromiseState.resolve=e,this.connectionPromiseState.reject=h;let g=setTimeout(()=>{this.connectionPromiseState.reject&&(this.connectionPromiseState.reject(Error(`Connection wait timeout`)),this.connectionPromiseState.clear())},this.options.connectionTimeout*2);this.connectionPromiseState.promise&&this.connectionPromiseState.promise.finally(()=>{clearTimeout(g)})})),this.connectionPromiseState.promise)}on(e,h){this.eventEmitter.on(e,h)}off(e,h){this.eventEmitter.off(e,h)}once(e,h){this.eventEmitter.once(e,h)}emit(e,h){let g={type:e,data:h};if(this.isConnected()){let e=this.connectionHandler.getWebSocket();if(e)try{e.send(encodeClientMessage$1(g))}catch{this.messageQueue.queueMessage(g)}}else this.messageQueue.queueMessage(g)}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.isConnectingRef.value=!1,this.connectionManager.setState(`disconnected`),this.connect()}disconnect(){this.connectionManager.cancelReconnect(),this.isConnectingRef.value=!1,this.connectionPromiseState.reject&&(this.connectionPromiseState.reject(Error(`Connection disconnected`)),this.connectionPromiseState.clear()),this.cleanupWebSocket(),this.connectionManager.setState(`disconnected`)}close(){this.connectionPromiseState.reject&&(this.connectionPromiseState.reject(Error(`Client closed`)),this.connectionPromiseState.clear()),this.disconnect(),this.eventEmitter.clear(),this.messageQueue.clear(),this.connectionManager.destroy()}};export{ConnectionManager,DEFAULT_RECONNECTION_CONFIG,PROTOCOL_VERSION,VeraniClient,decodeClientMessage,decodeFrame,decodeServerMessage,encodeClientMessage,encodeFrame,encodeServerMessage};
package/dist/verani.d.cts CHANGED
@@ -14,6 +14,16 @@ interface BroadcastOptions {
14
14
  /** Only send to specific client IDs */
15
15
  clientIds?: string[];
16
16
  }
17
+ /**
18
+ * RPC-safe version of BroadcastOptions for use over RPC calls.
19
+ * Excludes the `except` field since WebSocket cannot be serialized over RPC.
20
+ */
21
+ interface RpcBroadcastOptions {
22
+ /** Only send to specific user IDs */
23
+ userIds?: string[];
24
+ /** Only send to specific client IDs */
25
+ clientIds?: string[];
26
+ }
17
27
  /**
18
28
  * Extended Actor interface with Verani-specific methods
19
29
  */
@@ -147,10 +157,52 @@ declare function defineRoom<TMeta extends ConnectionMeta = ConnectionMeta>(def:
147
157
  //#endregion
148
158
  //#region src/actor/actor-runtime.d.ts
149
159
  /**
150
- * Actor stub interface returned by .get() method
160
+ * Actor stub interface returned by .get() method.
161
+ * Provides RPC access to actor methods that can be called remotely.
162
+ *
163
+ * Note: RPC methods return Promises even if the underlying method is synchronous.
164
+ * Methods that return non-serializable types (like WebSocket[] or DurableObjectStorage)
165
+ * are excluded from this interface.
151
166
  */
152
167
  interface ActorStub {
168
+ /**
169
+ * Standard fetch method for handling HTTP requests and WebSocket upgrades
170
+ */
153
171
  fetch(request: Request): Promise<Response>;
172
+ /**
173
+ * Sends a message to a specific user (all their sessions) via RPC.
174
+ * @param userId - The user ID to send to
175
+ * @param channel - The channel to send to
176
+ * @param data - Message data
177
+ * @returns Promise resolving to the number of sessions that received the message
178
+ */
179
+ sendToUser(userId: string, channel: string, data?: any): Promise<number>;
180
+ /**
181
+ * Broadcasts a message to all connections in a channel via RPC.
182
+ * Note: The `except` option from BroadcastOptions is not available over RPC
183
+ * since WebSocket cannot be serialized.
184
+ * @param channel - The channel to broadcast to
185
+ * @param data - The data to send
186
+ * @param opts - Broadcast options (filtering by userIds or clientIds)
187
+ * @returns Promise resolving to the number of connections that received the message
188
+ */
189
+ broadcast(channel: string, data: any, opts?: RpcBroadcastOptions): Promise<number>;
190
+ /**
191
+ * Gets the total number of active sessions via RPC.
192
+ * @returns Promise resolving to the number of connected WebSockets
193
+ */
194
+ getSessionCount(): Promise<number>;
195
+ /**
196
+ * Gets all unique user IDs currently connected via RPC.
197
+ * @returns Promise resolving to an array of unique user IDs
198
+ */
199
+ getConnectedUserIds(): Promise<string[]>;
200
+ /**
201
+ * Removes all WebSocket sessions that are not in OPEN state via RPC.
202
+ * This prevents stale connections from accumulating in memory.
203
+ * @returns Promise resolving to the number of sessions cleaned up
204
+ */
205
+ cleanupStaleSessions(): Promise<number>;
154
206
  }
155
207
  /**
156
208
  * Return type for createActorHandler - represents an Actor class constructor
@@ -171,4 +223,4 @@ declare function createActorHandler<TMeta extends ConnectionMeta = ConnectionMet
171
223
  declare function storeAttachment(ws: WebSocket, meta: ConnectionMeta): void;
172
224
  declare function restoreSessions(actor: any): void;
173
225
  //#endregion
174
- export { type ActorHandlerClass, type ActorStub, type BroadcastOptions, type ClientMessage, type ConnectionMeta, type MessageContext, type MessageFrame, PROTOCOL_VERSION, type RoomContext, type RoomDefinition, type ServerMessage, type VeraniActor, type VeraniMessage, createActorHandler, decodeClientMessage, decodeFrame, decodeServerMessage, defineRoom, encodeClientMessage, encodeFrame, encodeServerMessage, restoreSessions, storeAttachment };
226
+ export { type ActorHandlerClass, type ActorStub, type BroadcastOptions, type ClientMessage, type ConnectionMeta, type MessageContext, type MessageFrame, PROTOCOL_VERSION, type RoomContext, type RoomDefinition, type RpcBroadcastOptions, type ServerMessage, type VeraniActor, type VeraniMessage, createActorHandler, decodeClientMessage, decodeFrame, decodeServerMessage, defineRoom, encodeClientMessage, encodeFrame, encodeServerMessage, restoreSessions, storeAttachment };
package/dist/verani.d.mts CHANGED
@@ -14,6 +14,16 @@ interface BroadcastOptions {
14
14
  /** Only send to specific client IDs */
15
15
  clientIds?: string[];
16
16
  }
17
+ /**
18
+ * RPC-safe version of BroadcastOptions for use over RPC calls.
19
+ * Excludes the `except` field since WebSocket cannot be serialized over RPC.
20
+ */
21
+ interface RpcBroadcastOptions {
22
+ /** Only send to specific user IDs */
23
+ userIds?: string[];
24
+ /** Only send to specific client IDs */
25
+ clientIds?: string[];
26
+ }
17
27
  /**
18
28
  * Extended Actor interface with Verani-specific methods
19
29
  */
@@ -147,10 +157,52 @@ declare function defineRoom<TMeta extends ConnectionMeta = ConnectionMeta>(def:
147
157
  //#endregion
148
158
  //#region src/actor/actor-runtime.d.ts
149
159
  /**
150
- * Actor stub interface returned by .get() method
160
+ * Actor stub interface returned by .get() method.
161
+ * Provides RPC access to actor methods that can be called remotely.
162
+ *
163
+ * Note: RPC methods return Promises even if the underlying method is synchronous.
164
+ * Methods that return non-serializable types (like WebSocket[] or DurableObjectStorage)
165
+ * are excluded from this interface.
151
166
  */
152
167
  interface ActorStub {
168
+ /**
169
+ * Standard fetch method for handling HTTP requests and WebSocket upgrades
170
+ */
153
171
  fetch(request: Request): Promise<Response>;
172
+ /**
173
+ * Sends a message to a specific user (all their sessions) via RPC.
174
+ * @param userId - The user ID to send to
175
+ * @param channel - The channel to send to
176
+ * @param data - Message data
177
+ * @returns Promise resolving to the number of sessions that received the message
178
+ */
179
+ sendToUser(userId: string, channel: string, data?: any): Promise<number>;
180
+ /**
181
+ * Broadcasts a message to all connections in a channel via RPC.
182
+ * Note: The `except` option from BroadcastOptions is not available over RPC
183
+ * since WebSocket cannot be serialized.
184
+ * @param channel - The channel to broadcast to
185
+ * @param data - The data to send
186
+ * @param opts - Broadcast options (filtering by userIds or clientIds)
187
+ * @returns Promise resolving to the number of connections that received the message
188
+ */
189
+ broadcast(channel: string, data: any, opts?: RpcBroadcastOptions): Promise<number>;
190
+ /**
191
+ * Gets the total number of active sessions via RPC.
192
+ * @returns Promise resolving to the number of connected WebSockets
193
+ */
194
+ getSessionCount(): Promise<number>;
195
+ /**
196
+ * Gets all unique user IDs currently connected via RPC.
197
+ * @returns Promise resolving to an array of unique user IDs
198
+ */
199
+ getConnectedUserIds(): Promise<string[]>;
200
+ /**
201
+ * Removes all WebSocket sessions that are not in OPEN state via RPC.
202
+ * This prevents stale connections from accumulating in memory.
203
+ * @returns Promise resolving to the number of sessions cleaned up
204
+ */
205
+ cleanupStaleSessions(): Promise<number>;
154
206
  }
155
207
  /**
156
208
  * Return type for createActorHandler - represents an Actor class constructor
@@ -171,4 +223,4 @@ declare function createActorHandler<TMeta extends ConnectionMeta = ConnectionMet
171
223
  declare function storeAttachment(ws: WebSocket, meta: ConnectionMeta): void;
172
224
  declare function restoreSessions(actor: any): void;
173
225
  //#endregion
174
- export { type ActorHandlerClass, type ActorStub, type BroadcastOptions, type ClientMessage, type ConnectionMeta, type MessageContext, type MessageFrame, PROTOCOL_VERSION, type RoomContext, type RoomDefinition, type ServerMessage, type VeraniActor, type VeraniMessage, createActorHandler, decodeClientMessage, decodeFrame, decodeServerMessage, defineRoom, encodeClientMessage, encodeFrame, encodeServerMessage, restoreSessions, storeAttachment };
226
+ export { type ActorHandlerClass, type ActorStub, type BroadcastOptions, type ClientMessage, type ConnectionMeta, type MessageContext, type MessageFrame, PROTOCOL_VERSION, type RoomContext, type RoomDefinition, type RpcBroadcastOptions, type ServerMessage, type VeraniActor, type VeraniMessage, 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.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "A simple, focused realtime SDK for Cloudflare Actors with Socket.io-like semantics",
5
5
  "license": "ISC",
6
6
  "keywords": [