verani 0.8.3 → 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,149 +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
- **Don't have a Cloudflare Worker project yet?** Create one using [C3 (create-cloudflare)](https://developers.cloudflare.com/pages/get-started/c3/):
36
+ Don't have a Cloudflare Worker project? Create one:
32
37
 
33
38
  ```bash
34
39
  npm create cloudflare@latest my-verani-app
35
40
  cd my-verani-app
41
+ npm install verani @cloudflare/actors
36
42
  ```
37
43
 
38
- This creates a new Cloudflare Worker project ready for Verani. Choose "Hello World" or "Common" template when prompted.
44
+ ### Step 2: Create Your Connection Handler
39
45
 
40
- ### Server Side (Cloudflare Worker)
41
-
42
- **Suggested folder structure** (optional, for clarity):
43
- - `src/actors/chat.actor.ts` - Room definitions
44
- - `src/index.ts` - Export Durable Object classes
46
+ Create `src/actors/connection.ts`:
45
47
 
46
48
  ```typescript
47
- // src/actors/chat.actor.ts
48
- import { defineRoom, createActorHandler } from "verani";
49
-
50
- // Define your room with lifecycle hooks
51
- export const chatRoom = defineRoom({
52
- name: "chatRoom",
53
- 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
+ },
54
64
 
55
- onConnect(ctx) {
65
+ async onConnect(ctx) {
56
66
  console.log(`User ${ctx.meta.userId} connected`);
57
- // Use emit API (socket.io-like)
58
- ctx.actor.emit.to("default").emit("user.joined", {
59
- userId: ctx.meta.userId
60
- });
67
+ // Join chat room (persisted across hibernation)
68
+ await ctx.actor.joinRoom("chat");
61
69
  },
62
70
 
63
- onDisconnect(ctx) {
71
+ async onDisconnect(ctx) {
64
72
  console.log(`User ${ctx.meta.userId} disconnected`);
65
- ctx.actor.emit.to("default").emit("user.left", {
66
- userId: ctx.meta.userId
67
- });
73
+ // Room leave is handled automatically
68
74
  }
69
75
  });
70
76
 
71
- // Register event handlers (socket.io-like, recommended)
72
- chatRoom.on("chat.message", (ctx, data) => {
73
- // Broadcast to all in default channel
74
- 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", {
75
81
  from: ctx.meta.userId,
76
82
  text: data.text,
77
83
  timestamp: Date.now()
78
84
  });
79
85
  });
80
86
 
81
- // Create the Durable Object class from the room definition
82
- // Important: Each defineRoom() creates a room definition object.
83
- // createActorHandler() converts it into a Durable Object class that must be exported.
84
- 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" });
85
92
  ```
86
93
 
94
+ ### Step 3: Wire Up Your Worker
95
+
96
+ Update `src/index.ts`:
97
+
87
98
  ```typescript
88
- // src/index.ts
89
- import { createActorHandler } from "verani";
90
- import { chatRoom } from "./actors/chat.actor";
99
+ import { UserConnection, ChatRoom } from "./actors/connection";
91
100
 
92
- // Create and export the Durable Object class
93
- export const ChatRoom = createActorHandler(chatRoom);
94
- ```
101
+ // Export Durable Object classes
102
+ export { UserConnection, ChatRoom };
103
+
104
+ // Route WebSocket connections
105
+ export default {
106
+ async fetch(request: Request, env: Env, ctx: ExecutionContext) {
107
+ const url = new URL(request.url);
108
+
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
+ }
95
115
 
96
- ### Wrangler Configuration
116
+ return new Response("Not Found", { status: 404 });
117
+ }
118
+ };
119
+ ```
97
120
 
98
- **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**.
121
+ ### Step 4: Configure Wrangler
99
122
 
100
- The export name in `src/index.ts` **must match** the `class_name` in `wrangler.jsonc`:
123
+ Update `wrangler.jsonc`:
101
124
 
102
125
  ```jsonc
103
126
  {
127
+ "name": "my-verani-app",
128
+ "main": "src/index.ts",
129
+ "compatibility_date": "2024-01-01",
130
+
104
131
  "durable_objects": {
105
132
  "bindings": [
106
133
  {
107
- "class_name": "ChatRoom", // Must match export name
108
- "name": "ChatRoom" // Binding name in env
134
+ "class_name": "UserConnection",
135
+ "name": "CONNECTION_DO"
136
+ },
137
+ {
138
+ "class_name": "ChatRoom",
139
+ "name": "ROOM_DO"
109
140
  }
110
141
  ]
111
142
  },
143
+
112
144
  "migrations": [
113
145
  {
114
- "new_sqlite_classes": ["ChatRoom"],
146
+ "new_sqlite_classes": ["UserConnection", "ChatRoom"],
115
147
  "tag": "v1"
116
148
  }
117
149
  ]
118
150
  }
119
151
  ```
120
152
 
121
- **Three-way relationship** - these must all align:
122
-
123
- 1. **Room definition**: `defineRoom({ name: "ChatRoom" })` - The `name` property is optional but recommended for consistency
124
- 2. **Export** in `src/index.ts`: `export const ChatRoom = createActorHandler(chatRoom)` - The export name becomes the class name
125
- 3. **Class name** in `wrangler.jsonc`: `"class_name": "ChatRoom"` - Must match the export name exactly
126
-
127
- **Important Notes:**
128
- - `defineRoom()` returns a room definition object, **not** a Durable Object class
129
- - `createActorHandler(room)` creates the actual Durable Object class
130
- - Each room definition must be converted to a class with `createActorHandler()` and exported
131
- - For multiple rooms, you need multiple exports and multiple bindings in `wrangler.jsonc`
132
- - The export name (e.g., `ChatRoom`) becomes the class name and must match `class_name` in configuration
153
+ Important: Export names must match `class_name` in `wrangler.jsonc`.
133
154
 
134
- ### Client Side
155
+ ### Step 5: Build Your Client
135
156
 
136
157
  ```typescript
137
- import { VeraniClient } from "verani";
138
-
139
- // Connect to your Cloudflare Worker with ping/pong keepalive
140
- const client = new VeraniClient("wss://your-worker.dev/ws?userId=alice", {
141
- pingInterval: 5000, // Send ping every 5 seconds
142
- pongTimeout: 5000, // Expect pong within 5 seconds
143
- reconnection: {
144
- enabled: true,
145
- maxAttempts: 10,
146
- initialDelay: 1000,
147
- maxDelay: 30000
148
- }
149
- });
158
+ import { VeraniClient } from "verani/client";
159
+
160
+ const client = new VeraniClient("ws://localhost:8787/ws?userId=alice");
150
161
 
151
162
  // Listen for messages
152
163
  client.on("chat.message", (data) => {
@@ -154,153 +165,97 @@ client.on("chat.message", (data) => {
154
165
  });
155
166
 
156
167
  client.on("user.joined", (data) => {
157
- console.log(`User ${data.userId} joined`);
168
+ console.log(`User ${data.userId} joined!`);
158
169
  });
159
170
 
160
171
  // Send messages
161
172
  client.emit("chat.message", { text: "Hello, world!" });
162
173
 
163
- // Handle connection lifecycle
164
- client.onOpen(() => {
165
- console.log("Connected!");
166
- });
167
-
168
- client.onStateChange((state) => {
169
- console.log("Connection state:", state);
170
- });
171
-
172
- // Wait for connection before sending
174
+ // Wait for connection (optional)
173
175
  await client.waitForConnection();
174
- client.emit("ready", {});
175
176
  ```
176
177
 
177
- ## Key Concepts
178
-
179
- ### Actors = Rooms
180
-
181
- Each Cloudflare Actor instance represents a **logical container** for realtime communication:
182
-
183
- - **Chat room**: All users in the same chat share one Actor
184
- - **User notifications**: Each user gets their own Actor
185
- - **Game session**: Each game instance is one Actor
186
-
187
- ### Channels
188
-
189
- Inside an Actor, connections can join **channels** for selective message routing:
190
-
191
- ```typescript
192
- // Server: broadcast to specific channel using emit API
193
- ctx.actor.emit.to("game-updates").emit("update", data);
178
+ ### Step 6: Run It!
194
179
 
195
- // Or send to a specific user (all their sessions)
196
- ctx.emit.to("alice").emit("notification", { message: "Hello!" });
180
+ ```bash
181
+ # Start the server
182
+ npm run dev
183
+ # or
184
+ wrangler dev
197
185
 
198
- // Client: joins "default" channel automatically
199
- // 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)
200
188
  ```
201
189
 
202
- ### Hibernation
190
+ That's it! You now have a working realtime chat app.
203
191
 
204
- Verani handles Cloudflare's hibernation automatically:
205
-
206
- - Connection metadata survives hibernation via WebSocket attachments
207
- - Sessions are restored when the Actor wakes up
208
- - No manual state management needed
192
+ Need more help? Check out the [Quick Start Guide](./docs/getting-started/quick-start.md) for detailed examples.
209
193
 
210
194
  ## Documentation
211
195
 
212
- - **[Getting Started](./docs/getting-started/)** - Installation and quick start guide
213
- - **[API Reference](./docs/api/)** - Complete server and client API documentation
214
- - **[Guides](./docs/guides/)** - Configuration, deployment, scaling, and RPC
215
- - **[Examples](./docs/examples/)** - Common usage patterns and code samples
216
- - **[Concepts](./docs/concepts/)** - Architecture, hibernation, and core concepts
217
- - **[Security](./docs/security/)** - Authentication, authorization, and best practices
218
-
219
- ## More Examples
220
- **[Vchats](https://github.com/v0id-user/vchats)** - A simple chat application built with Verani
221
-
222
- ## Features
223
-
224
- ### Server (Actor) Side
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
225
203
 
226
- - **Socket.io-like event handlers** - `room.on()` and `room.off()` for clean event handling
227
- - **Emit API** - `ctx.emit` and `ctx.actor.emit.to()` for intuitive message sending
228
- - Room-based architecture with lifecycle hooks (`onConnect`, `onDisconnect`, `onHibernationRestore`)
229
- - WebSocket attachment management for hibernation
230
- - Selective broadcasting with filters (userIds, clientIds, except)
231
- - User and client ID tracking
232
- - **RPC methods** - Call Actor methods remotely from Workers or other Actors
233
- - Durable Object storage access for persistent state
234
- - Error boundaries and logging
235
- - Flexible metadata extraction from requests
204
+ ## Key Concepts
236
205
 
237
- ### Client Side
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
- - Automatic reconnection with exponential backoff
240
- - Connection state management (`getState()`, `getConnectionState()`, `isConnecting`)
241
- - Message queueing when disconnected
242
- - Event-based API (on/off/once/emit)
243
- - Promise-based connection waiting (`waitForConnection()`)
244
- - Lifecycle callbacks (`onOpen`, `onClose`, `onError`, `onStateChange`)
245
- - **Ping/pong keepalive** with automatic Page Visibility API resync
246
- - Configurable connection timeout and queue size
211
+ ## Features
247
212
 
248
- ### RPC Support
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
249
222
 
250
- Call Actor methods remotely from Workers or other Actors:
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`)
251
229
 
252
- ```typescript
253
- // In your Worker fetch handler
254
- const stub = ChatRoom.get("room-id");
230
+ ## Try the Examples
255
231
 
256
- // Send to user
257
- await stub.sendToUser("alice", "notifications", {
258
- type: "alert",
259
- message: "You have a new message"
260
- });
232
+ See Verani in action with working examples:
261
233
 
262
- // Broadcast to channel
263
- await stub.broadcast("default", { type: "announcement", text: "Hello!" });
264
-
265
- // Query state
266
- const count = await stub.getSessionCount();
267
- 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
268
238
  ```
269
239
 
270
- - Send messages to users from HTTP endpoints
271
- - Query actor state remotely
272
- - Broadcast from external events or scheduled tasks
273
- - Coordinate between multiple Actors
274
-
275
- ## Live Examples
276
-
277
- Try out Verani with working examples:
240
+ Then in another terminal, try:
278
241
 
279
242
  ```bash
280
- # Clone and run
281
- git clone https://github.com/v0id-user/verani
282
- cd verani
283
- bun install # or npm install
284
- bun run dev # or npm run dev
243
+ # Chat room example
244
+ bun run examples/clients/chat-client.ts
285
245
 
286
- # Open http://localhost:8787
287
- ```
246
+ # Presence tracking
247
+ bun run examples/clients/presence-client.ts
288
248
 
289
- See `examples/` for chat, presence, and notifications demos!
249
+ # Notifications feed
250
+ bun run examples/clients/notifications-client.ts
251
+ ```
290
252
 
291
- ## Project Status
253
+ See the [Examples README](./examples/README.md) for more details.
292
254
 
293
- Verani is in early stages and active development. Current version includes:
255
+ ## Real-World Example
294
256
 
295
- **Implemented:**
296
- - Core realtime messaging
297
- - Hibernation support
298
- - Client reconnection
299
- - Presence protocol with multi-device support
300
- - Persistent storage integration with Durable Object storage
257
+ [Vchats](https://github.com/v0id-user/vchats) - A complete chat application built with Verani
301
258
 
302
- **Coming Soon:**
303
- - React/framework adapters
304
259
 
305
260
  ## License
306
261
 
@@ -308,5 +263,5 @@ ISC
308
263
 
309
264
  ## Contributing
310
265
 
311
- Contributions welcome! Please read our contributing guidelines first.
266
+ Contributions welcome! Please read our [Contributing Guidelines](./CONTRIBUTING.md) first.
312
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};