@fluxstack/live-client 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +179 -0
- package/dist/index.d.ts +126 -7
- package/dist/index.js +422 -14
- package/dist/index.js.map +1 -1
- package/dist/live-client.browser.global.js +422 -14
- package/dist/live-client.browser.global.js.map +1 -1
- package/package.json +42 -39
package/README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# @fluxstack/live-client
|
|
2
|
+
|
|
3
|
+
Framework-agnostic browser client for `@fluxstack/live`.
|
|
4
|
+
|
|
5
|
+
Works with any UI framework (React, Vue, Svelte, vanilla JS) or no framework at all. For React-specific bindings, see `@fluxstack/live-react`.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
### NPM/Bun
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun add @fluxstack/live-client
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Browser (IIFE)
|
|
16
|
+
|
|
17
|
+
```html
|
|
18
|
+
<script src="/live-client.js"></script>
|
|
19
|
+
<script>
|
|
20
|
+
const counter = FluxstackLive.useLive('Counter', { count: 0 })
|
|
21
|
+
</script>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The IIFE bundle is served automatically by any transport adapter at `/live-client.js`.
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
### Vanilla JS (useLive)
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { useLive, onConnectionChange } from '@fluxstack/live-client'
|
|
32
|
+
|
|
33
|
+
// Mount a component (uses singleton connection)
|
|
34
|
+
const counter = useLive('Counter', { count: 0 })
|
|
35
|
+
|
|
36
|
+
// Listen for state changes
|
|
37
|
+
counter.on(state => {
|
|
38
|
+
document.getElementById('count').textContent = state.count
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// Call server actions
|
|
42
|
+
document.getElementById('btn').onclick = () => counter.call('increment')
|
|
43
|
+
|
|
44
|
+
// Connection status
|
|
45
|
+
onConnectionChange(connected => {
|
|
46
|
+
console.log(connected ? 'Online' : 'Offline')
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Manual Connection
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { LiveConnection, LiveComponentHandle } from '@fluxstack/live-client'
|
|
54
|
+
|
|
55
|
+
const conn = new LiveConnection({
|
|
56
|
+
url: 'ws://localhost:3000/api/live/ws',
|
|
57
|
+
autoReconnect: true,
|
|
58
|
+
reconnectInterval: 1000,
|
|
59
|
+
})
|
|
60
|
+
await conn.connect()
|
|
61
|
+
|
|
62
|
+
const counter = new LiveComponentHandle(conn, {
|
|
63
|
+
componentName: 'Counter',
|
|
64
|
+
defaultState: { count: 0 },
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
counter.on(state => console.log('Count:', state.count))
|
|
68
|
+
await counter.call('increment')
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## API
|
|
72
|
+
|
|
73
|
+
### `useLive(componentName, defaultState, options?)`
|
|
74
|
+
|
|
75
|
+
High-level API using a shared connection singleton:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
const handle = useLive('Counter', { count: 0 }, {
|
|
79
|
+
url: 'ws://localhost:3000/api/live/ws', // Auto-detected if omitted
|
|
80
|
+
room: 'my-room', // Optional room
|
|
81
|
+
singleton: true, // Singleton mount
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
handle.on(state => { ... }) // State change listener
|
|
85
|
+
handle.call('action', payload) // Call server action
|
|
86
|
+
handle.destroy() // Unmount component
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### `onConnectionChange(callback)`
|
|
90
|
+
|
|
91
|
+
Subscribe to connection status changes:
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
const unsubscribe = onConnectionChange(connected => { ... })
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### `getConnection()`
|
|
98
|
+
|
|
99
|
+
Access the shared connection singleton:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
const conn = getConnection()
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### `LiveConnection`
|
|
106
|
+
|
|
107
|
+
WebSocket connection manager with auto-reconnect, state rehydration, and message routing:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
const conn = new LiveConnection({
|
|
111
|
+
url: 'ws://localhost:3000/api/live/ws',
|
|
112
|
+
autoReconnect: true,
|
|
113
|
+
reconnectInterval: 1000,
|
|
114
|
+
auth: {
|
|
115
|
+
token: 'my-jwt-token',
|
|
116
|
+
provider: 'jwt',
|
|
117
|
+
},
|
|
118
|
+
})
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### `LiveComponentHandle`
|
|
122
|
+
|
|
123
|
+
Handle to a remote component. Manages mounting, state updates, and action calls:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
const handle = new LiveComponentHandle(conn, {
|
|
127
|
+
componentName: 'Counter',
|
|
128
|
+
defaultState: { count: 0 },
|
|
129
|
+
room: 'optional-room',
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
handle.on(state => { ... }) // State listener
|
|
133
|
+
handle.call('action', payload) // Call action
|
|
134
|
+
handle.$state // Current state
|
|
135
|
+
handle.$connected // Connection status
|
|
136
|
+
handle.destroy() // Unmount
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### `RoomManager`
|
|
140
|
+
|
|
141
|
+
Client-side room event management:
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
const rooms = new RoomManager(conn)
|
|
145
|
+
const room = rooms.join('chat-room')
|
|
146
|
+
room.on('message:new', data => { ... })
|
|
147
|
+
room.emit('typing', { user: 'Alice' })
|
|
148
|
+
room.leave()
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### `ChunkedUploader`
|
|
152
|
+
|
|
153
|
+
Stream file uploads over WebSocket:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
const uploader = new ChunkedUploader(conn, {
|
|
157
|
+
chunkSize: 64 * 1024,
|
|
158
|
+
onProgress: (progress) => { ... },
|
|
159
|
+
})
|
|
160
|
+
await uploader.upload(file)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### State Persistence
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import { persistState, getPersistedState, clearPersistedState } from '@fluxstack/live-client'
|
|
167
|
+
|
|
168
|
+
persistState('Counter', componentId, state, signature)
|
|
169
|
+
const saved = getPersistedState('Counter', componentId)
|
|
170
|
+
clearPersistedState('Counter', componentId)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Browser IIFE Build
|
|
174
|
+
|
|
175
|
+
The package includes a pre-built browser bundle at `dist/live-client.browser.global.js` that exposes a `FluxstackLive` global with all exports. Transport adapters serve this automatically at `/live-client.js`.
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT
|
package/dist/index.d.ts
CHANGED
|
@@ -45,6 +45,8 @@ declare class LiveConnection {
|
|
|
45
45
|
private reconnectTimeout;
|
|
46
46
|
private heartbeatInterval;
|
|
47
47
|
private componentCallbacks;
|
|
48
|
+
private binaryCallbacks;
|
|
49
|
+
private roomBinaryHandlers;
|
|
48
50
|
private pendingRequests;
|
|
49
51
|
private stateListeners;
|
|
50
52
|
private _state;
|
|
@@ -73,6 +75,12 @@ declare class LiveConnection {
|
|
|
73
75
|
sendMessageAndWait(message: WebSocketMessage, timeout?: number): Promise<WebSocketResponse>;
|
|
74
76
|
/** Send binary data and wait for response (for file uploads) */
|
|
75
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;
|
|
76
84
|
/** Register a component message callback */
|
|
77
85
|
registerComponent(componentId: string, callback: ComponentCallback): () => void;
|
|
78
86
|
/** Unregister a component */
|
|
@@ -139,12 +147,25 @@ declare class LiveComponentHandle<TState extends Record<string, any> = Record<st
|
|
|
139
147
|
* Returns the action's return value.
|
|
140
148
|
*/
|
|
141
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;
|
|
142
156
|
/**
|
|
143
157
|
* Subscribe to state changes.
|
|
144
158
|
* Callback receives the full new state and the delta (or null for full updates).
|
|
145
159
|
* Returns an unsubscribe function.
|
|
146
160
|
*/
|
|
147
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;
|
|
148
169
|
/**
|
|
149
170
|
* Subscribe to errors.
|
|
150
171
|
* Returns an unsubscribe function.
|
|
@@ -159,6 +180,12 @@ declare class LiveComponentHandle<TState extends Record<string, any> = Record<st
|
|
|
159
180
|
|
|
160
181
|
type EventHandler<T = any> = (data: T) => void;
|
|
161
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;
|
|
162
189
|
/** Message from client to server */
|
|
163
190
|
interface RoomClientMessage {
|
|
164
191
|
type: 'ROOM_JOIN' | 'ROOM_LEAVE' | 'ROOM_EMIT' | 'ROOM_STATE_GET' | 'ROOM_STATE_SET';
|
|
@@ -178,7 +205,7 @@ interface RoomServerMessage {
|
|
|
178
205
|
timestamp: number;
|
|
179
206
|
}
|
|
180
207
|
/** Interface of an individual room handle */
|
|
181
|
-
|
|
208
|
+
type RoomHandle<TState = any, TEvents extends Record<string, any> = Record<string, any>> = {
|
|
182
209
|
readonly id: string;
|
|
183
210
|
readonly joined: boolean;
|
|
184
211
|
readonly state: TState;
|
|
@@ -188,10 +215,16 @@ interface RoomHandle<TState = any, TEvents extends Record<string, any> = Record<
|
|
|
188
215
|
on: <K extends keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>) => Unsubscribe;
|
|
189
216
|
onSystem: (event: string, handler: EventHandler) => Unsubscribe;
|
|
190
217
|
setState: (updates: Partial<TState>) => void;
|
|
191
|
-
}
|
|
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>;
|
|
192
223
|
/** Proxy interface for $room - callable as function or object */
|
|
193
|
-
|
|
194
|
-
|
|
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>>;
|
|
195
228
|
readonly id: string | null;
|
|
196
229
|
readonly joined: boolean;
|
|
197
230
|
readonly state: TState;
|
|
@@ -201,13 +234,15 @@ interface RoomProxy<TState = any, TEvents extends Record<string, any> = Record<s
|
|
|
201
234
|
on: <K extends keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>) => Unsubscribe;
|
|
202
235
|
onSystem: (event: string, handler: EventHandler) => Unsubscribe;
|
|
203
236
|
setState: (updates: Partial<TState>) => void;
|
|
204
|
-
}
|
|
237
|
+
} & RoomStateFields<TState>;
|
|
205
238
|
interface RoomManagerOptions {
|
|
206
239
|
componentId: string | null;
|
|
207
240
|
defaultRoom?: string;
|
|
208
241
|
sendMessage: (msg: any) => void;
|
|
209
242
|
sendMessageAndWait: (msg: any, timeout?: number) => Promise<any>;
|
|
210
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;
|
|
211
246
|
}
|
|
212
247
|
/** Client-side room manager. Framework-agnostic. */
|
|
213
248
|
declare class RoomManager<TState = any, TEvents extends Record<string, any> = Record<string, any>> {
|
|
@@ -218,8 +253,15 @@ declare class RoomManager<TState = any, TEvents extends Record<string, any> = Re
|
|
|
218
253
|
private sendMessage;
|
|
219
254
|
private sendMessageAndWait;
|
|
220
255
|
private globalUnsubscribe;
|
|
256
|
+
private binaryUnsubscribe;
|
|
257
|
+
private onBinaryMessage;
|
|
258
|
+
private onMessageFactory;
|
|
221
259
|
constructor(options: RoomManagerOptions);
|
|
260
|
+
/** Re-subscribe message and binary handlers (needed after destroy/remount in React Strict Mode) */
|
|
261
|
+
resubscribe(): void;
|
|
222
262
|
private handleServerMessage;
|
|
263
|
+
/** Handle binary room frames (0x02 ROOM_EVENT, 0x03 ROOM_STATE) */
|
|
264
|
+
private handleBinaryFrame;
|
|
223
265
|
private getOrCreateRoom;
|
|
224
266
|
/** Create handle for a specific room (cached) */
|
|
225
267
|
createHandle(roomId: string): RoomHandle<TState, TEvents>;
|
|
@@ -229,7 +271,7 @@ declare class RoomManager<TState = any, TEvents extends Record<string, any> = Re
|
|
|
229
271
|
getJoinedRooms(): string[];
|
|
230
272
|
/** Update componentId (when component mounts) */
|
|
231
273
|
setComponentId(id: string | null): void;
|
|
232
|
-
/** Cleanup */
|
|
274
|
+
/** Cleanup — unsubscribes handlers but keeps factory refs for resubscribe() */
|
|
233
275
|
destroy(): void;
|
|
234
276
|
}
|
|
235
277
|
|
|
@@ -356,4 +398,81 @@ declare class StateValidator {
|
|
|
356
398
|
static updateValidation<T>(hybridState: HybridState<T>, source?: 'client' | 'server' | 'mount'): HybridState<T>;
|
|
357
399
|
}
|
|
358
400
|
|
|
359
|
-
|
|
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 };
|