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 CHANGED
@@ -4,140 +4,160 @@
4
4
 
5
5
  [![MADE BY #V0ID](https://img.shields.io/badge/MADE%20BY%20%23V0ID-F3EEE1.svg?style=for-the-badge)](https://github.com/v0id-user)
6
6
 
7
- </div>
7
+ Build realtime apps on Cloudflare with Socket.io-like simplicity
8
+
9
+ [Getting Started](#quick-start) • [Documentation](./docs/) • [Examples](./examples/)
8
10
 
9
- > A simple, focused realtime SDK for Cloudflare Actors with Socket.io-like semantics
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
- - **Hibernation Support**: Properly handles Cloudflare Actor hibernation out of the box
17
- - **Type Safe**: Built with TypeScript, full type safety throughout
18
- - **Simple Mental Model**: Rooms, channels, and broadcast semantics that just make sense
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
- ### Installation
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
- ### Server Side (Cloudflare Worker)
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
- **Suggested folder structure** (optional, for clarity):
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
- // src/actors/chat.actor.ts
39
- import { defineRoom, createActorHandler } from "verani";
40
-
41
- // Define your room with lifecycle hooks
42
- export const chatRoom = defineRoom({
43
- name: "chatRoom",
44
- websocketPath: "/chat",
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
- // Use emit API (socket.io-like)
49
- ctx.actor.emit.to("default").emit("user.joined", {
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
- ctx.actor.emit.to("default").emit("user.left", {
57
- userId: ctx.meta.userId
58
- });
73
+ // Room leave is handled automatically
59
74
  }
60
75
  });
61
76
 
62
- // Register event handlers (socket.io-like, recommended)
63
- chatRoom.on("chat.message", (ctx, data) => {
64
- // Broadcast to all in default channel
65
- ctx.actor.emit.to("default").emit("chat.message", {
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
- // Create the Durable Object class from the room definition
73
- // Important: Each defineRoom() creates a room definition object.
74
- // createActorHandler() converts it into a Durable Object class that must be exported.
75
- export const ChatRoom = createActorHandler(chatRoom);
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
- // src/index.ts
80
- import { createActorHandler } from "verani";
81
- import { chatRoom } from "./actors/chat.actor";
99
+ import { UserConnection, ChatRoom } from "./actors/connection";
82
100
 
83
- // Create and export the Durable Object class
84
- export const ChatRoom = createActorHandler(chatRoom);
85
- ```
101
+ // Export Durable Object classes
102
+ export { UserConnection, ChatRoom };
86
103
 
87
- ### Wrangler Configuration
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
- **Critical**: Each `defineRoom()` creates a room definition, and `createActorHandler()` converts it into a Durable Object class. This class **must be exported** and **declared in Wrangler configuration**.
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
- The export name in `src/index.ts` **must match** the `class_name` in `wrangler.jsonc`:
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": "ChatRoom", // Must match export name
99
- "name": "ChatRoom" // Binding name in env
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
- **Three-way relationship** - these must all align:
153
+ Important: Export names must match `class_name` in `wrangler.jsonc`.
113
154
 
114
- 1. **Room definition**: `defineRoom({ name: "ChatRoom" })` - The `name` property is optional but recommended for consistency
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
- // Connect to your Cloudflare Worker with ping/pong keepalive
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
- // Handle connection lifecycle
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
- ## Key Concepts
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
- ```typescript
183
- // Server: broadcast to specific channel using emit API
184
- ctx.actor.emit.to("game-updates").emit("update", data);
185
-
186
- // Or send to a specific user (all their sessions)
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
- // Client: joins "default" channel automatically
190
- // You can implement join/leave for custom channels
186
+ # In another terminal, run your client
187
+ # (or open multiple browser tabs with your client code)
191
188
  ```
192
189
 
193
- ### Hibernation
190
+ That's it! You now have a working realtime chat app.
194
191
 
195
- Verani handles Cloudflare's hibernation automatically:
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
- - **[Getting Started](./docs/getting-started/)** - Installation and quick start guide
204
- - **[API Reference](./docs/api/)** - Complete server and client API documentation
205
- - **[Guides](./docs/guides/)** - Configuration, deployment, scaling, and RPC
206
- - **[Examples](./docs/examples/)** - Common usage patterns and code samples
207
- - **[Concepts](./docs/concepts/)** - Architecture, hibernation, and core concepts
208
- - **[Security](./docs/security/)** - Authentication, authorization, and best practices
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
- ### Server (Actor) Side
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
- - Automatic reconnection with exponential backoff
231
- - Connection state management (`getState()`, `getConnectionState()`, `isConnecting`)
232
- - Message queueing when disconnected
233
- - Event-based API (on/off/once/emit)
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
- ### RPC Support
211
+ ## Features
240
212
 
241
- Call Actor methods remotely from Workers or other Actors:
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
- ```typescript
244
- // In your Worker fetch handler
245
- const stub = ChatRoom.get("room-id");
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
- // Send to user
248
- await stub.sendToUser("alice", "notifications", {
249
- type: "alert",
250
- message: "You have a new message"
251
- });
230
+ ## Try the Examples
252
231
 
253
- // Broadcast to channel
254
- await stub.broadcast("default", { type: "announcement", text: "Hello!" });
232
+ See Verani in action with working examples:
255
233
 
256
- // Query state
257
- const count = await stub.getSessionCount();
258
- const userIds = await stub.getConnectedUserIds();
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
- - Send messages to users from HTTP endpoints
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
- # Clone and run
272
- git clone https://github.com/v0id-user/verani
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
- # Open http://localhost:8787
278
- ```
246
+ # Presence tracking
247
+ bun run examples/clients/presence-client.ts
279
248
 
280
- See `examples/` for chat, presence, and notifications demos!
249
+ # Notifications feed
250
+ bun run examples/clients/notifications-client.ts
251
+ ```
281
252
 
282
- ## Project Status
253
+ See the [Examples README](./examples/README.md) for more details.
283
254
 
284
- Verani is in early stages and active development. Current version includes:
255
+ ## Real-World Example
285
256
 
286
- **Implemented:**
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 contributing guidelines first.
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};