@fluxstack/live 0.1.0 → 0.2.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 ADDED
@@ -0,0 +1,145 @@
1
+ # @fluxstack/live
2
+
3
+ Framework-agnostic core for real-time server-client state synchronization.
4
+
5
+ Live Components turn server-side classes into reactive state that syncs automatically with connected clients over WebSocket. Write your logic once on the server, and clients receive state updates in real-time.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ bun add @fluxstack/live
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```typescript
16
+ import { LiveServer, LiveComponent } from '@fluxstack/live'
17
+
18
+ // 1. Define a component
19
+ class Counter extends LiveComponent<{ count: number }> {
20
+ static componentName = 'Counter'
21
+ static defaultState = { count: 0 }
22
+ static publicActions = ['increment', 'decrement'] as const
23
+
24
+ increment() {
25
+ this.count++
26
+ }
27
+
28
+ decrement() {
29
+ this.count--
30
+ }
31
+ }
32
+
33
+ // 2. Create server with your transport adapter
34
+ import { ElysiaTransport } from '@fluxstack/live-elysia'
35
+
36
+ const server = new LiveServer({
37
+ transport: new ElysiaTransport(app),
38
+ componentsPath: './src/components',
39
+ })
40
+
41
+ await server.start()
42
+ ```
43
+
44
+ ## Features
45
+
46
+ - **LiveComponent** — Base class with reactive state proxy, auto-sync, and lifecycle hooks
47
+ - **LiveServer** — Orchestrator that wires transport, components, auth, rooms, and cluster
48
+ - **ComponentRegistry** — Auto-discovers components from a directory or manual registration
49
+ - **Rooms** — Built-in room system with typed events and cross-instance pub/sub
50
+ - **Auth** — Per-component and per-action authorization (`static auth`, `static actionAuth`)
51
+ - **State Signing** — HMAC-SHA256 state signing with hybrid nonce replay protection
52
+ - **Rate Limiting** — Token bucket rate limiter per connection
53
+ - **Security** — Payload sanitization against prototype pollution, message size limits
54
+ - **Binary Delta** — Efficient binary state diffs for high-frequency updates
55
+ - **File Upload** — Chunked file upload over WebSocket
56
+ - **Cluster** — `IClusterAdapter` interface for horizontal scaling (singleton coordination, action forwarding, state mirroring)
57
+ - **Monitoring** — Performance monitor with per-component metrics
58
+
59
+ ## Transport Adapters
60
+
61
+ This package is framework-agnostic. Use it with any transport adapter:
62
+
63
+ | Adapter | Package |
64
+ |---------|---------|
65
+ | Elysia | `@fluxstack/live-elysia` |
66
+ | Express | `@fluxstack/live-express` |
67
+ | Fastify | `@fluxstack/live-fastify` |
68
+
69
+ ## LiveComponent
70
+
71
+ ```typescript
72
+ import { LiveComponent } from '@fluxstack/live'
73
+
74
+ export class TodoList extends LiveComponent<typeof TodoList.defaultState> {
75
+ static componentName = 'TodoList'
76
+ static singleton = true
77
+ static publicActions = ['addTodo', 'toggleTodo'] as const
78
+ static defaultState = {
79
+ todos: [] as { id: string; text: string; done: boolean }[]
80
+ }
81
+
82
+ declare todos: typeof TodoList.defaultState['todos']
83
+
84
+ addTodo(payload: { text: string }) {
85
+ this.todos = [...this.todos, { id: crypto.randomUUID(), text: payload.text, done: false }]
86
+ }
87
+
88
+ toggleTodo(payload: { id: string }) {
89
+ this.todos = this.todos.map(t =>
90
+ t.id === payload.id ? { ...t, done: !t.done } : t
91
+ )
92
+ }
93
+ }
94
+ ```
95
+
96
+ ### Lifecycle Hooks
97
+
98
+ ```typescript
99
+ class MyComponent extends LiveComponent<State> {
100
+ protected onConnect() { } // WebSocket connected
101
+ protected async onMount() { } // Component fully mounted (async)
102
+ protected onRehydrate(prev) { } // State restored from client
103
+ protected onStateChange(changes) { } // After state mutation
104
+ protected onRoomJoin(roomId) { } // Joined a room
105
+ protected onRoomLeave(roomId) { } // Left a room
106
+ protected onAction(action, payload) { } // Before action (return false to cancel)
107
+ protected onDisconnect() { } // Connection lost
108
+ protected onDestroy() { } // Before cleanup (sync)
109
+ }
110
+ ```
111
+
112
+ ## LiveServer Options
113
+
114
+ ```typescript
115
+ new LiveServer({
116
+ transport, // Required: transport adapter
117
+ componentsPath: './components', // Auto-discover components
118
+ wsPath: '/api/live/ws', // WebSocket endpoint (default)
119
+ debug: false, // Debug mode
120
+ cluster: clusterAdapter, // IClusterAdapter for horizontal scaling
121
+ roomPubSub: roomAdapter, // IRoomPubSubAdapter for cross-instance rooms
122
+ allowedOrigins: ['https://...'], // CSRF protection
123
+ rateLimitMaxTokens: 100, // Rate limiter max tokens
124
+ rateLimitRefillRate: 10, // Tokens refilled per second
125
+ httpPrefix: '/api/live', // HTTP monitoring routes
126
+ })
127
+ ```
128
+
129
+ ## Horizontal Scaling
130
+
131
+ ```typescript
132
+ import { RedisClusterAdapter, RedisRoomAdapter } from '@fluxstack/live-redis'
133
+
134
+ const server = new LiveServer({
135
+ transport,
136
+ cluster: new RedisClusterAdapter({ redis }),
137
+ roomPubSub: new RedisRoomAdapter({ redis }),
138
+ })
139
+ ```
140
+
141
+ See `@fluxstack/live-redis` for details.
142
+
143
+ ## License
144
+
145
+ MIT
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,11 +252,62 @@ 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
+
249
299
  declare class LiveRoomManager {
250
300
  private roomEvents;
301
+ private pubsub?;
251
302
  private rooms;
252
303
  private componentRooms;
253
- constructor(roomEvents: RoomEventBus);
304
+ /**
305
+ * @param roomEvents - Local server-side event bus
306
+ * @param pubsub - Optional cross-instance pub/sub adapter (e.g. Redis).
307
+ * When provided, room events/state/membership are propagated
308
+ * to other server instances in the background.
309
+ */
310
+ constructor(roomEvents: RoomEventBus, pubsub?: IRoomPubSubAdapter | undefined);
254
311
  /**
255
312
  * Component joins a room
256
313
  */
@@ -262,7 +319,9 @@ declare class LiveRoomManager {
262
319
  */
263
320
  leaveRoom(componentId: string, roomId: string): void;
264
321
  /**
265
- * Component disconnects - leave all rooms
322
+ * Component disconnects - leave all rooms.
323
+ * Batches removals: removes member from all rooms first,
324
+ * then sends leave notifications in bulk.
266
325
  */
267
326
  cleanupComponent(componentId: string): void;
268
327
  /**
@@ -270,7 +329,8 @@ declare class LiveRoomManager {
270
329
  */
271
330
  emitToRoom(roomId: string, event: string, data: any, excludeComponentId?: string): number;
272
331
  /**
273
- * Update room state
332
+ * Update room state.
333
+ * Mutates state in-place with Object.assign to avoid full-object spread.
274
334
  */
275
335
  setRoomState(roomId: string, updates: any, excludeComponentId?: string): void;
276
336
  /**
@@ -278,7 +338,8 @@ declare class LiveRoomManager {
278
338
  */
279
339
  getRoomState<TState = any>(roomId: string): TState;
280
340
  /**
281
- * Broadcast to all members in a room
341
+ * Broadcast to all members in a room.
342
+ * Serializes the message ONCE and sends the same string to all members.
282
343
  */
283
344
  private broadcastToRoom;
284
345
  /**
@@ -446,33 +507,49 @@ interface StateSignatureConfig {
446
507
  compressionEnabled?: boolean;
447
508
  /** Enable encryption */
448
509
  encryptionEnabled?: boolean;
449
- /** Enable anti-replay nonces */
510
+ /** Enable anti-replay nonces (hybrid: stateless HMAC + replay detection) */
450
511
  nonceEnabled?: boolean;
451
- /** Maximum state age in ms */
512
+ /** Maximum state age in ms. Default: 1800000 (30 minutes) */
452
513
  maxStateAge?: number;
453
514
  /** Enable state backups */
454
515
  backupEnabled?: boolean;
455
516
  /** Max state backups to keep */
456
517
  maxBackups?: number;
518
+ /** Nonce TTL in ms. Nonces older than this are rejected. Default: 10000 (10 seconds) */
519
+ nonceTTL?: number;
457
520
  }
458
521
  declare class StateSignatureManager {
459
522
  private secret;
460
523
  private previousSecrets;
461
524
  private rotationTimer?;
462
- private usedNonces;
463
- private nonceCleanupTimer?;
464
525
  private stateBackups;
465
526
  private config;
527
+ private encryptionSalt;
528
+ private cachedEncryptionKey;
529
+ /** Replay detection: nonce → timestamp when it was first seen. Cleaned every 60s. */
530
+ private usedNonces;
531
+ private nonceCleanupTimer?;
466
532
  constructor(config?: StateSignatureConfig);
533
+ /**
534
+ * Generate a hybrid nonce: `timestamp:random:HMAC(timestamp:random, secret)`
535
+ * Self-validating via HMAC, unique via random bytes, replay-tracked via Map.
536
+ */
537
+ private generateNonce;
538
+ /**
539
+ * Validate a hybrid nonce: check format, HMAC, and TTL.
540
+ */
541
+ private validateNonce;
467
542
  signState(componentId: string, state: Record<string, unknown>, version: number, options?: {
468
543
  compress?: boolean;
469
544
  backup?: boolean;
470
- }): Promise<SignedState>;
471
- validateState(signedState: SignedState): Promise<{
545
+ }): SignedState;
546
+ validateState(signedState: SignedState, options?: {
547
+ skipNonce?: boolean;
548
+ }): {
472
549
  valid: boolean;
473
550
  error?: string;
474
- }>;
475
- extractData(signedState: SignedState): Promise<Record<string, unknown>>;
551
+ };
552
+ extractData(signedState: SignedState): Record<string, unknown>;
476
553
  getBackups(componentId: string): SignedState[];
477
554
  getLatestBackup(componentId: string): SignedState | null;
478
555
  private backupState;
@@ -481,6 +558,7 @@ declare class StateSignatureManager {
481
558
  private timingSafeEqual;
482
559
  private deriveEncryptionKey;
483
560
  private setupKeyRotation;
561
+ /** Remove nonces older than nonceTTL + 10s from the replay detection map. */
484
562
  private cleanupNonces;
485
563
  shutdown(): void;
486
564
  }
@@ -847,6 +925,8 @@ declare class WebSocketConnectionManager extends EventEmitter {
847
925
  private connections;
848
926
  private connectionMetrics;
849
927
  private connectionPools;
928
+ /** Reverse index: connectionId -> Set of poolIds for O(1) cleanup */
929
+ private connectionPoolIndex;
850
930
  private messageQueues;
851
931
  private healthCheckTimer?;
852
932
  private heartbeatTimer?;
@@ -910,8 +990,9 @@ declare function setLiveComponentContext(ctx: LiveComponentContext): void;
910
990
  */
911
991
  declare function getLiveComponentContext(): LiveComponentContext;
912
992
 
913
- /** @internal Symbol key for singleton emit override */
993
+ /** Symbol key for singleton emit override */
914
994
  declare const EMIT_OVERRIDE_KEY: unique symbol;
995
+
915
996
  declare abstract class LiveComponent<TState = ComponentState, TPrivate extends Record<string, any> = Record<string, any>> {
916
997
  /** Component name for registry lookup - must be defined in subclasses */
917
998
  static componentName: string;
@@ -933,6 +1014,35 @@ declare abstract class LiveComponent<TState = ComponentState, TPrivate extends R
933
1014
  * Per-action auth configuration.
934
1015
  */
935
1016
  static actionAuth?: LiveActionAuthMap;
1017
+ /**
1018
+ * Zod schemas for action payload validation.
1019
+ * When defined, payloads are validated before the action method is called.
1020
+ *
1021
+ * @example
1022
+ * static actionSchemas = {
1023
+ * sendMessage: z.object({ text: z.string().max(500) }),
1024
+ * updatePosition: z.object({ x: z.number(), y: z.number() }),
1025
+ * }
1026
+ */
1027
+ static actionSchemas?: Record<string, {
1028
+ safeParse: (data: unknown) => {
1029
+ success: boolean;
1030
+ error?: any;
1031
+ data?: any;
1032
+ };
1033
+ }>;
1034
+ /**
1035
+ * Rate limit for action execution.
1036
+ * Prevents clients from spamming expensive operations.
1037
+ *
1038
+ * @example
1039
+ * static actionRateLimit = { maxCalls: 10, windowMs: 1000, perAction: true }
1040
+ */
1041
+ static actionRateLimit?: {
1042
+ maxCalls: number;
1043
+ windowMs: number;
1044
+ perAction?: boolean;
1045
+ };
936
1046
  /**
937
1047
  * Data that survives HMR reloads.
938
1048
  */
@@ -943,7 +1053,6 @@ declare abstract class LiveComponent<TState = ComponentState, TPrivate extends R
943
1053
  */
944
1054
  static singleton?: boolean;
945
1055
  readonly id: string;
946
- private _state;
947
1056
  state: TState;
948
1057
  protected ws: GenericWebSocket;
949
1058
  room?: string;
@@ -951,27 +1060,30 @@ declare abstract class LiveComponent<TState = ComponentState, TPrivate extends R
951
1060
  broadcastToRoom: (message: BroadcastMessage) => void;
952
1061
  private _privateState;
953
1062
  private _authContext;
954
- private roomEventUnsubscribers;
955
- private joinedRooms;
1063
+ private _authContextSet;
956
1064
  protected roomType: string;
957
- private roomHandles;
958
- private _inStateChange;
959
1065
  [EMIT_OVERRIDE_KEY]: ((type: string, payload: any) => void) | null;
1066
+ private _stateManager;
1067
+ private _messaging;
1068
+ private _actionSecurity;
1069
+ private _roomProxyManager;
1070
+ static publicActions?: readonly string[];
960
1071
  constructor(initialState: Partial<TState>, ws: GenericWebSocket, options?: {
961
1072
  room?: string;
962
1073
  userId?: string;
963
1074
  });
964
- private createDirectStateAccessors;
965
- private createStateProxy;
966
1075
  get $private(): TPrivate;
967
1076
  get $room(): ServerRoomProxy;
968
1077
  /**
969
- * List of room IDs this component is participating in
1078
+ * List of room IDs this component is participating in.
1079
+ * Cached — invalidated on join/leave.
970
1080
  */
971
1081
  get $rooms(): string[];
972
1082
  get $auth(): LiveAuthContext;
973
- /** @internal */
1083
+ /** @internal - Immutable after first set to prevent privilege escalation */
974
1084
  setAuthContext(context: LiveAuthContext): void;
1085
+ /** @internal - Reset auth context (for registry use in reconnection) */
1086
+ _resetAuthContext(): void;
975
1087
  get $persistent(): Record<string, any>;
976
1088
  protected onConnect(): void;
977
1089
  protected onMount(): void | Promise<void>;
@@ -985,29 +1097,117 @@ declare abstract class LiveComponent<TState = ComponentState, TPrivate extends R
985
1097
  protected onClientJoin(connectionId: string, connectionCount: number): void;
986
1098
  protected onClientLeave(connectionId: string, connectionCount: number): void;
987
1099
  setState(updates: Partial<TState> | ((prev: TState) => Partial<TState>)): void;
1100
+ /**
1101
+ * Send a binary-encoded state delta directly over WebSocket.
1102
+ * Updates internal state (same as setState) then sends the encoder's output
1103
+ * as a binary frame: [0x01][idLen:u8][id_bytes:utf8][payload_bytes].
1104
+ * Bypasses the JSON batcher — ideal for high-frequency updates.
1105
+ */
1106
+ sendBinaryDelta(delta: Partial<TState>, encoder: (delta: Partial<TState>) => Uint8Array): void;
988
1107
  setValue<K extends keyof TState>(payload: {
989
1108
  key: K;
990
1109
  value: TState[K];
991
- }): Promise<{
1110
+ }): {
992
1111
  success: true;
993
1112
  key: K;
994
1113
  value: TState[K];
995
- }>;
996
- static publicActions?: readonly string[];
997
- private static readonly BLOCKED_ACTIONS;
1114
+ };
998
1115
  executeAction(action: string, payload: any): Promise<any>;
999
1116
  protected emit(type: string, payload: any): void;
1000
1117
  protected broadcast(type: string, payload: any, excludeCurrentUser?: boolean): void;
1001
1118
  protected emitRoomEvent(event: string, data: any, notifySelf?: boolean): number;
1002
1119
  protected onRoomEvent<T = any>(event: string, handler: (data: T) => void): void;
1003
1120
  protected emitRoomEventWithState(event: string, data: any, stateUpdates: Partial<TState>): number;
1004
- protected subscribeToRoom(roomId: string): Promise<void>;
1005
- protected unsubscribeFromRoom(): Promise<void>;
1121
+ protected subscribeToRoom(roomId: string): void;
1122
+ protected unsubscribeFromRoom(): void;
1006
1123
  private generateId;
1007
1124
  destroy(): void;
1008
1125
  getSerializableState(): TState;
1009
1126
  }
1010
1127
 
1128
+ /**
1129
+ * Adapter for cross-instance component synchronization.
1130
+ *
1131
+ * In single-instance mode, no cluster adapter is needed.
1132
+ * For horizontal scaling, provide a RedisClusterAdapter (or custom implementation).
1133
+ */
1134
+ interface IClusterAdapter {
1135
+ /** Unique identifier for this server instance. */
1136
+ readonly instanceId: string;
1137
+ /** Save component state to the shared store. */
1138
+ saveState(componentId: string, componentName: string, state: any): Promise<void>;
1139
+ /** Load component state from the shared store. */
1140
+ loadState(componentId: string): Promise<ClusterComponentState | null>;
1141
+ /** Remove component state from the shared store. */
1142
+ deleteState(componentId: string): Promise<void>;
1143
+ /** Publish a state delta to all server instances. */
1144
+ publishDelta(componentId: string, componentName: string, delta: any): Promise<void>;
1145
+ /** Register handler for incoming state deltas from other instances. */
1146
+ onDelta(handler: ClusterDeltaHandler): void;
1147
+ /** Attempt to claim ownership of a singleton (atomic).
1148
+ * Returns { claimed: true, recoveredState? } on success, { claimed: false } otherwise.
1149
+ * When claiming after a failover, recoveredState contains the previous owner's last state. */
1150
+ claimSingleton(componentName: string, componentId: string): Promise<ClusterSingletonClaim>;
1151
+ /** Get the current owner of a singleton. Returns null if not claimed. */
1152
+ getSingletonOwner(componentName: string): Promise<ClusterSingletonOwner | null>;
1153
+ /** Release ownership of a singleton (when last client disconnects). */
1154
+ releaseSingleton(componentName: string): Promise<void>;
1155
+ /** Verify this instance still owns a singleton (split-brain protection). */
1156
+ verifySingletonOwnership(componentName: string): Promise<boolean>;
1157
+ /** Register callback for when this instance loses ownership of a singleton (detected during heartbeat). */
1158
+ onOwnershipLost(handler: (componentName: string) => void): void;
1159
+ /** Save singleton state keyed by componentName (survives owner crash + claim expiry). */
1160
+ saveSingletonState(componentName: string, state: any): Promise<void>;
1161
+ /** Load the last known singleton state by componentName (for failover recovery). */
1162
+ loadSingletonState(componentName: string): Promise<any | null>;
1163
+ /** Forward an action to the owner server instance. Returns the action result. */
1164
+ forwardAction(request: ClusterActionRequest): Promise<ClusterActionResponse>;
1165
+ /** Register handler for incoming forwarded actions from other instances. */
1166
+ onActionForward(handler: (req: ClusterActionRequest) => Promise<ClusterActionResponse>): void;
1167
+ /** Start the adapter (subscribe to channels, start heartbeats, etc.). */
1168
+ start(): Promise<void>;
1169
+ /** Graceful shutdown (unsubscribe, clear intervals, disconnect). */
1170
+ shutdown(): Promise<void>;
1171
+ }
1172
+ /** State stored in the shared store for a component. */
1173
+ interface ClusterComponentState {
1174
+ componentName: string;
1175
+ state: any;
1176
+ instanceId: string;
1177
+ updatedAt: number;
1178
+ }
1179
+ /** Information about the owner of a singleton. */
1180
+ interface ClusterSingletonOwner {
1181
+ instanceId: string;
1182
+ componentId: string;
1183
+ }
1184
+ /** Request to forward an action to another server instance. */
1185
+ interface ClusterActionRequest {
1186
+ sourceInstanceId: string;
1187
+ targetInstanceId: string;
1188
+ componentId: string;
1189
+ componentName: string;
1190
+ action: string;
1191
+ payload: any;
1192
+ requestId: string;
1193
+ }
1194
+ /** Result of a singleton claim attempt. */
1195
+ interface ClusterSingletonClaim {
1196
+ /** Whether the claim was successful. */
1197
+ claimed: boolean;
1198
+ /** If claimed and previous state exists (failover recovery), the recovered state. */
1199
+ recoveredState?: any;
1200
+ }
1201
+ /** Response from a forwarded action. */
1202
+ interface ClusterActionResponse {
1203
+ success: boolean;
1204
+ result?: any;
1205
+ error?: string;
1206
+ requestId: string;
1207
+ }
1208
+ /** Handler for incoming state deltas from other instances. */
1209
+ type ClusterDeltaHandler = (componentId: string, componentName: string, delta: any, sourceInstanceId: string) => void;
1210
+
1011
1211
  interface ComponentMetadata {
1012
1212
  id: string;
1013
1213
  name: string;
@@ -1041,6 +1241,7 @@ interface ComponentRegistryDeps {
1041
1241
  debugger: LiveDebugger;
1042
1242
  stateSignature: StateSignatureManager;
1043
1243
  performanceMonitor: PerformanceMonitor;
1244
+ cluster?: IClusterAdapter;
1044
1245
  }
1045
1246
  declare class ComponentRegistry {
1046
1247
  private components;
@@ -1051,11 +1252,15 @@ declare class ComponentRegistry {
1051
1252
  private autoDiscoveredComponents;
1052
1253
  private healthCheckInterval?;
1053
1254
  private singletons;
1255
+ private remoteSingletons;
1256
+ private cluster?;
1054
1257
  private authManager;
1055
1258
  private debugger;
1056
1259
  private stateSignature;
1057
1260
  private performanceMonitor;
1058
1261
  constructor(deps: ComponentRegistryDeps);
1262
+ /** Set up handlers for incoming cluster messages (deltas, forwarded actions). */
1263
+ private setupClusterHandlers;
1059
1264
  private setupHealthMonitoring;
1060
1265
  registerComponent<TState>(definition: ComponentDefinition<TState>): void;
1061
1266
  registerComponentClass(name: string, componentClass: new (initialState: any, ws: GenericWebSocket, options?: {
@@ -1085,8 +1290,10 @@ declare class ComponentRegistry {
1085
1290
  private ensureWsData;
1086
1291
  private isSingletonComponent;
1087
1292
  private removeSingletonConnection;
1088
- unmountComponent(componentId: string, ws?: GenericWebSocket): Promise<void>;
1293
+ unmountComponent(componentId: string, ws?: GenericWebSocket): void;
1089
1294
  private getSingletonName;
1295
+ /** Find a remote singleton entry by componentId. */
1296
+ private findRemoteSingleton;
1090
1297
  executeAction(componentId: string, action: string, payload: any): Promise<any>;
1091
1298
  updateProperty(componentId: string, property: string, value: any): void;
1092
1299
  subscribeToRoom(componentId: string, roomId: string): void;
@@ -1110,6 +1317,13 @@ declare class ComponentRegistry {
1110
1317
  connections: number;
1111
1318
  };
1112
1319
  };
1320
+ remoteSingletons: {
1321
+ [k: string]: {
1322
+ componentId: string;
1323
+ ownerInstanceId: string;
1324
+ connections: number;
1325
+ };
1326
+ };
1113
1327
  roomDetails: {
1114
1328
  [k: string]: number;
1115
1329
  };
@@ -1176,6 +1390,19 @@ interface LiveServerOptions {
1176
1390
  componentsPath?: string;
1177
1391
  /** HTTP monitoring routes prefix. Set to false to disable. Defaults to '/api/live' */
1178
1392
  httpPrefix?: string | false;
1393
+ /** Allowed origins for WebSocket connections (CSRF protection).
1394
+ * When set, connections from unlisted origins are rejected.
1395
+ * Example: ['https://myapp.com', 'http://localhost:3000'] */
1396
+ allowedOrigins?: string[];
1397
+ /** Optional cross-instance pub/sub adapter for horizontal scaling (e.g. Redis).
1398
+ * When provided, room events, state changes, and membership are propagated
1399
+ * across server instances. Without this, rooms are local to the current instance. */
1400
+ roomPubSub?: IRoomPubSubAdapter;
1401
+ /** Optional cluster adapter for cross-instance component synchronization.
1402
+ * When provided, singleton components are coordinated across instances,
1403
+ * component state is mirrored to a shared store (Redis), and actions on
1404
+ * remote singletons are forwarded to the owner instance. */
1405
+ cluster?: IClusterAdapter;
1179
1406
  }
1180
1407
  declare class LiveServer {
1181
1408
  readonly roomEvents: RoomEventBus;
@@ -1211,6 +1438,27 @@ declare class LiveServer {
1211
1438
  private buildHttpRoutes;
1212
1439
  }
1213
1440
 
1441
+ interface PendingMessage {
1442
+ type: string;
1443
+ componentId: string;
1444
+ payload: any;
1445
+ timestamp: number;
1446
+ userId?: string;
1447
+ room?: string;
1448
+ [key: string]: any;
1449
+ }
1450
+ /**
1451
+ * Queue a message to be sent on the next microtask flush.
1452
+ * Messages are batched per-WS and sent as a JSON array.
1453
+ */
1454
+ declare function queueWsMessage(ws: GenericWebSocket, message: PendingMessage): void;
1455
+ /**
1456
+ * Send a message immediately (bypass batching).
1457
+ * Used for ACTION_RESPONSE and other request-response patterns
1458
+ * where the client is awaiting an immediate response.
1459
+ */
1460
+ declare function sendImmediate(ws: GenericWebSocket, data: string): void;
1461
+
1214
1462
  /**
1215
1463
  * Encode a binary chunk message for transmission.
1216
1464
  * @param header - JSON metadata about the chunk
@@ -1314,6 +1562,26 @@ declare class RoomStateManager {
1314
1562
  };
1315
1563
  }
1316
1564
 
1565
+ declare class InMemoryRoomAdapter implements IRoomStorageAdapter, IRoomPubSubAdapter {
1566
+ private rooms;
1567
+ getOrCreateRoom(roomId: string, initialState?: any): Promise<{
1568
+ state: any;
1569
+ created: boolean;
1570
+ }>;
1571
+ getState(roomId: string): Promise<any>;
1572
+ updateState(roomId: string, updates: any): Promise<void>;
1573
+ hasRoom(roomId: string): Promise<boolean>;
1574
+ deleteRoom(roomId: string): Promise<boolean>;
1575
+ getStats(): Promise<{
1576
+ totalRooms: number;
1577
+ rooms: Record<string, any>;
1578
+ }>;
1579
+ publish(_roomId: string, _event: string, _data: any): Promise<void>;
1580
+ subscribe(_roomId: string, _handler: (event: string, data: any) => void): Promise<() => void>;
1581
+ publishMembership(_roomId: string, _action: 'join' | 'leave', _componentId: string): Promise<void>;
1582
+ publishStateChange(_roomId: string, _updates: any): Promise<void>;
1583
+ }
1584
+
1317
1585
  type LiveLogCategory = 'lifecycle' | 'messages' | 'state' | 'performance' | 'rooms' | 'websocket';
1318
1586
  type LiveLogConfig = boolean | readonly LiveLogCategory[];
1319
1587
  /**
@@ -1397,4 +1665,4 @@ interface UseTypedLiveComponentReturn<T extends LiveComponent<any>> {
1397
1665
  };
1398
1666
  }
1399
1667
 
1400
- export { ANONYMOUS_CONTEXT, type ActionNames, type ActionPayload, type ActionReturn, type ActiveUpload, AnonymousContext, AuthenticatedContext, type BinaryChunkHeader, type BroadcastMessage, type ComponentDefinition, type ComponentMetadata, type ComponentMetrics, type ComponentPerformanceMetrics, ComponentRegistry, type ComponentSnapshot, type ComponentState, type ConnectionConfig, type ConnectionHealth, type ConnectionMetrics, ConnectionRateLimiter, DEFAULT_CHUNK_SIZE, DEFAULT_WS_PATH, type DebugEvent, type DebugEventType, type DebugSnapshot, type DebugWsMessage, 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 InferComponentState, type InferPrivateState, type LiveActionAuth, type LiveActionAuthMap, type LiveAuthContext, type LiveAuthCredentials, LiveAuthManager, type LiveAuthProvider, type LiveAuthResult, type LiveAuthUser, LiveComponent, type LiveComponentAuth, type LiveComponentContext, type LiveComponentInstance, LiveDebugger, type LiveLogCategory, type LiveLogConfig, type LiveMessage, LiveRoomManager, LiveServer, type LiveServerOptions, type LiveTransport, type LiveWSData, PROTOCOL_VERSION, type PerformanceAlert, type PerformanceConfig, PerformanceMonitor, RateLimiterRegistry, RoomEventBus, type RoomInfo, type RoomMessage, 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, createTypedRoomEventBus, createTypedRoomState, decodeBinaryChunk, encodeBinaryChunk, getLiveComponentContext, liveLog, liveWarn, registerComponentLogging, setLiveComponentContext, unregisterComponentLogging };
1668
+ export { ANONYMOUS_CONTEXT, type ActionNames, type ActionPayload, type ActionReturn, type ActiveUpload, AnonymousContext, AuthenticatedContext, 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 ComponentSnapshot, type ComponentState, type ConnectionConfig, type ConnectionHealth, type ConnectionMetrics, ConnectionRateLimiter, DEFAULT_CHUNK_SIZE, DEFAULT_WS_PATH, type DebugEvent, type DebugEventType, type DebugSnapshot, type DebugWsMessage, 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 LiveActionAuth, type LiveActionAuthMap, type LiveAuthContext, type LiveAuthCredentials, LiveAuthManager, type LiveAuthProvider, type LiveAuthResult, type LiveAuthUser, LiveComponent, type LiveComponentAuth, type LiveComponentContext, type LiveComponentInstance, LiveDebugger, type LiveLogCategory, type LiveLogConfig, type LiveMessage, LiveRoomManager, LiveServer, type LiveServerOptions, type LiveTransport, type LiveWSData, PROTOCOL_VERSION, type PerformanceAlert, type PerformanceConfig, PerformanceMonitor, RateLimiterRegistry, RoomEventBus, type RoomInfo, type RoomMessage, 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, createTypedRoomEventBus, createTypedRoomState, decodeBinaryChunk, encodeBinaryChunk, getLiveComponentContext, liveLog, liveWarn, queueWsMessage, registerComponentLogging, sendImmediate, setLiveComponentContext, unregisterComponentLogging };