@fluxstack/live 0.1.0 → 0.3.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 +145 -0
- package/dist/index.d.ts +608 -151
- package/dist/index.js +2373 -880
- package/dist/index.js.map +1 -1
- package/package.json +11 -4
package/dist/index.d.ts
CHANGED
|
@@ -144,6 +144,10 @@ interface LiveWSData {
|
|
|
144
144
|
userId?: string;
|
|
145
145
|
/** Auth context for the connection */
|
|
146
146
|
authContext?: LiveAuthContext;
|
|
147
|
+
/** Rooms joined by this connection (for per-connection limits) */
|
|
148
|
+
rooms?: Set<string>;
|
|
149
|
+
/** Origin header from the WebSocket upgrade request (set by transport adapter) */
|
|
150
|
+
origin?: string;
|
|
147
151
|
}
|
|
148
152
|
/** @deprecated Use GenericWebSocket instead */
|
|
149
153
|
type FluxStackWebSocket = GenericWebSocket;
|
|
@@ -233,6 +237,8 @@ declare function createTypedRoomEventBus<TRoomEvents extends Record<string, Reco
|
|
|
233
237
|
};
|
|
234
238
|
declare class RoomEventBus {
|
|
235
239
|
private subscriptions;
|
|
240
|
+
/** Reverse index: componentId -> Set of subscription keys for O(1) unsubscribeAll */
|
|
241
|
+
private componentIndex;
|
|
236
242
|
private getKey;
|
|
237
243
|
on(roomType: string, roomId: string, event: string, componentId: string, handler: EventHandler): () => void;
|
|
238
244
|
emit(roomType: string, roomId: string, event: string, data: any, excludeComponentId?: string): number;
|
|
@@ -246,31 +252,319 @@ declare class RoomEventBus {
|
|
|
246
252
|
};
|
|
247
253
|
}
|
|
248
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Adapter for room state storage.
|
|
257
|
+
*
|
|
258
|
+
* All methods return Promises to support async backends (Redis, DB, etc.).
|
|
259
|
+
* The InMemoryRoomAdapter resolves immediately for zero-overhead in single-instance mode.
|
|
260
|
+
*/
|
|
261
|
+
interface IRoomStorageAdapter {
|
|
262
|
+
/** Create or get a room. Returns current state and whether it was newly created. */
|
|
263
|
+
getOrCreateRoom(roomId: string, initialState?: any): Promise<{
|
|
264
|
+
state: any;
|
|
265
|
+
created: boolean;
|
|
266
|
+
}>;
|
|
267
|
+
/** Get the state of a room. Returns empty object if room doesn't exist. */
|
|
268
|
+
getState(roomId: string): Promise<any>;
|
|
269
|
+
/** Update room state (merge partial updates). */
|
|
270
|
+
updateState(roomId: string, updates: any): Promise<void>;
|
|
271
|
+
/** Check if a room exists. */
|
|
272
|
+
hasRoom(roomId: string): Promise<boolean>;
|
|
273
|
+
/** Delete a room. Returns true if the room existed. */
|
|
274
|
+
deleteRoom(roomId: string): Promise<boolean>;
|
|
275
|
+
/** Get storage statistics. */
|
|
276
|
+
getStats(): Promise<{
|
|
277
|
+
totalRooms: number;
|
|
278
|
+
rooms: Record<string, any>;
|
|
279
|
+
}>;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Adapter for cross-instance pub/sub (horizontal scaling).
|
|
283
|
+
*
|
|
284
|
+
* When running multiple server instances, this adapter propagates room events
|
|
285
|
+
* between instances. In single-instance mode (InMemoryRoomAdapter), all pub/sub
|
|
286
|
+
* operations are no-ops since events are already local.
|
|
287
|
+
*/
|
|
288
|
+
interface IRoomPubSubAdapter {
|
|
289
|
+
/** Publish an event to all server instances subscribed to this room. */
|
|
290
|
+
publish(roomId: string, event: string, data: any): Promise<void>;
|
|
291
|
+
/** Subscribe to events for a room (from other server instances). */
|
|
292
|
+
subscribe(roomId: string, handler: (event: string, data: any) => void): Promise<() => void>;
|
|
293
|
+
/** Publish a membership change (join/leave) to other instances. */
|
|
294
|
+
publishMembership(roomId: string, action: 'join' | 'leave', componentId: string): Promise<void>;
|
|
295
|
+
/** Publish a state change to other instances. */
|
|
296
|
+
publishStateChange(roomId: string, updates: any): Promise<void>;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
interface LiveRoomManagerInterface {
|
|
300
|
+
joinRoom<TState = any>(componentId: string, roomId: string, ws: any, initialState?: TState, options?: {
|
|
301
|
+
deepDiff?: boolean;
|
|
302
|
+
deepDiffDepth?: number;
|
|
303
|
+
serverOnlyState?: boolean;
|
|
304
|
+
}, joinContext?: {
|
|
305
|
+
userId?: string;
|
|
306
|
+
payload?: any;
|
|
307
|
+
}): {
|
|
308
|
+
state: TState;
|
|
309
|
+
rejected?: false;
|
|
310
|
+
} | {
|
|
311
|
+
rejected: true;
|
|
312
|
+
reason: string;
|
|
313
|
+
};
|
|
314
|
+
leaveRoom(componentId: string, roomId: string, leaveReason?: 'leave' | 'disconnect' | 'cleanup'): void;
|
|
315
|
+
cleanupComponent(componentId: string): void;
|
|
316
|
+
emitToRoom(roomId: string, event: string, data: any, excludeComponentId?: string): number;
|
|
317
|
+
setRoomState(roomId: string, updates: any, excludeComponentId?: string): void;
|
|
318
|
+
getRoomState<TState = any>(roomId: string): TState;
|
|
319
|
+
isInRoom(componentId: string, roomId: string): boolean;
|
|
320
|
+
getComponentRooms(componentId: string): string[];
|
|
321
|
+
getMemberCount?(roomId: string): number;
|
|
322
|
+
getRoomInstance?(roomId: string): LiveRoom<any, any, any> | undefined;
|
|
323
|
+
getStats(): any;
|
|
324
|
+
}
|
|
325
|
+
interface LiveComponentContext {
|
|
326
|
+
roomEvents: RoomEventBus;
|
|
327
|
+
roomManager: LiveRoomManagerInterface;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Set the global Live Component context.
|
|
331
|
+
* Called once by LiveServer.start() before any components are mounted.
|
|
332
|
+
*/
|
|
333
|
+
declare function setLiveComponentContext(ctx: LiveComponentContext): void;
|
|
334
|
+
/**
|
|
335
|
+
* Get the global Live Component context.
|
|
336
|
+
* Throws if LiveServer.start() hasn't been called yet.
|
|
337
|
+
*/
|
|
338
|
+
declare function getLiveComponentContext(): LiveComponentContext;
|
|
339
|
+
|
|
340
|
+
declare const BINARY_ROOM_EVENT = 2;
|
|
341
|
+
declare const BINARY_ROOM_STATE = 3;
|
|
342
|
+
interface RoomCodec {
|
|
343
|
+
encode(data: unknown): Uint8Array;
|
|
344
|
+
decode(buf: Uint8Array): unknown;
|
|
345
|
+
}
|
|
346
|
+
declare const msgpackCodec: RoomCodec;
|
|
347
|
+
declare const jsonCodec: RoomCodec;
|
|
348
|
+
type RoomCodecOption = 'msgpack' | 'json' | RoomCodec;
|
|
349
|
+
declare function resolveCodec(option?: RoomCodecOption): RoomCodec;
|
|
350
|
+
/**
|
|
351
|
+
* Build a binary room frame.
|
|
352
|
+
* Format: [frameType:u8][compIdLen:u8][compId:utf8][roomIdLen:u8][roomId:utf8][eventLen:u16BE][event:utf8][payload]
|
|
353
|
+
*/
|
|
354
|
+
declare function buildRoomFrame(frameType: number, componentId: string, roomId: string, event: string, payload: Uint8Array): Uint8Array;
|
|
355
|
+
/**
|
|
356
|
+
* Build frame tail (roomId + event + payload) that is shared across all members.
|
|
357
|
+
* Per-member: prepend [frameType:u8][compIdLen:u8][compId:utf8].
|
|
358
|
+
*/
|
|
359
|
+
declare function buildRoomFrameTail(roomId: string, event: string, payload: Uint8Array): Uint8Array;
|
|
360
|
+
/**
|
|
361
|
+
* Prepend per-member header to a shared tail.
|
|
362
|
+
* Header: [frameType:u8][compIdLen:u8][compId:utf8]
|
|
363
|
+
*/
|
|
364
|
+
declare function prependMemberHeader(frameType: number, componentId: string, tail: Uint8Array): Uint8Array;
|
|
365
|
+
/**
|
|
366
|
+
* Parse a binary room frame header (client-side).
|
|
367
|
+
* Returns parsed fields and the payload offset.
|
|
368
|
+
*/
|
|
369
|
+
declare function parseRoomFrame(buf: Uint8Array): {
|
|
370
|
+
frameType: number;
|
|
371
|
+
componentId: string;
|
|
372
|
+
roomId: string;
|
|
373
|
+
event: string;
|
|
374
|
+
payload: Uint8Array;
|
|
375
|
+
} | null;
|
|
376
|
+
|
|
377
|
+
interface RoomJoinContext {
|
|
378
|
+
componentId: string;
|
|
379
|
+
userId?: string;
|
|
380
|
+
payload?: any;
|
|
381
|
+
}
|
|
382
|
+
interface RoomLeaveContext {
|
|
383
|
+
componentId: string;
|
|
384
|
+
userId?: string;
|
|
385
|
+
reason: 'leave' | 'disconnect' | 'cleanup';
|
|
386
|
+
}
|
|
387
|
+
interface RoomEventContext {
|
|
388
|
+
componentId: string;
|
|
389
|
+
userId?: string;
|
|
390
|
+
}
|
|
391
|
+
interface LiveRoomOptions {
|
|
392
|
+
/** Enable deep diff for room state. Default: true */
|
|
393
|
+
deepDiff?: boolean;
|
|
394
|
+
/** Max recursion depth for deep diff. Default: 3 */
|
|
395
|
+
deepDiffDepth?: number;
|
|
396
|
+
/** Max number of members allowed. Undefined = unlimited */
|
|
397
|
+
maxMembers?: number;
|
|
398
|
+
/**
|
|
399
|
+
* Wire codec for room messages. Default: 'msgpack' (binary).
|
|
400
|
+
* - 'msgpack' — Built-in MessagePack encoder (zero deps, ~30% smaller, ~2-3x faster)
|
|
401
|
+
* - 'json' — Standard JSON (text-based, larger but human-readable)
|
|
402
|
+
* - Custom RoomCodec object with encode/decode methods
|
|
403
|
+
*/
|
|
404
|
+
codec?: RoomCodecOption;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Base class for typed rooms with lifecycle hooks and public/private state.
|
|
408
|
+
*
|
|
409
|
+
* @typeParam TState - Public state synced to all connected clients
|
|
410
|
+
* @typeParam TMeta - Private server-only metadata (never broadcasted)
|
|
411
|
+
* @typeParam TEvents - Event map for typed emit/on
|
|
412
|
+
*/
|
|
413
|
+
declare abstract class LiveRoom<TState extends Record<string, any> = Record<string, any>, TMeta extends Record<string, any> = Record<string, any>, TEvents extends Record<string, any> = Record<string, any>> {
|
|
414
|
+
/** Unique room type name. Used as prefix in compound room IDs (e.g. "chat:lobby"). */
|
|
415
|
+
static roomName: string;
|
|
416
|
+
/** Initial public state template. Cloned per room instance. */
|
|
417
|
+
static defaultState: Record<string, any>;
|
|
418
|
+
/** Initial private metadata template. Cloned per room instance. */
|
|
419
|
+
static defaultMeta: Record<string, any>;
|
|
420
|
+
/** Room-level options */
|
|
421
|
+
static $options?: LiveRoomOptions;
|
|
422
|
+
/** The unique room instance identifier (e.g. "chat:lobby") */
|
|
423
|
+
readonly id: string;
|
|
424
|
+
/** Public state — synced to all connected clients via setState(). */
|
|
425
|
+
state: TState;
|
|
426
|
+
/** Private metadata — NEVER leaves the server. Mutate directly. */
|
|
427
|
+
meta: TMeta;
|
|
428
|
+
/** @internal Type brand for client-side event inference.
|
|
429
|
+
* No runtime value. Usage: `$room<CounterRoom>('id').on(...)` */
|
|
430
|
+
readonly $events: TEvents;
|
|
431
|
+
/** @internal Reference to the room manager for broadcasting */
|
|
432
|
+
protected readonly _manager: LiveRoomManagerInterface;
|
|
433
|
+
constructor(id: string, manager: LiveRoomManagerInterface);
|
|
434
|
+
/**
|
|
435
|
+
* Update public state and broadcast changes to all room members.
|
|
436
|
+
* Uses deep diff by default — only changed fields are sent over the wire.
|
|
437
|
+
*/
|
|
438
|
+
setState(updates: Partial<TState>): void;
|
|
439
|
+
/**
|
|
440
|
+
* Emit a typed event to all members in this room.
|
|
441
|
+
* @returns Number of members notified
|
|
442
|
+
*/
|
|
443
|
+
emit<K extends keyof TEvents & string>(event: K, data: TEvents[K]): number;
|
|
444
|
+
/** Get current member count */
|
|
445
|
+
get memberCount(): number;
|
|
446
|
+
/**
|
|
447
|
+
* Called when a component attempts to join this room.
|
|
448
|
+
* Return false to reject the join.
|
|
449
|
+
*/
|
|
450
|
+
onJoin(_ctx: RoomJoinContext): void | false | Promise<void | false>;
|
|
451
|
+
/**
|
|
452
|
+
* Called after a component leaves this room.
|
|
453
|
+
*/
|
|
454
|
+
onLeave(_ctx: RoomLeaveContext): void | Promise<void>;
|
|
455
|
+
/**
|
|
456
|
+
* Called when an event is emitted to this room.
|
|
457
|
+
* Can intercept/validate events before broadcasting.
|
|
458
|
+
*/
|
|
459
|
+
onEvent(_event: string, _data: any, _ctx: RoomEventContext): void | Promise<void>;
|
|
460
|
+
/**
|
|
461
|
+
* Called once when the room is first created (first member joins).
|
|
462
|
+
*/
|
|
463
|
+
onCreate(): void | Promise<void>;
|
|
464
|
+
/**
|
|
465
|
+
* Called when the last member leaves and the room is about to be destroyed.
|
|
466
|
+
* Return false to keep the room alive (e.g., persist state).
|
|
467
|
+
*/
|
|
468
|
+
onDestroy(): void | false | Promise<void | false>;
|
|
469
|
+
}
|
|
470
|
+
/** Extract the public state type from a LiveRoom subclass */
|
|
471
|
+
type InferRoomState<R> = R extends LiveRoom<infer S, any, any> ? S : Record<string, any>;
|
|
472
|
+
/** Extract the private meta type from a LiveRoom subclass */
|
|
473
|
+
type InferRoomMeta<R> = R extends LiveRoom<any, infer M, any> ? M : Record<string, any>;
|
|
474
|
+
/** Extract the events type from a LiveRoom subclass */
|
|
475
|
+
type InferRoomEvents<R> = R extends LiveRoom<any, any, infer E> ? E : Record<string, any>;
|
|
476
|
+
/** LiveRoom class constructor type */
|
|
477
|
+
type LiveRoomClass<R extends LiveRoom = LiveRoom> = {
|
|
478
|
+
new (id: string, manager: LiveRoomManagerInterface): R;
|
|
479
|
+
roomName: string;
|
|
480
|
+
defaultState: Record<string, any>;
|
|
481
|
+
defaultMeta: Record<string, any>;
|
|
482
|
+
$options?: LiveRoomOptions;
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
declare class RoomRegistry {
|
|
486
|
+
private rooms;
|
|
487
|
+
/**
|
|
488
|
+
* Register a LiveRoom subclass.
|
|
489
|
+
* @throws If the class doesn't define a static roomName
|
|
490
|
+
*/
|
|
491
|
+
register(roomClass: LiveRoomClass): void;
|
|
492
|
+
/**
|
|
493
|
+
* Get a registered room class by name.
|
|
494
|
+
*/
|
|
495
|
+
get(name: string): LiveRoomClass | undefined;
|
|
496
|
+
/**
|
|
497
|
+
* Check if a room class is registered.
|
|
498
|
+
*/
|
|
499
|
+
has(name: string): boolean;
|
|
500
|
+
/**
|
|
501
|
+
* Resolve a compound room ID (e.g. "chat:lobby") to its registered class.
|
|
502
|
+
* Returns undefined if the prefix doesn't match any registered room.
|
|
503
|
+
*/
|
|
504
|
+
resolveFromId(roomId: string): LiveRoomClass | undefined;
|
|
505
|
+
/**
|
|
506
|
+
* Get all registered room names.
|
|
507
|
+
*/
|
|
508
|
+
getRegisteredNames(): string[];
|
|
509
|
+
/**
|
|
510
|
+
* Check if a value is a LiveRoom subclass.
|
|
511
|
+
*/
|
|
512
|
+
static isLiveRoomClass(cls: unknown): cls is LiveRoomClass;
|
|
513
|
+
}
|
|
514
|
+
|
|
249
515
|
declare class LiveRoomManager {
|
|
250
516
|
private roomEvents;
|
|
517
|
+
private pubsub?;
|
|
251
518
|
private rooms;
|
|
252
519
|
private componentRooms;
|
|
253
|
-
|
|
520
|
+
/** Room registry for LiveRoom class lookup. Set by LiveServer. */
|
|
521
|
+
roomRegistry?: RoomRegistry;
|
|
522
|
+
/**
|
|
523
|
+
* @param roomEvents - Local server-side event bus
|
|
524
|
+
* @param pubsub - Optional cross-instance pub/sub adapter (e.g. Redis).
|
|
525
|
+
* When provided, room events/state/membership are propagated
|
|
526
|
+
* to other server instances in the background.
|
|
527
|
+
*/
|
|
528
|
+
constructor(roomEvents: RoomEventBus, pubsub?: IRoomPubSubAdapter | undefined);
|
|
254
529
|
/**
|
|
255
|
-
* Component joins a room
|
|
530
|
+
* Component joins a room.
|
|
531
|
+
* @param options.deepDiff - Enable/disable deep diff for this room's state. Default: true
|
|
532
|
+
* @param joinContext - Optional context for LiveRoom lifecycle hooks (userId, payload)
|
|
256
533
|
*/
|
|
257
|
-
joinRoom<TState = any>(componentId: string, roomId: string, ws: GenericWebSocket, initialState?: TState
|
|
534
|
+
joinRoom<TState = any>(componentId: string, roomId: string, ws: GenericWebSocket, initialState?: TState, options?: {
|
|
535
|
+
deepDiff?: boolean;
|
|
536
|
+
deepDiffDepth?: number;
|
|
537
|
+
serverOnlyState?: boolean;
|
|
538
|
+
}, joinContext?: {
|
|
539
|
+
userId?: string;
|
|
540
|
+
payload?: any;
|
|
541
|
+
}): {
|
|
258
542
|
state: TState;
|
|
543
|
+
rejected?: false;
|
|
544
|
+
} | {
|
|
545
|
+
rejected: true;
|
|
546
|
+
reason: string;
|
|
259
547
|
};
|
|
260
548
|
/**
|
|
261
549
|
* Component leaves a room
|
|
550
|
+
* @param leaveReason - Why the component is leaving. Default: 'leave'
|
|
262
551
|
*/
|
|
263
|
-
leaveRoom(componentId: string, roomId: string): void;
|
|
552
|
+
leaveRoom(componentId: string, roomId: string, leaveReason?: 'leave' | 'disconnect' | 'cleanup'): void;
|
|
264
553
|
/**
|
|
265
|
-
* Component disconnects - leave all rooms
|
|
554
|
+
* Component disconnects - leave all rooms.
|
|
555
|
+
* Batches removals: calls onLeave hooks, removes member from all rooms,
|
|
556
|
+
* then sends leave notifications in bulk.
|
|
266
557
|
*/
|
|
267
558
|
cleanupComponent(componentId: string): void;
|
|
268
559
|
/**
|
|
269
|
-
* Emit event to all members in a room
|
|
560
|
+
* Emit event to all members in a room.
|
|
561
|
+
* For LiveRoom-backed rooms, calls onEvent() hook before broadcasting.
|
|
270
562
|
*/
|
|
271
563
|
emitToRoom(roomId: string, event: string, data: any, excludeComponentId?: string): number;
|
|
272
564
|
/**
|
|
273
|
-
* Update room state
|
|
565
|
+
* Update room state.
|
|
566
|
+
* When deepDiff is enabled (default), deep-diffs plain objects to send only changed fields.
|
|
567
|
+
* When disabled, uses shallow diff (reference equality) like classic behavior.
|
|
274
568
|
*/
|
|
275
569
|
setRoomState(roomId: string, updates: any, excludeComponentId?: string): void;
|
|
276
570
|
/**
|
|
@@ -278,17 +572,36 @@ declare class LiveRoomManager {
|
|
|
278
572
|
*/
|
|
279
573
|
getRoomState<TState = any>(roomId: string): TState;
|
|
280
574
|
/**
|
|
281
|
-
* Broadcast to all members in a room
|
|
575
|
+
* Broadcast to all members in a room.
|
|
576
|
+
*
|
|
577
|
+
* When the room has a binary codec (LiveRoom-backed), builds a binary frame
|
|
578
|
+
* once (encode payload + frame tail), then prepends per-member componentId header.
|
|
579
|
+
*
|
|
580
|
+
* When no codec (legacy rooms), uses JSON with serialize-once optimization:
|
|
581
|
+
* builds the JSON string template once, then inserts each member's componentId.
|
|
282
582
|
*/
|
|
283
583
|
private broadcastToRoom;
|
|
284
584
|
/**
|
|
285
585
|
* Check if component is in a room
|
|
286
586
|
*/
|
|
287
587
|
isInRoom(componentId: string, roomId: string): boolean;
|
|
588
|
+
/**
|
|
589
|
+
* Check if room state is server-only (no client writes)
|
|
590
|
+
*/
|
|
591
|
+
isServerOnlyState(roomId: string): boolean;
|
|
288
592
|
/**
|
|
289
593
|
* Get rooms for a component
|
|
290
594
|
*/
|
|
291
595
|
getComponentRooms(componentId: string): string[];
|
|
596
|
+
/**
|
|
597
|
+
* Get member count for a room
|
|
598
|
+
*/
|
|
599
|
+
getMemberCount(roomId: string): number;
|
|
600
|
+
/**
|
|
601
|
+
* Get the LiveRoom instance for a room (if backed by a typed room class).
|
|
602
|
+
* Used by ComponentRoomProxy to expose custom methods.
|
|
603
|
+
*/
|
|
604
|
+
getRoomInstance(roomId: string): LiveRoom<any, any, any> | undefined;
|
|
292
605
|
/**
|
|
293
606
|
* Get statistics
|
|
294
607
|
*/
|
|
@@ -302,80 +615,6 @@ declare class LiveRoomManager {
|
|
|
302
615
|
};
|
|
303
616
|
}
|
|
304
617
|
|
|
305
|
-
type DebugEventType = 'COMPONENT_MOUNT' | 'COMPONENT_UNMOUNT' | 'COMPONENT_REHYDRATE' | 'STATE_CHANGE' | 'ACTION_CALL' | 'ACTION_RESULT' | 'ACTION_ERROR' | 'ROOM_JOIN' | 'ROOM_LEAVE' | 'ROOM_EMIT' | 'ROOM_EVENT_RECEIVED' | 'WS_CONNECT' | 'WS_DISCONNECT' | 'ERROR' | 'LOG';
|
|
306
|
-
interface DebugEvent {
|
|
307
|
-
id: string;
|
|
308
|
-
timestamp: number;
|
|
309
|
-
type: DebugEventType;
|
|
310
|
-
componentId: string | null;
|
|
311
|
-
componentName: string | null;
|
|
312
|
-
data: Record<string, unknown>;
|
|
313
|
-
}
|
|
314
|
-
interface ComponentSnapshot {
|
|
315
|
-
componentId: string;
|
|
316
|
-
componentName: string;
|
|
317
|
-
/** Developer-defined label for easier identification in the debugger */
|
|
318
|
-
debugLabel?: string;
|
|
319
|
-
state: Record<string, unknown>;
|
|
320
|
-
rooms: string[];
|
|
321
|
-
mountedAt: number;
|
|
322
|
-
lastActivity: number;
|
|
323
|
-
actionCount: number;
|
|
324
|
-
stateChangeCount: number;
|
|
325
|
-
errorCount: number;
|
|
326
|
-
}
|
|
327
|
-
interface DebugSnapshot {
|
|
328
|
-
components: ComponentSnapshot[];
|
|
329
|
-
connections: number;
|
|
330
|
-
uptime: number;
|
|
331
|
-
totalEvents: number;
|
|
332
|
-
}
|
|
333
|
-
interface DebugWsMessage {
|
|
334
|
-
type: 'DEBUG_EVENT' | 'DEBUG_SNAPSHOT' | 'DEBUG_WELCOME' | 'DEBUG_DISABLED';
|
|
335
|
-
event?: DebugEvent;
|
|
336
|
-
snapshot?: DebugSnapshot;
|
|
337
|
-
enabled?: boolean;
|
|
338
|
-
timestamp: number;
|
|
339
|
-
}
|
|
340
|
-
declare class LiveDebugger {
|
|
341
|
-
private events;
|
|
342
|
-
private componentSnapshots;
|
|
343
|
-
private debugClients;
|
|
344
|
-
private _enabled;
|
|
345
|
-
private startTime;
|
|
346
|
-
private eventCounter;
|
|
347
|
-
constructor(enabled?: boolean);
|
|
348
|
-
get enabled(): boolean;
|
|
349
|
-
set enabled(value: boolean);
|
|
350
|
-
emit(type: DebugEventType | string, componentId: string | null, componentName: string | null, data?: Record<string, unknown>): void;
|
|
351
|
-
trackComponentMount(componentId: string, componentName: string, initialState: Record<string, unknown>, room?: string, debugLabel?: string): void;
|
|
352
|
-
trackComponentUnmount(componentId: string): void;
|
|
353
|
-
trackStateChange(componentId: string, delta: Record<string, unknown>, fullState: Record<string, unknown>, source?: 'proxy' | 'setState' | 'rehydrate'): void;
|
|
354
|
-
trackActionCall(componentId: string, action: string, payload: unknown): void;
|
|
355
|
-
trackActionResult(componentId: string, action: string, result: unknown, duration: number): void;
|
|
356
|
-
trackActionError(componentId: string, action: string, error: string, duration: number): void;
|
|
357
|
-
trackRoomJoin(componentId: string, roomId: string): void;
|
|
358
|
-
trackRoomLeave(componentId: string, roomId: string): void;
|
|
359
|
-
trackRoomEmit(componentId: string, roomId: string, event: string, data: unknown): void;
|
|
360
|
-
trackConnection(connectionId: string): void;
|
|
361
|
-
trackDisconnection(connectionId: string, componentCount: number): void;
|
|
362
|
-
trackError(componentId: string | null, error: string, context?: Record<string, unknown>): void;
|
|
363
|
-
registerDebugClient(ws: GenericWebSocket): void;
|
|
364
|
-
unregisterDebugClient(ws: GenericWebSocket): void;
|
|
365
|
-
getSnapshot(): DebugSnapshot;
|
|
366
|
-
getComponentState(componentId: string): ComponentSnapshot | null;
|
|
367
|
-
getEvents(filter?: {
|
|
368
|
-
componentId?: string;
|
|
369
|
-
type?: DebugEventType;
|
|
370
|
-
limit?: number;
|
|
371
|
-
}): DebugEvent[];
|
|
372
|
-
clearEvents(): void;
|
|
373
|
-
private broadcastEvent;
|
|
374
|
-
private sanitizeData;
|
|
375
|
-
private sanitizeState;
|
|
376
|
-
private updateSnapshot;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
618
|
declare class LiveAuthManager {
|
|
380
619
|
private providers;
|
|
381
620
|
private defaultProviderName?;
|
|
@@ -446,33 +685,49 @@ interface StateSignatureConfig {
|
|
|
446
685
|
compressionEnabled?: boolean;
|
|
447
686
|
/** Enable encryption */
|
|
448
687
|
encryptionEnabled?: boolean;
|
|
449
|
-
/** Enable anti-replay nonces */
|
|
688
|
+
/** Enable anti-replay nonces (hybrid: stateless HMAC + replay detection) */
|
|
450
689
|
nonceEnabled?: boolean;
|
|
451
|
-
/** Maximum state age in ms */
|
|
690
|
+
/** Maximum state age in ms. Default: 1800000 (30 minutes) */
|
|
452
691
|
maxStateAge?: number;
|
|
453
692
|
/** Enable state backups */
|
|
454
693
|
backupEnabled?: boolean;
|
|
455
694
|
/** Max state backups to keep */
|
|
456
695
|
maxBackups?: number;
|
|
696
|
+
/** Nonce TTL in ms. Nonces older than this are rejected. Default: 10000 (10 seconds) */
|
|
697
|
+
nonceTTL?: number;
|
|
457
698
|
}
|
|
458
699
|
declare class StateSignatureManager {
|
|
459
700
|
private secret;
|
|
460
701
|
private previousSecrets;
|
|
461
702
|
private rotationTimer?;
|
|
462
|
-
private usedNonces;
|
|
463
|
-
private nonceCleanupTimer?;
|
|
464
703
|
private stateBackups;
|
|
465
704
|
private config;
|
|
705
|
+
private encryptionSalt;
|
|
706
|
+
private cachedEncryptionKey;
|
|
707
|
+
/** Replay detection: nonce → timestamp when it was first seen. Cleaned every 60s. */
|
|
708
|
+
private usedNonces;
|
|
709
|
+
private nonceCleanupTimer?;
|
|
466
710
|
constructor(config?: StateSignatureConfig);
|
|
711
|
+
/**
|
|
712
|
+
* Generate a hybrid nonce: `timestamp:random:HMAC(timestamp:random, secret)`
|
|
713
|
+
* Self-validating via HMAC, unique via random bytes, replay-tracked via Map.
|
|
714
|
+
*/
|
|
715
|
+
private generateNonce;
|
|
716
|
+
/**
|
|
717
|
+
* Validate a hybrid nonce: check format, HMAC, and TTL.
|
|
718
|
+
*/
|
|
719
|
+
private validateNonce;
|
|
467
720
|
signState(componentId: string, state: Record<string, unknown>, version: number, options?: {
|
|
468
721
|
compress?: boolean;
|
|
469
722
|
backup?: boolean;
|
|
470
|
-
}):
|
|
471
|
-
validateState(signedState: SignedState
|
|
723
|
+
}): SignedState;
|
|
724
|
+
validateState(signedState: SignedState, options?: {
|
|
725
|
+
skipNonce?: boolean;
|
|
726
|
+
}): {
|
|
472
727
|
valid: boolean;
|
|
473
728
|
error?: string;
|
|
474
|
-
}
|
|
475
|
-
extractData(signedState: SignedState):
|
|
729
|
+
};
|
|
730
|
+
extractData(signedState: SignedState): Record<string, unknown>;
|
|
476
731
|
getBackups(componentId: string): SignedState[];
|
|
477
732
|
getLatestBackup(componentId: string): SignedState | null;
|
|
478
733
|
private backupState;
|
|
@@ -481,6 +736,7 @@ declare class StateSignatureManager {
|
|
|
481
736
|
private timingSafeEqual;
|
|
482
737
|
private deriveEncryptionKey;
|
|
483
738
|
private setupKeyRotation;
|
|
739
|
+
/** Remove nonces older than nonceTTL + 10s from the replay detection map. */
|
|
484
740
|
private cleanupNonces;
|
|
485
741
|
shutdown(): void;
|
|
486
742
|
}
|
|
@@ -847,6 +1103,8 @@ declare class WebSocketConnectionManager extends EventEmitter {
|
|
|
847
1103
|
private connections;
|
|
848
1104
|
private connectionMetrics;
|
|
849
1105
|
private connectionPools;
|
|
1106
|
+
/** Reverse index: connectionId -> Set of poolIds for O(1) cleanup */
|
|
1107
|
+
private connectionPoolIndex;
|
|
850
1108
|
private messageQueues;
|
|
851
1109
|
private healthCheckTimer?;
|
|
852
1110
|
private heartbeatTimer?;
|
|
@@ -873,45 +1131,19 @@ declare class WebSocketConnectionManager extends EventEmitter {
|
|
|
873
1131
|
shutdown(): void;
|
|
874
1132
|
}
|
|
875
1133
|
|
|
876
|
-
|
|
877
|
-
joinRoom<TState = any>(componentId: string, roomId: string, ws: any, initialState?: TState): {
|
|
878
|
-
state: TState;
|
|
879
|
-
};
|
|
880
|
-
leaveRoom(componentId: string, roomId: string): void;
|
|
881
|
-
cleanupComponent(componentId: string): void;
|
|
882
|
-
emitToRoom(roomId: string, event: string, data: any, excludeComponentId?: string): number;
|
|
883
|
-
setRoomState(roomId: string, updates: any, excludeComponentId?: string): void;
|
|
884
|
-
getRoomState<TState = any>(roomId: string): TState;
|
|
885
|
-
isInRoom(componentId: string, roomId: string): boolean;
|
|
886
|
-
getComponentRooms(componentId: string): string[];
|
|
887
|
-
getStats(): any;
|
|
888
|
-
}
|
|
889
|
-
interface LiveDebuggerInterface {
|
|
890
|
-
enabled: boolean;
|
|
891
|
-
trackStateChange(componentId: string, delta: Record<string, unknown>, fullState: Record<string, unknown>, source?: string): void;
|
|
892
|
-
trackActionCall(componentId: string, action: string, payload: unknown): void;
|
|
893
|
-
trackActionResult(componentId: string, action: string, result: unknown, duration: number): void;
|
|
894
|
-
trackActionError(componentId: string, action: string, error: string, duration: number): void;
|
|
895
|
-
trackRoomEmit(componentId: string, roomId: string, event: string, data: unknown): void;
|
|
896
|
-
}
|
|
897
|
-
interface LiveComponentContext {
|
|
898
|
-
roomEvents: RoomEventBus;
|
|
899
|
-
roomManager: LiveRoomManagerInterface;
|
|
900
|
-
debugger?: LiveDebuggerInterface;
|
|
901
|
-
}
|
|
902
|
-
/**
|
|
903
|
-
* Set the global Live Component context.
|
|
904
|
-
* Called once by LiveServer.start() before any components are mounted.
|
|
905
|
-
*/
|
|
906
|
-
declare function setLiveComponentContext(ctx: LiveComponentContext): void;
|
|
907
|
-
/**
|
|
908
|
-
* Get the global Live Component context.
|
|
909
|
-
* Throws if LiveServer.start() hasn't been called yet.
|
|
910
|
-
*/
|
|
911
|
-
declare function getLiveComponentContext(): LiveComponentContext;
|
|
912
|
-
|
|
913
|
-
/** @internal Symbol key for singleton emit override */
|
|
1134
|
+
/** Symbol key for singleton emit override */
|
|
914
1135
|
declare const EMIT_OVERRIDE_KEY: unique symbol;
|
|
1136
|
+
|
|
1137
|
+
interface ComponentOptions {
|
|
1138
|
+
/** Enable deep diff for plain objects in setState(). Default: false */
|
|
1139
|
+
deepDiff?: boolean;
|
|
1140
|
+
/** Enable deep diff for room state updates. Default: true */
|
|
1141
|
+
roomDeepDiff?: boolean;
|
|
1142
|
+
/** Max recursion depth for deep diff (component + room). Default: 3 */
|
|
1143
|
+
deepDiffDepth?: number;
|
|
1144
|
+
/** When true, room state can only be set from server-side code. Client ROOM_STATE_SET is rejected. Default: false */
|
|
1145
|
+
serverOnlyRoomState?: boolean;
|
|
1146
|
+
}
|
|
915
1147
|
declare abstract class LiveComponent<TState = ComponentState, TPrivate extends Record<string, any> = Record<string, any>> {
|
|
916
1148
|
/** Component name for registry lookup - must be defined in subclasses */
|
|
917
1149
|
static componentName: string;
|
|
@@ -933,6 +1165,35 @@ declare abstract class LiveComponent<TState = ComponentState, TPrivate extends R
|
|
|
933
1165
|
* Per-action auth configuration.
|
|
934
1166
|
*/
|
|
935
1167
|
static actionAuth?: LiveActionAuthMap;
|
|
1168
|
+
/**
|
|
1169
|
+
* Zod schemas for action payload validation.
|
|
1170
|
+
* When defined, payloads are validated before the action method is called.
|
|
1171
|
+
*
|
|
1172
|
+
* @example
|
|
1173
|
+
* static actionSchemas = {
|
|
1174
|
+
* sendMessage: z.object({ text: z.string().max(500) }),
|
|
1175
|
+
* updatePosition: z.object({ x: z.number(), y: z.number() }),
|
|
1176
|
+
* }
|
|
1177
|
+
*/
|
|
1178
|
+
static actionSchemas?: Record<string, {
|
|
1179
|
+
safeParse: (data: unknown) => {
|
|
1180
|
+
success: boolean;
|
|
1181
|
+
error?: any;
|
|
1182
|
+
data?: any;
|
|
1183
|
+
};
|
|
1184
|
+
}>;
|
|
1185
|
+
/**
|
|
1186
|
+
* Rate limit for action execution.
|
|
1187
|
+
* Prevents clients from spamming expensive operations.
|
|
1188
|
+
*
|
|
1189
|
+
* @example
|
|
1190
|
+
* static actionRateLimit = { maxCalls: 10, windowMs: 1000, perAction: true }
|
|
1191
|
+
*/
|
|
1192
|
+
static actionRateLimit?: {
|
|
1193
|
+
maxCalls: number;
|
|
1194
|
+
windowMs: number;
|
|
1195
|
+
perAction?: boolean;
|
|
1196
|
+
};
|
|
936
1197
|
/**
|
|
937
1198
|
* Data that survives HMR reloads.
|
|
938
1199
|
*/
|
|
@@ -942,8 +1203,14 @@ declare abstract class LiveComponent<TState = ComponentState, TPrivate extends R
|
|
|
942
1203
|
* All clients share the same state.
|
|
943
1204
|
*/
|
|
944
1205
|
static singleton?: boolean;
|
|
1206
|
+
/**
|
|
1207
|
+
* Component behavior options.
|
|
1208
|
+
*
|
|
1209
|
+
* @example
|
|
1210
|
+
* static $options = { deepDiff: true }
|
|
1211
|
+
*/
|
|
1212
|
+
static $options?: ComponentOptions;
|
|
945
1213
|
readonly id: string;
|
|
946
|
-
private _state;
|
|
947
1214
|
state: TState;
|
|
948
1215
|
protected ws: GenericWebSocket;
|
|
949
1216
|
room?: string;
|
|
@@ -951,27 +1218,53 @@ declare abstract class LiveComponent<TState = ComponentState, TPrivate extends R
|
|
|
951
1218
|
broadcastToRoom: (message: BroadcastMessage) => void;
|
|
952
1219
|
private _privateState;
|
|
953
1220
|
private _authContext;
|
|
954
|
-
private
|
|
955
|
-
private joinedRooms;
|
|
1221
|
+
private _authContextSet;
|
|
956
1222
|
protected roomType: string;
|
|
957
|
-
private roomHandles;
|
|
958
|
-
private _inStateChange;
|
|
959
1223
|
[EMIT_OVERRIDE_KEY]: ((type: string, payload: any) => void) | null;
|
|
1224
|
+
private _stateManager;
|
|
1225
|
+
private _messaging;
|
|
1226
|
+
private _actionSecurity;
|
|
1227
|
+
private _roomProxyManager;
|
|
1228
|
+
static publicActions?: readonly string[];
|
|
960
1229
|
constructor(initialState: Partial<TState>, ws: GenericWebSocket, options?: {
|
|
961
1230
|
room?: string;
|
|
962
1231
|
userId?: string;
|
|
963
1232
|
});
|
|
964
|
-
private createDirectStateAccessors;
|
|
965
|
-
private createStateProxy;
|
|
966
1233
|
get $private(): TPrivate;
|
|
967
|
-
get $room(): ServerRoomProxy;
|
|
968
1234
|
/**
|
|
969
|
-
*
|
|
1235
|
+
* Unified room accessor.
|
|
1236
|
+
*
|
|
1237
|
+
* Usage:
|
|
1238
|
+
* - `this.$room` — default room handle (legacy)
|
|
1239
|
+
* - `this.$room('roomId')` — untyped room handle (legacy)
|
|
1240
|
+
* - `this.$room(ChatRoom, 'lobby')` — typed handle with custom methods
|
|
1241
|
+
*/
|
|
1242
|
+
get $room(): ServerRoomProxy & {
|
|
1243
|
+
<R extends LiveRoom<any, any, any>>(roomClass: LiveRoomClass<R>, instanceId: string): R & {
|
|
1244
|
+
readonly id: string;
|
|
1245
|
+
join: (payload?: any) => {
|
|
1246
|
+
rejected?: false;
|
|
1247
|
+
} | {
|
|
1248
|
+
rejected: true;
|
|
1249
|
+
reason: string;
|
|
1250
|
+
};
|
|
1251
|
+
leave: () => void;
|
|
1252
|
+
emit: R['emit'];
|
|
1253
|
+
on: <K extends string>(event: K, handler: (data: any) => void) => () => void;
|
|
1254
|
+
setState: (updates: Partial<R['state']>) => void;
|
|
1255
|
+
readonly memberCount: number;
|
|
1256
|
+
};
|
|
1257
|
+
};
|
|
1258
|
+
/**
|
|
1259
|
+
* List of room IDs this component is participating in.
|
|
1260
|
+
* Cached — invalidated on join/leave.
|
|
970
1261
|
*/
|
|
971
1262
|
get $rooms(): string[];
|
|
972
1263
|
get $auth(): LiveAuthContext;
|
|
973
|
-
/** @internal */
|
|
1264
|
+
/** @internal - Immutable after first set to prevent privilege escalation */
|
|
974
1265
|
setAuthContext(context: LiveAuthContext): void;
|
|
1266
|
+
/** @internal - Reset auth context (for registry use in reconnection) */
|
|
1267
|
+
_resetAuthContext(): void;
|
|
975
1268
|
get $persistent(): Record<string, any>;
|
|
976
1269
|
protected onConnect(): void;
|
|
977
1270
|
protected onMount(): void | Promise<void>;
|
|
@@ -985,29 +1278,117 @@ declare abstract class LiveComponent<TState = ComponentState, TPrivate extends R
|
|
|
985
1278
|
protected onClientJoin(connectionId: string, connectionCount: number): void;
|
|
986
1279
|
protected onClientLeave(connectionId: string, connectionCount: number): void;
|
|
987
1280
|
setState(updates: Partial<TState> | ((prev: TState) => Partial<TState>)): void;
|
|
1281
|
+
/**
|
|
1282
|
+
* Send a binary-encoded state delta directly over WebSocket.
|
|
1283
|
+
* Updates internal state (same as setState) then sends the encoder's output
|
|
1284
|
+
* as a binary frame: [0x01][idLen:u8][id_bytes:utf8][payload_bytes].
|
|
1285
|
+
* Bypasses the JSON batcher — ideal for high-frequency updates.
|
|
1286
|
+
*/
|
|
1287
|
+
sendBinaryDelta(delta: Partial<TState>, encoder: (delta: Partial<TState>) => Uint8Array): void;
|
|
988
1288
|
setValue<K extends keyof TState>(payload: {
|
|
989
1289
|
key: K;
|
|
990
1290
|
value: TState[K];
|
|
991
|
-
}):
|
|
1291
|
+
}): {
|
|
992
1292
|
success: true;
|
|
993
1293
|
key: K;
|
|
994
1294
|
value: TState[K];
|
|
995
|
-
}
|
|
996
|
-
static publicActions?: readonly string[];
|
|
997
|
-
private static readonly BLOCKED_ACTIONS;
|
|
1295
|
+
};
|
|
998
1296
|
executeAction(action: string, payload: any): Promise<any>;
|
|
999
1297
|
protected emit(type: string, payload: any): void;
|
|
1000
1298
|
protected broadcast(type: string, payload: any, excludeCurrentUser?: boolean): void;
|
|
1001
1299
|
protected emitRoomEvent(event: string, data: any, notifySelf?: boolean): number;
|
|
1002
1300
|
protected onRoomEvent<T = any>(event: string, handler: (data: T) => void): void;
|
|
1003
1301
|
protected emitRoomEventWithState(event: string, data: any, stateUpdates: Partial<TState>): number;
|
|
1004
|
-
protected subscribeToRoom(roomId: string):
|
|
1005
|
-
protected unsubscribeFromRoom():
|
|
1302
|
+
protected subscribeToRoom(roomId: string): void;
|
|
1303
|
+
protected unsubscribeFromRoom(): void;
|
|
1006
1304
|
private generateId;
|
|
1007
1305
|
destroy(): void;
|
|
1008
1306
|
getSerializableState(): TState;
|
|
1009
1307
|
}
|
|
1010
1308
|
|
|
1309
|
+
/**
|
|
1310
|
+
* Adapter for cross-instance component synchronization.
|
|
1311
|
+
*
|
|
1312
|
+
* In single-instance mode, no cluster adapter is needed.
|
|
1313
|
+
* For horizontal scaling, provide a RedisClusterAdapter (or custom implementation).
|
|
1314
|
+
*/
|
|
1315
|
+
interface IClusterAdapter {
|
|
1316
|
+
/** Unique identifier for this server instance. */
|
|
1317
|
+
readonly instanceId: string;
|
|
1318
|
+
/** Save component state to the shared store. */
|
|
1319
|
+
saveState(componentId: string, componentName: string, state: any): Promise<void>;
|
|
1320
|
+
/** Load component state from the shared store. */
|
|
1321
|
+
loadState(componentId: string): Promise<ClusterComponentState | null>;
|
|
1322
|
+
/** Remove component state from the shared store. */
|
|
1323
|
+
deleteState(componentId: string): Promise<void>;
|
|
1324
|
+
/** Publish a state delta to all server instances. */
|
|
1325
|
+
publishDelta(componentId: string, componentName: string, delta: any): Promise<void>;
|
|
1326
|
+
/** Register handler for incoming state deltas from other instances. */
|
|
1327
|
+
onDelta(handler: ClusterDeltaHandler): void;
|
|
1328
|
+
/** Attempt to claim ownership of a singleton (atomic).
|
|
1329
|
+
* Returns { claimed: true, recoveredState? } on success, { claimed: false } otherwise.
|
|
1330
|
+
* When claiming after a failover, recoveredState contains the previous owner's last state. */
|
|
1331
|
+
claimSingleton(componentName: string, componentId: string): Promise<ClusterSingletonClaim>;
|
|
1332
|
+
/** Get the current owner of a singleton. Returns null if not claimed. */
|
|
1333
|
+
getSingletonOwner(componentName: string): Promise<ClusterSingletonOwner | null>;
|
|
1334
|
+
/** Release ownership of a singleton (when last client disconnects). */
|
|
1335
|
+
releaseSingleton(componentName: string): Promise<void>;
|
|
1336
|
+
/** Verify this instance still owns a singleton (split-brain protection). */
|
|
1337
|
+
verifySingletonOwnership(componentName: string): Promise<boolean>;
|
|
1338
|
+
/** Register callback for when this instance loses ownership of a singleton (detected during heartbeat). */
|
|
1339
|
+
onOwnershipLost(handler: (componentName: string) => void): void;
|
|
1340
|
+
/** Save singleton state keyed by componentName (survives owner crash + claim expiry). */
|
|
1341
|
+
saveSingletonState(componentName: string, state: any): Promise<void>;
|
|
1342
|
+
/** Load the last known singleton state by componentName (for failover recovery). */
|
|
1343
|
+
loadSingletonState(componentName: string): Promise<any | null>;
|
|
1344
|
+
/** Forward an action to the owner server instance. Returns the action result. */
|
|
1345
|
+
forwardAction(request: ClusterActionRequest): Promise<ClusterActionResponse>;
|
|
1346
|
+
/** Register handler for incoming forwarded actions from other instances. */
|
|
1347
|
+
onActionForward(handler: (req: ClusterActionRequest) => Promise<ClusterActionResponse>): void;
|
|
1348
|
+
/** Start the adapter (subscribe to channels, start heartbeats, etc.). */
|
|
1349
|
+
start(): Promise<void>;
|
|
1350
|
+
/** Graceful shutdown (unsubscribe, clear intervals, disconnect). */
|
|
1351
|
+
shutdown(): Promise<void>;
|
|
1352
|
+
}
|
|
1353
|
+
/** State stored in the shared store for a component. */
|
|
1354
|
+
interface ClusterComponentState {
|
|
1355
|
+
componentName: string;
|
|
1356
|
+
state: any;
|
|
1357
|
+
instanceId: string;
|
|
1358
|
+
updatedAt: number;
|
|
1359
|
+
}
|
|
1360
|
+
/** Information about the owner of a singleton. */
|
|
1361
|
+
interface ClusterSingletonOwner {
|
|
1362
|
+
instanceId: string;
|
|
1363
|
+
componentId: string;
|
|
1364
|
+
}
|
|
1365
|
+
/** Request to forward an action to another server instance. */
|
|
1366
|
+
interface ClusterActionRequest {
|
|
1367
|
+
sourceInstanceId: string;
|
|
1368
|
+
targetInstanceId: string;
|
|
1369
|
+
componentId: string;
|
|
1370
|
+
componentName: string;
|
|
1371
|
+
action: string;
|
|
1372
|
+
payload: any;
|
|
1373
|
+
requestId: string;
|
|
1374
|
+
}
|
|
1375
|
+
/** Result of a singleton claim attempt. */
|
|
1376
|
+
interface ClusterSingletonClaim {
|
|
1377
|
+
/** Whether the claim was successful. */
|
|
1378
|
+
claimed: boolean;
|
|
1379
|
+
/** If claimed and previous state exists (failover recovery), the recovered state. */
|
|
1380
|
+
recoveredState?: any;
|
|
1381
|
+
}
|
|
1382
|
+
/** Response from a forwarded action. */
|
|
1383
|
+
interface ClusterActionResponse {
|
|
1384
|
+
success: boolean;
|
|
1385
|
+
result?: any;
|
|
1386
|
+
error?: string;
|
|
1387
|
+
requestId: string;
|
|
1388
|
+
}
|
|
1389
|
+
/** Handler for incoming state deltas from other instances. */
|
|
1390
|
+
type ClusterDeltaHandler = (componentId: string, componentName: string, delta: any, sourceInstanceId: string) => void;
|
|
1391
|
+
|
|
1011
1392
|
interface ComponentMetadata {
|
|
1012
1393
|
id: string;
|
|
1013
1394
|
name: string;
|
|
@@ -1038,9 +1419,9 @@ interface StateMigration {
|
|
|
1038
1419
|
}
|
|
1039
1420
|
interface ComponentRegistryDeps {
|
|
1040
1421
|
authManager: LiveAuthManager;
|
|
1041
|
-
debugger: LiveDebugger;
|
|
1042
1422
|
stateSignature: StateSignatureManager;
|
|
1043
1423
|
performanceMonitor: PerformanceMonitor;
|
|
1424
|
+
cluster?: IClusterAdapter;
|
|
1044
1425
|
}
|
|
1045
1426
|
declare class ComponentRegistry {
|
|
1046
1427
|
private components;
|
|
@@ -1051,11 +1432,14 @@ declare class ComponentRegistry {
|
|
|
1051
1432
|
private autoDiscoveredComponents;
|
|
1052
1433
|
private healthCheckInterval?;
|
|
1053
1434
|
private singletons;
|
|
1435
|
+
private remoteSingletons;
|
|
1436
|
+
private cluster?;
|
|
1054
1437
|
private authManager;
|
|
1055
|
-
private debugger;
|
|
1056
1438
|
private stateSignature;
|
|
1057
1439
|
private performanceMonitor;
|
|
1058
1440
|
constructor(deps: ComponentRegistryDeps);
|
|
1441
|
+
/** Set up handlers for incoming cluster messages (deltas, forwarded actions). */
|
|
1442
|
+
private setupClusterHandlers;
|
|
1059
1443
|
private setupHealthMonitoring;
|
|
1060
1444
|
registerComponent<TState>(definition: ComponentDefinition<TState>): void;
|
|
1061
1445
|
registerComponentClass(name: string, componentClass: new (initialState: any, ws: GenericWebSocket, options?: {
|
|
@@ -1085,8 +1469,10 @@ declare class ComponentRegistry {
|
|
|
1085
1469
|
private ensureWsData;
|
|
1086
1470
|
private isSingletonComponent;
|
|
1087
1471
|
private removeSingletonConnection;
|
|
1088
|
-
unmountComponent(componentId: string, ws?: GenericWebSocket):
|
|
1472
|
+
unmountComponent(componentId: string, ws?: GenericWebSocket): void;
|
|
1089
1473
|
private getSingletonName;
|
|
1474
|
+
/** Find a remote singleton entry by componentId. */
|
|
1475
|
+
private findRemoteSingleton;
|
|
1090
1476
|
executeAction(componentId: string, action: string, payload: any): Promise<any>;
|
|
1091
1477
|
updateProperty(componentId: string, property: string, value: any): void;
|
|
1092
1478
|
subscribeToRoom(componentId: string, roomId: string): void;
|
|
@@ -1110,6 +1496,13 @@ declare class ComponentRegistry {
|
|
|
1110
1496
|
connections: number;
|
|
1111
1497
|
};
|
|
1112
1498
|
};
|
|
1499
|
+
remoteSingletons: {
|
|
1500
|
+
[k: string]: {
|
|
1501
|
+
componentId: string;
|
|
1502
|
+
ownerInstanceId: string;
|
|
1503
|
+
connections: number;
|
|
1504
|
+
};
|
|
1505
|
+
};
|
|
1113
1506
|
roomDetails: {
|
|
1114
1507
|
[k: string]: number;
|
|
1115
1508
|
};
|
|
@@ -1176,11 +1569,25 @@ interface LiveServerOptions {
|
|
|
1176
1569
|
componentsPath?: string;
|
|
1177
1570
|
/** HTTP monitoring routes prefix. Set to false to disable. Defaults to '/api/live' */
|
|
1178
1571
|
httpPrefix?: string | false;
|
|
1572
|
+
/** Allowed origins for WebSocket connections (CSRF protection).
|
|
1573
|
+
* When set, connections from unlisted origins are rejected.
|
|
1574
|
+
* Example: ['https://myapp.com', 'http://localhost:3000'] */
|
|
1575
|
+
allowedOrigins?: string[];
|
|
1576
|
+
/** Optional cross-instance pub/sub adapter for horizontal scaling (e.g. Redis).
|
|
1577
|
+
* When provided, room events, state changes, and membership are propagated
|
|
1578
|
+
* across server instances. Without this, rooms are local to the current instance. */
|
|
1579
|
+
roomPubSub?: IRoomPubSubAdapter;
|
|
1580
|
+
/** Optional cluster adapter for cross-instance component synchronization.
|
|
1581
|
+
* When provided, singleton components are coordinated across instances,
|
|
1582
|
+
* component state is mirrored to a shared store (Redis), and actions on
|
|
1583
|
+
* remote singletons are forwarded to the owner instance. */
|
|
1584
|
+
cluster?: IClusterAdapter;
|
|
1585
|
+
/** LiveRoom classes to register. These define typed rooms with lifecycle hooks. */
|
|
1586
|
+
rooms?: LiveRoomClass[];
|
|
1179
1587
|
}
|
|
1180
1588
|
declare class LiveServer {
|
|
1181
1589
|
readonly roomEvents: RoomEventBus;
|
|
1182
1590
|
readonly roomManager: LiveRoomManager;
|
|
1183
|
-
readonly debugger: LiveDebugger;
|
|
1184
1591
|
readonly authManager: LiveAuthManager;
|
|
1185
1592
|
readonly stateSignature: StateSignatureManager;
|
|
1186
1593
|
readonly performanceMonitor: PerformanceMonitor;
|
|
@@ -1188,6 +1595,7 @@ declare class LiveServer {
|
|
|
1188
1595
|
readonly connectionManager: WebSocketConnectionManager;
|
|
1189
1596
|
readonly registry: ComponentRegistry;
|
|
1190
1597
|
readonly rateLimiter: RateLimiterRegistry;
|
|
1598
|
+
readonly roomRegistry: RoomRegistry;
|
|
1191
1599
|
private transport;
|
|
1192
1600
|
private options;
|
|
1193
1601
|
constructor(options: LiveServerOptions);
|
|
@@ -1195,6 +1603,11 @@ declare class LiveServer {
|
|
|
1195
1603
|
* Register an auth provider.
|
|
1196
1604
|
*/
|
|
1197
1605
|
useAuth(provider: LiveAuthProvider): this;
|
|
1606
|
+
/**
|
|
1607
|
+
* Register a LiveRoom class.
|
|
1608
|
+
* Can be called before start() to register room types dynamically.
|
|
1609
|
+
*/
|
|
1610
|
+
useRoom(roomClass: LiveRoomClass): this;
|
|
1198
1611
|
/**
|
|
1199
1612
|
* Start the LiveServer: register WS + HTTP handlers on the transport.
|
|
1200
1613
|
*/
|
|
@@ -1211,6 +1624,32 @@ declare class LiveServer {
|
|
|
1211
1624
|
private buildHttpRoutes;
|
|
1212
1625
|
}
|
|
1213
1626
|
|
|
1627
|
+
interface PendingMessage {
|
|
1628
|
+
type: string;
|
|
1629
|
+
componentId: string;
|
|
1630
|
+
payload: any;
|
|
1631
|
+
timestamp: number;
|
|
1632
|
+
userId?: string;
|
|
1633
|
+
room?: string;
|
|
1634
|
+
[key: string]: any;
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* Queue a message to be sent on the next microtask flush.
|
|
1638
|
+
* Messages are batched per-WS and sent as a JSON array.
|
|
1639
|
+
*/
|
|
1640
|
+
declare function queueWsMessage(ws: GenericWebSocket, message: PendingMessage): void;
|
|
1641
|
+
/**
|
|
1642
|
+
* Send a binary message immediately (bypass batching).
|
|
1643
|
+
* Binary frames are never batched — they are self-framing.
|
|
1644
|
+
*/
|
|
1645
|
+
declare function sendBinaryImmediate(ws: GenericWebSocket, data: Uint8Array): void;
|
|
1646
|
+
/**
|
|
1647
|
+
* Send a message immediately (bypass batching).
|
|
1648
|
+
* Used for ACTION_RESPONSE and other request-response patterns
|
|
1649
|
+
* where the client is awaiting an immediate response.
|
|
1650
|
+
*/
|
|
1651
|
+
declare function sendImmediate(ws: GenericWebSocket, data: string): void;
|
|
1652
|
+
|
|
1214
1653
|
/**
|
|
1215
1654
|
* Encode a binary chunk message for transmission.
|
|
1216
1655
|
* @param header - JSON metadata about the chunk
|
|
@@ -1314,6 +1753,26 @@ declare class RoomStateManager {
|
|
|
1314
1753
|
};
|
|
1315
1754
|
}
|
|
1316
1755
|
|
|
1756
|
+
declare class InMemoryRoomAdapter implements IRoomStorageAdapter, IRoomPubSubAdapter {
|
|
1757
|
+
private rooms;
|
|
1758
|
+
getOrCreateRoom(roomId: string, initialState?: any): Promise<{
|
|
1759
|
+
state: any;
|
|
1760
|
+
created: boolean;
|
|
1761
|
+
}>;
|
|
1762
|
+
getState(roomId: string): Promise<any>;
|
|
1763
|
+
updateState(roomId: string, updates: any): Promise<void>;
|
|
1764
|
+
hasRoom(roomId: string): Promise<boolean>;
|
|
1765
|
+
deleteRoom(roomId: string): Promise<boolean>;
|
|
1766
|
+
getStats(): Promise<{
|
|
1767
|
+
totalRooms: number;
|
|
1768
|
+
rooms: Record<string, any>;
|
|
1769
|
+
}>;
|
|
1770
|
+
publish(_roomId: string, _event: string, _data: any): Promise<void>;
|
|
1771
|
+
subscribe(_roomId: string, _handler: (event: string, data: any) => void): Promise<() => void>;
|
|
1772
|
+
publishMembership(_roomId: string, _action: 'join' | 'leave', _componentId: string): Promise<void>;
|
|
1773
|
+
publishStateChange(_roomId: string, _updates: any): Promise<void>;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1317
1776
|
type LiveLogCategory = 'lifecycle' | 'messages' | 'state' | 'performance' | 'rooms' | 'websocket';
|
|
1318
1777
|
type LiveLogConfig = boolean | readonly LiveLogCategory[];
|
|
1319
1778
|
/**
|
|
@@ -1326,12 +1785,10 @@ declare function registerComponentLogging(componentId: string, config: LiveLogCo
|
|
|
1326
1785
|
declare function unregisterComponentLogging(componentId: string): void;
|
|
1327
1786
|
/**
|
|
1328
1787
|
* Log a message gated by the component's logging config.
|
|
1329
|
-
* Always forwarded to the Live Debugger when active.
|
|
1330
1788
|
*/
|
|
1331
1789
|
declare function liveLog(category: LiveLogCategory, componentId: string | null, message: string, ...args: unknown[]): void;
|
|
1332
1790
|
/**
|
|
1333
1791
|
* Warn-level log gated by config.
|
|
1334
|
-
* Always forwarded to the Live Debugger when active.
|
|
1335
1792
|
*/
|
|
1336
1793
|
declare function liveWarn(category: LiveLogCategory, componentId: string | null, message: string, ...args: unknown[]): void;
|
|
1337
1794
|
|
|
@@ -1397,4 +1854,4 @@ interface UseTypedLiveComponentReturn<T extends LiveComponent<any>> {
|
|
|
1397
1854
|
};
|
|
1398
1855
|
}
|
|
1399
1856
|
|
|
1400
|
-
export { ANONYMOUS_CONTEXT, type ActionNames, type ActionPayload, type ActionReturn, type ActiveUpload, AnonymousContext, AuthenticatedContext, type BinaryChunkHeader, type BroadcastMessage, type
|
|
1857
|
+
export { ANONYMOUS_CONTEXT, type ActionNames, type ActionPayload, type ActionReturn, type ActiveUpload, AnonymousContext, AuthenticatedContext, BINARY_ROOM_EVENT, BINARY_ROOM_STATE, type BinaryChunkHeader, type BroadcastMessage, type ClusterActionRequest, type ClusterActionResponse, type ClusterComponentState, type ClusterDeltaHandler, type ClusterSingletonClaim, type ClusterSingletonOwner, type ComponentDefinition, type ComponentMetadata, type ComponentMetrics, type ComponentPerformanceMetrics, ComponentRegistry, type ComponentState, type ConnectionConfig, type ConnectionHealth, type ConnectionMetrics, ConnectionRateLimiter, DEFAULT_CHUNK_SIZE, DEFAULT_WS_PATH, EMIT_OVERRIDE_KEY, type EventHandler, type ExtractActions, type FileChunkData, type FileUploadChunkMessage, type FileUploadCompleteMessage, type FileUploadCompleteResponse, type FileUploadConfig, FileUploadManager, type FileUploadProgressResponse, type FileUploadStartMessage, type FluxStackWSData, type FluxStackWebSocket, type GenericWebSocket, type HttpRequest, type HttpResponse, type HttpRouteDefinition, type HybridComponentOptions, type HybridState, type IClusterAdapter, type IRoomPubSubAdapter, type IRoomStorageAdapter, InMemoryRoomAdapter, type InferComponentState, type InferPrivateState, type InferRoomEvents, type InferRoomMeta, type InferRoomState, type LiveActionAuth, type LiveActionAuthMap, type LiveAuthContext, type LiveAuthCredentials, LiveAuthManager, type LiveAuthProvider, type LiveAuthResult, type LiveAuthUser, LiveComponent, type LiveComponentAuth, type LiveComponentContext, type LiveComponentInstance, type LiveLogCategory, type LiveLogConfig, type LiveMessage, LiveRoom, type LiveRoomClass, LiveRoomManager, type LiveRoomManagerInterface, type LiveRoomOptions, LiveServer, type LiveServerOptions, type LiveTransport, type LiveWSData, PROTOCOL_VERSION, type PerformanceAlert, type PerformanceConfig, PerformanceMonitor, RateLimiterRegistry, type RoomCodec, type RoomCodecOption, RoomEventBus, type RoomEventContext, type RoomInfo, type RoomJoinContext, type RoomLeaveContext, type RoomMessage, RoomRegistry, type RoomStateData, RoomStateManager, type RoomSubscription, type ServerRoomHandle, type ServerRoomProxy, type SignedState, type StateMigration, type StateSignatureConfig, StateSignatureManager, type TypedCall, type TypedCallAndWait, type TypedSetValue, type UseTypedLiveComponentReturn, type WebSocketConfig, WebSocketConnectionManager, type WebSocketMessage, type WebSocketResponse, buildRoomFrame, buildRoomFrameTail, createTypedRoomEventBus, createTypedRoomState, decodeBinaryChunk, encodeBinaryChunk, getLiveComponentContext, jsonCodec, liveLog, liveWarn, msgpackCodec, parseRoomFrame, prependMemberHeader, queueWsMessage, registerComponentLogging, resolveCodec, sendBinaryImmediate, sendImmediate, setLiveComponentContext, unregisterComponentLogging };
|