@fluxstack/live-client 0.3.1 → 0.4.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.
@@ -0,0 +1,478 @@
1
+ import { WebSocketMessage, WebSocketResponse, FileUploadCompleteResponse, BinaryChunkHeader } from '@fluxstack/live';
2
+
3
+ /** Auth credentials to send during WebSocket connection */
4
+ interface LiveAuthOptions {
5
+ /** JWT or opaque token */
6
+ token?: string;
7
+ /** Provider name (if multiple auth providers configured) */
8
+ provider?: string;
9
+ /** Additional credentials (publicKey, signature, etc.) */
10
+ [key: string]: unknown;
11
+ }
12
+ interface LiveConnectionOptions {
13
+ /** WebSocket URL. Auto-detected from window.location if omitted. */
14
+ url?: string;
15
+ /** Auth credentials to send on connection */
16
+ auth?: LiveAuthOptions;
17
+ /** Auto-connect on creation. Default: true */
18
+ autoConnect?: boolean;
19
+ /** Reconnect interval in ms. Default: 1000 */
20
+ reconnectInterval?: number;
21
+ /** Max reconnect attempts. Default: 5 */
22
+ maxReconnectAttempts?: number;
23
+ /** Heartbeat interval in ms. Default: 30000 */
24
+ heartbeatInterval?: number;
25
+ /** Enable debug logging. Default: false */
26
+ debug?: boolean;
27
+ }
28
+ interface LiveConnectionState {
29
+ connected: boolean;
30
+ connecting: boolean;
31
+ error: string | null;
32
+ connectionId: string | null;
33
+ authenticated: boolean;
34
+ }
35
+ type StateChangeCallback$1 = (state: LiveConnectionState) => void;
36
+ type ComponentCallback = (message: WebSocketResponse) => void;
37
+ /**
38
+ * Framework-agnostic WebSocket connection manager.
39
+ * Handles reconnection, heartbeat, request-response pattern, and message routing.
40
+ */
41
+ declare class LiveConnection {
42
+ private ws;
43
+ private options;
44
+ private reconnectAttempts;
45
+ private reconnectTimeout;
46
+ private heartbeatInterval;
47
+ private componentCallbacks;
48
+ private binaryCallbacks;
49
+ private roomBinaryHandlers;
50
+ private pendingRequests;
51
+ private stateListeners;
52
+ private _state;
53
+ constructor(options?: LiveConnectionOptions);
54
+ get state(): LiveConnectionState;
55
+ /** Subscribe to connection state changes */
56
+ onStateChange(callback: StateChangeCallback$1): () => void;
57
+ private setState;
58
+ private getWebSocketUrl;
59
+ private log;
60
+ /** Generate unique request ID */
61
+ generateRequestId(): string;
62
+ /** Connect to WebSocket server */
63
+ connect(): void;
64
+ /** Disconnect from WebSocket server */
65
+ disconnect(): void;
66
+ /** Manual reconnect */
67
+ reconnect(): void;
68
+ private attemptReconnect;
69
+ private startHeartbeat;
70
+ private stopHeartbeat;
71
+ private handleMessage;
72
+ /** Send message without waiting for response */
73
+ sendMessage(message: WebSocketMessage): Promise<void>;
74
+ /** Send message and wait for response */
75
+ sendMessageAndWait(message: WebSocketMessage, timeout?: number): Promise<WebSocketResponse>;
76
+ /** Send binary data and wait for response (for file uploads) */
77
+ sendBinaryAndWait(data: ArrayBuffer, requestId: string, timeout?: number): Promise<WebSocketResponse>;
78
+ /** Parse and route binary frames (state delta, room events, room state) */
79
+ private handleBinaryMessage;
80
+ /** Register a handler for binary room frames (0x02 / 0x03). Returns unsubscribe. */
81
+ registerRoomBinaryHandler(callback: (frame: Uint8Array) => void): () => void;
82
+ /** Register a binary message handler for a component */
83
+ registerBinaryHandler(componentId: string, callback: (payload: Uint8Array) => void): () => void;
84
+ /** Register a component message callback */
85
+ registerComponent(componentId: string, callback: ComponentCallback): () => void;
86
+ /** Unregister a component */
87
+ unregisterComponent(componentId: string): void;
88
+ /** Authenticate (or re-authenticate) the WebSocket connection */
89
+ authenticate(credentials: LiveAuthOptions): Promise<boolean>;
90
+ /** Get the raw WebSocket instance */
91
+ getWebSocket(): WebSocket | null;
92
+ /** Destroy the connection and clean up all resources */
93
+ destroy(): void;
94
+ }
95
+
96
+ interface LiveComponentOptions<TState = Record<string, any>> {
97
+ /** Initial state to merge with server defaults */
98
+ initialState?: Partial<TState>;
99
+ /** Room to join on mount */
100
+ room?: string;
101
+ /** User ID for component isolation */
102
+ userId?: string;
103
+ /** Auto-mount when connection is ready. Default: true */
104
+ autoMount?: boolean;
105
+ /** Enable debug logging. Default: false */
106
+ debug?: boolean;
107
+ }
108
+ type StateChangeCallback<TState> = (state: TState, delta: Partial<TState> | null) => void;
109
+ type ErrorCallback = (error: string) => void;
110
+ /**
111
+ * High-level handle for a live component instance.
112
+ * Manages mount lifecycle, state sync, and action calling.
113
+ * Framework-agnostic — works with vanilla JS, Vue, Svelte, etc.
114
+ */
115
+ declare class LiveComponentHandle<TState extends Record<string, any> = Record<string, any>> {
116
+ private connection;
117
+ private componentName;
118
+ private options;
119
+ private _componentId;
120
+ private _state;
121
+ private _mounted;
122
+ private _mounting;
123
+ private _error;
124
+ private stateListeners;
125
+ private errorListeners;
126
+ private unregisterComponent;
127
+ private unsubConnection;
128
+ constructor(connection: LiveConnection, componentName: string, options?: LiveComponentOptions<TState>);
129
+ /** Current component state */
130
+ get state(): Readonly<TState>;
131
+ /** Server-assigned component ID (null before mount) */
132
+ get componentId(): string | null;
133
+ /** Whether the component has been mounted */
134
+ get mounted(): boolean;
135
+ /** Whether the component is currently mounting */
136
+ get mounting(): boolean;
137
+ /** Last error message */
138
+ get error(): string | null;
139
+ /** Mount the component on the server */
140
+ mount(): Promise<void>;
141
+ /** Unmount the component from the server */
142
+ unmount(): Promise<void>;
143
+ /** Destroy the handle and clean up all resources */
144
+ destroy(): void;
145
+ /**
146
+ * Call an action on the server component.
147
+ * Returns the action's return value.
148
+ */
149
+ call<R = any>(action: string, payload?: Record<string, any>): Promise<R>;
150
+ /**
151
+ * Fire an action without waiting for a response (fire-and-forget).
152
+ * Useful for high-frequency operations like game input where the
153
+ * server doesn't need to send back a result.
154
+ */
155
+ fire(action: string, payload?: Record<string, any>): void;
156
+ /**
157
+ * Subscribe to state changes.
158
+ * Callback receives the full new state and the delta (or null for full updates).
159
+ * Returns an unsubscribe function.
160
+ */
161
+ onStateChange(callback: StateChangeCallback<TState>): () => void;
162
+ /**
163
+ * Register a binary decoder for this component.
164
+ * When the server sends a BINARY_STATE_DELTA frame targeting this component,
165
+ * the decoder converts the raw payload into a delta object which is merged into state.
166
+ * Returns an unsubscribe function.
167
+ */
168
+ setBinaryDecoder(decoder: (buffer: Uint8Array) => Record<string, any>): () => void;
169
+ /**
170
+ * Subscribe to errors.
171
+ * Returns an unsubscribe function.
172
+ */
173
+ onError(callback: ErrorCallback): () => void;
174
+ private handleServerMessage;
175
+ private notifyStateChange;
176
+ private notifyError;
177
+ private cleanup;
178
+ private log;
179
+ }
180
+
181
+ type EventHandler<T = any> = (data: T) => void;
182
+ type Unsubscribe = () => void;
183
+ /** Reserved keys on RoomHandle/RoomProxy — cannot be state fields */
184
+ type RoomReservedKeys = 'id' | 'joined' | 'state' | 'join' | 'leave' | 'emit' | 'on' | 'onSystem' | 'setState';
185
+ /** State fields accessible directly on handle/proxy (excludes reserved method names) */
186
+ type RoomStateFields<TState> = TState extends Record<string, any> ? {
187
+ readonly [K in Exclude<keyof TState, RoomReservedKeys>]: TState[K];
188
+ } : unknown;
189
+ /** Message from client to server */
190
+ interface RoomClientMessage {
191
+ type: 'ROOM_JOIN' | 'ROOM_LEAVE' | 'ROOM_EMIT' | 'ROOM_STATE_GET' | 'ROOM_STATE_SET';
192
+ componentId: string;
193
+ roomId: string;
194
+ event?: string;
195
+ data?: any;
196
+ timestamp: number;
197
+ }
198
+ /** Message from server to client */
199
+ interface RoomServerMessage {
200
+ type: 'ROOM_EVENT' | 'ROOM_STATE' | 'ROOM_SYSTEM' | 'ROOM_JOINED' | 'ROOM_LEFT';
201
+ componentId: string;
202
+ roomId: string;
203
+ event: string;
204
+ data: any;
205
+ timestamp: number;
206
+ }
207
+ /** Interface of an individual room handle */
208
+ type RoomHandle<TState = any, TEvents extends Record<string, any> = Record<string, any>> = {
209
+ readonly id: string;
210
+ readonly joined: boolean;
211
+ readonly state: TState;
212
+ join: (initialState?: TState) => Promise<void>;
213
+ leave: () => Promise<void>;
214
+ emit: <K extends keyof TEvents>(event: K, data: TEvents[K]) => void;
215
+ on: <K extends keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>) => Unsubscribe;
216
+ onSystem: (event: string, handler: EventHandler) => Unsubscribe;
217
+ setState: (updates: Partial<TState>) => void;
218
+ } & RoomStateFields<TState>;
219
+ /** Infer TEvents from a LiveRoom class (via $events brand) or use T directly as events map */
220
+ type InferRoomEvents<T> = T extends {
221
+ $events: infer E extends Record<string, any>;
222
+ } ? E : T extends Record<string, any> ? T : Record<string, any>;
223
+ /** Proxy interface for $room - callable as function or object */
224
+ type RoomProxy<TState = any, TEvents extends Record<string, any> = Record<string, any>> = {
225
+ /** Get a typed room handle. Pass the Room class or events interface as generic:
226
+ * `$room<CounterRoom>('counter:global').on('counter:updated', data => ...)` */
227
+ <T = TEvents>(roomId: string): RoomHandle<any, InferRoomEvents<T>>;
228
+ readonly id: string | null;
229
+ readonly joined: boolean;
230
+ readonly state: TState;
231
+ join: (initialState?: TState) => Promise<void>;
232
+ leave: () => Promise<void>;
233
+ emit: <K extends keyof TEvents>(event: K, data: TEvents[K]) => void;
234
+ on: <K extends keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>) => Unsubscribe;
235
+ onSystem: (event: string, handler: EventHandler) => Unsubscribe;
236
+ setState: (updates: Partial<TState>) => void;
237
+ } & RoomStateFields<TState>;
238
+ interface RoomManagerOptions {
239
+ componentId: string | null;
240
+ defaultRoom?: string;
241
+ sendMessage: (msg: any) => void;
242
+ sendMessageAndWait: (msg: any, timeout?: number) => Promise<any>;
243
+ onMessage: (handler: (msg: RoomServerMessage) => void) => Unsubscribe;
244
+ /** Optional: register for binary room frames (0x02 ROOM_EVENT, 0x03 ROOM_STATE) */
245
+ onBinaryMessage?: (handler: (frame: Uint8Array) => void) => Unsubscribe;
246
+ }
247
+ /** Client-side room manager. Framework-agnostic. */
248
+ declare class RoomManager<TState = any, TEvents extends Record<string, any> = Record<string, any>> {
249
+ private componentId;
250
+ private defaultRoom;
251
+ private rooms;
252
+ private handles;
253
+ private sendMessage;
254
+ private sendMessageAndWait;
255
+ private globalUnsubscribe;
256
+ private binaryUnsubscribe;
257
+ private onBinaryMessage;
258
+ private onMessageFactory;
259
+ constructor(options: RoomManagerOptions);
260
+ /** Re-subscribe message and binary handlers (needed after destroy/remount in React Strict Mode) */
261
+ resubscribe(): void;
262
+ private handleServerMessage;
263
+ /** Handle binary room frames (0x02 ROOM_EVENT, 0x03 ROOM_STATE) */
264
+ private handleBinaryFrame;
265
+ private getOrCreateRoom;
266
+ /** Create handle for a specific room (cached) */
267
+ createHandle(roomId: string): RoomHandle<TState, TEvents>;
268
+ /** Create the $room proxy */
269
+ createProxy(): RoomProxy<TState, TEvents>;
270
+ /** List of rooms currently joined */
271
+ getJoinedRooms(): string[];
272
+ /** Update componentId (when component mounts) */
273
+ setComponentId(id: string | null): void;
274
+ /** Cleanup — unsubscribes handlers but keeps factory refs for resubscribe() */
275
+ destroy(): void;
276
+ }
277
+
278
+ interface AdaptiveChunkConfig {
279
+ minChunkSize: number;
280
+ maxChunkSize: number;
281
+ initialChunkSize: number;
282
+ targetLatency: number;
283
+ adjustmentFactor: number;
284
+ measurementWindow: number;
285
+ }
286
+ interface ChunkMetrics {
287
+ chunkIndex: number;
288
+ chunkSize: number;
289
+ startTime: number;
290
+ endTime: number;
291
+ latency: number;
292
+ throughput: number;
293
+ success: boolean;
294
+ }
295
+ declare class AdaptiveChunkSizer {
296
+ private config;
297
+ private currentChunkSize;
298
+ private metrics;
299
+ private consecutiveErrors;
300
+ private consecutiveSuccesses;
301
+ constructor(config?: Partial<AdaptiveChunkConfig>);
302
+ getChunkSize(): number;
303
+ recordChunkStart(_chunkIndex: number): number;
304
+ recordChunkComplete(chunkIndex: number, chunkSize: number, startTime: number, success: boolean): void;
305
+ private adjustUp;
306
+ private adjustDown;
307
+ getAverageThroughput(): number;
308
+ getStats(): {
309
+ currentChunkSize: number;
310
+ averageThroughput: number;
311
+ consecutiveSuccesses: number;
312
+ consecutiveErrors: number;
313
+ totalMeasurements: number;
314
+ };
315
+ reset(): void;
316
+ }
317
+ /**
318
+ * Creates a binary message with header + data
319
+ * Format: [4 bytes header length LE][JSON header][binary data]
320
+ */
321
+ declare function createBinaryChunkMessage(header: BinaryChunkHeader, chunkData: Uint8Array): ArrayBuffer;
322
+ interface ChunkedUploadOptions {
323
+ chunkSize?: number;
324
+ maxFileSize?: number;
325
+ allowedTypes?: string[];
326
+ sendMessageAndWait: (message: any, timeout?: number) => Promise<any>;
327
+ sendBinaryAndWait?: (data: ArrayBuffer, requestId: string, timeout?: number) => Promise<any>;
328
+ onProgress?: (progress: number, bytesUploaded: number, totalBytes: number) => void;
329
+ onComplete?: (response: FileUploadCompleteResponse) => void;
330
+ onError?: (error: string) => void;
331
+ adaptiveChunking?: boolean;
332
+ adaptiveConfig?: Partial<AdaptiveChunkConfig>;
333
+ useBinaryProtocol?: boolean;
334
+ }
335
+ interface ChunkedUploadState {
336
+ uploading: boolean;
337
+ progress: number;
338
+ error: string | null;
339
+ uploadId: string | null;
340
+ bytesUploaded: number;
341
+ totalBytes: number;
342
+ }
343
+ /**
344
+ * Framework-agnostic chunked file uploader.
345
+ * Manages the upload lifecycle without any UI framework dependency.
346
+ */
347
+ declare class ChunkedUploader {
348
+ private componentId;
349
+ private options;
350
+ private abortController;
351
+ private adaptiveSizer;
352
+ private _state;
353
+ private stateListeners;
354
+ constructor(componentId: string, options: ChunkedUploadOptions);
355
+ get state(): ChunkedUploadState;
356
+ onStateChange(callback: (state: ChunkedUploadState) => void): () => void;
357
+ private setState;
358
+ uploadFile(file: File): Promise<void>;
359
+ cancelUpload(): void;
360
+ reset(): void;
361
+ }
362
+
363
+ interface PersistedState {
364
+ componentName: string;
365
+ signedState: any;
366
+ room?: string;
367
+ userId?: string;
368
+ lastUpdate: number;
369
+ }
370
+ declare function persistState(enabled: boolean, name: string, signedState: any, room?: string, userId?: string): void;
371
+ declare function getPersistedState(enabled: boolean, name: string): PersistedState | null;
372
+ declare function clearPersistedState(enabled: boolean, name: string): void;
373
+
374
+ interface StateValidation {
375
+ checksum: string;
376
+ version: number;
377
+ timestamp: number;
378
+ source: 'client' | 'server' | 'mount';
379
+ }
380
+ interface StateConflict {
381
+ property: string;
382
+ clientValue: any;
383
+ serverValue: any;
384
+ timestamp: number;
385
+ resolved: boolean;
386
+ }
387
+ interface HybridState<T> {
388
+ data: T;
389
+ validation: StateValidation;
390
+ status: 'synced' | 'pending' | 'conflict';
391
+ }
392
+ declare class StateValidator {
393
+ static generateChecksum(state: any): string;
394
+ static createValidation(state: any, source?: 'client' | 'server' | 'mount'): StateValidation;
395
+ static detectConflicts<T>(clientState: T, serverState: T, excludeFields?: string[]): StateConflict[];
396
+ static mergeStates<T>(clientState: T, serverState: T, conflicts: StateConflict[], strategy?: 'client' | 'server' | 'smart'): T;
397
+ static validateState<T>(hybridState: HybridState<T>): boolean;
398
+ static updateValidation<T>(hybridState: HybridState<T>, source?: 'client' | 'server' | 'mount'): HybridState<T>;
399
+ }
400
+
401
+ /** Status listeners for the shared connection */
402
+ type ConnectionStatusCallback = (connected: boolean) => void;
403
+ interface UseLiveOptions {
404
+ /** WebSocket URL. Auto-detected from window.location if omitted. */
405
+ url?: string;
406
+ /** Room to join on mount */
407
+ room?: string;
408
+ /** User ID for component isolation */
409
+ userId?: string;
410
+ /** Auto-mount when connected. Default: true */
411
+ autoMount?: boolean;
412
+ /** Enable debug logging. Default: false */
413
+ debug?: boolean;
414
+ }
415
+ interface UseLiveHandle<TState extends Record<string, any> = Record<string, any>> {
416
+ /** Call a server action */
417
+ call: <R = any>(action: string, payload?: Record<string, any>) => Promise<R>;
418
+ /** Subscribe to state changes. Returns unsubscribe function. */
419
+ on: (callback: (state: TState, delta: Partial<TState> | null) => void) => () => void;
420
+ /** Subscribe to errors. Returns unsubscribe function. */
421
+ onError: (callback: (error: string) => void) => () => void;
422
+ /** Current state (read-only snapshot) */
423
+ readonly state: Readonly<TState>;
424
+ /** Whether the component is mounted on the server */
425
+ readonly mounted: boolean;
426
+ /** Server-assigned component ID */
427
+ readonly componentId: string | null;
428
+ /** Last error message */
429
+ readonly error: string | null;
430
+ /** Destroy the component and clean up */
431
+ destroy: () => void;
432
+ /** Access the underlying LiveComponentHandle */
433
+ readonly handle: LiveComponentHandle<TState>;
434
+ }
435
+ /**
436
+ * Create a live component with minimal boilerplate.
437
+ * Manages the WebSocket connection automatically (singleton).
438
+ *
439
+ * @example Browser IIFE
440
+ * ```html
441
+ * <script src="/live-client.js"></script>
442
+ * <script>
443
+ * const counter = FluxstackLive.useLive('Counter', { count: 0 })
444
+ * counter.on(state => {
445
+ * document.getElementById('count').textContent = state.count
446
+ * })
447
+ * document.querySelector('.inc').onclick = () => counter.call('increment')
448
+ * </script>
449
+ * ```
450
+ *
451
+ * @example ES modules
452
+ * ```ts
453
+ * import { useLive } from '@fluxstack/live-client'
454
+ * const counter = useLive('Counter', { count: 0 }, { url: 'ws://localhost:3000/api/live/ws' })
455
+ * counter.on(state => console.log(state.count))
456
+ * counter.call('increment')
457
+ * ```
458
+ */
459
+ declare function useLive<TState extends Record<string, any> = Record<string, any>>(componentName: string, initialState: TState, options?: UseLiveOptions): UseLiveHandle<TState>;
460
+ /**
461
+ * Subscribe to the shared connection status (connected/disconnected).
462
+ * Useful for showing a global status indicator.
463
+ *
464
+ * @example
465
+ * ```js
466
+ * FluxstackLive.onConnectionChange(connected => {
467
+ * statusEl.textContent = connected ? 'Connected' : 'Disconnected'
468
+ * })
469
+ * ```
470
+ */
471
+ declare function onConnectionChange(callback: ConnectionStatusCallback): () => void;
472
+ /**
473
+ * Get or create the shared connection instance.
474
+ * Useful when you need direct access to the connection.
475
+ */
476
+ declare function getConnection(url?: string): LiveConnection;
477
+
478
+ export { type AdaptiveChunkConfig, AdaptiveChunkSizer, type ChunkMetrics, type ChunkedUploadOptions, type ChunkedUploadState, ChunkedUploader, type EventHandler, type HybridState, type LiveAuthOptions, LiveComponentHandle, type LiveComponentOptions, LiveConnection, type LiveConnectionOptions, type LiveConnectionState, type PersistedState, type RoomClientMessage, type RoomHandle, RoomManager, type RoomManagerOptions, type RoomProxy, type RoomServerMessage, type StateConflict, type StateValidation, StateValidator, type Unsubscribe, type UseLiveHandle, type UseLiveOptions, clearPersistedState, createBinaryChunkMessage, getConnection, getPersistedState, onConnectionChange, persistState, useLive };
package/package.json CHANGED
@@ -1,15 +1,18 @@
1
1
  {
2
2
  "name": "@fluxstack/live-client",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Browser WebSocket client for @fluxstack/live — connection, state sync, rooms, and file upload (framework-agnostic)",
5
5
  "type": "module",
6
- "main": "./dist/index.js",
6
+ "main": "./dist/index.cjs",
7
7
  "module": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",
9
+ "sideEffects": false,
9
10
  "exports": {
10
11
  ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "bun": "./src/index.ts",
11
14
  "import": "./dist/index.js",
12
- "types": "./dist/index.d.ts"
15
+ "require": "./dist/index.cjs"
13
16
  },
14
17
  "./browser": "./dist/live-client.browser.global.js"
15
18
  },
@@ -23,7 +26,7 @@
23
26
  "typecheck": "tsc --noEmit"
24
27
  },
25
28
  "dependencies": {
26
- "@fluxstack/live": "^0.3.0"
29
+ "@fluxstack/live": "^0.4.0"
27
30
  },
28
31
  "devDependencies": {
29
32
  "tsup": "^8.4.0",
@@ -37,5 +40,9 @@
37
40
  "url": "https://github.com/FluxStackCore/fluxstack-live",
38
41
  "directory": "packages/client"
39
42
  },
40
- "license": "MIT"
43
+ "license": "MIT",
44
+ "keywords": ["websocket", "client", "realtime", "state-sync", "live-components", "fluxstack"],
45
+ "engines": {
46
+ "node": ">=18.0.0"
47
+ }
41
48
  }