verani 0.1.13 → 0.3.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/README.md CHANGED
@@ -161,6 +161,7 @@ Verani handles Cloudflare's hibernation automatically:
161
161
  - WebSocket attachment management for hibernation
162
162
  - Selective broadcasting with filters
163
163
  - User and client ID tracking
164
+ - **RPC methods** - Call Actor methods remotely from Workers or other Actors
164
165
  - Error boundaries and logging
165
166
  - Flexible metadata extraction from requests
166
167
 
@@ -173,6 +174,13 @@ Verani handles Cloudflare's hibernation automatically:
173
174
  - Promise-based connection waiting
174
175
  - Lifecycle callbacks
175
176
 
177
+ ### RPC Support
178
+
179
+ - Send messages to users from HTTP endpoints
180
+ - Query actor state remotely
181
+ - Broadcast from external events or scheduled tasks
182
+ - Coordinate between multiple Actors
183
+
176
184
  ## Live Examples
177
185
 
178
186
  Try out Verani with working examples:
package/dist/verani.cjs CHANGED
@@ -1 +1 @@
1
- const require_types=require(`./types-083oWz55.cjs`);let __cloudflare_actors=require(`@cloudflare/actors`);function defaultExtractMeta(e){let a=crypto.randomUUID(),o=crypto.randomUUID(),s=new URL(e.url).searchParams.get(`channels`);return{userId:a,clientId:o,channels:s?s.split(`,`).map(e=>e.trim()).filter(Boolean):[`default`]}}function defineRoom(e){return{name:e.name,websocketPath:e.websocketPath,extractMeta:e.extractMeta||defaultExtractMeta,onConnect:e.onConnect,onDisconnect:e.onDisconnect,onMessage:e.onMessage,onError:e.onError}}function isValidConnectionMeta(e){return!(!e||typeof e!=`object`||typeof e.userId!=`string`||!e.userId||typeof e.clientId!=`string`||!e.clientId||!Array.isArray(e.channels)||!e.channels.every(e=>typeof e==`string`))}function storeAttachment(e,a){e.serializeAttachment(a)}function restoreSessions(e){let a=0,o=0;for(let s of e.ctx.getWebSockets()){if(s.readyState!==WebSocket.OPEN){o++;continue}let l=s.deserializeAttachment();if(!l){o++;continue}if(!isValidConnectionMeta(l)){o++;continue}e.sessions.set(s,{ws:s,meta:l}),a++}}function decodeFrame$1(a){return require_types.o(a)??{type:`invalid`}}function encodeFrame$1(a){return require_types.r(a)}function sanitizeToClassName(e){return e.replace(/^\/+/,``).split(/[-_\/\s]+/).map(e=>e.replace(/[^a-zA-Z0-9]/g,``)).filter(e=>e.length>0).map(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()).join(``)||`VeraniActor`}function createActorHandler(e){let o=sanitizeToClassName(e.name||e.websocketPath||`VeraniActor`);class s extends __cloudflare_actors.Actor{constructor(...e){super(...e),this.sessions=new Map}static configuration(a){return{locationHint:`me`,sockets:{upgradePath:e.websocketPath}}}async shouldUpgradeWebSocket(e){return!0}async fetch(a){let o=new URL(a.url),s=a.headers.get(`Upgrade`);return o.pathname===e.websocketPath&&s===`websocket`&&await this.shouldUpgradeWebSocket(a)?this.onWebSocketUpgrade(a):this.onRequest(a)}async onInit(){try{restoreSessions(this)}catch{}if(e.onHibernationRestore&&this.sessions.size>0)try{await e.onHibernationRestore(this)}catch{}else e.onHibernationRestore&&this.sessions.size}async onWebSocketConnect(a,o){let s;try{if(s=e.extractMeta?await e.extractMeta(o):{userId:`anonymous`,clientId:crypto.randomUUID(),channels:[`default`]},storeAttachment(a,s),e.onConnect){let o={actor:this,ws:a,meta:s};await e.onConnect(o)}this.sessions.set(a,{ws:a,meta:s})}catch(o){if(e.onError&&s)try{await e.onError(o,{actor:this,ws:a,meta:s})}catch{}a.close(1011,`Internal server error`)}}async onWebSocketMessage(a,o){let s;try{let c=decodeFrame$1(o);if(c&&c.type===`ping`){if(a.readyState===WebSocket.OPEN)try{a.send(encodeFrame$1({type:`pong`}))}catch{}return}if(!c||c.type===`invalid`||(s=this.sessions.get(a),!s))return;if(e.onMessage){let o={actor:this,ws:a,meta:s.meta,frame:c};await e.onMessage(o,c)}}catch(o){if(e.onError&&s)try{await e.onError(o,{actor:this,ws:a,meta:s.meta})}catch{}}}async onWebSocketDisconnect(a){try{let o=this.sessions.get(a);if(this.sessions.delete(a),o&&e.onDisconnect){let s={actor:this,ws:a,meta:o.meta};await e.onDisconnect(s)}}catch{}}cleanupStaleSessions(){let e=0,a=[];for(let[e,o]of this.sessions.entries())e.readyState!==WebSocket.OPEN&&a.push(e);for(let o of a)this.sessions.delete(o),e++;return e}broadcast(e,a,o){let s=0,c=encodeFrame$1({type:`event`,channel:e,data:a}),l=[];for(let{ws:a,meta:u}of this.sessions.values())if(u.channels.includes(e)&&!(o?.except&&a===o.except)&&!(o?.userIds&&!o.userIds.includes(u.userId))&&!(o?.clientIds&&!o.clientIds.includes(u.clientId))){if(a.readyState!==WebSocket.OPEN){l.push(a);continue}try{a.send(c),s++}catch{l.push(a)}}for(let e of l)this.sessions.delete(e);return l.length,s}getSessionCount(){return this.sessions.size}getConnectedUserIds(){let e=new Set;for(let{meta:a}of this.sessions.values())e.add(a.userId);return Array.from(e)}getUserSessions(e){let a=[];for(let{ws:o,meta:s}of this.sessions.values())s.userId===e&&a.push(o);return a}sendToUser(e,a,o){let s=0,c=encodeFrame$1({type:`event`,channel:a,data:o}),l=[];for(let{ws:o,meta:u}of this.sessions.values())if(u.userId===e&&u.channels.includes(a)){if(o.readyState!==WebSocket.OPEN){l.push(o);continue}try{o.send(c),s++}catch{l.push(o)}}for(let e of l)this.sessions.delete(e);return l.length,s}getStorage(){return this.ctx.storage}}return Object.defineProperty(s,`name`,{value:o,writable:!1,configurable:!0}),s}exports.PROTOCOL_VERSION=require_types.t,exports.createActorHandler=createActorHandler,exports.decodeClientMessage=require_types.a,exports.decodeFrame=require_types.o,exports.decodeServerMessage=require_types.s,exports.defineRoom=defineRoom,exports.encodeClientMessage=require_types.n,exports.encodeFrame=require_types.r,exports.encodeServerMessage=require_types.i,exports.restoreSessions=restoreSessions,exports.storeAttachment=storeAttachment;
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;
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/dist/verani.mjs CHANGED
@@ -1 +1 @@
1
- import{a as decodeClientMessage,i as encodeServerMessage,n as encodeClientMessage,o as decodeFrame,r as encodeFrame,s as decodeServerMessage,t as PROTOCOL_VERSION}from"./types-CJLnZrA8.mjs";import{Actor}from"@cloudflare/actors";function defaultExtractMeta(e){let d=crypto.randomUUID(),f=crypto.randomUUID(),p=new URL(e.url).searchParams.get(`channels`);return{userId:d,clientId:f,channels:p?p.split(`,`).map(e=>e.trim()).filter(Boolean):[`default`]}}function defineRoom(e){return{name:e.name,websocketPath:e.websocketPath,extractMeta:e.extractMeta||defaultExtractMeta,onConnect:e.onConnect,onDisconnect:e.onDisconnect,onMessage:e.onMessage,onError:e.onError}}function isValidConnectionMeta(e){return!(!e||typeof e!=`object`||typeof e.userId!=`string`||!e.userId||typeof e.clientId!=`string`||!e.clientId||!Array.isArray(e.channels)||!e.channels.every(e=>typeof e==`string`))}function storeAttachment(e,d){e.serializeAttachment(d)}function restoreSessions(e){let d=0,f=0;for(let p of e.ctx.getWebSockets()){if(p.readyState!==WebSocket.OPEN){f++;continue}let m=p.deserializeAttachment();if(!m){f++;continue}if(!isValidConnectionMeta(m)){f++;continue}e.sessions.set(p,{ws:p,meta:m}),d++}}function decodeFrame$1(e){return decodeFrame(e)??{type:`invalid`}}function encodeFrame$1(e){return encodeFrame(e)}function sanitizeToClassName(e){return e.replace(/^\/+/,``).split(/[-_\/\s]+/).map(e=>e.replace(/[^a-zA-Z0-9]/g,``)).filter(e=>e.length>0).map(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()).join(``)||`VeraniActor`}function createActorHandler(e){let d=sanitizeToClassName(e.name||e.websocketPath||`VeraniActor`);class f extends Actor{constructor(...e){super(...e),this.sessions=new Map}static configuration(d){return{locationHint:`me`,sockets:{upgradePath:e.websocketPath}}}async shouldUpgradeWebSocket(e){return!0}async fetch(d){let f=new URL(d.url),p=d.headers.get(`Upgrade`);return f.pathname===e.websocketPath&&p===`websocket`&&await this.shouldUpgradeWebSocket(d)?this.onWebSocketUpgrade(d):this.onRequest(d)}async onInit(){try{restoreSessions(this)}catch{}if(e.onHibernationRestore&&this.sessions.size>0)try{await e.onHibernationRestore(this)}catch{}else e.onHibernationRestore&&this.sessions.size}async onWebSocketConnect(d,f){let p;try{if(p=e.extractMeta?await e.extractMeta(f):{userId:`anonymous`,clientId:crypto.randomUUID(),channels:[`default`]},storeAttachment(d,p),e.onConnect){let f={actor:this,ws:d,meta:p};await e.onConnect(f)}this.sessions.set(d,{ws:d,meta:p})}catch(f){if(e.onError&&p)try{await e.onError(f,{actor:this,ws:d,meta:p})}catch{}d.close(1011,`Internal server error`)}}async onWebSocketMessage(d,f){let p;try{let m=decodeFrame$1(f);if(m&&m.type===`ping`){if(d.readyState===WebSocket.OPEN)try{d.send(encodeFrame$1({type:`pong`}))}catch{}return}if(!m||m.type===`invalid`||(p=this.sessions.get(d),!p))return;if(e.onMessage){let f={actor:this,ws:d,meta:p.meta,frame:m};await e.onMessage(f,m)}}catch(f){if(e.onError&&p)try{await e.onError(f,{actor:this,ws:d,meta:p.meta})}catch{}}}async onWebSocketDisconnect(d){try{let f=this.sessions.get(d);if(this.sessions.delete(d),f&&e.onDisconnect){let p={actor:this,ws:d,meta:f.meta};await e.onDisconnect(p)}}catch{}}cleanupStaleSessions(){let e=0,d=[];for(let[e,f]of this.sessions.entries())e.readyState!==WebSocket.OPEN&&d.push(e);for(let f of d)this.sessions.delete(f),e++;return e}broadcast(e,d,f){let p=0,m=encodeFrame$1({type:`event`,channel:e,data:d}),h=[];for(let{ws:d,meta:g}of this.sessions.values())if(g.channels.includes(e)&&!(f?.except&&d===f.except)&&!(f?.userIds&&!f.userIds.includes(g.userId))&&!(f?.clientIds&&!f.clientIds.includes(g.clientId))){if(d.readyState!==WebSocket.OPEN){h.push(d);continue}try{d.send(m),p++}catch{h.push(d)}}for(let e of h)this.sessions.delete(e);return h.length,p}getSessionCount(){return this.sessions.size}getConnectedUserIds(){let e=new Set;for(let{meta:d}of this.sessions.values())e.add(d.userId);return Array.from(e)}getUserSessions(e){let d=[];for(let{ws:f,meta:p}of this.sessions.values())p.userId===e&&d.push(f);return d}sendToUser(e,d,f){let p=0,m=encodeFrame$1({type:`event`,channel:d,data:f}),h=[];for(let{ws:f,meta:g}of this.sessions.values())if(g.userId===e&&g.channels.includes(d)){if(f.readyState!==WebSocket.OPEN){h.push(f);continue}try{f.send(m),p++}catch{h.push(f)}}for(let e of h)this.sessions.delete(e);return h.length,p}getStorage(){return this.ctx.storage}}return Object.defineProperty(f,`name`,{value:d,writable:!1,configurable:!0}),f}export{PROTOCOL_VERSION,createActorHandler,decodeClientMessage,decodeFrame,decodeServerMessage,defineRoom,encodeClientMessage,encodeFrame,encodeServerMessage,restoreSessions,storeAttachment};
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};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "verani",
3
- "version": "0.1.13",
3
+ "version": "0.3.0",
4
4
  "description": "A simple, focused realtime SDK for Cloudflare Actors with Socket.io-like semantics",
5
5
  "license": "ISC",
6
6
  "keywords": [