@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 +145 -0
- package/dist/index.d.ts +298 -30
- package/dist/index.js +1355 -495
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
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
|
-
|
|
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
|
-
}):
|
|
471
|
-
validateState(signedState: SignedState
|
|
545
|
+
}): SignedState;
|
|
546
|
+
validateState(signedState: SignedState, options?: {
|
|
547
|
+
skipNonce?: boolean;
|
|
548
|
+
}): {
|
|
472
549
|
valid: boolean;
|
|
473
550
|
error?: string;
|
|
474
|
-
}
|
|
475
|
-
extractData(signedState: SignedState):
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
}):
|
|
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):
|
|
1005
|
-
protected unsubscribeFromRoom():
|
|
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):
|
|
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 };
|