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 +16 -0
- package/README.md +212 -0
- package/dist/verani.cjs +1 -0
- package/dist/verani.d.cts +399 -0
- package/dist/verani.d.mts +399 -0
- package/dist/verani.mjs +1 -0
- package/package.json +55 -0
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
|
+
|
package/dist/verani.cjs
ADDED
|
@@ -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 };
|
package/dist/verani.mjs
ADDED
|
@@ -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
|
+
}
|