verani 0.4.4 → 0.4.6

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/verani.cjs CHANGED
@@ -1 +1 @@
1
- const require_types=require(`./types-083oWz55.cjs`);let __cloudflare_actors=require(`@cloudflare/actors`);var RoomEventEmitterImpl=class{constructor(){this.handlers=new Map}on(e,x){this.handlers.has(e)||this.handlers.set(e,new Set),this.handlers.get(e).add(x)}off(e,x){let S=this.handlers.get(e);S&&(x?(S.delete(x),S.size===0&&this.handlers.delete(e)):this.handlers.delete(e))}async emit(e,x,S){let C=this.handlers.get(e);if(C&&C.size>0){let e=[];for(let w of C)try{let C=w(x,S);C instanceof Promise&&e.push(C)}catch{}await Promise.all(e)}let w=this.handlers.get(`*`);if(w&&w.size>0){let e=[];for(let C of w)try{let w=C(x,S);w instanceof Promise&&e.push(w)}catch{}await Promise.all(e)}}hasHandlers(e){return this.handlers.has(e)&&this.handlers.get(e).size>0||this.handlers.has(`*`)&&this.handlers.get(`*`).size>0}getEventNames(){return Array.from(this.handlers.keys())}};function createRoomEventEmitter(){return new RoomEventEmitterImpl}function defaultExtractMeta(e){let x=crypto.randomUUID(),S=crypto.randomUUID(),C=new URL(e.url).searchParams.get(`channels`);return{userId:x,clientId:S,channels:C?C.split(`,`).map(e=>e.trim()).filter(Boolean):[`default`]}}function defineRoom(e){let x=e.eventEmitter||createRoomEventEmitter();return{name:e.name,websocketPath:e.websocketPath,extractMeta:e.extractMeta||(e=>defaultExtractMeta(e)),onConnect:e.onConnect,onDisconnect:e.onDisconnect,onMessage:e.onMessage,onError:e.onError,onHibernationRestore:e.onHibernationRestore,eventEmitter:x,on(e,S){x.on(e,S)},off(e,S){x.off(e,S)}}}function cleanupStaleSessions(e){let x=0,S=[];for(let[x,C]of e.entries())x.readyState!==WebSocket.OPEN&&S.push(x);for(let C of S)e.delete(C),x++;return x}function decodeFrame$1(x){return require_types.o(x)??{type:`invalid`}}function encodeFrame$1(x){return require_types.r(x)}function broadcast(e,x,S,C){let w=0,T=encodeFrame$1({type:`event`,channel:x,data:S}),E=[];for(let{ws:S,meta:D}of e.values())if(D.channels.includes(x)&&!(C?.except&&S===C.except)&&!(C?.userIds&&!C.userIds.includes(D.userId))&&!(C?.clientIds&&!C.clientIds.includes(D.clientId))){if(S.readyState!==WebSocket.OPEN){E.push(S);continue}try{S.send(T),w++}catch{E.push(S)}}for(let x of E)e.delete(x);return E.length,w}function sendToUser(e,x,S,C){let w=0,T=encodeFrame$1({type:`event`,channel:S,data:C}),E=[];for(let{ws:C,meta:D}of e.values())if(D.userId===x&&D.channels.includes(S)){if(C.readyState!==WebSocket.OPEN){E.push(C);continue}try{C.send(T),w++}catch{E.push(C)}}for(let x of E)e.delete(x);return E.length,w}function getSessionCount(e){return e.size}function getConnectedUserIds(e){let x=new Set;for(let{meta:S}of e.values())x.add(S.userId);return Array.from(x)}function getUserSessions(e,x){let S=[];for(let{ws:C,meta:w}of e.values())w.userId===x&&S.push(C);return S}function getStorage(e){return e.storage}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 createConfiguration(e){return function(x){return{locationHint:`me`,sockets:{upgradePath:e.websocketPath}}}}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,x){e.serializeAttachment(x)}function restoreSessions(e){let x=0,S=0;for(let C of e.ctx.getWebSockets()){if(C.readyState!==WebSocket.OPEN){S++;continue}let w=C.deserializeAttachment();if(!w){S++;continue}if(!isValidConnectionMeta(w)){S++;continue}e.sessions.set(C,{ws:C,meta:w}),x++}}async function onInit(e,x){try{restoreSessions(e)}catch{}if(x.onHibernationRestore&&e.sessions.size>0)try{await x.onHibernationRestore(e)}catch{}else x.onHibernationRestore&&e.sessions.size}function createUserEmitBuilder(e,x,S){return{emit(C,w){return sendToUser(x,e,S,{type:C,...w})}}}function createChannelEmitBuilder(e,x,S){return{emit(C,w){return broadcast(x,e,{type:C,...w},S)}}}function createSocketEmit(e){let x=e.meta.channels[0]||`default`;return{emit(S,C){if(e.ws.readyState===WebSocket.OPEN)try{let w={type:`event`,channel:x,data:{type:S,...C}};e.ws.send(encodeFrame$1(w))}catch{}},to(S){return e.meta.channels.includes(S)?createChannelEmitBuilder(S,e.actor.sessions,{except:e.ws}):createUserEmitBuilder(S,e.actor.sessions,x)}}}function createActorEmit(e){return{emit(x,S){let C={type:x,...S};return broadcast(e.sessions,`default`,C)},to(x){return createChannelEmitBuilder(x,e.sessions)}}}async function onWebSocketConnect(e,x,S,C){let w;try{w=x.extractMeta?await x.extractMeta(C):{userId:`anonymous`,clientId:crypto.randomUUID(),channels:[`default`]},storeAttachment(S,w);let T={actor:e,ws:S,meta:w,frame:{type:`connect`}};if(x.onConnect){let C={actor:e,ws:S,meta:w,emit:createSocketEmit(T)};await x.onConnect(C)}e.sessions.set(S,{ws:S,meta:w})}catch(C){if(x.onError&&w)try{let T={actor:e,ws:S,meta:w,frame:{type:`error`}};await x.onError(C,{actor:e,ws:S,meta:w,emit:createSocketEmit(T)})}catch{}S.close(1011,`Internal server error`)}}async function onWebSocketMessage(e,x,S,C){let w;try{let T=decodeFrame$1(C);if(T&&T.type===`ping`){if(S.readyState===WebSocket.OPEN)try{S.send(encodeFrame$1({type:`pong`}))}catch{}return}if(!T||T.type===`invalid`||(w=e.sessions.get(S),!w))return;let E={actor:e,ws:S,meta:w.meta,frame:T,emit:createSocketEmit({actor:e,ws:S,meta:w.meta,frame:T})},O=x.eventEmitter;O&&O.hasHandlers&&O.hasHandlers(T.type)?await O.emit(T.type,E,T.data||{}):x.onMessage&&await x.onMessage(E,T)}catch(C){if(x.onError&&w)try{await x.onError(C,{actor:e,ws:S,meta:w.meta,emit:createSocketEmit({actor:e,ws:S,meta:w.meta,frame:{type:`error`}})})}catch{}}}async function onWebSocketDisconnect(e,x,S){try{let C=e.sessions.get(S);if(e.sessions.delete(S),C&&x.onDisconnect){let w={actor:e,ws:S,meta:C.meta,frame:{type:`disconnect`}},T={actor:e,ws:S,meta:C.meta,emit:createSocketEmit(w)};await x.onDisconnect(T)}}catch{}}function createActorHandler(e){let S=sanitizeToClassName(e.name||e.websocketPath||`VeraniActor`);class C extends __cloudflare_actors.Actor{constructor(...e){super(...e),this.sessions=new Map,this.emit=createActorEmit(this)}static{this.configuration=createConfiguration(e)}async shouldUpgradeWebSocket(e){return!0}async fetch(x){let S=new URL(x.url),C=x.headers.get(`Upgrade`);return S.pathname===e.websocketPath&&C===`websocket`&&await this.shouldUpgradeWebSocket(x)?this.onWebSocketUpgrade(x):this.onRequest(x)}async onInit(){await onInit(this,e)}async onWebSocketConnect(x,S){await onWebSocketConnect(this,e,x,S)}async onWebSocketMessage(x,S){await onWebSocketMessage(this,e,x,S)}async onWebSocketDisconnect(x){await onWebSocketDisconnect(this,e,x)}cleanupStaleSessions(){return cleanupStaleSessions(this.sessions)}broadcast(e,x,S){return broadcast(this.sessions,e,x,S)}getSessionCount(){return getSessionCount(this.sessions)}getConnectedUserIds(){return getConnectedUserIds(this.sessions)}getUserSessions(e){return getUserSessions(this.sessions,e)}sendToUser(e,x,S){return sendToUser(this.sessions,e,x,S)}getStorage(){return getStorage(this.ctx)}}return Object.defineProperty(C,`name`,{value:S,writable:!1,configurable:!0}),C}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`);var RoomEventEmitterImpl=class{constructor(){this.handlers=new Map}on(c,T){this.handlers.has(c)||this.handlers.set(c,new Set),this.handlers.get(c).add(T)}off(c,T){let E=this.handlers.get(c);E&&(T?(E.delete(T),E.size===0&&this.handlers.delete(c)):this.handlers.delete(c))}async emit(c,T,E){let D=this.handlers.get(c);if(D&&D.size>0){let c=[];for(let O of D)try{let D=O(T,E);D instanceof Promise&&c.push(D)}catch{}await Promise.all(c)}let O=this.handlers.get(`*`);if(O&&O.size>0){let c=[];for(let D of O)try{let O=D(T,E);O instanceof Promise&&c.push(O)}catch{}await Promise.all(c)}}hasHandlers(c){return this.handlers.has(c)&&this.handlers.get(c).size>0||this.handlers.has(`*`)&&this.handlers.get(`*`).size>0}getEventNames(){return Array.from(this.handlers.keys())}rebuildHandlers(c){this.handlers.clear();for(let[T,E]of c.entries())this.handlers.set(T,new Set(E))}};function createRoomEventEmitter(){return new RoomEventEmitterImpl}function defaultExtractMeta(c){let T=crypto.randomUUID(),E=crypto.randomUUID(),D=new URL(c.url).searchParams.get(`channels`);return{userId:T,clientId:E,channels:D?D.split(`,`).map(c=>c.trim()).filter(Boolean):[`default`]}}function defineRoom(c){let T=c.eventEmitter||createRoomEventEmitter(),E=c._staticHandlers||new Map;return{name:c.name,websocketPath:c.websocketPath,extractMeta:c.extractMeta||(c=>defaultExtractMeta(c)),onConnect:c.onConnect,onDisconnect:c.onDisconnect,onMessage:c.onMessage,onError:c.onError,onHibernationRestore:c.onHibernationRestore,eventEmitter:T,_staticHandlers:E,on(c,D){T.on(c,D),E.has(c)||E.set(c,new Set),E.get(c).add(D)},off(c,D){T.off(c,D);let O=E.get(c);O&&(D?(O.delete(D),O.size===0&&E.delete(c)):E.delete(c))}}}function cleanupStaleSessions(c){let T=0,E=[];for(let[T,D]of c.entries())T.readyState!==WebSocket.OPEN&&E.push(T);for(let D of E)c.delete(D),T++;return T}function decodeFrame$1(T){return require_types.o(T)??{type:`invalid`}}function encodeFrame$1(T){return require_types.r(T)}function broadcast(c,T,E,D){let O=0,k=encodeFrame$1({type:`event`,channel:T,data:E}),A=[];for(let{ws:E,meta:j}of c.values())if(j.channels.includes(T)&&!(D?.except&&E===D.except)&&!(D?.userIds&&!D.userIds.includes(j.userId))&&!(D?.clientIds&&!D.clientIds.includes(j.clientId))){if(E.readyState!==WebSocket.OPEN){A.push(E);continue}try{E.send(k),O++}catch{A.push(E)}}for(let T of A)c.delete(T);return A.length,O}function sendToUser(c,T,E,D){let O=0,k=encodeFrame$1({type:`event`,channel:E,data:D}),A=[];for(let{ws:D,meta:j}of c.values())if(j.userId===T&&j.channels.includes(E)){if(D.readyState!==WebSocket.OPEN){A.push(D);continue}try{D.send(k),O++}catch{A.push(D)}}for(let T of A)c.delete(T);return A.length,O}function getSessionCount(c){return c.size}function getConnectedUserIds(c){let T=new Set;for(let{meta:E}of c.values())T.add(E.userId);return Array.from(T)}function getUserSessions(c,T){let E=[];for(let{ws:D,meta:O}of c.values())O.userId===T&&E.push(D);return E}function getStorage(c){return c.storage}function sanitizeToClassName(c){return c.replace(/^\/+/,``).split(/[-_\/\s]+/).map(c=>c.replace(/[^a-zA-Z0-9]/g,``)).filter(c=>c.length>0).map(c=>c.charAt(0).toUpperCase()+c.slice(1).toLowerCase()).join(``)||`VeraniActor`}function createConfiguration(c){return function(T){return{locationHint:`me`,sockets:{upgradePath:c.websocketPath}}}}function isValidConnectionMeta(c){return!(!c||typeof c!=`object`||typeof c.userId!=`string`||!c.userId||typeof c.clientId!=`string`||!c.clientId||!Array.isArray(c.channels)||!c.channels.every(c=>typeof c==`string`))}function storeAttachment(c,T){c.serializeAttachment(T)}function restoreSessions(c){let T=0,E=0;for(let D of c.ctx.getWebSockets()){if(D.readyState!==WebSocket.OPEN){E++;continue}let O=D.deserializeAttachment();if(!O){E++;continue}if(!isValidConnectionMeta(O)){E++;continue}c.sessions.set(D,{ws:D,meta:O}),T++}}async function onInit(c,T){if(T.eventEmitter&&T._staticHandlers)try{T.eventEmitter.rebuildHandlers(T._staticHandlers)}catch{}try{restoreSessions(c)}catch{}if(T.onHibernationRestore&&c.sessions.size>0)try{await T.onHibernationRestore(c)}catch{}else T.onHibernationRestore&&c.sessions.size}function createUserEmitBuilder(c,T,E){return{emit(D,O){return sendToUser(T,c,E,{type:D,...O})}}}function createChannelEmitBuilder(c,T,E){return{emit(D,O){return broadcast(T,c,{type:D,...O},E)}}}function createSocketEmit(c){let T=c.meta.channels[0]||`default`;return{emit(E,D){if(c.ws.readyState===WebSocket.OPEN)try{let O={type:`event`,channel:T,data:{type:E,...D}};c.ws.send(encodeFrame$1(O))}catch{}},to(E){return c.meta.channels.includes(E)?createChannelEmitBuilder(E,c.actor.sessions,{except:c.ws}):createUserEmitBuilder(E,c.actor.sessions,T)}}}function createActorEmit(c){return{emit(T,E){let D={type:T,...E};return broadcast(c.sessions,`default`,D)},to(T){return createChannelEmitBuilder(T,c.sessions)}}}async function onWebSocketConnect(c,T,E,D){let O;try{O=T.extractMeta?await T.extractMeta(D):{userId:`anonymous`,clientId:crypto.randomUUID(),channels:[`default`]},storeAttachment(E,O);let k={actor:c,ws:E,meta:O,frame:{type:`connect`}};if(T.onConnect){let D={actor:c,ws:E,meta:O,emit:createSocketEmit(k)};await T.onConnect(D)}c.sessions.set(E,{ws:E,meta:O})}catch(D){if(T.onError&&O)try{let k={actor:c,ws:E,meta:O,frame:{type:`error`}};await T.onError(D,{actor:c,ws:E,meta:O,emit:createSocketEmit(k)})}catch{}E.close(1011,`Internal server error`)}}async function onWebSocketMessage(c,T,E,D){let O;try{let k=decodeFrame$1(D);if(k&&k.type===`ping`){if(E.readyState===WebSocket.OPEN)try{E.send(encodeFrame$1({type:`pong`}))}catch{}return}if(!k||k.type===`invalid`||(O=c.sessions.get(E),!O))return;let A={actor:c,ws:E,meta:O.meta,frame:k,emit:createSocketEmit({actor:c,ws:E,meta:O.meta,frame:k})},M=T.eventEmitter;M&&M.hasHandlers&&M.hasHandlers(k.type)?await M.emit(k.type,A,k.data||{}):T.onMessage&&await T.onMessage(A,k)}catch(D){if(T.onError&&O)try{await T.onError(D,{actor:c,ws:E,meta:O.meta,emit:createSocketEmit({actor:c,ws:E,meta:O.meta,frame:{type:`error`}})})}catch{}}}async function onWebSocketDisconnect(c,T,E){try{let D=c.sessions.get(E);if(c.sessions.delete(E),D&&T.onDisconnect){let O={actor:c,ws:E,meta:D.meta,frame:{type:`disconnect`}},k={actor:c,ws:E,meta:D.meta,emit:createSocketEmit(O)};await T.onDisconnect(k)}}catch{}}function createFetch(c,T){return async function(E){let D=new URL(E.url),O=E.headers.get(`Upgrade`);return D.pathname===c.websocketPath&&O===`websocket`&&await T.shouldUpgradeWebSocket(E)?T.onWebSocketUpgrade(E):T.onRequest(E)}}function createRpcChannelEmitBuilder(c,T){return{async emit(E,D){return c.emitToChannel(T,E,D)}}}function createRpcUserEmitBuilder(c,T,E=`default`){return{async emit(E,D){return c.emitToUser(T,E,D)}}}function createRpcEmitBuilder(c,T,E=`default`){let D=!1;for(let{meta:E}of c.sessions.values())if(E.channels.includes(T)){D=!0;break}return D?createRpcChannelEmitBuilder(c,T):createRpcUserEmitBuilder(c,T,E)}function createActorHandler(c){let E=sanitizeToClassName(c.name||c.websocketPath||`VeraniActor`);class D extends __cloudflare_actors.Actor{constructor(...T){super(...T),this.sessions=new Map,this.emit=createActorEmit(this),this.fetch=createFetch(c,this)}static{this.configuration=createConfiguration(c)}async shouldUpgradeWebSocket(c){return!0}async onInit(){await onInit(this,c)}async onWebSocketConnect(T,E){await onWebSocketConnect(this,c,T,E)}async onWebSocketMessage(T,E){await onWebSocketMessage(this,c,T,E)}async onWebSocketDisconnect(T){await onWebSocketDisconnect(this,c,T)}cleanupStaleSessions(){return cleanupStaleSessions(this.sessions)}broadcast(c,T,E){return broadcast(this.sessions,c,T,E)}getSessionCount(){return getSessionCount(this.sessions)}getConnectedUserIds(){return getConnectedUserIds(this.sessions)}getUserSessions(c){return getUserSessions(this.sessions,c)}sendToUser(c,T,E){return sendToUser(this.sessions,c,T,E)}emitToChannel(c,T,E){let D={type:T,...E};return broadcast(this.sessions,c,D)}emitToUser(c,T,E){let D={type:T,...E};return sendToUser(this.sessions,c,`default`,D)}toChannel(c){return createRpcChannelEmitBuilder(this,c)}toUser(c){return createRpcUserEmitBuilder(this,c)}to(c){return createRpcEmitBuilder(this,c)}getStorage(){return getStorage(this.ctx)}}return Object.defineProperty(D,`name`,{value:E,writable:!1,configurable:!0}),D}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
@@ -111,6 +111,13 @@ interface RoomEventEmitter<TMeta extends ConnectionMeta = ConnectionMeta, E = un
111
111
  * @param data - Event data
112
112
  */
113
113
  emit(event: string, ctx: MessageContext<TMeta, E>, data: any): Promise<void>;
114
+ /**
115
+ * Rebuild handlers from static storage.
116
+ * Called after hibernation to restore handlers from the room definition.
117
+ * This method MUST be implemented by all event emitter implementations.
118
+ * @param staticHandlers - Map of event names to handler sets from static storage
119
+ */
120
+ rebuildHandlers(staticHandlers: Map<string, Set<EventHandler<TMeta, E>>>): void;
114
121
  }
115
122
  /**
116
123
  * Builder interface for targeting specific scopes when emitting
@@ -161,6 +168,19 @@ interface ActorEmit<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown>
161
168
  */
162
169
  to(channel: string): EmitBuilder<TMeta, E>;
163
170
  }
171
+ /**
172
+ * RPC-safe emit builder interface for use over RPC calls.
173
+ * Used by ActorStub to provide Socket.IO-like emit API over RPC.
174
+ */
175
+ interface RpcEmitBuilder {
176
+ /**
177
+ * Emit an event to the targeted scope (channel or user)
178
+ * @param event - Event name
179
+ * @param data - Event data
180
+ * @returns Promise resolving to the number of connections that received the message
181
+ */
182
+ emit(event: string, data?: any): Promise<number>;
183
+ }
164
184
  /**
165
185
  * Context provided to room lifecycle hooks
166
186
  */
@@ -237,6 +257,12 @@ interface RoomDefinition<TMeta extends ConnectionMeta = ConnectionMeta, E = unkn
237
257
  * If not provided, a default event emitter will be created.
238
258
  */
239
259
  eventEmitter?: RoomEventEmitter<TMeta, E>;
260
+ /**
261
+ * Static handler storage that persists across hibernation.
262
+ * Handlers registered via room.on() are stored here and rebuilt in onInit.
263
+ * @internal
264
+ */
265
+ _staticHandlers?: Map<string, Set<EventHandler<TMeta, E>>>;
240
266
  }
241
267
  //#endregion
242
268
  //#region src/actor/router.d.ts
@@ -279,6 +305,26 @@ interface ActorStub {
279
305
  */
280
306
  fetch(request: Request): Promise<Response>;
281
307
  /**
308
+ * Socket.IO-like emit API: Get a builder for emitting to a specific channel.
309
+ * Use `toChannel("default").emit(event, data)` to emit to the default channel.
310
+ * @param channel - Channel name
311
+ * @returns Promise resolving to an emit builder
312
+ */
313
+ toChannel(channel: string): Promise<RpcEmitBuilder>;
314
+ /**
315
+ * Socket.IO-like emit API: Get a builder for emitting to a specific user.
316
+ * @param userId - User ID
317
+ * @returns Promise resolving to an emit builder
318
+ */
319
+ toUser(userId: string): Promise<RpcEmitBuilder>;
320
+ /**
321
+ * Socket.IO-like emit API: Get a builder with smart routing (channel or user).
322
+ * @param target - Channel name or user ID
323
+ * @returns Promise resolving to an emit builder
324
+ */
325
+ to(target: string): Promise<RpcEmitBuilder>;
326
+ /**
327
+ * @deprecated Use `emit()` or `toChannel().emit()` instead for Socket.IO-like API.
282
328
  * Sends a message to a specific user (all their sessions) via RPC.
283
329
  * @param userId - The user ID to send to
284
330
  * @param channel - The channel to send to
@@ -287,6 +333,7 @@ interface ActorStub {
287
333
  */
288
334
  sendToUser(userId: string, channel: string, data?: any): Promise<number>;
289
335
  /**
336
+ * @deprecated Use `emit()` or `toChannel().emit()` instead for Socket.IO-like API.
290
337
  * Broadcasts a message to all connections in a channel via RPC.
291
338
  * Note: The `except` option from BroadcastOptions is not available over RPC
292
339
  * since WebSocket cannot be serialized.
package/dist/verani.d.mts CHANGED
@@ -111,6 +111,13 @@ interface RoomEventEmitter<TMeta extends ConnectionMeta = ConnectionMeta, E = un
111
111
  * @param data - Event data
112
112
  */
113
113
  emit(event: string, ctx: MessageContext<TMeta, E>, data: any): Promise<void>;
114
+ /**
115
+ * Rebuild handlers from static storage.
116
+ * Called after hibernation to restore handlers from the room definition.
117
+ * This method MUST be implemented by all event emitter implementations.
118
+ * @param staticHandlers - Map of event names to handler sets from static storage
119
+ */
120
+ rebuildHandlers(staticHandlers: Map<string, Set<EventHandler<TMeta, E>>>): void;
114
121
  }
115
122
  /**
116
123
  * Builder interface for targeting specific scopes when emitting
@@ -161,6 +168,19 @@ interface ActorEmit<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown>
161
168
  */
162
169
  to(channel: string): EmitBuilder<TMeta, E>;
163
170
  }
171
+ /**
172
+ * RPC-safe emit builder interface for use over RPC calls.
173
+ * Used by ActorStub to provide Socket.IO-like emit API over RPC.
174
+ */
175
+ interface RpcEmitBuilder {
176
+ /**
177
+ * Emit an event to the targeted scope (channel or user)
178
+ * @param event - Event name
179
+ * @param data - Event data
180
+ * @returns Promise resolving to the number of connections that received the message
181
+ */
182
+ emit(event: string, data?: any): Promise<number>;
183
+ }
164
184
  /**
165
185
  * Context provided to room lifecycle hooks
166
186
  */
@@ -237,6 +257,12 @@ interface RoomDefinition<TMeta extends ConnectionMeta = ConnectionMeta, E = unkn
237
257
  * If not provided, a default event emitter will be created.
238
258
  */
239
259
  eventEmitter?: RoomEventEmitter<TMeta, E>;
260
+ /**
261
+ * Static handler storage that persists across hibernation.
262
+ * Handlers registered via room.on() are stored here and rebuilt in onInit.
263
+ * @internal
264
+ */
265
+ _staticHandlers?: Map<string, Set<EventHandler<TMeta, E>>>;
240
266
  }
241
267
  //#endregion
242
268
  //#region src/actor/router.d.ts
@@ -279,6 +305,26 @@ interface ActorStub {
279
305
  */
280
306
  fetch(request: Request): Promise<Response>;
281
307
  /**
308
+ * Socket.IO-like emit API: Get a builder for emitting to a specific channel.
309
+ * Use `toChannel("default").emit(event, data)` to emit to the default channel.
310
+ * @param channel - Channel name
311
+ * @returns Promise resolving to an emit builder
312
+ */
313
+ toChannel(channel: string): Promise<RpcEmitBuilder>;
314
+ /**
315
+ * Socket.IO-like emit API: Get a builder for emitting to a specific user.
316
+ * @param userId - User ID
317
+ * @returns Promise resolving to an emit builder
318
+ */
319
+ toUser(userId: string): Promise<RpcEmitBuilder>;
320
+ /**
321
+ * Socket.IO-like emit API: Get a builder with smart routing (channel or user).
322
+ * @param target - Channel name or user ID
323
+ * @returns Promise resolving to an emit builder
324
+ */
325
+ to(target: string): Promise<RpcEmitBuilder>;
326
+ /**
327
+ * @deprecated Use `emit()` or `toChannel().emit()` instead for Socket.IO-like API.
282
328
  * Sends a message to a specific user (all their sessions) via RPC.
283
329
  * @param userId - The user ID to send to
284
330
  * @param channel - The channel to send to
@@ -287,6 +333,7 @@ interface ActorStub {
287
333
  */
288
334
  sendToUser(userId: string, channel: string, data?: any): Promise<number>;
289
335
  /**
336
+ * @deprecated Use `emit()` or `toChannel().emit()` instead for Socket.IO-like API.
290
337
  * Broadcasts a message to all connections in a channel via RPC.
291
338
  * Note: The `except` option from BroadcastOptions is not available over RPC
292
339
  * since WebSocket cannot be serialized.
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";var RoomEventEmitterImpl=class{constructor(){this.handlers=new Map}on(u,O){this.handlers.has(u)||this.handlers.set(u,new Set),this.handlers.get(u).add(O)}off(u,O){let k=this.handlers.get(u);k&&(O?(k.delete(O),k.size===0&&this.handlers.delete(u)):this.handlers.delete(u))}async emit(u,O,k){let A=this.handlers.get(u);if(A&&A.size>0){let u=[];for(let j of A)try{let A=j(O,k);A instanceof Promise&&u.push(A)}catch{}await Promise.all(u)}let j=this.handlers.get(`*`);if(j&&j.size>0){let u=[];for(let A of j)try{let j=A(O,k);j instanceof Promise&&u.push(j)}catch{}await Promise.all(u)}}hasHandlers(u){return this.handlers.has(u)&&this.handlers.get(u).size>0||this.handlers.has(`*`)&&this.handlers.get(`*`).size>0}getEventNames(){return Array.from(this.handlers.keys())}};function createRoomEventEmitter(){return new RoomEventEmitterImpl}function defaultExtractMeta(u){let O=crypto.randomUUID(),k=crypto.randomUUID(),A=new URL(u.url).searchParams.get(`channels`);return{userId:O,clientId:k,channels:A?A.split(`,`).map(u=>u.trim()).filter(Boolean):[`default`]}}function defineRoom(u){let O=u.eventEmitter||createRoomEventEmitter();return{name:u.name,websocketPath:u.websocketPath,extractMeta:u.extractMeta||(u=>defaultExtractMeta(u)),onConnect:u.onConnect,onDisconnect:u.onDisconnect,onMessage:u.onMessage,onError:u.onError,onHibernationRestore:u.onHibernationRestore,eventEmitter:O,on(u,k){O.on(u,k)},off(u,k){O.off(u,k)}}}function cleanupStaleSessions(u){let O=0,k=[];for(let[O,A]of u.entries())O.readyState!==WebSocket.OPEN&&k.push(O);for(let A of k)u.delete(A),O++;return O}function decodeFrame$1(u){return decodeFrame(u)??{type:`invalid`}}function encodeFrame$1(u){return encodeFrame(u)}function broadcast(u,O,k,A){let j=0,M=encodeFrame$1({type:`event`,channel:O,data:k}),N=[];for(let{ws:k,meta:P}of u.values())if(P.channels.includes(O)&&!(A?.except&&k===A.except)&&!(A?.userIds&&!A.userIds.includes(P.userId))&&!(A?.clientIds&&!A.clientIds.includes(P.clientId))){if(k.readyState!==WebSocket.OPEN){N.push(k);continue}try{k.send(M),j++}catch{N.push(k)}}for(let O of N)u.delete(O);return N.length,j}function sendToUser(u,O,k,A){let j=0,M=encodeFrame$1({type:`event`,channel:k,data:A}),N=[];for(let{ws:A,meta:P}of u.values())if(P.userId===O&&P.channels.includes(k)){if(A.readyState!==WebSocket.OPEN){N.push(A);continue}try{A.send(M),j++}catch{N.push(A)}}for(let O of N)u.delete(O);return N.length,j}function getSessionCount(u){return u.size}function getConnectedUserIds(u){let O=new Set;for(let{meta:k}of u.values())O.add(k.userId);return Array.from(O)}function getUserSessions(u,O){let k=[];for(let{ws:A,meta:j}of u.values())j.userId===O&&k.push(A);return k}function getStorage(u){return u.storage}function sanitizeToClassName(u){return u.replace(/^\/+/,``).split(/[-_\/\s]+/).map(u=>u.replace(/[^a-zA-Z0-9]/g,``)).filter(u=>u.length>0).map(u=>u.charAt(0).toUpperCase()+u.slice(1).toLowerCase()).join(``)||`VeraniActor`}function createConfiguration(u){return function(O){return{locationHint:`me`,sockets:{upgradePath:u.websocketPath}}}}function isValidConnectionMeta(u){return!(!u||typeof u!=`object`||typeof u.userId!=`string`||!u.userId||typeof u.clientId!=`string`||!u.clientId||!Array.isArray(u.channels)||!u.channels.every(u=>typeof u==`string`))}function storeAttachment(u,O){u.serializeAttachment(O)}function restoreSessions(u){let O=0,k=0;for(let A of u.ctx.getWebSockets()){if(A.readyState!==WebSocket.OPEN){k++;continue}let j=A.deserializeAttachment();if(!j){k++;continue}if(!isValidConnectionMeta(j)){k++;continue}u.sessions.set(A,{ws:A,meta:j}),O++}}async function onInit(u,O){try{restoreSessions(u)}catch{}if(O.onHibernationRestore&&u.sessions.size>0)try{await O.onHibernationRestore(u)}catch{}else O.onHibernationRestore&&u.sessions.size}function createUserEmitBuilder(u,O,k){return{emit(A,j){return sendToUser(O,u,k,{type:A,...j})}}}function createChannelEmitBuilder(u,O,k){return{emit(A,j){return broadcast(O,u,{type:A,...j},k)}}}function createSocketEmit(u){let O=u.meta.channels[0]||`default`;return{emit(k,A){if(u.ws.readyState===WebSocket.OPEN)try{let j={type:`event`,channel:O,data:{type:k,...A}};u.ws.send(encodeFrame$1(j))}catch{}},to(k){return u.meta.channels.includes(k)?createChannelEmitBuilder(k,u.actor.sessions,{except:u.ws}):createUserEmitBuilder(k,u.actor.sessions,O)}}}function createActorEmit(u){return{emit(O,k){let A={type:O,...k};return broadcast(u.sessions,`default`,A)},to(O){return createChannelEmitBuilder(O,u.sessions)}}}async function onWebSocketConnect(u,O,k,A){let j;try{j=O.extractMeta?await O.extractMeta(A):{userId:`anonymous`,clientId:crypto.randomUUID(),channels:[`default`]},storeAttachment(k,j);let M={actor:u,ws:k,meta:j,frame:{type:`connect`}};if(O.onConnect){let A={actor:u,ws:k,meta:j,emit:createSocketEmit(M)};await O.onConnect(A)}u.sessions.set(k,{ws:k,meta:j})}catch(A){if(O.onError&&j)try{let M={actor:u,ws:k,meta:j,frame:{type:`error`}};await O.onError(A,{actor:u,ws:k,meta:j,emit:createSocketEmit(M)})}catch{}k.close(1011,`Internal server error`)}}async function onWebSocketMessage(u,O,k,A){let j;try{let M=decodeFrame$1(A);if(M&&M.type===`ping`){if(k.readyState===WebSocket.OPEN)try{k.send(encodeFrame$1({type:`pong`}))}catch{}return}if(!M||M.type===`invalid`||(j=u.sessions.get(k),!j))return;let N={actor:u,ws:k,meta:j.meta,frame:M,emit:createSocketEmit({actor:u,ws:k,meta:j.meta,frame:M})},P=O.eventEmitter;P&&P.hasHandlers&&P.hasHandlers(M.type)?await P.emit(M.type,N,M.data||{}):O.onMessage&&await O.onMessage(N,M)}catch(A){if(O.onError&&j)try{await O.onError(A,{actor:u,ws:k,meta:j.meta,emit:createSocketEmit({actor:u,ws:k,meta:j.meta,frame:{type:`error`}})})}catch{}}}async function onWebSocketDisconnect(u,O,k){try{let A=u.sessions.get(k);if(u.sessions.delete(k),A&&O.onDisconnect){let j={actor:u,ws:k,meta:A.meta,frame:{type:`disconnect`}},M={actor:u,ws:k,meta:A.meta,emit:createSocketEmit(j)};await O.onDisconnect(M)}}catch{}}function createActorHandler(u){let O=sanitizeToClassName(u.name||u.websocketPath||`VeraniActor`);class k extends Actor{constructor(...u){super(...u),this.sessions=new Map,this.emit=createActorEmit(this)}static{this.configuration=createConfiguration(u)}async shouldUpgradeWebSocket(u){return!0}async fetch(O){let k=new URL(O.url),A=O.headers.get(`Upgrade`);return k.pathname===u.websocketPath&&A===`websocket`&&await this.shouldUpgradeWebSocket(O)?this.onWebSocketUpgrade(O):this.onRequest(O)}async onInit(){await onInit(this,u)}async onWebSocketConnect(O,k){await onWebSocketConnect(this,u,O,k)}async onWebSocketMessage(O,k){await onWebSocketMessage(this,u,O,k)}async onWebSocketDisconnect(O){await onWebSocketDisconnect(this,u,O)}cleanupStaleSessions(){return cleanupStaleSessions(this.sessions)}broadcast(u,O,k){return broadcast(this.sessions,u,O,k)}getSessionCount(){return getSessionCount(this.sessions)}getConnectedUserIds(){return getConnectedUserIds(this.sessions)}getUserSessions(u){return getUserSessions(this.sessions,u)}sendToUser(u,O,k){return sendToUser(this.sessions,u,O,k)}getStorage(){return getStorage(this.ctx)}}return Object.defineProperty(k,`name`,{value:O,writable:!1,configurable:!0}),k}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";var RoomEventEmitterImpl=class{constructor(){this.handlers=new Map}on(u,M){this.handlers.has(u)||this.handlers.set(u,new Set),this.handlers.get(u).add(M)}off(u,M){let N=this.handlers.get(u);N&&(M?(N.delete(M),N.size===0&&this.handlers.delete(u)):this.handlers.delete(u))}async emit(u,M,N){let P=this.handlers.get(u);if(P&&P.size>0){let u=[];for(let F of P)try{let P=F(M,N);P instanceof Promise&&u.push(P)}catch{}await Promise.all(u)}let F=this.handlers.get(`*`);if(F&&F.size>0){let u=[];for(let P of F)try{let F=P(M,N);F instanceof Promise&&u.push(F)}catch{}await Promise.all(u)}}hasHandlers(u){return this.handlers.has(u)&&this.handlers.get(u).size>0||this.handlers.has(`*`)&&this.handlers.get(`*`).size>0}getEventNames(){return Array.from(this.handlers.keys())}rebuildHandlers(u){this.handlers.clear();for(let[M,N]of u.entries())this.handlers.set(M,new Set(N))}};function createRoomEventEmitter(){return new RoomEventEmitterImpl}function defaultExtractMeta(u){let M=crypto.randomUUID(),N=crypto.randomUUID(),P=new URL(u.url).searchParams.get(`channels`);return{userId:M,clientId:N,channels:P?P.split(`,`).map(u=>u.trim()).filter(Boolean):[`default`]}}function defineRoom(u){let M=u.eventEmitter||createRoomEventEmitter(),N=u._staticHandlers||new Map;return{name:u.name,websocketPath:u.websocketPath,extractMeta:u.extractMeta||(u=>defaultExtractMeta(u)),onConnect:u.onConnect,onDisconnect:u.onDisconnect,onMessage:u.onMessage,onError:u.onError,onHibernationRestore:u.onHibernationRestore,eventEmitter:M,_staticHandlers:N,on(u,P){M.on(u,P),N.has(u)||N.set(u,new Set),N.get(u).add(P)},off(u,P){M.off(u,P);let F=N.get(u);F&&(P?(F.delete(P),F.size===0&&N.delete(u)):N.delete(u))}}}function cleanupStaleSessions(u){let M=0,N=[];for(let[M,P]of u.entries())M.readyState!==WebSocket.OPEN&&N.push(M);for(let P of N)u.delete(P),M++;return M}function decodeFrame$1(u){return decodeFrame(u)??{type:`invalid`}}function encodeFrame$1(u){return encodeFrame(u)}function broadcast(u,M,N,P){let F=0,I=encodeFrame$1({type:`event`,channel:M,data:N}),L=[];for(let{ws:N,meta:R}of u.values())if(R.channels.includes(M)&&!(P?.except&&N===P.except)&&!(P?.userIds&&!P.userIds.includes(R.userId))&&!(P?.clientIds&&!P.clientIds.includes(R.clientId))){if(N.readyState!==WebSocket.OPEN){L.push(N);continue}try{N.send(I),F++}catch{L.push(N)}}for(let M of L)u.delete(M);return L.length,F}function sendToUser(u,M,N,P){let F=0,I=encodeFrame$1({type:`event`,channel:N,data:P}),L=[];for(let{ws:P,meta:R}of u.values())if(R.userId===M&&R.channels.includes(N)){if(P.readyState!==WebSocket.OPEN){L.push(P);continue}try{P.send(I),F++}catch{L.push(P)}}for(let M of L)u.delete(M);return L.length,F}function getSessionCount(u){return u.size}function getConnectedUserIds(u){let M=new Set;for(let{meta:N}of u.values())M.add(N.userId);return Array.from(M)}function getUserSessions(u,M){let N=[];for(let{ws:P,meta:F}of u.values())F.userId===M&&N.push(P);return N}function getStorage(u){return u.storage}function sanitizeToClassName(u){return u.replace(/^\/+/,``).split(/[-_\/\s]+/).map(u=>u.replace(/[^a-zA-Z0-9]/g,``)).filter(u=>u.length>0).map(u=>u.charAt(0).toUpperCase()+u.slice(1).toLowerCase()).join(``)||`VeraniActor`}function createConfiguration(u){return function(M){return{locationHint:`me`,sockets:{upgradePath:u.websocketPath}}}}function isValidConnectionMeta(u){return!(!u||typeof u!=`object`||typeof u.userId!=`string`||!u.userId||typeof u.clientId!=`string`||!u.clientId||!Array.isArray(u.channels)||!u.channels.every(u=>typeof u==`string`))}function storeAttachment(u,M){u.serializeAttachment(M)}function restoreSessions(u){let M=0,N=0;for(let P of u.ctx.getWebSockets()){if(P.readyState!==WebSocket.OPEN){N++;continue}let F=P.deserializeAttachment();if(!F){N++;continue}if(!isValidConnectionMeta(F)){N++;continue}u.sessions.set(P,{ws:P,meta:F}),M++}}async function onInit(u,M){if(M.eventEmitter&&M._staticHandlers)try{M.eventEmitter.rebuildHandlers(M._staticHandlers)}catch{}try{restoreSessions(u)}catch{}if(M.onHibernationRestore&&u.sessions.size>0)try{await M.onHibernationRestore(u)}catch{}else M.onHibernationRestore&&u.sessions.size}function createUserEmitBuilder(u,M,N){return{emit(P,F){return sendToUser(M,u,N,{type:P,...F})}}}function createChannelEmitBuilder(u,M,N){return{emit(P,F){return broadcast(M,u,{type:P,...F},N)}}}function createSocketEmit(u){let M=u.meta.channels[0]||`default`;return{emit(N,P){if(u.ws.readyState===WebSocket.OPEN)try{let F={type:`event`,channel:M,data:{type:N,...P}};u.ws.send(encodeFrame$1(F))}catch{}},to(N){return u.meta.channels.includes(N)?createChannelEmitBuilder(N,u.actor.sessions,{except:u.ws}):createUserEmitBuilder(N,u.actor.sessions,M)}}}function createActorEmit(u){return{emit(M,N){let P={type:M,...N};return broadcast(u.sessions,`default`,P)},to(M){return createChannelEmitBuilder(M,u.sessions)}}}async function onWebSocketConnect(u,M,N,P){let F;try{F=M.extractMeta?await M.extractMeta(P):{userId:`anonymous`,clientId:crypto.randomUUID(),channels:[`default`]},storeAttachment(N,F);let I={actor:u,ws:N,meta:F,frame:{type:`connect`}};if(M.onConnect){let P={actor:u,ws:N,meta:F,emit:createSocketEmit(I)};await M.onConnect(P)}u.sessions.set(N,{ws:N,meta:F})}catch(P){if(M.onError&&F)try{let I={actor:u,ws:N,meta:F,frame:{type:`error`}};await M.onError(P,{actor:u,ws:N,meta:F,emit:createSocketEmit(I)})}catch{}N.close(1011,`Internal server error`)}}async function onWebSocketMessage(u,M,N,P){let F;try{let I=decodeFrame$1(P);if(I&&I.type===`ping`){if(N.readyState===WebSocket.OPEN)try{N.send(encodeFrame$1({type:`pong`}))}catch{}return}if(!I||I.type===`invalid`||(F=u.sessions.get(N),!F))return;let L={actor:u,ws:N,meta:F.meta,frame:I,emit:createSocketEmit({actor:u,ws:N,meta:F.meta,frame:I})},R=M.eventEmitter;R&&R.hasHandlers&&R.hasHandlers(I.type)?await R.emit(I.type,L,I.data||{}):M.onMessage&&await M.onMessage(L,I)}catch(P){if(M.onError&&F)try{await M.onError(P,{actor:u,ws:N,meta:F.meta,emit:createSocketEmit({actor:u,ws:N,meta:F.meta,frame:{type:`error`}})})}catch{}}}async function onWebSocketDisconnect(u,M,N){try{let P=u.sessions.get(N);if(u.sessions.delete(N),P&&M.onDisconnect){let F={actor:u,ws:N,meta:P.meta,frame:{type:`disconnect`}},I={actor:u,ws:N,meta:P.meta,emit:createSocketEmit(F)};await M.onDisconnect(I)}}catch{}}function createFetch(u,M){return async function(N){let P=new URL(N.url),F=N.headers.get(`Upgrade`);return P.pathname===u.websocketPath&&F===`websocket`&&await M.shouldUpgradeWebSocket(N)?M.onWebSocketUpgrade(N):M.onRequest(N)}}function createRpcChannelEmitBuilder(u,M){return{async emit(N,P){return u.emitToChannel(M,N,P)}}}function createRpcUserEmitBuilder(u,M,N=`default`){return{async emit(N,P){return u.emitToUser(M,N,P)}}}function createRpcEmitBuilder(u,M,N=`default`){let P=!1;for(let{meta:N}of u.sessions.values())if(N.channels.includes(M)){P=!0;break}return P?createRpcChannelEmitBuilder(u,M):createRpcUserEmitBuilder(u,M,N)}function createActorHandler(u){let M=sanitizeToClassName(u.name||u.websocketPath||`VeraniActor`);class N extends Actor{constructor(...M){super(...M),this.sessions=new Map,this.emit=createActorEmit(this),this.fetch=createFetch(u,this)}static{this.configuration=createConfiguration(u)}async shouldUpgradeWebSocket(u){return!0}async onInit(){await onInit(this,u)}async onWebSocketConnect(M,N){await onWebSocketConnect(this,u,M,N)}async onWebSocketMessage(M,N){await onWebSocketMessage(this,u,M,N)}async onWebSocketDisconnect(M){await onWebSocketDisconnect(this,u,M)}cleanupStaleSessions(){return cleanupStaleSessions(this.sessions)}broadcast(u,M,N){return broadcast(this.sessions,u,M,N)}getSessionCount(){return getSessionCount(this.sessions)}getConnectedUserIds(){return getConnectedUserIds(this.sessions)}getUserSessions(u){return getUserSessions(this.sessions,u)}sendToUser(u,M,N){return sendToUser(this.sessions,u,M,N)}emitToChannel(u,M,N){let P={type:M,...N};return broadcast(this.sessions,u,P)}emitToUser(u,M,N){let P={type:M,...N};return sendToUser(this.sessions,u,`default`,P)}toChannel(u){return createRpcChannelEmitBuilder(this,u)}toUser(u){return createRpcUserEmitBuilder(this,u)}to(u){return createRpcEmitBuilder(this,u)}getStorage(){return getStorage(this.ctx)}}return Object.defineProperty(N,`name`,{value:M,writable:!1,configurable:!0}),N}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.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "A simple, focused realtime SDK for Cloudflare Actors with Socket.io-like semantics",
5
5
  "license": "ISC",
6
6
  "keywords": [