verani 0.11.1 → 0.12.2

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.
@@ -0,0 +1 @@
1
+ import{n as encodeFrame$1}from"./encode-CFndA-BQ.mjs";import{Actor}from"@cloudflare/actors";function storeAttachment(e,f){e.serializeAttachment(f)}function encodeFrame(f){return encodeFrame$1(f)}const PERSIST_ERROR_HANDLER=Symbol(`PERSIST_ERROR_HANDLER`),PERSISTED_STATE=Symbol(`PERSISTED_STATE`),STATE_READY=Symbol(`STATE_READY`);var PersistNotReadyError=class extends Error{constructor(e){super(`Cannot access persisted state key "${e}" before storage is initialized. Wait for onInit to complete or check actor.isStateReady().`),this.name=`PersistNotReadyError`}},PersistError=class extends Error{constructor(e,f){super(`Failed to persist key "${e}": ${f.message}`),this.name=`PersistError`,this.originalCause=f}};function safeSerialize(e){let f=new WeakSet;return JSON.stringify(e,(e,p)=>{if(p instanceof Date)return{__type:`Date`,value:p.toISOString()};if(p instanceof RegExp)return{__type:`RegExp`,source:p.source,flags:p.flags};if(p instanceof Map)return{__type:`Map`,entries:Array.from(p.entries())};if(p instanceof Set)return{__type:`Set`,values:Array.from(p.values())};if(p instanceof Error)return{__type:`Error`,name:p.name,message:p.message};if(typeof p==`object`&&p){if(f.has(p))return;f.add(p)}if(!(typeof p==`function`||typeof p==`symbol`))return p})}function safeDeserialize(e){return JSON.parse(e,(e,f)=>{if(f&&typeof f==`object`&&f.__type)switch(f.__type){case`Date`:return new Date(f.value);case`RegExp`:return new RegExp(f.source,f.flags);case`Map`:return new Map(f.entries);case`Set`:return new Set(f.values);case`Error`:{let e=Error(f.message);return e.name=f.name,e}}return f})}function createShallowProxy(e,f,p){return new Proxy(e,{set(e,p,m){let h=Reflect.set(e,p,m);return h&&typeof p==`string`&&f(p,m),h},deleteProperty(e,f){let m=Reflect.deleteProperty(e,f);return m&&typeof f==`string`&&p(f),m}})}async function initializePersistedState(e,f,p=[],m={}){let{shallow:v=!0,throwOnError:b=!0}=m,x=e.ctx.storage,S=p.length>0?p.map(String):Object.keys(f),C={...f};for(let e of S)try{let f=await x.get(`_verani_persist:${e}`);if(f!==void 0)try{C[e]=safeDeserialize(f)}catch(f){if(b)throw new PersistError(e,f)}}catch(f){if(b&&!(f instanceof PersistError))throw new PersistError(e,f)}let w=async(f,p)=>{if(S.includes(f))try{let e=safeSerialize(p);await x.put(`_verani_persist:${f}`,e)}catch(p){let m=e[PERSIST_ERROR_HANDLER];if(m&&m(f,p),b)throw new PersistError(f,p)}},T=async f=>{if(S.includes(f))try{await x.delete(`_verani_persist:${f}`)}catch(p){let m=e[PERSIST_ERROR_HANDLER];if(m&&m(f,p),b)throw new PersistError(f,p)}},E;return E=v?createShallowProxy(C,(e,f)=>{w(String(e),f)},e=>{T(String(e))}):createDeepProxy(C,(e,f)=>{w(e,f)},e=>{T(e)},S),e[STATE_READY]=!0,e[PERSISTED_STATE]=E,E}function createDeepProxy(e,f,p,m,h,g){return new Proxy(e,{get(_,v){let y=Reflect.get(_,v);if(y&&typeof y==`object`&&!Array.isArray(y)){let _=g??String(v);if(m.includes(_)||g!==void 0)return createDeepProxy(y,f,p,m,h??e,_)}return y},set(e,p,_){let v=Reflect.set(e,p,_);if(v){let e=g??String(p);m.includes(e)&&(g&&h?f(g,h[g]):f(String(p),_))}return v},deleteProperty(e,_){let v=Reflect.deleteProperty(e,_);if(v){let e=g??String(_);m.includes(e)&&(g&&h?f(g,h[g]):p(String(_)))}return v}})}function isStateReady(e){return e[STATE_READY]===!0}function getPersistedState(e){if(!isStateReady(e))throw new PersistNotReadyError(`state`);return e[PERSISTED_STATE]}function setPersistErrorHandler(e,f){e[PERSIST_ERROR_HANDLER]=f}const setPeristErrorHandler=setPersistErrorHandler;async function persistKey(e,f,p){let m=safeSerialize(p);await e.ctx.storage.put(`_verani_persist:${f}`,m)}async function deletePersistedKey(e,f){await e.ctx.storage.delete(`_verani_persist:${f}`)}async function getPersistedKeys(e){let f=await e.ctx.storage.list({prefix:`_verani_persist:`});return Array.from(f.keys()).map(e=>e.replace(`_verani_persist:`,``))}async function clearPersistedState(e){let f=await e.ctx.storage.list({prefix:`_verani_persist:`}),p=Array.from(f.keys());await e.ctx.storage.delete(p)}const WS=Symbol(`WS`),META=Symbol(`META`),ROOMS=Symbol(`ROOMS`);function createConnectionHandler(e){let h=e.name||`VeraniConnectionDO`,v=e.websocketPath||`/ws`;class y extends Actor{constructor(...f){super(...f),this[WS]=null,this[META]=null,this[ROOMS]=new Map,this[STATE_READY]=!1,this[PERSISTED_STATE]=e.state?{...e.state}:{},this.handlers=new Map}get connectionState(){return this[PERSISTED_STATE]}isStateReady(){return isStateReady(this)}static configuration(e){return{sockets:{upgradePath:v}}}getRoomBinding(f){if(!e.rooms)throw Error(`Cannot access room "${f}": no "rooms" config provided in ConnectionDefinition. Add rooms: { "${f}": "YourRoomBinding" } to your definition.`);let p=e.rooms[f]??e.rooms[f.split(`:`,1)[0]];if(!p)throw Error(`Cannot access room "${f}": not found in "rooms" config. Available rooms: ${Object.keys(e.rooms).join(`, `)}`);let m=this.env[p];if(!m)throw Error(`Room "${f}" binding "${p}" not found in environment. Check your wrangler.toml durable_objects bindings.`);return m}getConnectionBinding(){if(!e.connectionBinding)throw Error(`Cannot resolve ConnectionDO: no "connectionBinding" provided in ConnectionDefinition. Add connectionBinding: "YourConnectionBinding" to your definition.`);let f=this.env[e.connectionBinding];if(!f)throw Error(`ConnectionDO binding "${e.connectionBinding}" not found in environment. Check your wrangler.toml durable_objects bindings.`);return f}createEmit(){let e=this;return{emit(f,p){e.sendToWebSocket(f,p)},to(f){return f.startsWith(`room:`)?e.createRoomEmitBuilder(f.slice(5)):e.createUserEmitBuilder(f)},toRoom(f,p){return e.createRoomEmitBuilder(f,p)},toUser(f){return e.createUserEmitBuilder(f)}}}createRoomEmitBuilder(e,f){let p=this;return{async emit(m,h){try{let g=p.getRoomBinding(e).get(e),_=f?.includeSelf||!p[META]?.userId?void 0:{exceptUserId:p[META].userId};return await g.broadcast(m,h,_)}catch{return 0}}}}createUserEmitBuilder(e){let f=this;return{async emit(p,m){try{return await f.getConnectionBinding().get(e).deliverMessage(p,m)?1:0}catch{return 0}}}}sendToWebSocket(e,f){let p=this[WS];if(!p||p.readyState!==WebSocket.OPEN)return!1;try{let h={type:`event`,channel:`default`,data:{type:e,...f}};return p.send(encodeFrame(h)),!0}catch{return!1}}createContext(){return{actor:this,ws:this[WS],meta:this[META],emit:this.createEmit(),state:this.connectionState}}async onInit(){e.state&&(e.onPersistError&&setPeristErrorHandler(this,e.onPersistError),this[PERSISTED_STATE]=await initializePersistedState(this,e.state,e.persistedKeys,e.persistOptions));let f=await this.ctx.storage.get(`_connection_rooms`);if(f&&Array.isArray(f)&&(this[ROOMS]=new Map(f.map(e=>[e.roomName,e.metadata]))),e.handlers)for(let[f,p]of e.handlers.entries())this.handlers.set(f,p);let p=this.ctx.getWebSockets();if(p.length>0){let f=p[0];if(f.readyState===WebSocket.OPEN){this[WS]=f;let p=f.deserializeAttachment();p&&(this[META]=p,await this.rejoinRoomsAfterHibernation(),e.onHibernationRestore&&await e.onHibernationRestore(this))}}}shouldUpgradeSocket(e){return!0}onWebSocketUpgrade(e){let f=new WebSocketPair,[p,m]=Object.values(f);this.ctx.acceptWebSocket(m);let h=new Response(null,{status:101,webSocket:p});return Promise.resolve().then(()=>{this.onWebSocketConnect(m,e)}),h}async onWebSocketConnect(f,m){this[WS]&&this[WS].readyState===WebSocket.OPEN&&this[WS].close(1e3,`New connection established`);let h;if(h=e.extractMeta?await e.extractMeta(m):{userId:`anonymous`,clientId:crypto.randomUUID(),channels:[`default`]},storeAttachment(f,h),this[WS]=f,this[META]=h,e.onConnect)try{await e.onConnect(this.createContext())}catch(p){e.onError&&await e.onError(p,this.createContext()),f.close(1011,`Connection handler error`);return}}async onWebSocketMessage(f,p){if(this[META])try{let f=typeof p==`string`?p:p.toString(),m=JSON.parse(f),h=m,g=h.data,_=g?.type||h.type,v=this.handlers.get(_);if(v){await v(this.createContext(),g);return}e.onMessage&&await e.onMessage(this.createContext(),m)}catch(f){e.onError&&await e.onError(f,this.createContext())}}async onWebSocketDisconnect(f){if(this[META]&&e.onDisconnect)try{await e.onDisconnect(this.createContext())}catch{}for(let e of this[ROOMS].keys())try{await this.leaveRoomInternal(e)}catch{}this[WS]=null}async deliverMessage(e,f){return this.sendToWebSocket(e,f)}async deliverSystemEvent(e,f){this.sendToWebSocket(`system:${e}`,f)}async getUserId(){return this[META]?.userId??null}async isConnected(){return this[WS]!==null&&this[WS].readyState===WebSocket.OPEN}async joinRoom(e,f){if(!this[META])throw Error(`Cannot join room: not connected`);await this.getRoomBinding(e).get(e).join(this[META].userId,f),this[ROOMS].set(e,f),await this.persistRooms()}async persistRooms(){let e=Array.from(this[ROOMS].entries()).map(([e,f])=>({roomName:e,metadata:f}));await this.ctx.storage.put(`_connection_rooms`,e)}async rejoinRoomsAfterHibernation(){if(this[ROOMS].size===0)return;let e=this[META]?.userId;if(!e)return;let f=[];for(let[p,m]of this[ROOMS].entries())try{await this.getRoomBinding(p).get(p).join(e,m)}catch{f.push(p)}if(f.length>0){for(let e of f)this[ROOMS].delete(e);await this.persistRooms()}}async leaveRoomInternal(e){this[META]&&await this.getRoomBinding(e).get(e).leave(this[META].userId)}async leaveRoom(e){if(!this[META])throw Error(`Cannot leave room: not connected`);await this.leaveRoomInternal(e),this[ROOMS].delete(e),await this.persistRooms()}async getRooms(){return Array.from(this[ROOMS].keys())}getStorage(){return this.ctx.storage}on(e,f){this.handlers.set(e,f)}off(e){this.handlers.delete(e)}async destroy(){for(let e of this[ROOMS].keys())try{await this.leaveRoomInternal(e)}catch{}this[WS]&&this[WS].readyState===WebSocket.OPEN&&this[WS].close(1e3,`Actor destroyed`),e.onDestroy&&this[META]&&await e.onDestroy(this.createContext()),await super.destroy()}}return Object.defineProperty(y,`name`,{value:h,writable:!1,configurable:!0}),y}function defineConnection(e){let f=new Map;return{...e,handlers:f,on(e,p){f.set(e,p)},off(e){f.delete(e)}}}export{clearPersistedState as a,getPersistedState as c,persistKey as d,safeDeserialize as f,storeAttachment as g,setPersistErrorHandler as h,PersistNotReadyError as i,initializePersistedState as l,setPeristErrorHandler as m,defineConnection as n,deletePersistedKey as o,safeSerialize as p,PersistError as r,getPersistedKeys as s,createConnectionHandler as t,isStateReady as u};
@@ -0,0 +1,404 @@
1
+ import { n as ConnectionMeta } from "./types-6m1L8QLb.mjs";
2
+ import { Actor, ActorConfiguration } from "@cloudflare/actors";
3
+
4
+ //#region src/actor/types.d.ts
5
+
6
+ /**
7
+ * Options for broadcasting messages to connections
8
+ */
9
+ interface BroadcastOptions {
10
+ /** Exclude specific WebSocket from receiving the broadcast (local only) */
11
+ except?: WebSocket;
12
+ /** Exclude a specific userId from receiving the broadcast (RPC-safe) */
13
+ exceptUserId?: string;
14
+ /** Only send to specific user IDs */
15
+ userIds?: string[];
16
+ /** Only send to specific client IDs */
17
+ clientIds?: string[];
18
+ }
19
+ interface RoomEmitOptions {
20
+ /** Include the sender when broadcasting to a room */
21
+ includeSelf?: boolean;
22
+ }
23
+ /**
24
+ * RPC-safe version of BroadcastOptions for use over RPC calls.
25
+ * Excludes the `except` field since WebSocket cannot be serialized over RPC.
26
+ */
27
+ interface RpcBroadcastOptions {
28
+ /** Exclude a specific userId from receiving the broadcast */
29
+ exceptUserId?: string;
30
+ /** Only send to specific user IDs */
31
+ userIds?: string[];
32
+ /** Only send to specific client IDs */
33
+ clientIds?: string[];
34
+ }
35
+ /**
36
+ * Represents a member in a RoomDO
37
+ */
38
+ interface RoomMember {
39
+ /** The user's unique identifier */
40
+ userId: string;
41
+ /** Timestamp when the user joined this room */
42
+ joinedAt: number;
43
+ /** Optional metadata associated with this member */
44
+ metadata?: Record<string, unknown>;
45
+ }
46
+ /**
47
+ * Definition for creating a RoomDO (coordination Durable Object)
48
+ *
49
+ * RoomDOs manage room membership and coordinate message delivery between
50
+ * ConnectionDOs. They do NOT hold WebSocket connections directly.
51
+ */
52
+ interface RoomCoordinatorDefinition<E = unknown> {
53
+ /** Optional room name for debugging */
54
+ name?: string;
55
+ /**
56
+ * Environment binding key for the ConnectionDO class.
57
+ * Must match the binding name in wrangler.toml/wrangler.jsonc.
58
+ * Required for broadcast message delivery to work.
59
+ *
60
+ * @example "UserConnection"
61
+ */
62
+ connectionBinding?: string;
63
+ /**
64
+ * Maximum consecutive delivery failures before a member is considered stale
65
+ * and automatically removed from the room. Default: 3
66
+ */
67
+ maxDeliveryFailures?: number;
68
+ /**
69
+ * Called when the RoomDO initializes or wakes from hibernation
70
+ */
71
+ onInit?(roomState: Record<string, unknown>): void | Promise<void>;
72
+ /**
73
+ * Called when a user joins this room
74
+ */
75
+ onJoin?(roomState: Record<string, unknown>, userId: string, metadata?: Record<string, unknown>): void | Promise<void>;
76
+ /**
77
+ * Called when a user leaves this room
78
+ */
79
+ onLeave?(roomState: Record<string, unknown>, userId: string): void | Promise<void>;
80
+ /**
81
+ * Called before the actor is destroyed and all storage is cleared.
82
+ * Use for cleanup logic (e.g., notifying other services).
83
+ */
84
+ onDestroy?(roomState: Record<string, unknown>): void | Promise<void>;
85
+ }
86
+ /**
87
+ * Stub interface for ConnectionDO - supports RPC calls from other DOs
88
+ *
89
+ * ConnectionDOs own a single WebSocket connection and receive messages
90
+ * from RoomDOs via RPC for delivery to their connected client.
91
+ */
92
+ interface ConnectionActorStub {
93
+ /**
94
+ * Standard fetch method for handling HTTP requests and WebSocket upgrades
95
+ */
96
+ fetch(request: Request): Promise<Response>;
97
+ /**
98
+ * Deliver a message to this connection's WebSocket (called via RPC from RoomDO)
99
+ * @param event - Event name
100
+ * @param data - Event data
101
+ * @returns Promise resolving to true if delivered, false if connection is closed
102
+ */
103
+ deliverMessage<TData = unknown>(event: string, data?: TData): Promise<boolean>;
104
+ /**
105
+ * Deliver a system event to this connection (presence updates, room events, etc.)
106
+ * @param type - System event type
107
+ * @param payload - Event payload
108
+ */
109
+ deliverSystemEvent<TPayload = unknown>(type: string, payload?: TPayload): Promise<void>;
110
+ /**
111
+ * Get the userId this connection belongs to
112
+ */
113
+ getUserId(): Promise<string | null>;
114
+ /**
115
+ * Check if this connection is still active
116
+ */
117
+ isConnected(): Promise<boolean>;
118
+ /**
119
+ * Join a room (registers with the RoomDO)
120
+ * @param roomName - Name of the room to join
121
+ * @param metadata - Optional metadata to include with membership
122
+ */
123
+ joinRoom(roomName: string, metadata?: Record<string, unknown>): Promise<void>;
124
+ /**
125
+ * Leave a room (unregisters from the RoomDO)
126
+ * @param roomName - Name of the room to leave
127
+ */
128
+ leaveRoom(roomName: string): Promise<void>;
129
+ /**
130
+ * Get list of rooms this connection is a member of
131
+ */
132
+ getRooms(): Promise<string[]>;
133
+ }
134
+ /**
135
+ * Environment type with Durable Object bindings for Verani.
136
+ * Users should extend this with their actual binding names from wrangler.toml.
137
+ *
138
+ * @example
139
+ * ```typescript
140
+ * interface Env extends VeraniEnv {
141
+ * UserConnection: DurableObjectNamespace;
142
+ * PresenceRoom: DurableObjectNamespace;
143
+ * ChatRoom: DurableObjectNamespace;
144
+ * }
145
+ * ```
146
+ */
147
+ interface VeraniEnv {
148
+ [key: string]: unknown;
149
+ }
150
+ /**
151
+ * ConnectionActor interface for single-WebSocket-per-DO model
152
+ *
153
+ * Each ConnectionActor owns exactly one WebSocket connection for a single user.
154
+ */
155
+ interface ConnectionActor<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown, TState extends Record<string, unknown> = Record<string, unknown>> extends Actor<E> {
156
+ /**
157
+ * The single WebSocket connection owned by this DO (null if not connected)
158
+ */
159
+ ws: WebSocket | null;
160
+ /**
161
+ * Connection metadata for this user
162
+ */
163
+ meta: TMeta | null;
164
+ /**
165
+ * Set of room names this connection is a member of
166
+ */
167
+ rooms: Set<string>;
168
+ /**
169
+ * User-defined persisted state for this connection actor
170
+ */
171
+ connectionState: TState;
172
+ /**
173
+ * Check if this connection has an active WebSocket
174
+ */
175
+ isConnected(): boolean;
176
+ /**
177
+ * Deliver a message to this connection's WebSocket
178
+ * Called via RPC from RoomDO
179
+ */
180
+ deliverMessage<TData = unknown>(event: string, data?: TData): Promise<boolean>;
181
+ /**
182
+ * Deliver a system event (presence, room events, etc.)
183
+ */
184
+ deliverSystemEvent<TPayload = unknown>(type: string, payload?: TPayload): Promise<void>;
185
+ /**
186
+ * Join a room (register with RoomDO)
187
+ */
188
+ joinRoom(roomName: string, metadata?: Record<string, unknown>): Promise<void>;
189
+ /**
190
+ * Leave a room (unregister from RoomDO)
191
+ */
192
+ leaveRoom(roomName: string): Promise<void>;
193
+ /**
194
+ * Socket.io-like emit API for this connection
195
+ */
196
+ emit: ConnectionEmit<TMeta, E>;
197
+ /**
198
+ * Access the Durable Object storage API
199
+ */
200
+ getStorage(): DurableObjectStorage;
201
+ }
202
+ /**
203
+ * Connection-level emit API (for single-connection DO)
204
+ * Routes to appropriate RoomDO or ConnectionDO via RPC
205
+ */
206
+ interface ConnectionEmit<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> {
207
+ /**
208
+ * Emit to this connection's WebSocket
209
+ * @param event - Event name
210
+ * @param data - Event data
211
+ */
212
+ emit<TData = unknown>(event: string, data?: TData): void;
213
+ /**
214
+ * Target a specific room or user for emitting
215
+ * @param target - Room name (if starts with "room:") or userId
216
+ * @returns Builder for emitting to the target via RPC
217
+ */
218
+ to(target: string): AsyncEmitBuilder;
219
+ /**
220
+ * Target a specific room for broadcasting
221
+ * @param roomName - Room name
222
+ * @returns Builder for broadcasting to the room via RPC
223
+ */
224
+ toRoom(roomName: string, options?: RoomEmitOptions): AsyncEmitBuilder;
225
+ /**
226
+ * Target a specific user for direct messaging
227
+ * @param userId - User ID
228
+ * @returns Builder for sending to the user via RPC
229
+ */
230
+ toUser(userId: string): AsyncEmitBuilder;
231
+ }
232
+ /**
233
+ * Async emit builder for RPC-based emit operations
234
+ */
235
+ interface AsyncEmitBuilder {
236
+ /**
237
+ * Emit to the targeted scope via RPC
238
+ * @param event - Event name
239
+ * @param data - Event data
240
+ * @returns Promise resolving to number of recipients
241
+ */
242
+ emit<TData = unknown>(event: string, data?: TData): Promise<number>;
243
+ }
244
+ //#endregion
245
+ //#region src/actor/connection-actor.d.ts
246
+ /**
247
+ * Definition for creating a ConnectionDO (per-user connection handler)
248
+ */
249
+ interface ConnectionDefinition<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown, TState extends Record<string, unknown> = Record<string, unknown>> {
250
+ /** Optional name for debugging */
251
+ name?: string;
252
+ /** WebSocket upgrade path (default: "/ws") */
253
+ websocketPath?: string;
254
+ /**
255
+ * Map of room names to their environment binding keys.
256
+ * Must match binding names in wrangler.toml/wrangler.jsonc.
257
+ * Required for room features (joinRoom, toRoom, etc.).
258
+ *
259
+ * @example { "presence": "PresenceRoom", "chat": "ChatRoom" }
260
+ */
261
+ rooms?: Record<string, string>;
262
+ /**
263
+ * Environment binding key for the ConnectionDO class.
264
+ * Must match the binding name in wrangler.toml/wrangler.jsonc.
265
+ * Required for user-to-user messaging (toUser).
266
+ *
267
+ * @example "UserConnection"
268
+ */
269
+ connectionBinding?: string;
270
+ /**
271
+ * Extract metadata from the connection request
272
+ */
273
+ extractMeta?(req: Request): TMeta | Promise<TMeta>;
274
+ /**
275
+ * Called when a WebSocket connection is established
276
+ */
277
+ onConnect?(ctx: ConnectionContext<TMeta, E, TState>): void | Promise<void>;
278
+ /**
279
+ * Called when the WebSocket connection is closed
280
+ */
281
+ onDisconnect?(ctx: ConnectionContext<TMeta, E, TState>): void | Promise<void>;
282
+ /**
283
+ * Called when a message is received from the WebSocket
284
+ */
285
+ onMessage?(ctx: ConnectionContext<TMeta, E, TState>, frame: unknown): void | Promise<void>;
286
+ /**
287
+ * Called when an error occurs
288
+ */
289
+ onError?(error: Error, ctx: ConnectionContext<TMeta, E, TState>): void | Promise<void>;
290
+ /**
291
+ * Called after waking from hibernation
292
+ */
293
+ onHibernationRestore?(actor: ConnectionHandlerInstance<TMeta, E, TState>): void | Promise<void>;
294
+ /**
295
+ * Event handlers map (socket.io-like)
296
+ */
297
+ handlers?: Map<string, (ctx: ConnectionContext<TMeta, E, TState>, data: unknown) => void | Promise<void>>;
298
+ /**
299
+ * Initial state for this connection
300
+ */
301
+ state?: TState;
302
+ /**
303
+ * Keys to persist to storage
304
+ */
305
+ persistedKeys?: (string & keyof TState)[];
306
+ /**
307
+ * Persistence options
308
+ */
309
+ persistOptions?: {
310
+ shallow?: boolean;
311
+ throwOnError?: boolean;
312
+ };
313
+ /**
314
+ * Called when persistence fails
315
+ */
316
+ onPersistError?(key: string, error: Error): void;
317
+ /**
318
+ * Called before the actor is destroyed and all storage is cleared.
319
+ * Use for cleanup (e.g., leaving rooms, notifying services).
320
+ */
321
+ onDestroy?(ctx: ConnectionContext<TMeta, E, TState>): void | Promise<void>;
322
+ }
323
+ /**
324
+ * Instance interface for ConnectionHandler actors
325
+ * Used for typing the actor parameter in lifecycle hooks
326
+ */
327
+ interface ConnectionHandlerInstance<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown, TState extends Record<string, unknown> = Record<string, unknown>> {
328
+ connectionState: TState;
329
+ isStateReady(): boolean;
330
+ getStorage(): DurableObjectStorage;
331
+ joinRoom(roomName: string, metadata?: Record<string, unknown>): Promise<void>;
332
+ leaveRoom(roomName: string): Promise<void>;
333
+ getRooms(): Promise<string[]>;
334
+ }
335
+ /**
336
+ * Context provided to connection lifecycle hooks
337
+ */
338
+ interface ConnectionContext<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown, TState extends Record<string, unknown> = Record<string, unknown>> {
339
+ /** The connection actor instance */
340
+ actor: ConnectionHandlerInstance<TMeta, E, TState>;
341
+ /** The WebSocket connection (may be null after disconnect) */
342
+ ws: WebSocket | null;
343
+ /** Connection metadata */
344
+ meta: TMeta;
345
+ /** Socket.io-like emit API */
346
+ emit: ConnectionEmit<TMeta, E>;
347
+ /** Persisted state */
348
+ state: TState;
349
+ }
350
+ /**
351
+ * Return type for createConnectionHandler
352
+ */
353
+ type ConnectionHandlerClass<E = unknown> = {
354
+ new (state: any, env: E): Actor<E>;
355
+ get(userId: string): ConnectionActorStub;
356
+ configuration(request?: Request): ActorConfiguration;
357
+ };
358
+ /**
359
+ * Creates a Connection handler (per-user Durable Object)
360
+ *
361
+ * Each ConnectionDO owns exactly ONE WebSocket connection for a single user.
362
+ * Messages are received via RPC from RoomDOs and delivered to the WebSocket.
363
+ *
364
+ * @param definition - Connection definition with lifecycle hooks
365
+ * @returns ConnectionDO class for Cloudflare Workers
366
+ *
367
+ * @example
368
+ * ```typescript
369
+ * const UserConnection = createConnectionHandler({
370
+ * name: "user-connection",
371
+ * extractMeta: (req) => ({
372
+ * userId: extractUserIdFromToken(req),
373
+ * clientId: crypto.randomUUID(),
374
+ * channels: ["default"]
375
+ * }),
376
+ * onConnect: (ctx) => {
377
+ * console.log(`User ${ctx.meta.userId} connected`);
378
+ * ctx.actor.joinRoom("presence");
379
+ * },
380
+ * onDisconnect: (ctx) => {
381
+ * console.log(`User ${ctx.meta.userId} disconnected`);
382
+ * }
383
+ * });
384
+ *
385
+ * // In Worker:
386
+ * const userId = extractUserId(request);
387
+ * const stub = UserConnection.get(userId);
388
+ * return stub.fetch(request);
389
+ * ```
390
+ */
391
+ declare function createConnectionHandler<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown, TState extends Record<string, unknown> = Record<string, unknown>>(definition: ConnectionDefinition<TMeta, E, TState>): ConnectionHandlerClass<E>;
392
+ /**
393
+ * Helper to define a connection with socket.io-like event handlers
394
+ */
395
+ interface ConnectionDefinitionWithHandlers<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown, TState extends Record<string, unknown> = Record<string, unknown>> extends ConnectionDefinition<TMeta, E, TState> {
396
+ on<TData = unknown>(event: string, handler: (ctx: ConnectionContext<TMeta, E, TState>, data: TData) => void | Promise<void>): void;
397
+ off(event: string): void;
398
+ }
399
+ /**
400
+ * Define a connection with socket.io-like convenience methods
401
+ */
402
+ declare function defineConnection<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown, TState extends Record<string, unknown> = Record<string, unknown>>(def: ConnectionDefinition<TMeta, E, TState>): ConnectionDefinitionWithHandlers<TMeta, E, TState>;
403
+ //#endregion
404
+ export { VeraniEnv as _, ConnectionHandlerInstance as a, AsyncEmitBuilder as c, ConnectionActorStub as d, ConnectionEmit as f, RpcBroadcastOptions as g, RoomMember as h, ConnectionHandlerClass as i, BroadcastOptions as l, RoomEmitOptions as m, ConnectionDefinition as n, createConnectionHandler as o, RoomCoordinatorDefinition as p, ConnectionDefinitionWithHandlers as r, defineConnection as s, ConnectionContext as t, ConnectionActor as u };