verani 0.8.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +153 -189
- package/dist/actor-runtime-83AF97II.mjs +1 -0
- package/dist/{actor-runtime-BOmyu8Vl.d.mts → actor-runtime-BqF6_7z_.d.mts} +201 -2
- package/dist/actor-runtime-CCoZUVrQ.cjs +1 -0
- package/dist/{actor-runtime-1TpQk-wg.d.cts → actor-runtime-gFDsFVsU.d.cts} +201 -2
- package/dist/client-C3K_PiKE.cjs +1 -0
- package/dist/client-DQnA1Oel.mjs +1 -0
- package/dist/client.cjs +1 -1
- package/dist/client.mjs +1 -1
- package/dist/typed-client.cjs +1 -1
- package/dist/typed-client.mjs +1 -1
- package/dist/typed.cjs +1 -1
- package/dist/typed.d.cts +1 -1
- package/dist/typed.d.mts +1 -1
- package/dist/typed.mjs +1 -1
- package/dist/verani.cjs +1 -1
- package/dist/verani.d.cts +213 -2
- package/dist/verani.d.mts +213 -2
- package/dist/verani.mjs +1 -1
- package/package.json +2 -1
- package/dist/actor-runtime-Fr0n1XGB.cjs +0 -1
- package/dist/actor-runtime-mfMYeKoB.mjs +0 -1
- package/dist/client-oItOePHL.mjs +0 -1
- package/dist/client-ofEyep6Z.cjs +0 -1
package/README.md
CHANGED
|
@@ -4,140 +4,160 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://github.com/v0id-user)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Build realtime apps on Cloudflare with Socket.io-like simplicity
|
|
8
|
+
|
|
9
|
+
[Getting Started](#quick-start) • [Documentation](./docs/) • [Examples](./examples/)
|
|
8
10
|
|
|
9
|
-
>
|
|
11
|
+
</div>
|
|
10
12
|
|
|
11
|
-
Verani brings the familiar developer experience of Socket.io to Cloudflare's Durable Objects (Actors), with proper hibernation support and minimal overhead.
|
|
13
|
+
Verani brings the familiar developer experience of Socket.io to Cloudflare's Durable Objects (Actors), with proper hibernation support and minimal overhead. Build realtime chat, presence systems, notifications, and more—all running on Cloudflare's edge.
|
|
12
14
|
|
|
13
15
|
## Why Verani?
|
|
14
16
|
|
|
15
17
|
- **Familiar API**: If you've used Socket.io, you already know how to use Verani
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
18
|
+
- **Horizontally scalable**: Per-connection architecture - each user gets their own Durable Object
|
|
19
|
+
- **Hibernation support**: Handles Cloudflare Actor hibernation automatically
|
|
20
|
+
- **Type safe**: Built with TypeScript, full type safety throughout
|
|
21
|
+
- **Simple mental model**: Connections, rooms, and broadcast semantics that just make sense
|
|
19
22
|
- **Modern DX**: Automatic reconnection, error handling, and connection lifecycle management
|
|
23
|
+
- **Edge-ready**: Built for Cloudflare Workers and Durable Objects
|
|
24
|
+
- **Cost-efficient**: Idle connections hibernate and cost nothing
|
|
20
25
|
|
|
21
26
|
## Quick Start
|
|
22
27
|
|
|
23
|
-
|
|
28
|
+
Get a realtime chat app running in 5 minutes.
|
|
29
|
+
|
|
30
|
+
### Step 1: Install
|
|
24
31
|
|
|
25
32
|
```bash
|
|
26
33
|
npm install verani @cloudflare/actors
|
|
27
|
-
# or
|
|
28
|
-
bun add verani @cloudflare/actors
|
|
29
34
|
```
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
Don't have a Cloudflare Worker project? Create one:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm create cloudflare@latest my-verani-app
|
|
40
|
+
cd my-verani-app
|
|
41
|
+
npm install verani @cloudflare/actors
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Step 2: Create Your Connection Handler
|
|
32
45
|
|
|
33
|
-
|
|
34
|
-
- `src/actors/chat.actor.ts` - Room definitions
|
|
35
|
-
- `src/index.ts` - Export Durable Object classes
|
|
46
|
+
Create `src/actors/connection.ts`:
|
|
36
47
|
|
|
37
48
|
```typescript
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
import { defineConnection, createConnectionHandler, createRoomHandler } from "verani";
|
|
50
|
+
|
|
51
|
+
// Define connection handler (one WebSocket per user)
|
|
52
|
+
const userConnection = defineConnection({
|
|
53
|
+
name: "UserConnection",
|
|
54
|
+
|
|
55
|
+
extractMeta(req) {
|
|
56
|
+
const url = new URL(req.url);
|
|
57
|
+
const userId = url.searchParams.get("userId") || crypto.randomUUID();
|
|
58
|
+
return {
|
|
59
|
+
userId,
|
|
60
|
+
clientId: crypto.randomUUID(),
|
|
61
|
+
channels: ["default"]
|
|
62
|
+
};
|
|
63
|
+
},
|
|
45
64
|
|
|
46
|
-
onConnect(ctx) {
|
|
65
|
+
async onConnect(ctx) {
|
|
47
66
|
console.log(`User ${ctx.meta.userId} connected`);
|
|
48
|
-
//
|
|
49
|
-
ctx.actor.
|
|
50
|
-
userId: ctx.meta.userId
|
|
51
|
-
});
|
|
67
|
+
// Join chat room (persisted across hibernation)
|
|
68
|
+
await ctx.actor.joinRoom("chat");
|
|
52
69
|
},
|
|
53
70
|
|
|
54
|
-
onDisconnect(ctx) {
|
|
71
|
+
async onDisconnect(ctx) {
|
|
55
72
|
console.log(`User ${ctx.meta.userId} disconnected`);
|
|
56
|
-
|
|
57
|
-
userId: ctx.meta.userId
|
|
58
|
-
});
|
|
73
|
+
// Room leave is handled automatically
|
|
59
74
|
}
|
|
60
75
|
});
|
|
61
76
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
// Broadcast to
|
|
65
|
-
ctx.
|
|
77
|
+
// Handle messages (socket.io-like API)
|
|
78
|
+
userConnection.on("chat.message", async (ctx, data) => {
|
|
79
|
+
// Broadcast to everyone in the chat room
|
|
80
|
+
await ctx.emit.toRoom("chat").emit("chat.message", {
|
|
66
81
|
from: ctx.meta.userId,
|
|
67
82
|
text: data.text,
|
|
68
83
|
timestamp: Date.now()
|
|
69
84
|
});
|
|
70
85
|
});
|
|
71
86
|
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
87
|
+
// Export the connection handler
|
|
88
|
+
export const UserConnection = createConnectionHandler(userConnection);
|
|
89
|
+
|
|
90
|
+
// Export the room coordinator
|
|
91
|
+
export const ChatRoom = createRoomHandler({ name: "ChatRoom" });
|
|
76
92
|
```
|
|
77
93
|
|
|
94
|
+
### Step 3: Wire Up Your Worker
|
|
95
|
+
|
|
96
|
+
Update `src/index.ts`:
|
|
97
|
+
|
|
78
98
|
```typescript
|
|
79
|
-
|
|
80
|
-
import { createActorHandler } from "verani";
|
|
81
|
-
import { chatRoom } from "./actors/chat.actor";
|
|
99
|
+
import { UserConnection, ChatRoom } from "./actors/connection";
|
|
82
100
|
|
|
83
|
-
//
|
|
84
|
-
export
|
|
85
|
-
```
|
|
101
|
+
// Export Durable Object classes
|
|
102
|
+
export { UserConnection, ChatRoom };
|
|
86
103
|
|
|
87
|
-
|
|
104
|
+
// Route WebSocket connections
|
|
105
|
+
export default {
|
|
106
|
+
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
|
|
107
|
+
const url = new URL(request.url);
|
|
88
108
|
|
|
89
|
-
|
|
109
|
+
if (url.pathname.startsWith("/ws")) {
|
|
110
|
+
// Extract userId and route to user-specific DO
|
|
111
|
+
const userId = url.searchParams.get("userId") || crypto.randomUUID();
|
|
112
|
+
const stub = UserConnection.get(userId);
|
|
113
|
+
return stub.fetch(request);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return new Response("Not Found", { status: 404 });
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
```
|
|
90
120
|
|
|
91
|
-
|
|
121
|
+
### Step 4: Configure Wrangler
|
|
122
|
+
|
|
123
|
+
Update `wrangler.jsonc`:
|
|
92
124
|
|
|
93
125
|
```jsonc
|
|
94
126
|
{
|
|
127
|
+
"name": "my-verani-app",
|
|
128
|
+
"main": "src/index.ts",
|
|
129
|
+
"compatibility_date": "2024-01-01",
|
|
130
|
+
|
|
95
131
|
"durable_objects": {
|
|
96
132
|
"bindings": [
|
|
97
133
|
{
|
|
98
|
-
"class_name": "
|
|
99
|
-
"name": "
|
|
134
|
+
"class_name": "UserConnection",
|
|
135
|
+
"name": "CONNECTION_DO"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"class_name": "ChatRoom",
|
|
139
|
+
"name": "ROOM_DO"
|
|
100
140
|
}
|
|
101
141
|
]
|
|
102
142
|
},
|
|
143
|
+
|
|
103
144
|
"migrations": [
|
|
104
145
|
{
|
|
105
|
-
"new_sqlite_classes": ["ChatRoom"],
|
|
146
|
+
"new_sqlite_classes": ["UserConnection", "ChatRoom"],
|
|
106
147
|
"tag": "v1"
|
|
107
148
|
}
|
|
108
149
|
]
|
|
109
150
|
}
|
|
110
151
|
```
|
|
111
152
|
|
|
112
|
-
|
|
153
|
+
Important: Export names must match `class_name` in `wrangler.jsonc`.
|
|
113
154
|
|
|
114
|
-
|
|
115
|
-
2. **Export** in `src/index.ts`: `export const ChatRoom = createActorHandler(chatRoom)` - The export name becomes the class name
|
|
116
|
-
3. **Class name** in `wrangler.jsonc`: `"class_name": "ChatRoom"` - Must match the export name exactly
|
|
117
|
-
|
|
118
|
-
**Important Notes:**
|
|
119
|
-
- `defineRoom()` returns a room definition object, **not** a Durable Object class
|
|
120
|
-
- `createActorHandler(room)` creates the actual Durable Object class
|
|
121
|
-
- Each room definition must be converted to a class with `createActorHandler()` and exported
|
|
122
|
-
- For multiple rooms, you need multiple exports and multiple bindings in `wrangler.jsonc`
|
|
123
|
-
- The export name (e.g., `ChatRoom`) becomes the class name and must match `class_name` in configuration
|
|
124
|
-
|
|
125
|
-
### Client Side
|
|
155
|
+
### Step 5: Build Your Client
|
|
126
156
|
|
|
127
157
|
```typescript
|
|
128
|
-
import { VeraniClient } from "verani";
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const client = new VeraniClient("wss://your-worker.dev/ws?userId=alice", {
|
|
132
|
-
pingInterval: 5000, // Send ping every 5 seconds
|
|
133
|
-
pongTimeout: 5000, // Expect pong within 5 seconds
|
|
134
|
-
reconnection: {
|
|
135
|
-
enabled: true,
|
|
136
|
-
maxAttempts: 10,
|
|
137
|
-
initialDelay: 1000,
|
|
138
|
-
maxDelay: 30000
|
|
139
|
-
}
|
|
140
|
-
});
|
|
158
|
+
import { VeraniClient } from "verani/client";
|
|
159
|
+
|
|
160
|
+
const client = new VeraniClient("ws://localhost:8787/ws?userId=alice");
|
|
141
161
|
|
|
142
162
|
// Listen for messages
|
|
143
163
|
client.on("chat.message", (data) => {
|
|
@@ -145,153 +165,97 @@ client.on("chat.message", (data) => {
|
|
|
145
165
|
});
|
|
146
166
|
|
|
147
167
|
client.on("user.joined", (data) => {
|
|
148
|
-
console.log(`User ${data.userId} joined
|
|
168
|
+
console.log(`User ${data.userId} joined!`);
|
|
149
169
|
});
|
|
150
170
|
|
|
151
171
|
// Send messages
|
|
152
172
|
client.emit("chat.message", { text: "Hello, world!" });
|
|
153
173
|
|
|
154
|
-
//
|
|
155
|
-
client.onOpen(() => {
|
|
156
|
-
console.log("Connected!");
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
client.onStateChange((state) => {
|
|
160
|
-
console.log("Connection state:", state);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
// Wait for connection before sending
|
|
174
|
+
// Wait for connection (optional)
|
|
164
175
|
await client.waitForConnection();
|
|
165
|
-
client.emit("ready", {});
|
|
166
176
|
```
|
|
167
177
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
### Actors = Rooms
|
|
171
|
-
|
|
172
|
-
Each Cloudflare Actor instance represents a **logical container** for realtime communication:
|
|
173
|
-
|
|
174
|
-
- **Chat room**: All users in the same chat share one Actor
|
|
175
|
-
- **User notifications**: Each user gets their own Actor
|
|
176
|
-
- **Game session**: Each game instance is one Actor
|
|
177
|
-
|
|
178
|
-
### Channels
|
|
179
|
-
|
|
180
|
-
Inside an Actor, connections can join **channels** for selective message routing:
|
|
178
|
+
### Step 6: Run It!
|
|
181
179
|
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
ctx.emit.to("alice").emit("notification", { message: "Hello!" });
|
|
180
|
+
```bash
|
|
181
|
+
# Start the server
|
|
182
|
+
npm run dev
|
|
183
|
+
# or
|
|
184
|
+
wrangler dev
|
|
188
185
|
|
|
189
|
-
|
|
190
|
-
|
|
186
|
+
# In another terminal, run your client
|
|
187
|
+
# (or open multiple browser tabs with your client code)
|
|
191
188
|
```
|
|
192
189
|
|
|
193
|
-
|
|
190
|
+
That's it! You now have a working realtime chat app.
|
|
194
191
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
- Connection metadata survives hibernation via WebSocket attachments
|
|
198
|
-
- Sessions are restored when the Actor wakes up
|
|
199
|
-
- No manual state management needed
|
|
192
|
+
Need more help? Check out the [Quick Start Guide](./docs/getting-started/quick-start.md) for detailed examples.
|
|
200
193
|
|
|
201
194
|
## Documentation
|
|
202
195
|
|
|
203
|
-
-
|
|
204
|
-
-
|
|
205
|
-
-
|
|
206
|
-
-
|
|
207
|
-
-
|
|
208
|
-
-
|
|
209
|
-
|
|
210
|
-
## More Examples
|
|
211
|
-
**[Vchats](https://github.com/v0id-user/vchats)** - A simple chat application built with Verani
|
|
212
|
-
|
|
213
|
-
## Features
|
|
196
|
+
- [Live Documentation](https://verani-docs.cloudflare-c49.workers.dev/docs/getting-started/installation) - Installation and quick start guide
|
|
197
|
+
- [Getting Started](./docs/getting-started/) - Installation and quick start guide
|
|
198
|
+
- [API Reference](./docs/api/) - Complete server and client API documentation
|
|
199
|
+
- [Guides](./docs/guides/) - Configuration, deployment, scaling, and RPC
|
|
200
|
+
- [Examples](./docs/examples/) - Common usage patterns and code samples
|
|
201
|
+
- [Concepts](./docs/concepts/) - Architecture, hibernation, and core concepts
|
|
202
|
+
- [Security](./docs/security/) - Authentication, authorization, and best practices
|
|
214
203
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
- **Socket.io-like event handlers** - `room.on()` and `room.off()` for clean event handling
|
|
218
|
-
- **Emit API** - `ctx.emit` and `ctx.actor.emit.to()` for intuitive message sending
|
|
219
|
-
- Room-based architecture with lifecycle hooks (`onConnect`, `onDisconnect`, `onHibernationRestore`)
|
|
220
|
-
- WebSocket attachment management for hibernation
|
|
221
|
-
- Selective broadcasting with filters (userIds, clientIds, except)
|
|
222
|
-
- User and client ID tracking
|
|
223
|
-
- **RPC methods** - Call Actor methods remotely from Workers or other Actors
|
|
224
|
-
- Durable Object storage access for persistent state
|
|
225
|
-
- Error boundaries and logging
|
|
226
|
-
- Flexible metadata extraction from requests
|
|
227
|
-
|
|
228
|
-
### Client Side
|
|
204
|
+
## Key Concepts
|
|
229
205
|
|
|
230
|
-
-
|
|
231
|
-
-
|
|
232
|
-
-
|
|
233
|
-
-
|
|
234
|
-
- Promise-based connection waiting (`waitForConnection()`)
|
|
235
|
-
- Lifecycle callbacks (`onOpen`, `onClose`, `onError`, `onStateChange`)
|
|
236
|
-
- **Ping/pong keepalive** with automatic Page Visibility API resync
|
|
237
|
-
- Configurable connection timeout and queue size
|
|
206
|
+
- **ConnectionDO** = A Durable Object that owns ONE WebSocket per user
|
|
207
|
+
- **RoomDO** = A Durable Object that coordinates room membership and broadcasts
|
|
208
|
+
- **Emit** = Send messages (`ctx.emit.toRoom("chat").emit("event", data)`)
|
|
209
|
+
- **Hibernation** = Handled automatically, state persisted and restored
|
|
238
210
|
|
|
239
|
-
|
|
211
|
+
## Features
|
|
240
212
|
|
|
241
|
-
|
|
213
|
+
### Server-Side
|
|
214
|
+
- **Per-connection DOs**: Each user gets their own Durable Object (horizontally scalable)
|
|
215
|
+
- **Socket.io-like API**: `connection.on()`, `ctx.emit.toRoom()`, familiar patterns
|
|
216
|
+
- **Room coordination**: RoomDOs manage membership and broadcast via RPC
|
|
217
|
+
- **Lifecycle hooks**: `onConnect`, `onDisconnect`, `onMessage` for full control
|
|
218
|
+
- **RPC support**: DO-to-DO communication for message delivery
|
|
219
|
+
- **Automatic hibernation**: State persisted and restored automatically
|
|
220
|
+
- **Persistent state**: Built-in support for state that survives hibernation
|
|
221
|
+
- **Type safety**: Full TypeScript support with type inference
|
|
242
222
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
223
|
+
### Client-Side
|
|
224
|
+
- **Automatic reconnection**: Exponential backoff with configurable retry logic
|
|
225
|
+
- **Message queueing**: Messages queued when disconnected, sent on reconnect
|
|
226
|
+
- **Keepalive**: Built-in ping/pong to detect dead connections
|
|
227
|
+
- **Event-based API**: Familiar `on()`, `emit()`, `once()`, `off()` methods
|
|
228
|
+
- **Connection state**: Track connection lifecycle (`connecting`, `connected`, `disconnected`)
|
|
246
229
|
|
|
247
|
-
|
|
248
|
-
await stub.sendToUser("alice", "notifications", {
|
|
249
|
-
type: "alert",
|
|
250
|
-
message: "You have a new message"
|
|
251
|
-
});
|
|
230
|
+
## Try the Examples
|
|
252
231
|
|
|
253
|
-
|
|
254
|
-
await stub.broadcast("default", { type: "announcement", text: "Hello!" });
|
|
232
|
+
See Verani in action with working examples:
|
|
255
233
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
234
|
+
```bash
|
|
235
|
+
git clone https://github.com/v0id-user/verani
|
|
236
|
+
cd verani
|
|
237
|
+
bun install && bun run dev
|
|
259
238
|
```
|
|
260
239
|
|
|
261
|
-
|
|
262
|
-
- Query actor state remotely
|
|
263
|
-
- Broadcast from external events or scheduled tasks
|
|
264
|
-
- Coordinate between multiple Actors
|
|
265
|
-
|
|
266
|
-
## Live Examples
|
|
267
|
-
|
|
268
|
-
Try out Verani with working examples:
|
|
240
|
+
Then in another terminal, try:
|
|
269
241
|
|
|
270
242
|
```bash
|
|
271
|
-
#
|
|
272
|
-
|
|
273
|
-
cd verani
|
|
274
|
-
bun install # or npm install
|
|
275
|
-
bun run dev # or npm run dev
|
|
243
|
+
# Chat room example
|
|
244
|
+
bun run examples/clients/chat-client.ts
|
|
276
245
|
|
|
277
|
-
#
|
|
278
|
-
|
|
246
|
+
# Presence tracking
|
|
247
|
+
bun run examples/clients/presence-client.ts
|
|
279
248
|
|
|
280
|
-
|
|
249
|
+
# Notifications feed
|
|
250
|
+
bun run examples/clients/notifications-client.ts
|
|
251
|
+
```
|
|
281
252
|
|
|
282
|
-
|
|
253
|
+
See the [Examples README](./examples/README.md) for more details.
|
|
283
254
|
|
|
284
|
-
|
|
255
|
+
## Real-World Example
|
|
285
256
|
|
|
286
|
-
|
|
287
|
-
- Core realtime messaging
|
|
288
|
-
- Hibernation support
|
|
289
|
-
- Client reconnection
|
|
290
|
-
- Presence protocol with multi-device support
|
|
291
|
-
- Persistent storage integration with Durable Object storage
|
|
257
|
+
[Vchats](https://github.com/v0id-user/vchats) - A complete chat application built with Verani
|
|
292
258
|
|
|
293
|
-
**Coming Soon:**
|
|
294
|
-
- React/framework adapters
|
|
295
259
|
|
|
296
260
|
## License
|
|
297
261
|
|
|
@@ -299,5 +263,5 @@ ISC
|
|
|
299
263
|
|
|
300
264
|
## Contributing
|
|
301
265
|
|
|
302
|
-
Contributions welcome! Please read our
|
|
266
|
+
Contributions welcome! Please read our [Contributing Guidelines](./CONTRIBUTING.md) first.
|
|
303
267
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a as decodeFrame$1,n as encodeFrame$1}from"./encode-BhJqnsto.mjs";import{Actor}from"@cloudflare/actors";function isValidConnectionMeta(d){return!(!d||typeof d!=`object`||typeof d.userId!=`string`||!d.userId||typeof d.clientId!=`string`||!d.clientId||!Array.isArray(d.channels)||!d.channels.every(d=>typeof d==`string`))}function storeAttachment(d,F){d.serializeAttachment(F)}function restoreSessions(d){let F=0,I=0;for(let R of d.ctx.getWebSockets()){if(R.readyState!==WebSocket.OPEN){I++;continue}let z=R.deserializeAttachment();if(!z){I++;continue}if(!isValidConnectionMeta(z)){I++;continue}d.sessions.set(R,{ws:R,meta:z}),F++}}function decodeFrame(F){return decodeFrame$1(F)??{type:`invalid`}}function encodeFrame(d){return encodeFrame$1(d)}const PERSIST_ERROR_HANDLER=Symbol(`PERSIST_ERROR_HANDLER`),PERSISTED_STATE=Symbol(`PERSISTED_STATE`),STATE_READY=Symbol(`STATE_READY`);var PersistNotReadyError=class extends Error{constructor(d){super(`Cannot access persisted state key "${d}" before storage is initialized. Wait for onInit to complete or check actor.isStateReady().`),this.name=`PersistNotReadyError`}},PersistError=class extends Error{constructor(d,F){super(`Failed to persist key "${d}": ${F.message}`),this.name=`PersistError`,this.originalCause=F}};function safeSerialize(d){let F=new WeakSet;return JSON.stringify(d,(d,I)=>{if(I instanceof Date)return{__type:`Date`,value:I.toISOString()};if(I instanceof RegExp)return{__type:`RegExp`,source:I.source,flags:I.flags};if(I instanceof Map)return{__type:`Map`,entries:Array.from(I.entries())};if(I instanceof Set)return{__type:`Set`,values:Array.from(I.values())};if(I instanceof Error)return{__type:`Error`,name:I.name,message:I.message};if(typeof I==`object`&&I){if(F.has(I))return;F.add(I)}if(!(typeof I==`function`||typeof I==`symbol`))return I})}function safeDeserialize(d){return JSON.parse(d,(d,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 d=Error(F.message);return d.name=F.name,d}}return F})}function createShallowProxy(d,F,I){return new Proxy(d,{set(d,I,L){let R=Reflect.set(d,I,L);return R&&typeof I==`string`&&F(I,L),R},deleteProperty(d,F){let L=Reflect.deleteProperty(d,F);return L&&typeof F==`string`&&I(F),L}})}async function initializePersistedState(d,F,I=[],L={}){let{shallow:R=!0,throwOnError:z=!0}=L,B=d.ctx.storage,V=I.length>0?I.map(String):Object.keys(F),W={...F};for(let d of V)try{let F=await B.get(`_verani_persist:${d}`);if(F!==void 0)try{W[d]=safeDeserialize(F)}catch(F){if(z)throw new PersistError(d,F)}}catch(F){if(z&&!(F instanceof PersistError))throw new PersistError(d,F)}let G=async(F,I)=>{if(V.includes(F))try{let d=safeSerialize(I);await B.put(`_verani_persist:${F}`,d)}catch(I){let L=d[PERSIST_ERROR_HANDLER];if(L&&L(F,I),z)throw new PersistError(F,I)}},K=async F=>{if(V.includes(F))try{await B.delete(`_verani_persist:${F}`)}catch(I){let L=d[PERSIST_ERROR_HANDLER];if(L&&L(F,I),z)throw new PersistError(F,I)}},q;return q=R?createShallowProxy(W,(d,F)=>{G(String(d),F)},d=>{K(String(d))}):createDeepProxy(W,(d,F)=>{G(d,F)},d=>{K(d)},V),d[STATE_READY]=!0,d[PERSISTED_STATE]=q,q}function createDeepProxy(d,F,I,L,R){return new Proxy(d,{get(d,z){let B=Reflect.get(d,z);if(B&&typeof B==`object`&&!Array.isArray(B)){let d=R??String(z);if(L.includes(d)||R!==void 0)return createDeepProxy(B,F,I,L,d)}return B},set(d,I,z){let B=Reflect.set(d,I,z);if(B){let d=R??String(I);L.includes(d)&&(R?F(R,void 0):F(String(I),z))}return B},deleteProperty(d,z){let B=Reflect.deleteProperty(d,z);if(B){let d=R??String(z);L.includes(d)&&(R?F(R,void 0):I(String(z)))}return B}})}function isStateReady(d){return d[STATE_READY]===!0}function getPersistedState(d){if(!isStateReady(d))throw new PersistNotReadyError(`state`);return d[PERSISTED_STATE]}function setPeristErrorHandler(d,F){d[PERSIST_ERROR_HANDLER]=F}async function persistKey(d,F,I){let L=d.ctx.storage,R=safeSerialize(I);await L.put(`_verani_persist:${F}`,R)}async function deletePersistedKey(d,F){await d.ctx.storage.delete(`_verani_persist:${F}`)}async function getPersistedKeys(d){let F=await d.ctx.storage.list({prefix:`_verani_persist:`});return Array.from(F.keys()).map(d=>d.replace(`_verani_persist:`,``))}async function clearPersistedState(d){let F=d.ctx.storage,I=await F.list({prefix:`_verani_persist:`}),L=Array.from(I.keys());await F.delete(L)}var RoomEventEmitterImpl=class{constructor(){this.handlers=new Map}on(d,F){this.handlers.has(d)||this.handlers.set(d,new Set),this.handlers.get(d).add(F)}off(d,F){let I=this.handlers.get(d);I&&(F?(I.delete(F),I.size===0&&this.handlers.delete(d)):this.handlers.delete(d))}async emit(d,F,I){let L=this.handlers.get(d);if(L&&L.size>0){let d=[];for(let R of L)try{let L=R(F,I);L instanceof Promise&&d.push(L)}catch{}await Promise.all(d)}let R=this.handlers.get(`*`);if(R&&R.size>0){let d=[];for(let L of R)try{let R=L(F,I);R instanceof Promise&&d.push(R)}catch{}await Promise.all(d)}}hasHandlers(d){return this.handlers.has(d)&&this.handlers.get(d).size>0||this.handlers.has(`*`)&&this.handlers.get(`*`).size>0}getEventNames(){return Array.from(this.handlers.keys())}rebuildHandlers(d){this.handlers.clear();for(let[F,I]of d.entries())this.handlers.set(F,new Set(I))}};function createRoomEventEmitter(){return new RoomEventEmitterImpl}function defaultExtractMeta(d){let F=crypto.randomUUID(),I=crypto.randomUUID(),L=new URL(d.url).searchParams.get(`channels`);return{userId:F,clientId:I,channels:L?L.split(`,`).map(d=>d.trim()).filter(Boolean):[`default`]}}function defineRoom(d){let F=d.eventEmitter||createRoomEventEmitter(),I=d._staticHandlers||new Map;return{name:d.name,websocketPath:d.websocketPath,extractMeta:d.extractMeta||(d=>defaultExtractMeta(d)),onConnect:d.onConnect,onDisconnect:d.onDisconnect,onMessage:d.onMessage,onError:d.onError,onHibernationRestore:d.onHibernationRestore,eventEmitter:F,_staticHandlers:I,state:d.state,persistedKeys:d.persistedKeys,persistOptions:d.persistOptions,onPersistError:d.onPersistError,on(d,L){F.on(d,L),I.has(d)||I.set(d,new Set),I.get(d).add(L)},off(d,L){F.off(d,L);let R=I.get(d);R&&(L?(R.delete(L),R.size===0&&I.delete(d)):I.delete(d))}}}function cleanupStaleSessions(d){let F=0,I=[];for(let[F,L]of d.entries())F.readyState!==WebSocket.OPEN&&I.push(F);for(let L of I)d.delete(L),F++;return F}function broadcast(d,F,I,L){let R=0,z=encodeFrame({type:`event`,channel:F,data:I}),B=[];for(let{ws:I,meta:V}of d.values())if(V.channels.includes(F)&&!(L?.except&&I===L.except)&&!(L?.userIds&&!L.userIds.includes(V.userId))&&!(L?.clientIds&&!L.clientIds.includes(V.clientId))){if(I.readyState!==WebSocket.OPEN){B.push(I);continue}try{I.send(z),R++}catch{B.push(I)}}for(let F of B)d.delete(F);return B.length,R}function sendToUser(d,F,I,L){let R=0,z=encodeFrame({type:`event`,channel:I,data:L}),B=[];for(let{ws:L,meta:V}of d.values())if(V.userId===F&&V.channels.includes(I)){if(L.readyState!==WebSocket.OPEN){B.push(L);continue}try{L.send(z),R++}catch{B.push(L)}}for(let F of B)d.delete(F);return B.length,R}function getSessionCount(d){return d.size}function getConnectedUserIds(d){let F=new Set;for(let{meta:I}of d.values())F.add(I.userId);return Array.from(F)}function getUserSessions(d,F){let I=[];for(let{ws:L,meta:R}of d.values())R.userId===F&&I.push(L);return I}function getStorage(d){return d.storage}function sanitizeToClassName(d){return d.replace(/^\/+/,``).split(/[-_\/\s]+/).map(d=>d.replace(/[^a-zA-Z0-9]/g,``)).filter(d=>d.length>0).map(d=>d.charAt(0).toUpperCase()+d.slice(1).toLowerCase()).join(``)||`VeraniActor`}function createConfiguration(d){return function(F){return{sockets:{upgradePath:d.websocketPath}}}}async function onInit(d,F){if(F.eventEmitter&&F._staticHandlers)try{F.eventEmitter.rebuildHandlers(F._staticHandlers)}catch{}try{restoreSessions(d)}catch{}if(F.onHibernationRestore&&d.sessions.size>0)try{await F.onHibernationRestore(d)}catch{}else F.onHibernationRestore&&d.sessions.size}function createUserEmitBuilder(d,F,I){return{emit(L,R){return sendToUser(F,d,I,{type:L,...R})}}}function createChannelEmitBuilder(d,F,I){return{emit(L,R){return broadcast(F,d,{type:L,...R},I)}}}function createSocketEmit(d){let F=d.meta.channels[0]||`default`;return{emit(I,L){if(d.ws.readyState===WebSocket.OPEN)try{let R={type:`event`,channel:F,data:{type:I,...L}};d.ws.send(encodeFrame(R))}catch{}},to(I){return d.meta.channels.includes(I)?createChannelEmitBuilder(I,d.actor.sessions,{except:d.ws}):createUserEmitBuilder(I,d.actor.sessions,F)}}}function createActorEmit(d){return{emit(F,I){let L={type:F,...I};return broadcast(d.sessions,`default`,L)},to(F){return createChannelEmitBuilder(F,d.sessions)}}}async function onWebSocketConnect(d,F,I,L){let z;try{if(z=F.extractMeta?await F.extractMeta(L):{userId:`anonymous`,clientId:crypto.randomUUID(),channels:[`default`]},storeAttachment(I,z),F.onConnect){let L={actor:d,ws:I,meta:z,frame:{type:`connect`}};L.emit=createSocketEmit(L);let R={actor:d,ws:I,meta:z,emit:L.emit};await F.onConnect(R)}d.sessions.set(I,{ws:I,meta:z})}catch(L){if(F.onError&&z)try{let R={actor:d,ws:I,meta:z,frame:{type:`error`}};R.emit=createSocketEmit(R),await F.onError(L,{actor:d,ws:I,meta:z,emit:R.emit})}catch{}I.close(1011,`Internal server error`)}}async function onWebSocketMessage(d,F,I,L){let R;try{let z=decodeFrame(L);if(z&&z.type===`ping`){if(I.readyState===WebSocket.OPEN)try{I.send(encodeFrame({type:`pong`}))}catch{}return}if(!z||z.type===`invalid`||(R=d.sessions.get(I),!R))return;let H={actor:d,ws:I,meta:R.meta,frame:z,emit:createSocketEmit({actor:d,ws:I,meta:R.meta,frame:z})},U=F.eventEmitter;U?.hasHandlers(z.type)?await U.emit(z.type,H,z.data||{}):F.onMessage&&await F.onMessage(H,z)}catch(L){if(F.onError&&R)try{await F.onError(L,{actor:d,ws:I,meta:R.meta,emit:createSocketEmit({actor:d,ws:I,meta:R.meta,frame:{type:`error`}})})}catch{}}}async function onWebSocketDisconnect(d,F,I){try{let L=d.sessions.get(I);if(d.sessions.delete(I),L&&F.onDisconnect){let R={actor:d,ws:I,meta:L.meta,frame:{type:`disconnect`}};R.emit=createSocketEmit(R);let z={actor:d,ws:I,meta:L.meta,emit:R.emit};await F.onDisconnect(z)}}catch{}}function createFetch(d,F){return async function(I){let L=new URL(I.url),R=I.headers.get(`Upgrade`);return L.pathname===d.websocketPath&&R===`websocket`&&await F.shouldUpgradeWebSocket(I)?F.onWebSocketUpgrade(I):F.onRequest(I)}}function createActorHandler(d){let F=sanitizeToClassName(d.name||d.websocketPath||`VeraniActor`),L=d;class R extends Actor{constructor(...F){super(...F),this.sessions=new Map,this.emit=createActorEmit(this),this[STATE_READY]=!1,this[PERSISTED_STATE]=d.state?{...d.state}:{},this.fetch=createFetch(L,this)}get roomState(){return this[PERSISTED_STATE]}isStateReady(){return isStateReady(this)}static{this.configuration=createConfiguration(L)}async shouldUpgradeWebSocket(d){return!0}async onInit(){if(d.state){d.onPersistError&&setPeristErrorHandler(this,d.onPersistError);let F=d.persistedKeys;this[PERSISTED_STATE]=await initializePersistedState(this,d.state,F,d.persistOptions)}await onInit(this,L)}async onWebSocketConnect(d,F){await onWebSocketConnect(this,L,d,F)}async onWebSocketMessage(d,F){await onWebSocketMessage(this,L,d,F)}async onWebSocketDisconnect(d){await onWebSocketDisconnect(this,L,d)}cleanupStaleSessions(){return cleanupStaleSessions(this.sessions)}broadcast(d,F,I){return broadcast(this.sessions,d,F,I)}getSessionCount(){return getSessionCount(this.sessions)}getConnectedUserIds(){return getConnectedUserIds(this.sessions)}getUserSessions(d){return getUserSessions(this.sessions,d)}sendToUser(d,F,I){return sendToUser(this.sessions,d,F,I)}emitToChannel(d,F,I){let L={type:F,...I};return broadcast(this.sessions,d,L)}emitToUser(d,F,I){let L=encodeFrame({type:`event`,channel:`default`,data:{type:F,...I}}),R=0,z=[];for(let{ws:F,meta:I}of this.sessions.values())if(I.userId===d){if(F.readyState!==WebSocket.OPEN){z.push(F);continue}try{F.send(L),R++}catch{z.push(F)}}for(let d of z)this.sessions.delete(d);return R}getStorage(){return getStorage(this.ctx)}}return Object.defineProperty(R,`name`,{value:F,writable:!1,configurable:!0}),R}export{encodeFrame as _,PersistNotReadyError as a,deletePersistedKey as c,initializePersistedState as d,isStateReady as f,setPeristErrorHandler as g,safeSerialize as h,PersistError as i,getPersistedKeys as l,safeDeserialize as m,defineRoom as n,STATE_READY as o,persistKey as p,PERSISTED_STATE as r,clearPersistedState as s,createActorHandler as t,getPersistedState as u,restoreSessions as v,storeAttachment as y};
|