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.
- package/dist/connection-actor-B5AzPIkG.mjs +1 -0
- package/dist/connection-actor-D1QRniw1.d.mts +404 -0
- package/dist/connection-actor-D61IMDIa.d.cts +404 -0
- package/dist/connection-actor-K3TH2ycP.cjs +1 -0
- package/dist/typed.cjs +1 -1
- package/dist/typed.d.cts +69 -63
- package/dist/typed.d.mts +69 -63
- package/dist/typed.mjs +1 -1
- package/dist/verani.cjs +11 -1
- package/dist/verani.d.cts +43 -204
- package/dist/verani.d.mts +43 -204
- package/dist/verani.mjs +11 -1
- package/package.json +99 -108
- package/dist/actor-runtime-BoaZjQPs.cjs +0 -1
- package/dist/actor-runtime-DCvCJ1UN.d.mts +0 -701
- package/dist/actor-runtime-DecQvWnw.mjs +0 -1
- package/dist/actor-runtime-Dqodm1HR.d.cts +0 -701
|
@@ -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 };
|