verani 0.8.3 → 0.10.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 +147 -192
- package/dist/actor-runtime-BoaZjQPs.cjs +1 -0
- package/dist/{actor-runtime-1TpQk-wg.d.cts → actor-runtime-DCvCJ1UN.d.mts} +220 -16
- package/dist/actor-runtime-DecQvWnw.mjs +1 -0
- package/dist/{actor-runtime-BOmyu8Vl.d.mts → actor-runtime-Dqodm1HR.d.cts} +220 -16
- package/dist/client-Cf_oqaqp.cjs +1 -0
- package/dist/{client-C3BlxAY_.d.cts → client-D7P17bMd.d.mts} +4 -4
- package/dist/{client-CV5RrpZX.d.mts → client-JCkzlxJL.d.cts} +4 -4
- package/dist/client-oy-w-Ky5.mjs +1 -0
- package/dist/client.cjs +1 -1
- package/dist/client.d.cts +3 -3
- package/dist/client.d.mts +3 -3
- package/dist/client.mjs +1 -1
- package/dist/{decode--RaAAv0U.d.cts → decode-DwdLTFvb.d.mts} +4 -4
- package/dist/{decode-BUzd77kA.d.mts → decode-otE_3S_-.d.cts} +4 -4
- package/dist/encode-CFndA-BQ.mjs +1 -0
- package/dist/encode-SjZrKIyU.cjs +1 -0
- package/dist/typed-client.cjs +1 -1
- package/dist/typed-client.d.cts +1 -1
- package/dist/typed-client.d.mts +1 -1
- package/dist/typed-client.mjs +1 -1
- package/dist/typed.cjs +1 -1
- package/dist/typed.d.cts +2 -2
- package/dist/typed.d.mts +2 -2
- package/dist/typed.mjs +1 -1
- package/dist/{types-DOI6EHQJ.d.cts → types-6m1L8QLb.d.mts} +16 -13
- package/dist/{types-_41fL9Dn.d.mts → types-DIIeb2YQ.d.cts} +16 -13
- package/dist/verani.cjs +1 -1
- package/dist/verani.d.cts +239 -7
- package/dist/verani.d.mts +239 -7
- 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/dist/encode-BYFW_6TI.cjs +0 -1
- package/dist/encode-BhJqnsto.mjs +0 -1
package/README.md
CHANGED
|
@@ -4,149 +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:
|
|
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
|
-
|
|
44
|
+
### Step 2: Create Your Connection Handler
|
|
39
45
|
|
|
40
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
//
|
|
58
|
-
ctx.actor.
|
|
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
|
-
|
|
66
|
-
userId: ctx.meta.userId
|
|
67
|
-
});
|
|
73
|
+
// Room leave is handled automatically
|
|
68
74
|
}
|
|
69
75
|
});
|
|
70
76
|
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
// Broadcast to
|
|
74
|
-
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", {
|
|
75
81
|
from: ctx.meta.userId,
|
|
76
82
|
text: data.text,
|
|
77
83
|
timestamp: Date.now()
|
|
78
84
|
});
|
|
79
85
|
});
|
|
80
86
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
89
|
-
import { createActorHandler } from "verani";
|
|
90
|
-
import { chatRoom } from "./actors/chat.actor";
|
|
99
|
+
import { UserConnection, ChatRoom } from "./actors/connection";
|
|
91
100
|
|
|
92
|
-
//
|
|
93
|
-
export
|
|
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
|
-
|
|
116
|
+
return new Response("Not Found", { status: 404 });
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
```
|
|
97
120
|
|
|
98
|
-
|
|
121
|
+
### Step 4: Configure Wrangler
|
|
99
122
|
|
|
100
|
-
|
|
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": "
|
|
108
|
-
"name": "
|
|
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
|
-
|
|
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
|
|
155
|
+
### Step 5: Build Your Client
|
|
135
156
|
|
|
136
157
|
```typescript
|
|
137
|
-
import { VeraniClient } from "verani";
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
180
|
+
```bash
|
|
181
|
+
# Start the server
|
|
182
|
+
npm run dev
|
|
183
|
+
# or
|
|
184
|
+
wrangler dev
|
|
197
185
|
|
|
198
|
-
|
|
199
|
-
|
|
186
|
+
# In another terminal, run your client
|
|
187
|
+
# (or open multiple browser tabs with your client code)
|
|
200
188
|
```
|
|
201
189
|
|
|
202
|
-
|
|
190
|
+
That's it! You now have a working realtime chat app.
|
|
203
191
|
|
|
204
|
-
|
|
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
|
-
-
|
|
213
|
-
-
|
|
214
|
-
-
|
|
215
|
-
-
|
|
216
|
-
-
|
|
217
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
253
|
-
// In your Worker fetch handler
|
|
254
|
-
const stub = ChatRoom.get("room-id");
|
|
230
|
+
## Try the Examples
|
|
255
231
|
|
|
256
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
281
|
-
|
|
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
|
-
#
|
|
287
|
-
|
|
246
|
+
# Presence tracking
|
|
247
|
+
bun run examples/clients/presence-client.ts
|
|
288
248
|
|
|
289
|
-
|
|
249
|
+
# Notifications feed
|
|
250
|
+
bun run examples/clients/notifications-client.ts
|
|
251
|
+
```
|
|
290
252
|
|
|
291
|
-
|
|
253
|
+
See the [Examples README](./examples/README.md) for more details.
|
|
292
254
|
|
|
293
|
-
|
|
255
|
+
## Real-World Example
|
|
294
256
|
|
|
295
|
-
|
|
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
|
|
266
|
+
Contributions welcome! Please read our [Contributing Guidelines](./CONTRIBUTING.md) first.
|
|
312
267
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const require_encode=require(`./encode-SjZrKIyU.cjs`);let __cloudflare_actors=require(`@cloudflare/actors`);function isValidConnectionMeta(s){if(typeof s!=`object`||!s)return!1;let N=s;return!(typeof N.userId!=`string`||!N.userId||typeof N.clientId!=`string`||!N.clientId||!Array.isArray(N.channels)||!N.channels.every(s=>typeof s==`string`))}function storeAttachment(s,N){s.serializeAttachment(N)}function restoreSessions(s){let N=0,F=0;for(let I of s.ctx.getWebSockets()){if(I.readyState!==WebSocket.OPEN){F++;continue}let L=I.deserializeAttachment();if(!L){F++;continue}if(!isValidConnectionMeta(L)){F++;continue}let R=L;s.sessions.set(I,{ws:I,meta:R}),N++}}function decodeFrame(N){return require_encode.a(N)??{type:`invalid`}}function encodeFrame(N){return require_encode.n(N)}const PERSIST_ERROR_HANDLER=Symbol(`PERSIST_ERROR_HANDLER`),PERSISTED_STATE=Symbol(`PERSISTED_STATE`),STATE_READY=Symbol(`STATE_READY`);var PersistNotReadyError=class extends Error{constructor(s){super(`Cannot access persisted state key "${s}" before storage is initialized. Wait for onInit to complete or check actor.isStateReady().`),this.name=`PersistNotReadyError`}},PersistError=class extends Error{constructor(s,N){super(`Failed to persist key "${s}": ${N.message}`),this.name=`PersistError`,this.originalCause=N}};function safeSerialize(s){let N=new WeakSet;return JSON.stringify(s,(s,P)=>{if(P instanceof Date)return{__type:`Date`,value:P.toISOString()};if(P instanceof RegExp)return{__type:`RegExp`,source:P.source,flags:P.flags};if(P instanceof Map)return{__type:`Map`,entries:Array.from(P.entries())};if(P instanceof Set)return{__type:`Set`,values:Array.from(P.values())};if(P instanceof Error)return{__type:`Error`,name:P.name,message:P.message};if(typeof P==`object`&&P){if(N.has(P))return;N.add(P)}if(!(typeof P==`function`||typeof P==`symbol`))return P})}function safeDeserialize(s){return JSON.parse(s,(s,N)=>{if(N&&typeof N==`object`&&N.__type)switch(N.__type){case`Date`:return new Date(N.value);case`RegExp`:return new RegExp(N.source,N.flags);case`Map`:return new Map(N.entries);case`Set`:return new Set(N.values);case`Error`:{let s=Error(N.message);return s.name=N.name,s}}return N})}function createShallowProxy(s,N,P){return new Proxy(s,{set(s,P,F){let I=Reflect.set(s,P,F);return I&&typeof P==`string`&&N(P,F),I},deleteProperty(s,N){let F=Reflect.deleteProperty(s,N);return F&&typeof N==`string`&&P(N),F}})}async function initializePersistedState(s,N,P=[],F={}){let{shallow:I=!0,throwOnError:L=!0}=F,R=s.ctx.storage,H=P.length>0?P.map(String):Object.keys(N),U={...N};for(let s of H)try{let N=await R.get(`_verani_persist:${s}`);if(N!==void 0)try{U[s]=safeDeserialize(N)}catch(N){if(L)throw new PersistError(s,N)}}catch(N){if(L&&!(N instanceof PersistError))throw new PersistError(s,N)}let W=async(N,P)=>{if(H.includes(N))try{let s=safeSerialize(P);await R.put(`_verani_persist:${N}`,s)}catch(P){let F=s[PERSIST_ERROR_HANDLER];if(F&&F(N,P),L)throw new PersistError(N,P)}},G=async N=>{if(H.includes(N))try{await R.delete(`_verani_persist:${N}`)}catch(P){let F=s[PERSIST_ERROR_HANDLER];if(F&&F(N,P),L)throw new PersistError(N,P)}},K;return K=I?createShallowProxy(U,(s,N)=>{W(String(s),N)},s=>{G(String(s))}):createDeepProxy(U,(s,N)=>{W(s,N)},s=>{G(s)},H),s[STATE_READY]=!0,s[PERSISTED_STATE]=K,K}function createDeepProxy(s,N,P,F,I){return new Proxy(s,{get(s,L){let R=Reflect.get(s,L);if(R&&typeof R==`object`&&!Array.isArray(R)){let s=I??String(L);if(F.includes(s)||I!==void 0)return createDeepProxy(R,N,P,F,s)}return R},set(s,P,L){let R=Reflect.set(s,P,L);if(R){let s=I??String(P);F.includes(s)&&(I?N(I,void 0):N(String(P),L))}return R},deleteProperty(s,L){let R=Reflect.deleteProperty(s,L);if(R){let s=I??String(L);F.includes(s)&&(I?N(I,void 0):P(String(L)))}return R}})}function isStateReady(s){return s[STATE_READY]===!0}function getPersistedState(s){if(!isStateReady(s))throw new PersistNotReadyError(`state`);return s[PERSISTED_STATE]}function setPeristErrorHandler(s,N){s[PERSIST_ERROR_HANDLER]=N}async function persistKey(s,N,P){let F=s.ctx.storage,I=safeSerialize(P);await F.put(`_verani_persist:${N}`,I)}async function deletePersistedKey(s,N){await s.ctx.storage.delete(`_verani_persist:${N}`)}async function getPersistedKeys(s){let N=await s.ctx.storage.list({prefix:`_verani_persist:`});return Array.from(N.keys()).map(s=>s.replace(`_verani_persist:`,``))}async function clearPersistedState(s){let N=s.ctx.storage,P=await N.list({prefix:`_verani_persist:`}),F=Array.from(P.keys());await N.delete(F)}var RoomEventEmitterImpl=class{constructor(){this.handlers=new Map}on(s,N){this.handlers.has(s)||this.handlers.set(s,new Set),this.handlers.get(s).add(N)}off(s,N){let P=this.handlers.get(s);P&&(N?(P.delete(N),P.size===0&&this.handlers.delete(s)):this.handlers.delete(s))}async emit(s,N,P){let F=this.handlers.get(s);if(F&&F.size>0){let s=[];for(let I of F)try{let F=I(N,P);F instanceof Promise&&s.push(F)}catch{}await Promise.all(s)}let I=this.handlers.get(`*`);if(I&&I.size>0){let s=[];for(let F of I)try{let I=F(N,P);I instanceof Promise&&s.push(I)}catch{}await Promise.all(s)}}hasHandlers(s){return this.handlers.has(s)&&this.handlers.get(s).size>0||this.handlers.has(`*`)&&this.handlers.get(`*`).size>0}getEventNames(){return Array.from(this.handlers.keys())}rebuildHandlers(s){this.handlers.clear();for(let[N,P]of s.entries())this.handlers.set(N,new Set(P))}};function createRoomEventEmitter(){return new RoomEventEmitterImpl}function defaultExtractMeta(s){let N=crypto.randomUUID(),P=crypto.randomUUID(),F=new URL(s.url).searchParams.get(`channels`);return{userId:N,clientId:P,channels:F?F.split(`,`).map(s=>s.trim()).filter(Boolean):[`default`]}}function defineRoom(s){let N=s.eventEmitter||createRoomEventEmitter(),P=s._staticHandlers||new Map;return{name:s.name,websocketPath:s.websocketPath,extractMeta:s.extractMeta||(s=>defaultExtractMeta(s)),onConnect:s.onConnect,onDisconnect:s.onDisconnect,onMessage:s.onMessage,onError:s.onError,onHibernationRestore:s.onHibernationRestore,eventEmitter:N,_staticHandlers:P,state:s.state,persistedKeys:s.persistedKeys,persistOptions:s.persistOptions,onPersistError:s.onPersistError,on(s,F){N.on(s,F),P.has(s)||P.set(s,new Set),P.get(s).add(F)},off(s,F){N.off(s,F);let I=P.get(s);I&&(F?(I.delete(F),I.size===0&&P.delete(s)):P.delete(s))}}}function cleanupStaleSessions(s){let N=0,P=[];for(let[N,F]of s.entries())N.readyState!==WebSocket.OPEN&&P.push(N);for(let F of P)s.delete(F),N++;return N}function broadcast(s,N,P,F){let I=0,L=encodeFrame({type:`event`,channel:N,data:P}),z=[];for(let{ws:P,meta:R}of s.values())if(R.channels.includes(N)&&!(F?.except&&P===F.except)&&!(F?.userIds&&!F.userIds.includes(R.userId))&&!(F?.clientIds&&!F.clientIds.includes(R.clientId))){if(P.readyState!==WebSocket.OPEN){z.push(P);continue}try{P.send(L),I++}catch{z.push(P)}}for(let N of z)s.delete(N);return z.length,I}function sendToUser(s,N,P,F){let I=0,L=encodeFrame({type:`event`,channel:P,data:F}),z=[];for(let{ws:F,meta:R}of s.values())if(R.userId===N&&R.channels.includes(P)){if(F.readyState!==WebSocket.OPEN){z.push(F);continue}try{F.send(L),I++}catch{z.push(F)}}for(let N of z)s.delete(N);return z.length,I}function getSessionCount(s){return s.size}function getConnectedUserIds(s){let N=new Set;for(let{meta:P}of s.values())N.add(P.userId);return Array.from(N)}function getUserSessions(s,N){let P=[];for(let{ws:F,meta:I}of s.values())I.userId===N&&P.push(F);return P}function getStorage(s){return s.storage}function sanitizeToClassName(s){return s.replace(/^\/+/,``).split(/[-_\/\s]+/).map(s=>s.replace(/[^a-zA-Z0-9]/g,``)).filter(s=>s.length>0).map(s=>s.charAt(0).toUpperCase()+s.slice(1).toLowerCase()).join(``)||`VeraniActor`}function createConfiguration(s){return function(N){return{sockets:{upgradePath:s.websocketPath}}}}async function onInit(s,N){if(N.eventEmitter&&N._staticHandlers)try{N.eventEmitter.rebuildHandlers(N._staticHandlers)}catch{}try{restoreSessions(s)}catch{}if(N.onHibernationRestore&&s.sessions.size>0)try{await N.onHibernationRestore(s)}catch{}else N.onHibernationRestore&&s.sessions.size}function createUserEmitBuilder(s,N,P){return{emit(F,I){return sendToUser(N,s,P,{type:F,...I})}}}function createChannelEmitBuilder(s,N,P){return{emit(F,I){return broadcast(N,s,{type:F,...I},P)}}}function createSocketEmit(s){let N=s.meta.channels[0]||`default`;return{emit(P,F){if(s.ws.readyState===WebSocket.OPEN)try{let I={type:`event`,channel:N,data:{type:P,...F}};s.ws.send(encodeFrame(I))}catch{}},to(P){return s.meta.channels.includes(P)?createChannelEmitBuilder(P,s.actor.sessions,{except:s.ws}):createUserEmitBuilder(P,s.actor.sessions,N)}}}function createActorEmit(s){return{emit(N,P){let F={type:N,...P};return broadcast(s.sessions,`default`,F)},to(N){return createChannelEmitBuilder(N,s.sessions)}}}async function onWebSocketConnect(s,N,P,I){let L;try{if(L=N.extractMeta?await N.extractMeta(I):{userId:`anonymous`,clientId:crypto.randomUUID(),channels:[`default`]},storeAttachment(P,L),N.onConnect){let F={actor:s,ws:P,meta:L,frame:{type:`connect`}};F.emit=createSocketEmit(F);let I={actor:s,ws:P,meta:L,emit:F.emit};await N.onConnect(I)}s.sessions.set(P,{ws:P,meta:L})}catch(F){if(N.onError&&L)try{let I={actor:s,ws:P,meta:L,frame:{type:`error`}};I.emit=createSocketEmit(I),await N.onError(F,{actor:s,ws:P,meta:L,emit:I.emit})}catch{}P.close(1011,`Internal server error`)}}async function onWebSocketMessage(s,N,P,F){let I;try{let z=decodeFrame(F);if(z&&z.type===`ping`){if(P.readyState===WebSocket.OPEN)try{P.send(encodeFrame({type:`pong`}))}catch{}return}if(!z||z.type===`invalid`||(I=s.sessions.get(P),!I))return;let B={actor:s,ws:P,meta:I.meta,frame:z,emit:createSocketEmit({actor:s,ws:P,meta:I.meta,frame:z})},V=N.eventEmitter;V?.hasHandlers(z.type)?await V.emit(z.type,B,z.data||{}):N.onMessage&&await N.onMessage(B,z)}catch(F){if(N.onError&&I)try{await N.onError(F,{actor:s,ws:P,meta:I.meta,emit:createSocketEmit({actor:s,ws:P,meta:I.meta,frame:{type:`error`}})})}catch{}}}async function onWebSocketDisconnect(s,N,P){try{let F=s.sessions.get(P);if(s.sessions.delete(P),F&&N.onDisconnect){let I={actor:s,ws:P,meta:F.meta,frame:{type:`disconnect`}};I.emit=createSocketEmit(I);let L={actor:s,ws:P,meta:F.meta,emit:I.emit};await N.onDisconnect(L)}}catch{}}function createFetch(s,N){return async function(P){let F=new URL(P.url),I=P.headers.get(`Upgrade`);return F.pathname===s.websocketPath&&I===`websocket`&&await N.shouldUpgradeWebSocket(P)?N.onWebSocketUpgrade(P):N.onRequest(P)}}function createActorHandler(s){let P=sanitizeToClassName(s.name||s.websocketPath||`VeraniActor`),F=s;class I extends __cloudflare_actors.Actor{constructor(...N){super(...N),this.sessions=new Map,this.emit=createActorEmit(this),this[STATE_READY]=!1,this[PERSISTED_STATE]=s.state?{...s.state}:{},this.fetch=createFetch(F,this)}get roomState(){return this[PERSISTED_STATE]}isStateReady(){return isStateReady(this)}static{this.configuration=createConfiguration(F)}async shouldUpgradeWebSocket(s){return!0}async onInit(){if(s.state){s.onPersistError&&setPeristErrorHandler(this,s.onPersistError);let N=s.persistedKeys;this[PERSISTED_STATE]=await initializePersistedState(this,s.state,N,s.persistOptions)}await onInit(this,F)}async onWebSocketConnect(s,N){await onWebSocketConnect(this,F,s,N)}async onWebSocketMessage(s,N){await onWebSocketMessage(this,F,s,N)}async onWebSocketDisconnect(s){await onWebSocketDisconnect(this,F,s)}cleanupStaleSessions(){return cleanupStaleSessions(this.sessions)}broadcast(s,N,P){return broadcast(this.sessions,s,N,P)}getSessionCount(){return getSessionCount(this.sessions)}getConnectedUserIds(){return getConnectedUserIds(this.sessions)}getUserSessions(s){return getUserSessions(this.sessions,s)}sendToUser(s,N,P){return sendToUser(this.sessions,s,N,P)}emitToChannel(s,N,P){let F={type:N,...P};return broadcast(this.sessions,s,F)}emitToUser(s,N,P){let F=encodeFrame({type:`event`,channel:`default`,data:{type:N,...P}}),I=0,L=[];for(let{ws:N,meta:P}of this.sessions.values())if(P.userId===s){if(N.readyState!==WebSocket.OPEN){L.push(N);continue}try{N.send(F),I++}catch{L.push(N)}}for(let s of L)this.sessions.delete(s);return I}getStorage(){return getStorage(this.ctx)}}return Object.defineProperty(I,`name`,{value:P,writable:!1,configurable:!0}),I}Object.defineProperty(exports,`_`,{enumerable:!0,get:function(){return encodeFrame}}),Object.defineProperty(exports,`a`,{enumerable:!0,get:function(){return PersistNotReadyError}}),Object.defineProperty(exports,`c`,{enumerable:!0,get:function(){return deletePersistedKey}}),Object.defineProperty(exports,`d`,{enumerable:!0,get:function(){return initializePersistedState}}),Object.defineProperty(exports,`f`,{enumerable:!0,get:function(){return isStateReady}}),Object.defineProperty(exports,`g`,{enumerable:!0,get:function(){return setPeristErrorHandler}}),Object.defineProperty(exports,`h`,{enumerable:!0,get:function(){return safeSerialize}}),Object.defineProperty(exports,`i`,{enumerable:!0,get:function(){return PersistError}}),Object.defineProperty(exports,`l`,{enumerable:!0,get:function(){return getPersistedKeys}}),Object.defineProperty(exports,`m`,{enumerable:!0,get:function(){return safeDeserialize}}),Object.defineProperty(exports,`n`,{enumerable:!0,get:function(){return defineRoom}}),Object.defineProperty(exports,`o`,{enumerable:!0,get:function(){return STATE_READY}}),Object.defineProperty(exports,`p`,{enumerable:!0,get:function(){return persistKey}}),Object.defineProperty(exports,`r`,{enumerable:!0,get:function(){return PERSISTED_STATE}}),Object.defineProperty(exports,`s`,{enumerable:!0,get:function(){return clearPersistedState}}),Object.defineProperty(exports,`t`,{enumerable:!0,get:function(){return createActorHandler}}),Object.defineProperty(exports,`u`,{enumerable:!0,get:function(){return getPersistedState}}),Object.defineProperty(exports,`v`,{enumerable:!0,get:function(){return restoreSessions}}),Object.defineProperty(exports,`y`,{enumerable:!0,get:function(){return storeAttachment}});
|