verani 0.7.0 → 0.8.1

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
@@ -35,7 +35,7 @@ import { defineRoom, createActorHandler } from "verani";
35
35
 
36
36
  // Define your room with lifecycle hooks
37
37
  export const chatRoom = defineRoom({
38
- name: "chat",
38
+ name: "chatRoom",
39
39
  websocketPath: "/chat",
40
40
 
41
41
  onConnect(ctx) {
@@ -64,13 +64,17 @@ chatRoom.on("chat.message", (ctx, data) => {
64
64
  });
65
65
  });
66
66
 
67
- // Create the Durable Object class
67
+ // Create the Durable Object class from the room definition
68
+ // Important: Each defineRoom() creates a room definition object.
69
+ // createActorHandler() converts it into a Durable Object class that must be exported.
68
70
  export const ChatRoom = createActorHandler(chatRoom);
69
71
  ```
70
72
 
71
73
  ### Wrangler Configuration
72
74
 
73
- **Critical**: Your Durable Object export names in `src/index.ts` **must match** the `class_name` in `wrangler.jsonc`:
75
+ **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**.
76
+
77
+ The export name in `src/index.ts` **must match** the `class_name` in `wrangler.jsonc`:
74
78
 
75
79
  ```jsonc
76
80
  {
@@ -91,10 +95,18 @@ export const ChatRoom = createActorHandler(chatRoom);
91
95
  }
92
96
  ```
93
97
 
94
- The three-way relationship:
95
- 1. **Export** in `src/index.ts`: `export { ChatRoom }`
96
- 2. **Class name** in `wrangler.jsonc`: `"class_name": "ChatRoom"`
97
- 3. **Env binding**: Access via `env.ChatRoom` in your fetch handler
98
+ **Three-way relationship** - these must all align:
99
+
100
+ 1. **Room definition**: `defineRoom({ name: "ChatRoom" })` - The `name` property is optional but recommended for consistency
101
+ 2. **Export** in `src/index.ts`: `export const ChatRoom = createActorHandler(chatRoom)` - The export name becomes the class name
102
+ 3. **Class name** in `wrangler.jsonc`: `"class_name": "ChatRoom"` - Must match the export name exactly
103
+
104
+ **Important Notes:**
105
+ - `defineRoom()` returns a room definition object, **not** a Durable Object class
106
+ - `createActorHandler(room)` creates the actual Durable Object class
107
+ - Each room definition must be converted to a class with `createActorHandler()` and exported
108
+ - For multiple rooms, you need multiple exports and multiple bindings in `wrangler.jsonc`
109
+ - The export name (e.g., `ChatRoom`) becomes the class name and must match `class_name` in configuration
98
110
 
99
111
  ### Client Side
100
112
 
@@ -181,6 +193,9 @@ Verani handles Cloudflare's hibernation automatically:
181
193
  - **[Concepts](./docs/concepts/)** - Architecture, hibernation, and core concepts
182
194
  - **[Security](./docs/security/)** - Authentication, authorization, and best practices
183
195
 
196
+ ## More Examples
197
+ **[Vchats](https://github.com/v0id-user/vchats)** - A simple chat application built with Verani
198
+
184
199
  ## Features
185
200
 
186
201
  ### Server (Actor) Side
@@ -1,8 +1,104 @@
1
- import { n as ConnectionMeta, r as MessageFrame } from "./types-BefWc1M_.cjs";
1
+ import { n as ConnectionMeta, r as MessageFrame } from "./types-DOI6EHQJ.cjs";
2
2
  import { Actor, ActorConfiguration } from "@cloudflare/actors";
3
3
 
4
- //#region src/actor/types.d.ts
4
+ //#region src/actor/persist.d.ts
5
5
 
6
+ declare const PERSIST_ERROR_HANDLER: unique symbol;
7
+ declare const PERSISTED_STATE: unique symbol;
8
+ declare const STATE_READY: unique symbol;
9
+ /**
10
+ * Error thrown when persisted state is accessed before initialization
11
+ */
12
+ declare class PersistNotReadyError extends Error {
13
+ constructor(key: string);
14
+ }
15
+ /**
16
+ * Error thrown when persistence operation fails
17
+ */
18
+ declare class PersistError extends Error {
19
+ readonly originalCause: Error;
20
+ constructor(key: string, cause: Error);
21
+ }
22
+ /**
23
+ * Options for SafePersist behavior
24
+ */
25
+ interface SafePersistOptions {
26
+ /**
27
+ * If true, only track top-level property changes (no deep proxy).
28
+ * This is safer and more predictable. Default: true
29
+ */
30
+ shallow?: boolean;
31
+ /**
32
+ * If true, throw errors instead of swallowing them. Default: true
33
+ */
34
+ throwOnError?: boolean;
35
+ }
36
+ /**
37
+ * Safely serialize a value for storage.
38
+ * Handles circular references and special types gracefully.
39
+ */
40
+ declare function safeSerialize(value: unknown): string;
41
+ /**
42
+ * Safely deserialize a value from storage.
43
+ * Restores special types that were serialized.
44
+ */
45
+ declare function safeDeserialize(json: string): unknown;
46
+ /**
47
+ * Interface for actors that support safe persistence
48
+ */
49
+ interface PersistableActor {
50
+ /** Durable Object storage interface */
51
+ ctx: {
52
+ storage: DurableObjectStorage;
53
+ };
54
+ /** Whether the state has been initialized from storage */
55
+ [STATE_READY]?: boolean;
56
+ /** The persisted state object */
57
+ [PERSISTED_STATE]?: Record<string, unknown>;
58
+ /** Error handler for persistence failures */
59
+ [PERSIST_ERROR_HANDLER]?: (key: string, error: Error) => void;
60
+ }
61
+ /**
62
+ * Initialize persisted state from Durable Object storage.
63
+ * Call this in the actor's onInit or constructor after storage is available.
64
+ *
65
+ * @param actor - The actor instance
66
+ * @param initialState - Default state values
67
+ * @param persistedKeys - Keys to persist (empty = all keys)
68
+ * @param options - Persistence options
69
+ */
70
+ declare function initializePersistedState<T extends Record<string, unknown>>(actor: PersistableActor, initialState: T, persistedKeys?: (keyof T)[], options?: SafePersistOptions): Promise<T>;
71
+ /**
72
+ * Helper to check if state is ready for access
73
+ */
74
+ declare function isStateReady(actor: PersistableActor): boolean;
75
+ /**
76
+ * Helper to get persisted state with type safety.
77
+ * Throws if state is not yet initialized.
78
+ */
79
+ declare function getPersistedState<T extends Record<string, unknown>>(actor: PersistableActor): T;
80
+ /**
81
+ * Set the error handler for persistence failures
82
+ */
83
+ declare function setPeristErrorHandler(actor: PersistableActor, handler: (key: string, error: Error) => void): void;
84
+ /**
85
+ * Manually persist a specific key (useful for batch updates)
86
+ */
87
+ declare function persistKey(actor: PersistableActor, key: string, value: unknown): Promise<void>;
88
+ /**
89
+ * Manually delete a persisted key
90
+ */
91
+ declare function deletePersistedKey(actor: PersistableActor, key: string): Promise<void>;
92
+ /**
93
+ * Get all persisted keys from storage
94
+ */
95
+ declare function getPersistedKeys(actor: PersistableActor): Promise<string[]>;
96
+ /**
97
+ * Clear all persisted state
98
+ */
99
+ declare function clearPersistedState(actor: PersistableActor): Promise<void>;
100
+ //#endregion
101
+ //#region src/actor/types.d.ts
6
102
  /**
7
103
  * Options for broadcasting messages to connections
8
104
  */
@@ -101,7 +197,7 @@ interface ActorStub {
101
197
  /**
102
198
  * Extended Actor interface with Verani-specific methods
103
199
  */
104
- interface VeraniActor<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> extends Actor<E> {
200
+ interface VeraniActor<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown, TState extends Record<string, unknown> = Record<string, unknown>> extends Actor<E> {
105
201
  /**
106
202
  * Map of active WebSocket sessions keyed by their WebSocket instance.
107
203
  * Each entry contains the WebSocket and its associated metadata.
@@ -111,6 +207,17 @@ interface VeraniActor<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown
111
207
  ws: WebSocket;
112
208
  meta: TMeta;
113
209
  }>;
210
+ /**
211
+ * User-defined persisted state for this actor.
212
+ * Access this after onInit completes. Changes to tracked keys are automatically persisted.
213
+ * @see RoomDefinition.state and RoomDefinition.persistedKeys
214
+ */
215
+ roomState: TState;
216
+ /**
217
+ * Check if the persisted state has been initialized.
218
+ * Returns true after onInit completes and state is loaded from storage.
219
+ */
220
+ isStateReady(): boolean;
114
221
  /**
115
222
  * Broadcast a message to all connections in a channel.
116
223
  * Performs channel, userId, clientId, and exclusion filtering.
@@ -161,37 +268,37 @@ interface VeraniActor<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown
161
268
  /**
162
269
  * Event handler function type for socket.io-like event handling
163
270
  */
164
- type EventHandler<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> = (ctx: MessageContext<TMeta, E>, data: any) => void | Promise<void>;
271
+ type EventHandler<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown, TState extends Record<string, unknown> = Record<string, unknown>> = (ctx: MessageContext<TMeta, E, TState>, data: any) => void | Promise<void>;
165
272
  /**
166
273
  * Event emitter interface for room-level event handling
167
274
  */
168
- interface RoomEventEmitter<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> {
275
+ interface RoomEventEmitter<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown, TState extends Record<string, unknown> = Record<string, unknown>> {
169
276
  /**
170
277
  * Register an event handler
171
278
  * @param event - Event name (supports wildcard "*")
172
279
  * @param handler - Handler function
173
280
  */
174
- on(event: string, handler: EventHandler<TMeta, E>): void;
281
+ on(event: string, handler: EventHandler<TMeta, E, TState>): void;
175
282
  /**
176
283
  * Remove an event handler
177
284
  * @param event - Event name
178
285
  * @param handler - Optional specific handler to remove, or remove all handlers for event
179
286
  */
180
- off(event: string, handler?: EventHandler<TMeta, E>): void;
287
+ off(event: string, handler?: EventHandler<TMeta, E, TState>): void;
181
288
  /**
182
289
  * Emit an event to registered handlers
183
290
  * @param event - Event name
184
291
  * @param ctx - Message context
185
292
  * @param data - Event data
186
293
  */
187
- emit(event: string, ctx: MessageContext<TMeta, E>, data: any): Promise<void>;
294
+ emit(event: string, ctx: MessageContext<TMeta, E, TState>, data: any): Promise<void>;
188
295
  /**
189
296
  * Rebuild handlers from static storage.
190
297
  * Called after hibernation to restore handlers from the room definition.
191
298
  * This method MUST be implemented by all event emitter implementations.
192
299
  * @param staticHandlers - Map of event names to handler sets from static storage
193
300
  */
194
- rebuildHandlers(staticHandlers: Map<string, Set<EventHandler<TMeta, E>>>): void;
301
+ rebuildHandlers(staticHandlers: Map<string, Set<EventHandler<TMeta, E, TState>>>): void;
195
302
  }
196
303
  /**
197
304
  * Builder interface for targeting specific scopes when emitting
@@ -245,9 +352,9 @@ interface ActorEmit<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown>
245
352
  /**
246
353
  * Context provided to room lifecycle hooks
247
354
  */
248
- interface RoomContext<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> {
355
+ interface RoomContext<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown, TState extends Record<string, unknown> = Record<string, unknown>> {
249
356
  /** The actor instance handling this connection */
250
- actor: VeraniActor<TMeta, E>;
357
+ actor: VeraniActor<TMeta, E, TState>;
251
358
  /** The WebSocket connection */
252
359
  ws: WebSocket;
253
360
  /** Connection metadata */
@@ -258,7 +365,7 @@ interface RoomContext<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown
258
365
  /**
259
366
  * Context for onMessage hook with frame included
260
367
  */
261
- interface MessageContext<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> extends RoomContext<TMeta, E> {
368
+ interface MessageContext<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown, TState extends Record<string, unknown> = Record<string, unknown>> extends RoomContext<TMeta, E, TState> {
262
369
  /** The received message frame */
263
370
  frame: MessageFrame;
264
371
  }
@@ -268,8 +375,12 @@ interface MessageContext<TMeta extends ConnectionMeta = ConnectionMeta, E = unkn
268
375
  * **Important:** All lifecycle hooks are properly awaited if they return a Promise.
269
376
  * This ensures async operations complete before the actor proceeds to the next step
270
377
  * or potentially enters hibernation.
378
+ *
379
+ * @template TMeta - Connection metadata type
380
+ * @template E - Environment type
381
+ * @template TState - Room state type (for persistence)
271
382
  */
272
- interface RoomDefinition<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown> {
383
+ interface RoomDefinition<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown, TState extends Record<string, unknown> = Record<string, unknown>> {
273
384
  /** Optional room name for debugging */
274
385
  name?: string;
275
386
  /** WebSocket upgrade path (default: "/ws") */
@@ -285,13 +396,13 @@ interface RoomDefinition<TMeta extends ConnectionMeta = ConnectionMeta, E = unkn
285
396
  * sessions map after this hook completes successfully. If this hook throws, the
286
397
  * connection is closed and no orphaned session is created.
287
398
  */
288
- onConnect?(ctx: RoomContext<TMeta, E>): void | Promise<void>;
399
+ onConnect?(ctx: RoomContext<TMeta, E, TState>): void | Promise<void>;
289
400
  /**
290
401
  * Called when a WebSocket connection is closed.
291
402
  * This hook is awaited if it returns a Promise. The session is removed from the
292
403
  * sessions map before this hook is called.
293
404
  */
294
- onDisconnect?(ctx: RoomContext<TMeta, E>): void | Promise<void>;
405
+ onDisconnect?(ctx: RoomContext<TMeta, E, TState>): void | Promise<void>;
295
406
  /**
296
407
  * Called when a message is received from a connection.
297
408
  * This hook is awaited if it returns a Promise. The actor will not process
@@ -300,30 +411,71 @@ interface RoomDefinition<TMeta extends ConnectionMeta = ConnectionMeta, E = unkn
300
411
  * **Note:** If event handlers are registered via `eventEmitter`, they take priority.
301
412
  * This hook is used as a fallback when no matching event handler is found.
302
413
  */
303
- onMessage?(ctx: MessageContext<TMeta, E>, frame: MessageFrame): void | Promise<void>;
414
+ onMessage?(ctx: MessageContext<TMeta, E, TState>, frame: MessageFrame): void | Promise<void>;
304
415
  /**
305
416
  * Called when an error occurs in a lifecycle hook.
306
417
  * This hook is also awaited if it returns a Promise.
307
418
  */
308
- onError?(error: Error, ctx: RoomContext<TMeta, E>): void | Promise<void>;
419
+ onError?(error: Error, ctx: RoomContext<TMeta, E, TState>): void | Promise<void>;
309
420
  /**
310
421
  * Called after actor wakes from hibernation and sessions are restored.
311
422
  * This hook is awaited if it returns a Promise. It is called even if some
312
423
  * sessions failed to restore, allowing you to handle partial restoration scenarios.
313
424
  */
314
- onHibernationRestore?(actor: VeraniActor<TMeta, E>): void | Promise<void>;
425
+ onHibernationRestore?(actor: VeraniActor<TMeta, E, TState>): void | Promise<void>;
315
426
  /**
316
427
  * Event emitter for socket.io-like event handling.
317
428
  * If provided, event handlers registered here will be called for matching message types.
318
429
  * If not provided, a default event emitter will be created.
319
430
  */
320
- eventEmitter?: RoomEventEmitter<TMeta, E>;
431
+ eventEmitter?: RoomEventEmitter<TMeta, E, TState>;
321
432
  /**
322
433
  * Static handler storage that persists across hibernation.
323
434
  * Handlers registered via room.on() are stored here and rebuilt in onInit.
324
435
  * @internal
325
436
  */
326
- _staticHandlers?: Map<string, Set<EventHandler<TMeta, E>>>;
437
+ _staticHandlers?: Map<string, Set<EventHandler<TMeta, E, TState>>>;
438
+ /**
439
+ * Initial state for this room. This object defines the default values
440
+ * for your room's state. Access via `actor.roomState` in lifecycle hooks.
441
+ *
442
+ * @example
443
+ * ```typescript
444
+ * const room = defineRoom({
445
+ * state: {
446
+ * messageCount: 0,
447
+ * lastActivity: null as Date | null,
448
+ * settings: { maxUsers: 100 }
449
+ * },
450
+ * persistedKeys: ['messageCount', 'settings'],
451
+ * // ...
452
+ * });
453
+ * ```
454
+ */
455
+ state?: TState;
456
+ /**
457
+ * Keys from `state` to persist to Durable Object storage.
458
+ * If empty or undefined, no state is persisted.
459
+ * Changes to these keys are automatically saved and restored on hibernation wake.
460
+ *
461
+ * @example
462
+ * ```typescript
463
+ * persistedKeys: ['messageCount', 'settings'] // Only these keys are persisted
464
+ * ```
465
+ */
466
+ persistedKeys?: (string & keyof TState)[];
467
+ /**
468
+ * Options for state persistence behavior.
469
+ */
470
+ persistOptions?: SafePersistOptions;
471
+ /**
472
+ * Called when persistence fails for a key.
473
+ * Use this to handle errors gracefully (e.g., notify admins, fallback behavior).
474
+ *
475
+ * @param key - The state key that failed to persist
476
+ * @param error - The error that occurred
477
+ */
478
+ onPersistError?(key: string, error: Error): void;
327
479
  }
328
480
  //#endregion
329
481
  //#region src/actor/actor-runtime.d.ts
@@ -340,6 +492,6 @@ type ActorHandlerClass<E = unknown> = {
340
492
  * @param room - The room definition with lifecycle hooks
341
493
  * @returns Actor class for Cloudflare Workers (extends DurableObject)
342
494
  */
343
- declare function createActorHandler<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown>(room: RoomDefinition<TMeta, E>): ActorHandlerClass<E>;
495
+ declare function createActorHandler<TMeta extends ConnectionMeta = ConnectionMeta, E = unknown, TState extends Record<string, unknown> = Record<string, unknown>>(room: RoomDefinition<TMeta, E, TState>): ActorHandlerClass<E>;
344
496
  //#endregion
345
- export { EventHandler as a, RoomDefinition as c, BroadcastOptions as i, RpcBroadcastOptions as l, createActorHandler as n, MessageContext as o, ActorStub as r, RoomContext as s, ActorHandlerClass as t, VeraniActor as u };
497
+ export { safeSerialize as C, safeDeserialize as S, getPersistedKeys as _, EventHandler as a, isStateReady as b, RoomDefinition as c, PersistError as d, PersistNotReadyError as f, deletePersistedKey as g, clearPersistedState as h, BroadcastOptions as i, RpcBroadcastOptions as l, SafePersistOptions as m, createActorHandler as n, MessageContext as o, PersistableActor as p, ActorStub as r, RoomContext as s, ActorHandlerClass as t, VeraniActor as u, getPersistedState as v, setPeristErrorHandler as w, persistKey as x, initializePersistedState as y };