verani 0.1.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/LICENSE ADDED
@@ -0,0 +1,16 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2025, Verani contributors
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16
+
package/README.md ADDED
@@ -0,0 +1,212 @@
1
+ # Verani
2
+
3
+ > A simple, focused realtime SDK for Cloudflare Actors with Socket.io-like semantics
4
+
5
+ Verani brings the familiar developer experience of Socket.io to Cloudflare's Durable Objects (Actors), with proper hibernation support and minimal overhead.
6
+
7
+ ## Why Verani?
8
+
9
+ - **Familiar API**: If you've used Socket.io, you already know how to use Verani
10
+ - **Hibernation Support**: Properly handles Cloudflare Actor hibernation out of the box
11
+ - **Type Safe**: Built with TypeScript, full type safety throughout
12
+ - **Simple Mental Model**: Rooms, channels, and broadcast semantics that just make sense
13
+ - **Production Ready**: Automatic reconnection, error handling, and connection lifecycle management
14
+
15
+ ## Quick Start
16
+
17
+ ### Installation
18
+
19
+ ```bash
20
+ npm install verani @cloudflare/actors
21
+ # or
22
+ bun add verani @cloudflare/actors
23
+ ```
24
+
25
+ ### Server Side (Cloudflare Worker)
26
+
27
+ ```typescript
28
+ import { defineRoom, createActorHandler } from "verani";
29
+
30
+ // Define your room with lifecycle hooks
31
+ export const chatRoom = defineRoom({
32
+ name: "chat",
33
+
34
+ onConnect(ctx) {
35
+ console.log(`User ${ctx.meta.userId} connected`);
36
+ ctx.actor.broadcast("default", {
37
+ type: "user.joined",
38
+ userId: ctx.meta.userId
39
+ });
40
+ },
41
+
42
+ onMessage(ctx, frame) {
43
+ if (frame.type === "chat.message") {
44
+ // Broadcast to everyone except sender
45
+ ctx.actor.broadcast("default", {
46
+ type: "chat.message",
47
+ from: ctx.meta.userId,
48
+ text: frame.data.text
49
+ }, { except: ctx.ws });
50
+ }
51
+ },
52
+
53
+ onDisconnect(ctx) {
54
+ console.log(`User ${ctx.meta.userId} disconnected`);
55
+ }
56
+ });
57
+
58
+ // Create the Durable Object class
59
+ const ChatRoom = createActorHandler(chatRoom);
60
+
61
+ // Export it - the name must match wrangler.jsonc
62
+ export { ChatRoom };
63
+ ```
64
+
65
+ ### Wrangler Configuration
66
+
67
+ **Critical**: Your Durable Object export names in `src/index.ts` **must match** the `class_name` in `wrangler.jsonc`:
68
+
69
+ ```jsonc
70
+ {
71
+ "durable_objects": {
72
+ "bindings": [
73
+ {
74
+ "class_name": "ChatRoom", // Must match export name
75
+ "name": "ChatRoom" // Binding name in env
76
+ }
77
+ ]
78
+ },
79
+ "migrations": [
80
+ {
81
+ "new_sqlite_classes": ["ChatRoom"],
82
+ "tag": "v1"
83
+ }
84
+ ]
85
+ }
86
+ ```
87
+
88
+ The three-way relationship:
89
+ 1. **Export** in `src/index.ts`: `export { ChatRoom }`
90
+ 2. **Class name** in `wrangler.jsonc`: `"class_name": "ChatRoom"`
91
+ 3. **Env binding**: Access via `env.CHAT` in your fetch handler
92
+
93
+ ### Client Side
94
+
95
+ ```typescript
96
+ import { VeraniClient } from "verani";
97
+
98
+ // Connect to your Cloudflare Worker
99
+ const client = new VeraniClient("wss://your-worker.dev/ws?userId=alice");
100
+
101
+ // Listen for messages
102
+ client.on("chat.message", (data) => {
103
+ console.log(`${data.from}: ${data.text}`);
104
+ });
105
+
106
+ // Send messages
107
+ client.emit("chat.message", { text: "Hello, world!" });
108
+
109
+ // Handle connection lifecycle
110
+ client.onOpen(() => {
111
+ console.log("Connected!");
112
+ });
113
+
114
+ client.onStateChange((state) => {
115
+ console.log("Connection state:", state);
116
+ });
117
+ ```
118
+
119
+ ## Key Concepts
120
+
121
+ ### Actors = Rooms
122
+
123
+ Each Cloudflare Actor instance represents a **logical container** for realtime communication:
124
+
125
+ - **Chat room**: All users in the same chat share one Actor
126
+ - **User notifications**: Each user gets their own Actor
127
+ - **Game session**: Each game instance is one Actor
128
+
129
+ ### Channels
130
+
131
+ Inside an Actor, connections can join **channels** for selective message routing:
132
+
133
+ ```typescript
134
+ // Server: broadcast to specific channel
135
+ ctx.actor.broadcast("game-updates", data);
136
+
137
+ // Client: joins "default" channel automatically
138
+ // You can implement join/leave for custom channels
139
+ ```
140
+
141
+ ### Hibernation
142
+
143
+ Verani handles Cloudflare's hibernation automatically:
144
+
145
+ - Connection metadata survives hibernation via WebSocket attachments
146
+ - Sessions are restored when the Actor wakes up
147
+ - No manual state management needed
148
+
149
+ ## Documentation
150
+
151
+ - **[Getting Started](./docs/GETTING_STARTED.md)** - Step-by-step tutorial
152
+ - **[Mental Model](./docs/MENTAL_MODEL.md)** - Understanding Verani's architecture
153
+ - **[API Reference](./docs/API.md)** - Complete API documentation
154
+ - **[Examples](./docs/EXAMPLES.md)** - Common usage patterns
155
+ - **[Security Guide](./docs/SECURITY.md)** - Authentication, authorization, and best practices
156
+ - **[Deployment](./docs/DEPLOYMENT.md)** - Deploy to Cloudflare Workers
157
+
158
+ ## Features
159
+
160
+ ### Server (Actor) Side
161
+
162
+ - Room-based architecture with lifecycle hooks
163
+ - WebSocket attachment management for hibernation
164
+ - Selective broadcasting with filters
165
+ - User and client ID tracking
166
+ - Error boundaries and logging
167
+ - Flexible metadata extraction from requests
168
+
169
+ ### Client Side
170
+
171
+ - Automatic reconnection with exponential backoff
172
+ - Connection state management
173
+ - Message queueing when disconnected
174
+ - Event-based API (on/off/once/emit)
175
+ - Promise-based connection waiting
176
+ - Lifecycle callbacks
177
+
178
+ ## Live Examples
179
+
180
+ Try out Verani with working examples:
181
+
182
+ ```bash
183
+ # Clone and run
184
+ git clone https://github.com/your-org/verani
185
+ cd verani
186
+ bun install # or npm install
187
+ bun run dev # or npm run dev
188
+
189
+ # Open http://localhost:8787
190
+ ```
191
+
192
+ See `examples/` for chat, presence, and notifications demos!
193
+
194
+ ## Project Status
195
+
196
+ Verani is in active development. Current version is an MVP focused on core functionality:
197
+
198
+ - Core realtime messaging
199
+ - Hibernation support
200
+ - Client reconnection
201
+ - Presence protocol (coming soon)
202
+ - Persistent storage integration (coming soon)
203
+ - React/framework adapters (coming soon)
204
+
205
+ ## License
206
+
207
+ ISC
208
+
209
+ ## Contributing
210
+
211
+ Contributions welcome! Please read our contributing guidelines first.
212
+
@@ -0,0 +1 @@
1
+ let __cloudflare_actors=require(`@cloudflare/actors`);function extractUserId(e){let b=new URL(e.url),x=b.searchParams.get(`userId`)||b.searchParams.get(`user_id`);if(x)return x;let S=e.headers.get(`Authorization`);return S?.startsWith(`Bearer `)?S.substring(7):e.headers.get(`X-User-ID`)||`anonymous`}function extractClientId(e){let b=new URL(e.url);return b.searchParams.get(`clientId`)||b.searchParams.get(`client_id`)||e.headers.get(`X-Client-ID`)||crypto.randomUUID()}function defaultExtractMeta(e){let S=extractUserId(e),C=extractClientId(e),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 parseJWT(e){try{let b=e.split(`.`);if(b.length!==3)return null;let x=b[1],S=atob(x.replace(/-/g,`+`).replace(/_/g,`/`));return JSON.parse(S)}catch{return null}}function storeAttachment(e,b){e.serializeAttachment(b)}function restoreSessions(e){let b=0;for(let x of e.ctx.getWebSockets()){let S=x.deserializeAttachment();S&&(e.sessions.set(x,{ws:x,meta:S}),b++)}}function isValidFrame(e){return e&&typeof e==`object`&&typeof e.type==`string`&&(e.channel===void 0||typeof e.channel==`string`)}function decodeFrame(e){try{let b=typeof e==`string`?e:e.toString(),x=JSON.parse(b);return isValidFrame(x)?x:null}catch{return null}}function decodeClientMessage(e){return decodeFrame(e)}function decodeServerMessage(e){return decodeFrame(e)}function encodeFrame(e){try{return JSON.stringify(e)}catch(e){throw Error(`Failed to encode frame: ${e instanceof Error?e.message:`unknown error`}`)}}function encodeClientMessage(e){return encodeFrame(e)}function encodeServerMessage(e){return encodeFrame(e)}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(b){let x=sanitizeToClassName(b.name||b.websocketPath||`VeraniActor`);class S extends __cloudflare_actors.Actor{constructor(...e){super(...e),this.sessions=new Map}static configuration(e){return{locationHint:`me`,sockets:{upgradePath:b.websocketPath,autoResponse:{ping:`ping`,pong:`pong`}}}}async shouldUpgradeWebSocket(e){return!0}async fetch(e){let x=new URL(e.url),S=e.headers.get(`Upgrade`);return x.pathname===b.websocketPath&&S===`websocket`&&await this.shouldUpgradeWebSocket(e)?this.onWebSocketUpgrade(e):this.onRequest(e)}async onInit(){try{restoreSessions(this),b.onHibernationRestore&&this.sessions.size>0&&await b.onHibernationRestore(this)}catch{}}onWebSocketConnect(e,x){try{let S;if(S=b.extractMeta?b.extractMeta(x):{userId:`anonymous`,clientId:crypto.randomUUID(),channels:[`default`]},storeAttachment(e,S),this.sessions.set(e,{ws:e,meta:S}),b.onConnect){let x={actor:this,ws:e,meta:S};b.onConnect(x)}}catch(x){if(b.onError)try{b.onError(x,{actor:this,ws:e,meta:{userId:`unknown`,clientId:`unknown`,channels:[]}})}catch{}e.close(1011,`Internal server error`)}}onWebSocketMessage(e,x){let S;try{let C=decodeFrame$1(x);if(S=this.sessions.get(e),!S)return;if(b.onMessage){let x={actor:this,ws:e,meta:S.meta,frame:C};b.onMessage(x,C)}}catch(x){if(b.onError&&S)try{b.onError(x,{actor:this,ws:e,meta:S.meta})}catch{}}}onWebSocketDisconnect(e){try{let x=this.sessions.get(e);if(this.sessions.delete(e),x&&b.onDisconnect){let S={actor:this,ws:e,meta:x.meta};b.onDisconnect(S)}}catch{}}broadcast(e,b,x){let S=0,C=encodeFrame$1({type:`event`,channel:e,data:b});for(let{ws:b,meta:w}of this.sessions.values())if(w.channels.includes(e)&&!(x?.except&&b===x.except)&&!(x?.userIds&&!x.userIds.includes(w.userId))&&!(x?.clientIds&&!x.clientIds.includes(w.clientId)))try{b.send(C),S++}catch{}return S}getSessionCount(){return this.sessions.size}getConnectedUserIds(){let e=new Set;for(let{meta:b}of this.sessions.values())e.add(b.userId);return Array.from(e)}getUserSessions(e){let b=[];for(let{ws:x,meta:S}of this.sessions.values())S.userId===e&&b.push(x);return b}sendToUser(e,b,x){let S=0,C=encodeFrame$1({type:b,data:x});for(let{ws:b,meta:x}of this.sessions.values())if(x.userId===e)try{b.send(C),S++}catch{}return S}getStorage(){return this.ctx.storage}}return Object.defineProperty(S,`name`,{value:x,writable:!1,configurable:!0}),S}function encodeClientMessage$1(e){return encodeClientMessage(e)}function decodeServerMessage$1(e){return decodeServerMessage(e)}const DEFAULT_RECONNECTION_CONFIG={enabled:!0,maxAttempts:10,initialDelay:1e3,maxDelay:3e4,backoffMultiplier:1.5};var ConnectionManager=class{constructor(e=DEFAULT_RECONNECTION_CONFIG,b){this.config=e,this.onStateChange=b,this.state=`disconnected`,this.reconnectAttempts=0,this.currentDelay=e.initialDelay}getState(){return this.state}setState(e){this.state!==e&&(this.state=e,this.onStateChange?.(e))}resetReconnection(){this.reconnectAttempts=0,this.currentDelay=this.config.initialDelay,this.clearReconnectTimer()}scheduleReconnect(e){return this.config.enabled?this.config.maxAttempts>0&&this.reconnectAttempts>=this.config.maxAttempts?(this.setState(`error`),!1):(this.clearReconnectTimer(),this.setState(`reconnecting`),this.reconnectAttempts++,this.reconnectTimer=setTimeout(()=>{e(),this.currentDelay=Math.min(this.currentDelay*this.config.backoffMultiplier,this.config.maxDelay)},this.currentDelay),!0):!1}cancelReconnect(){this.clearReconnectTimer(),this.state===`reconnecting`&&this.setState(`disconnected`)}clearReconnectTimer(){this.reconnectTimer!==void 0&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=void 0)}getReconnectAttempts(){return this.reconnectAttempts}getNextDelay(){return this.currentDelay}destroy(){this.clearReconnectTimer()}},VeraniClient=class{constructor(e,b={}){this.url=e,this.listeners=new Map,this.messageQueue=[],this.options={reconnection:{enabled:b.reconnection?.enabled??DEFAULT_RECONNECTION_CONFIG.enabled,maxAttempts:b.reconnection?.maxAttempts??DEFAULT_RECONNECTION_CONFIG.maxAttempts,initialDelay:b.reconnection?.initialDelay??DEFAULT_RECONNECTION_CONFIG.initialDelay,maxDelay:b.reconnection?.maxDelay??DEFAULT_RECONNECTION_CONFIG.maxDelay,backoffMultiplier:b.reconnection?.backoffMultiplier??DEFAULT_RECONNECTION_CONFIG.backoffMultiplier},maxQueueSize:b.maxQueueSize??100,connectionTimeout:b.connectionTimeout??1e4},this.connectionManager=new ConnectionManager(this.options.reconnection,e=>{this.onStateChangeCallback?.(e)}),this.connect()}connect(){try{this.connectionManager.setState(`connecting`),this.ws=new WebSocket(this.url);let e=setTimeout(()=>{this.connectionManager.getState()===`connecting`&&(this.ws?.close(),this.handleConnectionError(Error(`Connection timeout`)))},this.options.connectionTimeout);this.ws.addEventListener(`open`,()=>{clearTimeout(e),this.handleOpen()}),this.ws.addEventListener(`message`,e=>{this.handleMessage(e)}),this.ws.addEventListener(`close`,b=>{clearTimeout(e),this.handleClose(b)}),this.ws.addEventListener(`error`,b=>{clearTimeout(e),this.handleError(b)})}catch(e){this.handleConnectionError(e)}}handleOpen(){this.connectionManager.setState(`connected`),this.connectionManager.resetReconnection(),this.flushMessageQueue(),this.connectionResolve&&(this.connectionResolve(),this.connectionPromise=void 0,this.connectionResolve=void 0,this.connectionReject=void 0),this.onOpenCallback?.()}handleMessage(e){let b=decodeServerMessage$1(e.data);if(!b)return;let x=b.type,S=b.data;b.type===`event`&&b.data&&typeof b.data==`object`&&`type`in b.data&&(x=b.data.type,S=b.data);let C=this.listeners.get(x);if(C)for(let e of C)try{e(S)}catch{}}handleClose(e){this.connectionManager.setState(`disconnected`),this.connectionReject&&=(this.connectionReject(Error(`Connection closed: ${e.reason||`Unknown reason`}`)),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0),this.onCloseCallback?.(e),e.code!==1e3&&e.code!==1001&&this.connectionManager.scheduleReconnect(()=>this.connect())}handleError(e){this.onErrorCallback?.(e)}handleConnectionError(e){this.connectionReject&&=(this.connectionReject(e),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0),this.connectionManager.scheduleReconnect(()=>this.connect())}flushMessageQueue(){if(!(!this.ws||this.ws.readyState!==WebSocket.OPEN))for(;this.messageQueue.length>0;){let e=this.messageQueue.shift();try{this.ws.send(encodeClientMessage$1(e))}catch{}}}getState(){return this.connectionManager.getState()}isConnected(){return this.ws?.readyState===WebSocket.OPEN}waitForConnection(){return this.isConnected()?Promise.resolve():(this.connectionPromise||=new Promise((e,b)=>{this.connectionResolve=e,this.connectionReject=b}),this.connectionPromise)}on(e,b){this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(b)}off(e,b){let x=this.listeners.get(e);x&&(x.delete(b),x.size===0&&this.listeners.delete(e))}once(e,b){let x=S=>{this.off(e,x),b(S)};this.on(e,x)}emit(e,b){let x={type:e,data:b};if(this.isConnected())try{this.ws.send(encodeClientMessage$1(x))}catch{this.queueMessage(x)}else this.queueMessage(x)}queueMessage(e){this.messageQueue.length>=this.options.maxQueueSize&&this.messageQueue.shift(),this.messageQueue.push(e)}onOpen(e){this.onOpenCallback=e}onClose(e){this.onCloseCallback=e}onError(e){this.onErrorCallback=e}onStateChange(e){this.onStateChangeCallback=e}reconnect(){this.disconnect(),this.connect()}disconnect(){this.connectionManager.cancelReconnect(),this.ws&&=(this.ws.close(1e3,`Client disconnect`),void 0)}close(){this.disconnect(),this.listeners.clear(),this.messageQueue=[],this.connectionManager.destroy()}};const PROTOCOL_VERSION=`1.0.0`;exports.ConnectionManager=ConnectionManager,exports.DEFAULT_RECONNECTION_CONFIG=DEFAULT_RECONNECTION_CONFIG,exports.PROTOCOL_VERSION=`1.0.0`,exports.VeraniClient=VeraniClient,exports.createActorHandler=createActorHandler,exports.decodeClientMessage=decodeClientMessage,exports.decodeFrame=decodeFrame,exports.decodeServerMessage=decodeServerMessage,exports.defineRoom=defineRoom,exports.encodeClientMessage=encodeClientMessage,exports.encodeFrame=encodeFrame,exports.encodeServerMessage=encodeServerMessage,exports.parseJWT=parseJWT,exports.restoreSessions=restoreSessions,exports.storeAttachment=storeAttachment;
@@ -0,0 +1,399 @@
1
+ import { Actor, ActorConfiguration } from "@cloudflare/actors";
2
+
3
+ //#region src/shared/types.d.ts
4
+
5
+ /**
6
+ * Core message types shared between client and server
7
+ */
8
+ /**
9
+ * Base message frame structure used for all WebSocket communication
10
+ */
11
+ interface MessageFrame {
12
+ type: string;
13
+ channel?: string;
14
+ data?: any;
15
+ }
16
+ /**
17
+ * Message sent from client to server
18
+ */
19
+ interface ClientMessage extends MessageFrame {
20
+ type: string;
21
+ channel?: string;
22
+ data?: any;
23
+ }
24
+ /**
25
+ * Message sent from server to client
26
+ */
27
+ interface ServerMessage extends MessageFrame {
28
+ type: string;
29
+ channel?: string;
30
+ data?: any;
31
+ }
32
+ /**
33
+ * Connection metadata attached to each WebSocket
34
+ */
35
+ interface ConnectionMeta {
36
+ userId: string;
37
+ clientId: string;
38
+ channels: string[];
39
+ }
40
+ /**
41
+ * Unified message type for both directions
42
+ */
43
+ type VeraniMessage = ClientMessage | ServerMessage;
44
+ /**
45
+ * Protocol version for future compatibility
46
+ */
47
+ declare const PROTOCOL_VERSION = "1.0.0";
48
+ //#endregion
49
+ //#region src/actor/types.d.ts
50
+ /**
51
+ * Options for broadcasting messages to connections
52
+ */
53
+ interface BroadcastOptions {
54
+ /** Exclude specific WebSocket from receiving the broadcast */
55
+ except?: WebSocket;
56
+ /** Only send to specific user IDs */
57
+ userIds?: string[];
58
+ /** Only send to specific client IDs */
59
+ clientIds?: string[];
60
+ }
61
+ /**
62
+ * Extended Actor interface with Verani-specific methods
63
+ */
64
+ interface VeraniActor<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> extends Actor<E> {
65
+ sessions: Map<WebSocket, {
66
+ ws: WebSocket;
67
+ meta: TMeta;
68
+ }>;
69
+ broadcast(channel: string, data: any, opts?: BroadcastOptions): number;
70
+ getSessionCount(): number;
71
+ getConnectedUserIds(): string[];
72
+ getUserSessions(userId: string): WebSocket[];
73
+ sendToUser(userId: string, type: string, data?: any): number;
74
+ getStorage(): DurableObjectStorage;
75
+ }
76
+ /**
77
+ * Context provided to room lifecycle hooks
78
+ */
79
+ interface RoomContext<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> {
80
+ /** The actor instance handling this connection */
81
+ actor: VeraniActor<TMeta, E>;
82
+ /** The WebSocket connection */
83
+ ws: WebSocket;
84
+ /** Connection metadata */
85
+ meta: TMeta;
86
+ }
87
+ /**
88
+ * Context for onMessage hook with frame included
89
+ */
90
+ interface MessageContext<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> extends RoomContext<TMeta, E> {
91
+ /** The received message frame */
92
+ frame: MessageFrame;
93
+ }
94
+ /**
95
+ * Room definition with lifecycle hooks
96
+ */
97
+ interface RoomDefinition<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> {
98
+ /** Optional room name for debugging */
99
+ name?: string;
100
+ /** WebSocket upgrade path (default: "/ws") */
101
+ websocketPath: string;
102
+ /** Extract metadata from the connection request */
103
+ extractMeta?(req: Request): TMeta | Promise<TMeta>;
104
+ /** Called when a new WebSocket connection is established */
105
+ onConnect?(ctx: RoomContext<TMeta, E>): void | Promise<void>;
106
+ /** Called when a WebSocket connection is closed */
107
+ onDisconnect?(ctx: RoomContext<TMeta, E>): void | Promise<void>;
108
+ /** Called when a message is received from a connection */
109
+ onMessage?(ctx: MessageContext<TMeta, E>, frame: MessageFrame): void | Promise<void>;
110
+ /** Called when an error occurs in a lifecycle hook */
111
+ onError?(error: Error, ctx: RoomContext<TMeta, E>): void | Promise<void>;
112
+ /** Called after actor wakes from hibernation and sessions are restored */
113
+ onHibernationRestore?(actor: VeraniActor<TMeta, E>): void | Promise<void>;
114
+ }
115
+ //#endregion
116
+ //#region src/actor/router.d.ts
117
+ /**
118
+ * Defines a room with lifecycle hooks and metadata extraction
119
+ * @param def - Room definition with optional hooks
120
+ * @returns Normalized room definition with defaults
121
+ */
122
+ declare function defineRoom<TMeta extends ConnectionMeta = ConnectionMeta>(def: RoomDefinition<TMeta>): RoomDefinition<TMeta>;
123
+ /**
124
+ * Helper to parse JWT tokens (basic implementation)
125
+ * In production, use a proper JWT library
126
+ * @param token - JWT token string
127
+ * @returns Decoded payload or null if invalid
128
+ */
129
+ declare function parseJWT(token: string): any;
130
+ //#endregion
131
+ //#region src/actor/actor-runtime.d.ts
132
+ /**
133
+ * Actor stub interface returned by .get() method
134
+ */
135
+ interface ActorStub {
136
+ fetch(request: Request): Promise<Response>;
137
+ }
138
+ /**
139
+ * Return type for createActorHandler - represents an Actor class constructor
140
+ */
141
+ type ActorHandlerClass<E = unknown> = {
142
+ new (state: any, env: E): Actor<E>;
143
+ get(id: string): ActorStub;
144
+ configuration(request?: Request): ActorConfiguration;
145
+ };
146
+ /**
147
+ * Creates an Actor handler from a room definition
148
+ * @param room - The room definition with lifecycle hooks
149
+ * @returns Actor class for Cloudflare Workers (extends DurableObject)
150
+ */
151
+ declare function createActorHandler<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown>(room: RoomDefinition<TMeta, E>): ActorHandlerClass<E>;
152
+ //#endregion
153
+ //#region src/actor/attachment.d.ts
154
+ declare function storeAttachment(ws: WebSocket, meta: ConnectionMeta): void;
155
+ declare function restoreSessions(actor: any): void;
156
+ //#endregion
157
+ //#region src/client/connection.d.ts
158
+ /**
159
+ * Connection state management for Verani client
160
+ */
161
+ type ConnectionState = "connecting" | "connected" | "disconnected" | "reconnecting" | "error";
162
+ interface ReconnectionConfig {
163
+ /** Enable automatic reconnection */
164
+ enabled: boolean;
165
+ /** Maximum number of reconnection attempts (0 = infinite) */
166
+ maxAttempts: number;
167
+ /** Initial delay in milliseconds */
168
+ initialDelay: number;
169
+ /** Maximum delay in milliseconds */
170
+ maxDelay: number;
171
+ /** Backoff multiplier for exponential backoff */
172
+ backoffMultiplier: number;
173
+ }
174
+ declare const DEFAULT_RECONNECTION_CONFIG: ReconnectionConfig;
175
+ /**
176
+ * Manages WebSocket connection lifecycle and reconnection logic
177
+ */
178
+ declare class ConnectionManager {
179
+ private config;
180
+ private onStateChange?;
181
+ private state;
182
+ private reconnectAttempts;
183
+ private reconnectTimer?;
184
+ private currentDelay;
185
+ constructor(config?: ReconnectionConfig, onStateChange?: ((state: ConnectionState) => void) | undefined);
186
+ /**
187
+ * Gets the current connection state
188
+ */
189
+ getState(): ConnectionState;
190
+ /**
191
+ * Updates the connection state and notifies listeners
192
+ */
193
+ setState(newState: ConnectionState): void;
194
+ /**
195
+ * Resets reconnection state (called on successful connection)
196
+ */
197
+ resetReconnection(): void;
198
+ /**
199
+ * Schedules a reconnection attempt
200
+ */
201
+ scheduleReconnect(connectFn: () => void): boolean;
202
+ /**
203
+ * Cancels any pending reconnection
204
+ */
205
+ cancelReconnect(): void;
206
+ /**
207
+ * Clears the reconnect timer
208
+ */
209
+ private clearReconnectTimer;
210
+ /**
211
+ * Gets the current reconnection attempt count
212
+ */
213
+ getReconnectAttempts(): number;
214
+ /**
215
+ * Gets the next reconnection delay
216
+ */
217
+ getNextDelay(): number;
218
+ /**
219
+ * Cleanup method
220
+ */
221
+ destroy(): void;
222
+ }
223
+ //#endregion
224
+ //#region src/client/client.d.ts
225
+ /**
226
+ * Client options for configuring the Verani client
227
+ */
228
+ interface VeraniClientOptions {
229
+ /** Reconnection configuration */
230
+ reconnection?: Partial<ReconnectionConfig>;
231
+ /** Maximum number of messages to queue when disconnected */
232
+ maxQueueSize?: number;
233
+ /** Connection timeout in milliseconds */
234
+ connectionTimeout?: number;
235
+ }
236
+ /**
237
+ * Verani WebSocket client with automatic reconnection and lifecycle management
238
+ */
239
+ declare class VeraniClient {
240
+ private url;
241
+ private ws?;
242
+ private listeners;
243
+ private connectionManager;
244
+ private messageQueue;
245
+ private options;
246
+ private onOpenCallback?;
247
+ private onCloseCallback?;
248
+ private onErrorCallback?;
249
+ private onStateChangeCallback?;
250
+ private connectionPromise?;
251
+ private connectionResolve?;
252
+ private connectionReject?;
253
+ /**
254
+ * Creates a new Verani client
255
+ * @param url - WebSocket URL to connect to
256
+ * @param options - Client configuration options
257
+ */
258
+ constructor(url: string, options?: VeraniClientOptions);
259
+ /**
260
+ * Establishes WebSocket connection
261
+ */
262
+ private connect;
263
+ /**
264
+ * Handles successful WebSocket connection
265
+ */
266
+ private handleOpen;
267
+ /**
268
+ * Handles incoming WebSocket messages
269
+ */
270
+ private handleMessage;
271
+ /**
272
+ * Handles WebSocket closure
273
+ */
274
+ private handleClose;
275
+ /**
276
+ * Handles WebSocket errors
277
+ */
278
+ private handleError;
279
+ /**
280
+ * Handles connection errors
281
+ */
282
+ private handleConnectionError;
283
+ /**
284
+ * Flushes queued messages when connection is established
285
+ */
286
+ private flushMessageQueue;
287
+ /**
288
+ * Gets the current connection state
289
+ */
290
+ getState(): ConnectionState;
291
+ /**
292
+ * Checks if the client is currently connected
293
+ */
294
+ isConnected(): boolean;
295
+ /**
296
+ * Waits for the connection to be established
297
+ * @returns Promise that resolves when connected
298
+ */
299
+ waitForConnection(): Promise<void>;
300
+ /**
301
+ * Registers an event listener
302
+ * @param event - Event type to listen for
303
+ * @param callback - Callback function to invoke when event is received
304
+ */
305
+ on(event: string, callback: (data: any) => void): void;
306
+ /**
307
+ * Removes an event listener
308
+ * @param event - Event type to remove listener from
309
+ * @param callback - Callback function to remove
310
+ */
311
+ off(event: string, callback: (data: any) => void): void;
312
+ /**
313
+ * Registers a one-time event listener
314
+ * @param event - Event type to listen for
315
+ * @param callback - Callback function to invoke once
316
+ */
317
+ once(event: string, callback: (data: any) => void): void;
318
+ /**
319
+ * Sends a message to the server
320
+ * @param type - Message type
321
+ * @param data - Optional message data
322
+ */
323
+ emit(type: string, data?: any): void;
324
+ /**
325
+ * Queues a message for sending when connected
326
+ */
327
+ private queueMessage;
328
+ /**
329
+ * Registers lifecycle callback for connection open
330
+ */
331
+ onOpen(callback: () => void): void;
332
+ /**
333
+ * Registers lifecycle callback for connection close
334
+ */
335
+ onClose(callback: (event: CloseEvent) => void): void;
336
+ /**
337
+ * Registers lifecycle callback for connection error
338
+ */
339
+ onError(callback: (error: Event) => void): void;
340
+ /**
341
+ * Registers lifecycle callback for state changes
342
+ */
343
+ onStateChange(callback: (state: ConnectionState) => void): void;
344
+ /**
345
+ * Manually triggers a reconnection
346
+ */
347
+ reconnect(): void;
348
+ /**
349
+ * Closes the connection without reconnecting
350
+ */
351
+ disconnect(): void;
352
+ /**
353
+ * Closes the connection and cleans up resources
354
+ */
355
+ close(): void;
356
+ }
357
+ //#endregion
358
+ //#region src/shared/encode.d.ts
359
+ /**
360
+ * Encodes a message frame to JSON string for transmission
361
+ * @param frame - The message frame to encode
362
+ * @returns JSON string representation of the frame
363
+ * @throws Error if encoding fails
364
+ */
365
+ declare function encodeFrame(frame: MessageFrame): string;
366
+ /**
367
+ * Encodes a client message to JSON string
368
+ * @param message - The client message to encode
369
+ * @returns JSON string representation
370
+ */
371
+ declare function encodeClientMessage(message: MessageFrame): string;
372
+ /**
373
+ * Encodes a server message to JSON string
374
+ * @param message - The server message to encode
375
+ * @returns JSON string representation
376
+ */
377
+ declare function encodeServerMessage(message: MessageFrame): string;
378
+ //#endregion
379
+ //#region src/shared/decode.d.ts
380
+ /**
381
+ * Decodes a raw message into a MessageFrame
382
+ * @param raw - Raw data from WebSocket (string, ArrayBuffer, etc)
383
+ * @returns Decoded MessageFrame or null if invalid
384
+ */
385
+ declare function decodeFrame(raw: any): MessageFrame | null;
386
+ /**
387
+ * Decodes a client message
388
+ * @param raw - Raw data from client WebSocket
389
+ * @returns Decoded message or null if invalid
390
+ */
391
+ declare function decodeClientMessage(raw: any): MessageFrame | null;
392
+ /**
393
+ * Decodes a server message
394
+ * @param raw - Raw data from server WebSocket
395
+ * @returns Decoded message or null if invalid
396
+ */
397
+ declare function decodeServerMessage(raw: any): MessageFrame | null;
398
+ //#endregion
399
+ export { type BroadcastOptions, type ClientMessage, ConnectionManager, type ConnectionMeta, type ConnectionState, DEFAULT_RECONNECTION_CONFIG, type MessageContext, type MessageFrame, PROTOCOL_VERSION, type ReconnectionConfig, type RoomContext, type RoomDefinition, type ServerMessage, type VeraniActor, VeraniClient, type VeraniClientOptions, type VeraniMessage, createActorHandler, decodeClientMessage, decodeFrame, decodeServerMessage, defineRoom, encodeClientMessage, encodeFrame, encodeServerMessage, parseJWT, restoreSessions, storeAttachment };
@@ -0,0 +1,399 @@
1
+ import { Actor, ActorConfiguration } from "@cloudflare/actors";
2
+
3
+ //#region src/shared/types.d.ts
4
+
5
+ /**
6
+ * Core message types shared between client and server
7
+ */
8
+ /**
9
+ * Base message frame structure used for all WebSocket communication
10
+ */
11
+ interface MessageFrame {
12
+ type: string;
13
+ channel?: string;
14
+ data?: any;
15
+ }
16
+ /**
17
+ * Message sent from client to server
18
+ */
19
+ interface ClientMessage extends MessageFrame {
20
+ type: string;
21
+ channel?: string;
22
+ data?: any;
23
+ }
24
+ /**
25
+ * Message sent from server to client
26
+ */
27
+ interface ServerMessage extends MessageFrame {
28
+ type: string;
29
+ channel?: string;
30
+ data?: any;
31
+ }
32
+ /**
33
+ * Connection metadata attached to each WebSocket
34
+ */
35
+ interface ConnectionMeta {
36
+ userId: string;
37
+ clientId: string;
38
+ channels: string[];
39
+ }
40
+ /**
41
+ * Unified message type for both directions
42
+ */
43
+ type VeraniMessage = ClientMessage | ServerMessage;
44
+ /**
45
+ * Protocol version for future compatibility
46
+ */
47
+ declare const PROTOCOL_VERSION = "1.0.0";
48
+ //#endregion
49
+ //#region src/actor/types.d.ts
50
+ /**
51
+ * Options for broadcasting messages to connections
52
+ */
53
+ interface BroadcastOptions {
54
+ /** Exclude specific WebSocket from receiving the broadcast */
55
+ except?: WebSocket;
56
+ /** Only send to specific user IDs */
57
+ userIds?: string[];
58
+ /** Only send to specific client IDs */
59
+ clientIds?: string[];
60
+ }
61
+ /**
62
+ * Extended Actor interface with Verani-specific methods
63
+ */
64
+ interface VeraniActor<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> extends Actor<E> {
65
+ sessions: Map<WebSocket, {
66
+ ws: WebSocket;
67
+ meta: TMeta;
68
+ }>;
69
+ broadcast(channel: string, data: any, opts?: BroadcastOptions): number;
70
+ getSessionCount(): number;
71
+ getConnectedUserIds(): string[];
72
+ getUserSessions(userId: string): WebSocket[];
73
+ sendToUser(userId: string, type: string, data?: any): number;
74
+ getStorage(): DurableObjectStorage;
75
+ }
76
+ /**
77
+ * Context provided to room lifecycle hooks
78
+ */
79
+ interface RoomContext<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> {
80
+ /** The actor instance handling this connection */
81
+ actor: VeraniActor<TMeta, E>;
82
+ /** The WebSocket connection */
83
+ ws: WebSocket;
84
+ /** Connection metadata */
85
+ meta: TMeta;
86
+ }
87
+ /**
88
+ * Context for onMessage hook with frame included
89
+ */
90
+ interface MessageContext<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> extends RoomContext<TMeta, E> {
91
+ /** The received message frame */
92
+ frame: MessageFrame;
93
+ }
94
+ /**
95
+ * Room definition with lifecycle hooks
96
+ */
97
+ interface RoomDefinition<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> {
98
+ /** Optional room name for debugging */
99
+ name?: string;
100
+ /** WebSocket upgrade path (default: "/ws") */
101
+ websocketPath: string;
102
+ /** Extract metadata from the connection request */
103
+ extractMeta?(req: Request): TMeta | Promise<TMeta>;
104
+ /** Called when a new WebSocket connection is established */
105
+ onConnect?(ctx: RoomContext<TMeta, E>): void | Promise<void>;
106
+ /** Called when a WebSocket connection is closed */
107
+ onDisconnect?(ctx: RoomContext<TMeta, E>): void | Promise<void>;
108
+ /** Called when a message is received from a connection */
109
+ onMessage?(ctx: MessageContext<TMeta, E>, frame: MessageFrame): void | Promise<void>;
110
+ /** Called when an error occurs in a lifecycle hook */
111
+ onError?(error: Error, ctx: RoomContext<TMeta, E>): void | Promise<void>;
112
+ /** Called after actor wakes from hibernation and sessions are restored */
113
+ onHibernationRestore?(actor: VeraniActor<TMeta, E>): void | Promise<void>;
114
+ }
115
+ //#endregion
116
+ //#region src/actor/router.d.ts
117
+ /**
118
+ * Defines a room with lifecycle hooks and metadata extraction
119
+ * @param def - Room definition with optional hooks
120
+ * @returns Normalized room definition with defaults
121
+ */
122
+ declare function defineRoom<TMeta extends ConnectionMeta = ConnectionMeta>(def: RoomDefinition<TMeta>): RoomDefinition<TMeta>;
123
+ /**
124
+ * Helper to parse JWT tokens (basic implementation)
125
+ * In production, use a proper JWT library
126
+ * @param token - JWT token string
127
+ * @returns Decoded payload or null if invalid
128
+ */
129
+ declare function parseJWT(token: string): any;
130
+ //#endregion
131
+ //#region src/actor/actor-runtime.d.ts
132
+ /**
133
+ * Actor stub interface returned by .get() method
134
+ */
135
+ interface ActorStub {
136
+ fetch(request: Request): Promise<Response>;
137
+ }
138
+ /**
139
+ * Return type for createActorHandler - represents an Actor class constructor
140
+ */
141
+ type ActorHandlerClass<E = unknown> = {
142
+ new (state: any, env: E): Actor<E>;
143
+ get(id: string): ActorStub;
144
+ configuration(request?: Request): ActorConfiguration;
145
+ };
146
+ /**
147
+ * Creates an Actor handler from a room definition
148
+ * @param room - The room definition with lifecycle hooks
149
+ * @returns Actor class for Cloudflare Workers (extends DurableObject)
150
+ */
151
+ declare function createActorHandler<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown>(room: RoomDefinition<TMeta, E>): ActorHandlerClass<E>;
152
+ //#endregion
153
+ //#region src/actor/attachment.d.ts
154
+ declare function storeAttachment(ws: WebSocket, meta: ConnectionMeta): void;
155
+ declare function restoreSessions(actor: any): void;
156
+ //#endregion
157
+ //#region src/client/connection.d.ts
158
+ /**
159
+ * Connection state management for Verani client
160
+ */
161
+ type ConnectionState = "connecting" | "connected" | "disconnected" | "reconnecting" | "error";
162
+ interface ReconnectionConfig {
163
+ /** Enable automatic reconnection */
164
+ enabled: boolean;
165
+ /** Maximum number of reconnection attempts (0 = infinite) */
166
+ maxAttempts: number;
167
+ /** Initial delay in milliseconds */
168
+ initialDelay: number;
169
+ /** Maximum delay in milliseconds */
170
+ maxDelay: number;
171
+ /** Backoff multiplier for exponential backoff */
172
+ backoffMultiplier: number;
173
+ }
174
+ declare const DEFAULT_RECONNECTION_CONFIG: ReconnectionConfig;
175
+ /**
176
+ * Manages WebSocket connection lifecycle and reconnection logic
177
+ */
178
+ declare class ConnectionManager {
179
+ private config;
180
+ private onStateChange?;
181
+ private state;
182
+ private reconnectAttempts;
183
+ private reconnectTimer?;
184
+ private currentDelay;
185
+ constructor(config?: ReconnectionConfig, onStateChange?: ((state: ConnectionState) => void) | undefined);
186
+ /**
187
+ * Gets the current connection state
188
+ */
189
+ getState(): ConnectionState;
190
+ /**
191
+ * Updates the connection state and notifies listeners
192
+ */
193
+ setState(newState: ConnectionState): void;
194
+ /**
195
+ * Resets reconnection state (called on successful connection)
196
+ */
197
+ resetReconnection(): void;
198
+ /**
199
+ * Schedules a reconnection attempt
200
+ */
201
+ scheduleReconnect(connectFn: () => void): boolean;
202
+ /**
203
+ * Cancels any pending reconnection
204
+ */
205
+ cancelReconnect(): void;
206
+ /**
207
+ * Clears the reconnect timer
208
+ */
209
+ private clearReconnectTimer;
210
+ /**
211
+ * Gets the current reconnection attempt count
212
+ */
213
+ getReconnectAttempts(): number;
214
+ /**
215
+ * Gets the next reconnection delay
216
+ */
217
+ getNextDelay(): number;
218
+ /**
219
+ * Cleanup method
220
+ */
221
+ destroy(): void;
222
+ }
223
+ //#endregion
224
+ //#region src/client/client.d.ts
225
+ /**
226
+ * Client options for configuring the Verani client
227
+ */
228
+ interface VeraniClientOptions {
229
+ /** Reconnection configuration */
230
+ reconnection?: Partial<ReconnectionConfig>;
231
+ /** Maximum number of messages to queue when disconnected */
232
+ maxQueueSize?: number;
233
+ /** Connection timeout in milliseconds */
234
+ connectionTimeout?: number;
235
+ }
236
+ /**
237
+ * Verani WebSocket client with automatic reconnection and lifecycle management
238
+ */
239
+ declare class VeraniClient {
240
+ private url;
241
+ private ws?;
242
+ private listeners;
243
+ private connectionManager;
244
+ private messageQueue;
245
+ private options;
246
+ private onOpenCallback?;
247
+ private onCloseCallback?;
248
+ private onErrorCallback?;
249
+ private onStateChangeCallback?;
250
+ private connectionPromise?;
251
+ private connectionResolve?;
252
+ private connectionReject?;
253
+ /**
254
+ * Creates a new Verani client
255
+ * @param url - WebSocket URL to connect to
256
+ * @param options - Client configuration options
257
+ */
258
+ constructor(url: string, options?: VeraniClientOptions);
259
+ /**
260
+ * Establishes WebSocket connection
261
+ */
262
+ private connect;
263
+ /**
264
+ * Handles successful WebSocket connection
265
+ */
266
+ private handleOpen;
267
+ /**
268
+ * Handles incoming WebSocket messages
269
+ */
270
+ private handleMessage;
271
+ /**
272
+ * Handles WebSocket closure
273
+ */
274
+ private handleClose;
275
+ /**
276
+ * Handles WebSocket errors
277
+ */
278
+ private handleError;
279
+ /**
280
+ * Handles connection errors
281
+ */
282
+ private handleConnectionError;
283
+ /**
284
+ * Flushes queued messages when connection is established
285
+ */
286
+ private flushMessageQueue;
287
+ /**
288
+ * Gets the current connection state
289
+ */
290
+ getState(): ConnectionState;
291
+ /**
292
+ * Checks if the client is currently connected
293
+ */
294
+ isConnected(): boolean;
295
+ /**
296
+ * Waits for the connection to be established
297
+ * @returns Promise that resolves when connected
298
+ */
299
+ waitForConnection(): Promise<void>;
300
+ /**
301
+ * Registers an event listener
302
+ * @param event - Event type to listen for
303
+ * @param callback - Callback function to invoke when event is received
304
+ */
305
+ on(event: string, callback: (data: any) => void): void;
306
+ /**
307
+ * Removes an event listener
308
+ * @param event - Event type to remove listener from
309
+ * @param callback - Callback function to remove
310
+ */
311
+ off(event: string, callback: (data: any) => void): void;
312
+ /**
313
+ * Registers a one-time event listener
314
+ * @param event - Event type to listen for
315
+ * @param callback - Callback function to invoke once
316
+ */
317
+ once(event: string, callback: (data: any) => void): void;
318
+ /**
319
+ * Sends a message to the server
320
+ * @param type - Message type
321
+ * @param data - Optional message data
322
+ */
323
+ emit(type: string, data?: any): void;
324
+ /**
325
+ * Queues a message for sending when connected
326
+ */
327
+ private queueMessage;
328
+ /**
329
+ * Registers lifecycle callback for connection open
330
+ */
331
+ onOpen(callback: () => void): void;
332
+ /**
333
+ * Registers lifecycle callback for connection close
334
+ */
335
+ onClose(callback: (event: CloseEvent) => void): void;
336
+ /**
337
+ * Registers lifecycle callback for connection error
338
+ */
339
+ onError(callback: (error: Event) => void): void;
340
+ /**
341
+ * Registers lifecycle callback for state changes
342
+ */
343
+ onStateChange(callback: (state: ConnectionState) => void): void;
344
+ /**
345
+ * Manually triggers a reconnection
346
+ */
347
+ reconnect(): void;
348
+ /**
349
+ * Closes the connection without reconnecting
350
+ */
351
+ disconnect(): void;
352
+ /**
353
+ * Closes the connection and cleans up resources
354
+ */
355
+ close(): void;
356
+ }
357
+ //#endregion
358
+ //#region src/shared/encode.d.ts
359
+ /**
360
+ * Encodes a message frame to JSON string for transmission
361
+ * @param frame - The message frame to encode
362
+ * @returns JSON string representation of the frame
363
+ * @throws Error if encoding fails
364
+ */
365
+ declare function encodeFrame(frame: MessageFrame): string;
366
+ /**
367
+ * Encodes a client message to JSON string
368
+ * @param message - The client message to encode
369
+ * @returns JSON string representation
370
+ */
371
+ declare function encodeClientMessage(message: MessageFrame): string;
372
+ /**
373
+ * Encodes a server message to JSON string
374
+ * @param message - The server message to encode
375
+ * @returns JSON string representation
376
+ */
377
+ declare function encodeServerMessage(message: MessageFrame): string;
378
+ //#endregion
379
+ //#region src/shared/decode.d.ts
380
+ /**
381
+ * Decodes a raw message into a MessageFrame
382
+ * @param raw - Raw data from WebSocket (string, ArrayBuffer, etc)
383
+ * @returns Decoded MessageFrame or null if invalid
384
+ */
385
+ declare function decodeFrame(raw: any): MessageFrame | null;
386
+ /**
387
+ * Decodes a client message
388
+ * @param raw - Raw data from client WebSocket
389
+ * @returns Decoded message or null if invalid
390
+ */
391
+ declare function decodeClientMessage(raw: any): MessageFrame | null;
392
+ /**
393
+ * Decodes a server message
394
+ * @param raw - Raw data from server WebSocket
395
+ * @returns Decoded message or null if invalid
396
+ */
397
+ declare function decodeServerMessage(raw: any): MessageFrame | null;
398
+ //#endregion
399
+ export { type BroadcastOptions, type ClientMessage, ConnectionManager, type ConnectionMeta, type ConnectionState, DEFAULT_RECONNECTION_CONFIG, type MessageContext, type MessageFrame, PROTOCOL_VERSION, type ReconnectionConfig, type RoomContext, type RoomDefinition, type ServerMessage, type VeraniActor, VeraniClient, type VeraniClientOptions, type VeraniMessage, createActorHandler, decodeClientMessage, decodeFrame, decodeServerMessage, defineRoom, encodeClientMessage, encodeFrame, encodeServerMessage, parseJWT, restoreSessions, storeAttachment };
@@ -0,0 +1 @@
1
+ import{Actor}from"@cloudflare/actors";function extractUserId(e){let b=new URL(e.url),x=b.searchParams.get(`userId`)||b.searchParams.get(`user_id`);if(x)return x;let S=e.headers.get(`Authorization`);return S?.startsWith(`Bearer `)?S.substring(7):e.headers.get(`X-User-ID`)||`anonymous`}function extractClientId(e){let b=new URL(e.url);return b.searchParams.get(`clientId`)||b.searchParams.get(`client_id`)||e.headers.get(`X-Client-ID`)||crypto.randomUUID()}function defaultExtractMeta(e){let S=extractUserId(e),C=extractClientId(e),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 parseJWT(e){try{let b=e.split(`.`);if(b.length!==3)return null;let x=b[1],S=atob(x.replace(/-/g,`+`).replace(/_/g,`/`));return JSON.parse(S)}catch{return null}}function storeAttachment(e,b){e.serializeAttachment(b)}function restoreSessions(e){let b=0;for(let x of e.ctx.getWebSockets()){let S=x.deserializeAttachment();S&&(e.sessions.set(x,{ws:x,meta:S}),b++)}}function isValidFrame(e){return e&&typeof e==`object`&&typeof e.type==`string`&&(e.channel===void 0||typeof e.channel==`string`)}function decodeFrame(e){try{let b=typeof e==`string`?e:e.toString(),x=JSON.parse(b);return isValidFrame(x)?x:null}catch{return null}}function decodeClientMessage(e){return decodeFrame(e)}function decodeServerMessage(e){return decodeFrame(e)}function encodeFrame(e){try{return JSON.stringify(e)}catch(e){throw Error(`Failed to encode frame: ${e instanceof Error?e.message:`unknown error`}`)}}function encodeClientMessage(e){return encodeFrame(e)}function encodeServerMessage(e){return encodeFrame(e)}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(b){let x=sanitizeToClassName(b.name||b.websocketPath||`VeraniActor`);class S extends Actor{constructor(...e){super(...e),this.sessions=new Map}static configuration(e){return{locationHint:`me`,sockets:{upgradePath:b.websocketPath,autoResponse:{ping:`ping`,pong:`pong`}}}}async shouldUpgradeWebSocket(e){return!0}async fetch(e){let x=new URL(e.url),S=e.headers.get(`Upgrade`);return x.pathname===b.websocketPath&&S===`websocket`&&await this.shouldUpgradeWebSocket(e)?this.onWebSocketUpgrade(e):this.onRequest(e)}async onInit(){try{restoreSessions(this),b.onHibernationRestore&&this.sessions.size>0&&await b.onHibernationRestore(this)}catch{}}onWebSocketConnect(e,x){try{let S;if(S=b.extractMeta?b.extractMeta(x):{userId:`anonymous`,clientId:crypto.randomUUID(),channels:[`default`]},storeAttachment(e,S),this.sessions.set(e,{ws:e,meta:S}),b.onConnect){let x={actor:this,ws:e,meta:S};b.onConnect(x)}}catch(x){if(b.onError)try{b.onError(x,{actor:this,ws:e,meta:{userId:`unknown`,clientId:`unknown`,channels:[]}})}catch{}e.close(1011,`Internal server error`)}}onWebSocketMessage(e,x){let S;try{let C=decodeFrame$1(x);if(S=this.sessions.get(e),!S)return;if(b.onMessage){let x={actor:this,ws:e,meta:S.meta,frame:C};b.onMessage(x,C)}}catch(x){if(b.onError&&S)try{b.onError(x,{actor:this,ws:e,meta:S.meta})}catch{}}}onWebSocketDisconnect(e){try{let x=this.sessions.get(e);if(this.sessions.delete(e),x&&b.onDisconnect){let S={actor:this,ws:e,meta:x.meta};b.onDisconnect(S)}}catch{}}broadcast(e,b,x){let S=0,C=encodeFrame$1({type:`event`,channel:e,data:b});for(let{ws:b,meta:w}of this.sessions.values())if(w.channels.includes(e)&&!(x?.except&&b===x.except)&&!(x?.userIds&&!x.userIds.includes(w.userId))&&!(x?.clientIds&&!x.clientIds.includes(w.clientId)))try{b.send(C),S++}catch{}return S}getSessionCount(){return this.sessions.size}getConnectedUserIds(){let e=new Set;for(let{meta:b}of this.sessions.values())e.add(b.userId);return Array.from(e)}getUserSessions(e){let b=[];for(let{ws:x,meta:S}of this.sessions.values())S.userId===e&&b.push(x);return b}sendToUser(e,b,x){let S=0,C=encodeFrame$1({type:b,data:x});for(let{ws:b,meta:x}of this.sessions.values())if(x.userId===e)try{b.send(C),S++}catch{}return S}getStorage(){return this.ctx.storage}}return Object.defineProperty(S,`name`,{value:x,writable:!1,configurable:!0}),S}function encodeClientMessage$1(e){return encodeClientMessage(e)}function decodeServerMessage$1(e){return decodeServerMessage(e)}const DEFAULT_RECONNECTION_CONFIG={enabled:!0,maxAttempts:10,initialDelay:1e3,maxDelay:3e4,backoffMultiplier:1.5};var ConnectionManager=class{constructor(e=DEFAULT_RECONNECTION_CONFIG,b){this.config=e,this.onStateChange=b,this.state=`disconnected`,this.reconnectAttempts=0,this.currentDelay=e.initialDelay}getState(){return this.state}setState(e){this.state!==e&&(this.state=e,this.onStateChange?.(e))}resetReconnection(){this.reconnectAttempts=0,this.currentDelay=this.config.initialDelay,this.clearReconnectTimer()}scheduleReconnect(e){return this.config.enabled?this.config.maxAttempts>0&&this.reconnectAttempts>=this.config.maxAttempts?(this.setState(`error`),!1):(this.clearReconnectTimer(),this.setState(`reconnecting`),this.reconnectAttempts++,this.reconnectTimer=setTimeout(()=>{e(),this.currentDelay=Math.min(this.currentDelay*this.config.backoffMultiplier,this.config.maxDelay)},this.currentDelay),!0):!1}cancelReconnect(){this.clearReconnectTimer(),this.state===`reconnecting`&&this.setState(`disconnected`)}clearReconnectTimer(){this.reconnectTimer!==void 0&&(clearTimeout(this.reconnectTimer),this.reconnectTimer=void 0)}getReconnectAttempts(){return this.reconnectAttempts}getNextDelay(){return this.currentDelay}destroy(){this.clearReconnectTimer()}},VeraniClient=class{constructor(e,b={}){this.url=e,this.listeners=new Map,this.messageQueue=[],this.options={reconnection:{enabled:b.reconnection?.enabled??DEFAULT_RECONNECTION_CONFIG.enabled,maxAttempts:b.reconnection?.maxAttempts??DEFAULT_RECONNECTION_CONFIG.maxAttempts,initialDelay:b.reconnection?.initialDelay??DEFAULT_RECONNECTION_CONFIG.initialDelay,maxDelay:b.reconnection?.maxDelay??DEFAULT_RECONNECTION_CONFIG.maxDelay,backoffMultiplier:b.reconnection?.backoffMultiplier??DEFAULT_RECONNECTION_CONFIG.backoffMultiplier},maxQueueSize:b.maxQueueSize??100,connectionTimeout:b.connectionTimeout??1e4},this.connectionManager=new ConnectionManager(this.options.reconnection,e=>{this.onStateChangeCallback?.(e)}),this.connect()}connect(){try{this.connectionManager.setState(`connecting`),this.ws=new WebSocket(this.url);let e=setTimeout(()=>{this.connectionManager.getState()===`connecting`&&(this.ws?.close(),this.handleConnectionError(Error(`Connection timeout`)))},this.options.connectionTimeout);this.ws.addEventListener(`open`,()=>{clearTimeout(e),this.handleOpen()}),this.ws.addEventListener(`message`,e=>{this.handleMessage(e)}),this.ws.addEventListener(`close`,b=>{clearTimeout(e),this.handleClose(b)}),this.ws.addEventListener(`error`,b=>{clearTimeout(e),this.handleError(b)})}catch(e){this.handleConnectionError(e)}}handleOpen(){this.connectionManager.setState(`connected`),this.connectionManager.resetReconnection(),this.flushMessageQueue(),this.connectionResolve&&(this.connectionResolve(),this.connectionPromise=void 0,this.connectionResolve=void 0,this.connectionReject=void 0),this.onOpenCallback?.()}handleMessage(e){let b=decodeServerMessage$1(e.data);if(!b)return;let x=b.type,S=b.data;b.type===`event`&&b.data&&typeof b.data==`object`&&`type`in b.data&&(x=b.data.type,S=b.data);let C=this.listeners.get(x);if(C)for(let e of C)try{e(S)}catch{}}handleClose(e){this.connectionManager.setState(`disconnected`),this.connectionReject&&=(this.connectionReject(Error(`Connection closed: ${e.reason||`Unknown reason`}`)),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0),this.onCloseCallback?.(e),e.code!==1e3&&e.code!==1001&&this.connectionManager.scheduleReconnect(()=>this.connect())}handleError(e){this.onErrorCallback?.(e)}handleConnectionError(e){this.connectionReject&&=(this.connectionReject(e),this.connectionPromise=void 0,this.connectionResolve=void 0,void 0),this.connectionManager.scheduleReconnect(()=>this.connect())}flushMessageQueue(){if(!(!this.ws||this.ws.readyState!==WebSocket.OPEN))for(;this.messageQueue.length>0;){let e=this.messageQueue.shift();try{this.ws.send(encodeClientMessage$1(e))}catch{}}}getState(){return this.connectionManager.getState()}isConnected(){return this.ws?.readyState===WebSocket.OPEN}waitForConnection(){return this.isConnected()?Promise.resolve():(this.connectionPromise||=new Promise((e,b)=>{this.connectionResolve=e,this.connectionReject=b}),this.connectionPromise)}on(e,b){this.listeners.has(e)||this.listeners.set(e,new Set),this.listeners.get(e).add(b)}off(e,b){let x=this.listeners.get(e);x&&(x.delete(b),x.size===0&&this.listeners.delete(e))}once(e,b){let x=S=>{this.off(e,x),b(S)};this.on(e,x)}emit(e,b){let x={type:e,data:b};if(this.isConnected())try{this.ws.send(encodeClientMessage$1(x))}catch{this.queueMessage(x)}else this.queueMessage(x)}queueMessage(e){this.messageQueue.length>=this.options.maxQueueSize&&this.messageQueue.shift(),this.messageQueue.push(e)}onOpen(e){this.onOpenCallback=e}onClose(e){this.onCloseCallback=e}onError(e){this.onErrorCallback=e}onStateChange(e){this.onStateChangeCallback=e}reconnect(){this.disconnect(),this.connect()}disconnect(){this.connectionManager.cancelReconnect(),this.ws&&=(this.ws.close(1e3,`Client disconnect`),void 0)}close(){this.disconnect(),this.listeners.clear(),this.messageQueue=[],this.connectionManager.destroy()}};const PROTOCOL_VERSION=`1.0.0`;export{ConnectionManager,DEFAULT_RECONNECTION_CONFIG,PROTOCOL_VERSION,VeraniClient,createActorHandler,decodeClientMessage,decodeFrame,decodeServerMessage,defineRoom,encodeClientMessage,encodeFrame,encodeServerMessage,parseJWT,restoreSessions,storeAttachment};
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "verani",
3
+ "version": "0.1.0",
4
+ "description": "A simple, focused realtime SDK for Cloudflare Actors with Socket.io-like semantics",
5
+ "license": "ISC",
6
+ "keywords": [
7
+ "cloudflare",
8
+ "actors",
9
+ "durable-objects",
10
+ "realtime",
11
+ "websocket",
12
+ "socket.io"
13
+ ],
14
+ "main": "./dist/verani.cjs",
15
+ "module": "./dist/verani.mjs",
16
+ "types": "./dist/verani.d.cts",
17
+ "exports": {
18
+ ".": {
19
+ "import": {
20
+ "types": "./dist/verani.d.mts",
21
+ "default": "./dist/verani.mjs"
22
+ },
23
+ "require": {
24
+ "types": "./dist/verani.d.cts",
25
+ "default": "./dist/verani.cjs"
26
+ }
27
+ }
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "scripts": {
35
+ "build": "tsdown",
36
+ "prepublishOnly": "npm run build",
37
+ "deploy": "wrangler deploy",
38
+ "dev": "wrangler dev",
39
+ "start": "wrangler dev",
40
+ "test": "vitest",
41
+ "cf-typegen": "wrangler types"
42
+ },
43
+ "devDependencies": {
44
+ "@cloudflare/vitest-pool-workers": "^0.8.19",
45
+ "esbuild-fix-imports-plugin": "^1.0.23",
46
+ "tsdown": "^0.16.7",
47
+ "typescript": "^5",
48
+ "vitest": "~3.2.0",
49
+ "wrangler": "4.51.0"
50
+ },
51
+ "peerDependencies": {
52
+ "typescript": "^5",
53
+ "@cloudflare/actors": "^0.0.1-beta.6"
54
+ }
55
+ }