verani 0.3.1 → 0.4.0

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`);function defaultExtractMeta(e){let g=crypto.randomUUID(),_=crypto.randomUUID(),v=new URL(e.url).searchParams.get(`channels`);return{userId:g,clientId:_,channels:v?v.split(`,`).map(e=>e.trim()).filter(Boolean):[`default`]}}function defineRoom(e){return{name:e.name,websocketPath:e.websocketPath,extractMeta:e.extractMeta||defaultExtractMeta,onConnect:e.onConnect,onDisconnect:e.onDisconnect,onMessage:e.onMessage,onError:e.onError}}function cleanupStaleSessions(e){let g=0,_=[];for(let[g,v]of e.entries())g.readyState!==WebSocket.OPEN&&_.push(g);for(let v of _)e.delete(v),g++;return g}function decodeFrame$1(g){return require_types.o(g)??{type:`invalid`}}function encodeFrame$1(g){return require_types.r(g)}function broadcast(e,g,_,v){let y=0,b=encodeFrame$1({type:`event`,channel:g,data:_}),S=[];for(let{ws:_,meta:x}of e.values())if(x.channels.includes(g)&&!(v?.except&&_===v.except)&&!(v?.userIds&&!v.userIds.includes(x.userId))&&!(v?.clientIds&&!v.clientIds.includes(x.clientId))){if(_.readyState!==WebSocket.OPEN){S.push(_);continue}try{_.send(b),y++}catch{S.push(_)}}for(let g of S)e.delete(g);return S.length,y}function sendToUser(e,g,_,v){let y=0,b=encodeFrame$1({type:`event`,channel:_,data:v}),S=[];for(let{ws:v,meta:x}of e.values())if(x.userId===g&&x.channels.includes(_)){if(v.readyState!==WebSocket.OPEN){S.push(v);continue}try{v.send(b),y++}catch{S.push(v)}}for(let g of S)e.delete(g);return S.length,y}function getSessionCount(e){return e.size}function getConnectedUserIds(e){let g=new Set;for(let{meta:_}of e.values())g.add(_.userId);return Array.from(g)}function getUserSessions(e,g){let _=[];for(let{ws:v,meta:y}of e.values())y.userId===g&&_.push(v);return _}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(g){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,g){e.serializeAttachment(g)}function restoreSessions(e){let g=0,_=0;for(let v of e.ctx.getWebSockets()){if(v.readyState!==WebSocket.OPEN){_++;continue}let y=v.deserializeAttachment();if(!y){_++;continue}if(!isValidConnectionMeta(y)){_++;continue}e.sessions.set(v,{ws:v,meta:y}),g++}}async function onInit(e,g){try{restoreSessions(e)}catch{}if(g.onHibernationRestore&&e.sessions.size>0)try{await g.onHibernationRestore(e)}catch{}else g.onHibernationRestore&&e.sessions.size}async function onWebSocketConnect(e,g,_,v){let y;try{if(y=g.extractMeta?await g.extractMeta(v):{userId:`anonymous`,clientId:crypto.randomUUID(),channels:[`default`]},storeAttachment(_,y),g.onConnect){let v={actor:e,ws:_,meta:y};await g.onConnect(v)}e.sessions.set(_,{ws:_,meta:y})}catch(v){if(g.onError&&y)try{await g.onError(v,{actor:e,ws:_,meta:y})}catch{}_.close(1011,`Internal server error`)}}async function onWebSocketMessage(e,g,_,v){let y;try{let S=decodeFrame$1(v);if(S&&S.type===`ping`){if(_.readyState===WebSocket.OPEN)try{_.send(encodeFrame$1({type:`pong`}))}catch{}return}if(!S||S.type===`invalid`||(y=e.sessions.get(_),!y))return;if(g.onMessage){let v={actor:e,ws:_,meta:y.meta,frame:S};await g.onMessage(v,S)}}catch(v){if(g.onError&&y)try{await g.onError(v,{actor:e,ws:_,meta:y.meta})}catch{}}}async function onWebSocketDisconnect(e,g,_){try{let v=e.sessions.get(_);if(e.sessions.delete(_),v&&g.onDisconnect){let y={actor:e,ws:_,meta:v.meta};await g.onDisconnect(y)}}catch{}}function createActorHandler(e){let _=sanitizeToClassName(e.name||e.websocketPath||`VeraniActor`);class v extends __cloudflare_actors.Actor{constructor(...e){super(...e),this.sessions=new Map}static{this.configuration=createConfiguration(e)}async shouldUpgradeWebSocket(e){return!0}async fetch(g){let _=new URL(g.url),v=g.headers.get(`Upgrade`);return _.pathname===e.websocketPath&&v===`websocket`&&await this.shouldUpgradeWebSocket(g)?this.onWebSocketUpgrade(g):this.onRequest(g)}async onInit(){await onInit(this,e)}async onWebSocketConnect(g,_){await onWebSocketConnect(this,e,g,_)}async onWebSocketMessage(g,_){await onWebSocketMessage(this,e,g,_)}async onWebSocketDisconnect(g){await onWebSocketDisconnect(this,e,g)}cleanupStaleSessions(){return cleanupStaleSessions(this.sessions)}broadcast(e,g,_){return broadcast(this.sessions,e,g,_)}getSessionCount(){return getSessionCount(this.sessions)}getConnectedUserIds(){return getConnectedUserIds(this.sessions)}getUserSessions(e){return getUserSessions(this.sessions,e)}sendToUser(e,g,_){return sendToUser(this.sessions,e,g,_)}getStorage(){return getStorage(this.ctx)}}return Object.defineProperty(v,`name`,{value:_,writable:!1,configurable:!0}),v}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(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||defaultExtractMeta,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;
package/dist/verani.d.cts CHANGED
@@ -79,6 +79,87 @@ interface VeraniActor<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown
79
79
  * @see @src/actor/actor-runtime.ts getStorage()
80
80
  */
81
81
  getStorage(): DurableObjectStorage;
82
+ /**
83
+ * Socket.io-like emit API for actor-level broadcasting
84
+ */
85
+ emit: ActorEmit<TMeta, E>;
86
+ }
87
+ /**
88
+ * Event handler function type for socket.io-like event handling
89
+ */
90
+ type EventHandler<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> = (ctx: MessageContext<TMeta, E>, data: any) => void | Promise<void>;
91
+ /**
92
+ * Event emitter interface for room-level event handling
93
+ */
94
+ interface RoomEventEmitter<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> {
95
+ /**
96
+ * Register an event handler
97
+ * @param event - Event name (supports wildcard "*")
98
+ * @param handler - Handler function
99
+ */
100
+ on(event: string, handler: EventHandler<TMeta, E>): void;
101
+ /**
102
+ * Remove an event handler
103
+ * @param event - Event name
104
+ * @param handler - Optional specific handler to remove, or remove all handlers for event
105
+ */
106
+ off(event: string, handler?: EventHandler<TMeta, E>): void;
107
+ /**
108
+ * Emit an event to registered handlers
109
+ * @param event - Event name
110
+ * @param ctx - Message context
111
+ * @param data - Event data
112
+ */
113
+ emit(event: string, ctx: MessageContext<TMeta, E>, data: any): Promise<void>;
114
+ }
115
+ /**
116
+ * Builder interface for targeting specific scopes when emitting
117
+ */
118
+ interface EmitBuilder<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> {
119
+ /**
120
+ * Emit to the targeted scope
121
+ * @param event - Event name
122
+ * @param data - Event data
123
+ * @returns Number of connections that received the message
124
+ */
125
+ emit(event: string, data?: any): number;
126
+ }
127
+ /**
128
+ * Socket-level emit API (available on context)
129
+ * Allows emitting to current socket, user, or channel
130
+ */
131
+ interface SocketEmit<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> {
132
+ /**
133
+ * Emit to the current socket
134
+ * @param event - Event name
135
+ * @param data - Event data
136
+ */
137
+ emit(event: string, data?: any): void;
138
+ /**
139
+ * Target a specific user or channel for emitting
140
+ * @param target - User ID or channel name
141
+ * @returns Builder for emitting to the target
142
+ */
143
+ to(target: string): EmitBuilder<TMeta, E>;
144
+ }
145
+ /**
146
+ * Actor-level emit API (available on actor)
147
+ * Allows broadcasting to channels
148
+ */
149
+ interface ActorEmit<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> {
150
+ /**
151
+ * Broadcast to default channel
152
+ * @param event - Event name
153
+ * @param data - Event data
154
+ * @returns Number of connections that received the message
155
+ */
156
+ emit(event: string, data?: any): number;
157
+ /**
158
+ * Target a specific channel for broadcasting
159
+ * @param channel - Channel name
160
+ * @returns Builder for emitting to the channel
161
+ */
162
+ to(channel: string): EmitBuilder<TMeta, E>;
82
163
  }
83
164
  /**
84
165
  * Context provided to room lifecycle hooks
@@ -90,6 +171,8 @@ interface RoomContext<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown
90
171
  ws: WebSocket;
91
172
  /** Connection metadata */
92
173
  meta: TMeta;
174
+ /** Socket.io-like emit API for this connection */
175
+ emit: SocketEmit<TMeta, E>;
93
176
  }
94
177
  /**
95
178
  * Context for onMessage hook with frame included
@@ -132,6 +215,9 @@ interface RoomDefinition<TMeta extends ConnectionMeta = ConnectionMeta, E = unkn
132
215
  * Called when a message is received from a connection.
133
216
  * This hook is awaited if it returns a Promise. The actor will not process
134
217
  * other messages from this connection until this hook completes.
218
+ *
219
+ * **Note:** If event handlers are registered via `eventEmitter`, they take priority.
220
+ * This hook is used as a fallback when no matching event handler is found.
135
221
  */
136
222
  onMessage?(ctx: MessageContext<TMeta, E>, frame: MessageFrame): void | Promise<void>;
137
223
  /**
@@ -145,15 +231,38 @@ interface RoomDefinition<TMeta extends ConnectionMeta = ConnectionMeta, E = unkn
145
231
  * sessions failed to restore, allowing you to handle partial restoration scenarios.
146
232
  */
147
233
  onHibernationRestore?(actor: VeraniActor<TMeta, E>): void | Promise<void>;
234
+ /**
235
+ * Event emitter for socket.io-like event handling.
236
+ * If provided, event handlers registered here will be called for matching message types.
237
+ * If not provided, a default event emitter will be created.
238
+ */
239
+ eventEmitter?: RoomEventEmitter<TMeta, E>;
148
240
  }
149
241
  //#endregion
150
242
  //#region src/actor/router.d.ts
243
+ /**
244
+ * Extended room definition with socket.io-like convenience methods
245
+ */
246
+ interface RoomDefinitionWithHandlers<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> extends RoomDefinition<TMeta, E> {
247
+ /**
248
+ * Register an event handler (socket.io-like API)
249
+ * @param event - Event name
250
+ * @param handler - Handler function
251
+ */
252
+ on(event: string, handler: (ctx: any, data: any) => void | Promise<void>): void;
253
+ /**
254
+ * Remove an event handler (socket.io-like API)
255
+ * @param event - Event name
256
+ * @param handler - Optional specific handler to remove
257
+ */
258
+ off(event: string, handler?: (ctx: any, data: any) => void | Promise<void>): void;
259
+ }
151
260
  /**
152
261
  * Defines a room with lifecycle hooks and metadata extraction
153
262
  * @param def - Room definition with optional hooks
154
- * @returns Normalized room definition with defaults
263
+ * @returns Normalized room definition with defaults and socket.io-like event handler methods
155
264
  */
156
- declare function defineRoom<TMeta extends ConnectionMeta = ConnectionMeta>(def: RoomDefinition<TMeta>): RoomDefinition<TMeta>;
265
+ declare function defineRoom<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown>(def: RoomDefinition<TMeta, E>): RoomDefinitionWithHandlers<TMeta, E>;
157
266
  //#endregion
158
267
  //#region src/actor/actor-runtime.d.ts
159
268
  /**
package/dist/verani.d.mts CHANGED
@@ -79,6 +79,87 @@ interface VeraniActor<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown
79
79
  * @see @src/actor/actor-runtime.ts getStorage()
80
80
  */
81
81
  getStorage(): DurableObjectStorage;
82
+ /**
83
+ * Socket.io-like emit API for actor-level broadcasting
84
+ */
85
+ emit: ActorEmit<TMeta, E>;
86
+ }
87
+ /**
88
+ * Event handler function type for socket.io-like event handling
89
+ */
90
+ type EventHandler<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> = (ctx: MessageContext<TMeta, E>, data: any) => void | Promise<void>;
91
+ /**
92
+ * Event emitter interface for room-level event handling
93
+ */
94
+ interface RoomEventEmitter<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> {
95
+ /**
96
+ * Register an event handler
97
+ * @param event - Event name (supports wildcard "*")
98
+ * @param handler - Handler function
99
+ */
100
+ on(event: string, handler: EventHandler<TMeta, E>): void;
101
+ /**
102
+ * Remove an event handler
103
+ * @param event - Event name
104
+ * @param handler - Optional specific handler to remove, or remove all handlers for event
105
+ */
106
+ off(event: string, handler?: EventHandler<TMeta, E>): void;
107
+ /**
108
+ * Emit an event to registered handlers
109
+ * @param event - Event name
110
+ * @param ctx - Message context
111
+ * @param data - Event data
112
+ */
113
+ emit(event: string, ctx: MessageContext<TMeta, E>, data: any): Promise<void>;
114
+ }
115
+ /**
116
+ * Builder interface for targeting specific scopes when emitting
117
+ */
118
+ interface EmitBuilder<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> {
119
+ /**
120
+ * Emit to the targeted scope
121
+ * @param event - Event name
122
+ * @param data - Event data
123
+ * @returns Number of connections that received the message
124
+ */
125
+ emit(event: string, data?: any): number;
126
+ }
127
+ /**
128
+ * Socket-level emit API (available on context)
129
+ * Allows emitting to current socket, user, or channel
130
+ */
131
+ interface SocketEmit<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> {
132
+ /**
133
+ * Emit to the current socket
134
+ * @param event - Event name
135
+ * @param data - Event data
136
+ */
137
+ emit(event: string, data?: any): void;
138
+ /**
139
+ * Target a specific user or channel for emitting
140
+ * @param target - User ID or channel name
141
+ * @returns Builder for emitting to the target
142
+ */
143
+ to(target: string): EmitBuilder<TMeta, E>;
144
+ }
145
+ /**
146
+ * Actor-level emit API (available on actor)
147
+ * Allows broadcasting to channels
148
+ */
149
+ interface ActorEmit<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> {
150
+ /**
151
+ * Broadcast to default channel
152
+ * @param event - Event name
153
+ * @param data - Event data
154
+ * @returns Number of connections that received the message
155
+ */
156
+ emit(event: string, data?: any): number;
157
+ /**
158
+ * Target a specific channel for broadcasting
159
+ * @param channel - Channel name
160
+ * @returns Builder for emitting to the channel
161
+ */
162
+ to(channel: string): EmitBuilder<TMeta, E>;
82
163
  }
83
164
  /**
84
165
  * Context provided to room lifecycle hooks
@@ -90,6 +171,8 @@ interface RoomContext<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown
90
171
  ws: WebSocket;
91
172
  /** Connection metadata */
92
173
  meta: TMeta;
174
+ /** Socket.io-like emit API for this connection */
175
+ emit: SocketEmit<TMeta, E>;
93
176
  }
94
177
  /**
95
178
  * Context for onMessage hook with frame included
@@ -132,6 +215,9 @@ interface RoomDefinition<TMeta extends ConnectionMeta = ConnectionMeta, E = unkn
132
215
  * Called when a message is received from a connection.
133
216
  * This hook is awaited if it returns a Promise. The actor will not process
134
217
  * other messages from this connection until this hook completes.
218
+ *
219
+ * **Note:** If event handlers are registered via `eventEmitter`, they take priority.
220
+ * This hook is used as a fallback when no matching event handler is found.
135
221
  */
136
222
  onMessage?(ctx: MessageContext<TMeta, E>, frame: MessageFrame): void | Promise<void>;
137
223
  /**
@@ -145,15 +231,38 @@ interface RoomDefinition<TMeta extends ConnectionMeta = ConnectionMeta, E = unkn
145
231
  * sessions failed to restore, allowing you to handle partial restoration scenarios.
146
232
  */
147
233
  onHibernationRestore?(actor: VeraniActor<TMeta, E>): void | Promise<void>;
234
+ /**
235
+ * Event emitter for socket.io-like event handling.
236
+ * If provided, event handlers registered here will be called for matching message types.
237
+ * If not provided, a default event emitter will be created.
238
+ */
239
+ eventEmitter?: RoomEventEmitter<TMeta, E>;
148
240
  }
149
241
  //#endregion
150
242
  //#region src/actor/router.d.ts
243
+ /**
244
+ * Extended room definition with socket.io-like convenience methods
245
+ */
246
+ interface RoomDefinitionWithHandlers<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> extends RoomDefinition<TMeta, E> {
247
+ /**
248
+ * Register an event handler (socket.io-like API)
249
+ * @param event - Event name
250
+ * @param handler - Handler function
251
+ */
252
+ on(event: string, handler: (ctx: any, data: any) => void | Promise<void>): void;
253
+ /**
254
+ * Remove an event handler (socket.io-like API)
255
+ * @param event - Event name
256
+ * @param handler - Optional specific handler to remove
257
+ */
258
+ off(event: string, handler?: (ctx: any, data: any) => void | Promise<void>): void;
259
+ }
151
260
  /**
152
261
  * Defines a room with lifecycle hooks and metadata extraction
153
262
  * @param def - Room definition with optional hooks
154
- * @returns Normalized room definition with defaults
263
+ * @returns Normalized room definition with defaults and socket.io-like event handler methods
155
264
  */
156
- declare function defineRoom<TMeta extends ConnectionMeta = ConnectionMeta>(def: RoomDefinition<TMeta>): RoomDefinition<TMeta>;
265
+ declare function defineRoom<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown>(def: RoomDefinition<TMeta, E>): RoomDefinitionWithHandlers<TMeta, E>;
157
266
  //#endregion
158
267
  //#region src/actor/actor-runtime.d.ts
159
268
  /**
package/dist/verani.mjs CHANGED
@@ -1 +1 @@
1
- import{a as decodeClientMessage,i as encodeServerMessage,n as encodeClientMessage,o as decodeFrame,r as encodeFrame,s as decodeServerMessage,t as PROTOCOL_VERSION}from"./types-CJLnZrA8.mjs";import{Actor}from"@cloudflare/actors";function defaultExtractMeta(e){let S=crypto.randomUUID(),C=crypto.randomUUID(),w=new URL(e.url).searchParams.get(`channels`);return{userId:S,clientId:C,channels:w?w.split(`,`).map(e=>e.trim()).filter(Boolean):[`default`]}}function defineRoom(e){return{name:e.name,websocketPath:e.websocketPath,extractMeta:e.extractMeta||defaultExtractMeta,onConnect:e.onConnect,onDisconnect:e.onDisconnect,onMessage:e.onMessage,onError:e.onError}}function cleanupStaleSessions(e){let S=0,C=[];for(let[S,w]of e.entries())S.readyState!==WebSocket.OPEN&&C.push(S);for(let w of C)e.delete(w),S++;return S}function decodeFrame$1(e){return decodeFrame(e)??{type:`invalid`}}function encodeFrame$1(e){return encodeFrame(e)}function broadcast(e,S,C,w){let T=0,E=encodeFrame$1({type:`event`,channel:S,data:C}),D=[];for(let{ws:C,meta:O}of e.values())if(O.channels.includes(S)&&!(w?.except&&C===w.except)&&!(w?.userIds&&!w.userIds.includes(O.userId))&&!(w?.clientIds&&!w.clientIds.includes(O.clientId))){if(C.readyState!==WebSocket.OPEN){D.push(C);continue}try{C.send(E),T++}catch{D.push(C)}}for(let S of D)e.delete(S);return D.length,T}function sendToUser(e,S,C,w){let T=0,E=encodeFrame$1({type:`event`,channel:C,data:w}),D=[];for(let{ws:w,meta:O}of e.values())if(O.userId===S&&O.channels.includes(C)){if(w.readyState!==WebSocket.OPEN){D.push(w);continue}try{w.send(E),T++}catch{D.push(w)}}for(let S of D)e.delete(S);return D.length,T}function getSessionCount(e){return e.size}function getConnectedUserIds(e){let S=new Set;for(let{meta:C}of e.values())S.add(C.userId);return Array.from(S)}function getUserSessions(e,S){let C=[];for(let{ws:w,meta:T}of e.values())T.userId===S&&C.push(w);return C}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(S){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,S){e.serializeAttachment(S)}function restoreSessions(e){let S=0,C=0;for(let w of e.ctx.getWebSockets()){if(w.readyState!==WebSocket.OPEN){C++;continue}let T=w.deserializeAttachment();if(!T){C++;continue}if(!isValidConnectionMeta(T)){C++;continue}e.sessions.set(w,{ws:w,meta:T}),S++}}async function onInit(e,S){try{restoreSessions(e)}catch{}if(S.onHibernationRestore&&e.sessions.size>0)try{await S.onHibernationRestore(e)}catch{}else S.onHibernationRestore&&e.sessions.size}async function onWebSocketConnect(e,S,C,w){let T;try{if(T=S.extractMeta?await S.extractMeta(w):{userId:`anonymous`,clientId:crypto.randomUUID(),channels:[`default`]},storeAttachment(C,T),S.onConnect){let w={actor:e,ws:C,meta:T};await S.onConnect(w)}e.sessions.set(C,{ws:C,meta:T})}catch(w){if(S.onError&&T)try{await S.onError(w,{actor:e,ws:C,meta:T})}catch{}C.close(1011,`Internal server error`)}}async function onWebSocketMessage(e,S,C,w){let T;try{let E=decodeFrame$1(w);if(E&&E.type===`ping`){if(C.readyState===WebSocket.OPEN)try{C.send(encodeFrame$1({type:`pong`}))}catch{}return}if(!E||E.type===`invalid`||(T=e.sessions.get(C),!T))return;if(S.onMessage){let w={actor:e,ws:C,meta:T.meta,frame:E};await S.onMessage(w,E)}}catch(w){if(S.onError&&T)try{await S.onError(w,{actor:e,ws:C,meta:T.meta})}catch{}}}async function onWebSocketDisconnect(e,S,C){try{let w=e.sessions.get(C);if(e.sessions.delete(C),w&&S.onDisconnect){let T={actor:e,ws:C,meta:w.meta};await S.onDisconnect(T)}}catch{}}function createActorHandler(e){let S=sanitizeToClassName(e.name||e.websocketPath||`VeraniActor`);class C extends Actor{constructor(...e){super(...e),this.sessions=new Map}static{this.configuration=createConfiguration(e)}async shouldUpgradeWebSocket(e){return!0}async fetch(S){let C=new URL(S.url),w=S.headers.get(`Upgrade`);return C.pathname===e.websocketPath&&w===`websocket`&&await this.shouldUpgradeWebSocket(S)?this.onWebSocketUpgrade(S):this.onRequest(S)}async onInit(){await onInit(this,e)}async onWebSocketConnect(S,C){await onWebSocketConnect(this,e,S,C)}async onWebSocketMessage(S,C){await onWebSocketMessage(this,e,S,C)}async onWebSocketDisconnect(S){await onWebSocketDisconnect(this,e,S)}cleanupStaleSessions(){return cleanupStaleSessions(this.sessions)}broadcast(e,S,C){return broadcast(this.sessions,e,S,C)}getSessionCount(){return getSessionCount(this.sessions)}getConnectedUserIds(){return getConnectedUserIds(this.sessions)}getUserSessions(e){return getUserSessions(this.sessions,e)}sendToUser(e,S,C){return sendToUser(this.sessions,e,S,C)}getStorage(){return getStorage(this.ctx)}}return Object.defineProperty(C,`name`,{value:S,writable:!1,configurable:!0}),C}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,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||defaultExtractMeta,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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verani",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "A simple, focused realtime SDK for Cloudflare Actors with Socket.io-like semantics",
5
5
  "license": "ISC",
6
6
  "keywords": [