@fluxstack/live-client 0.2.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/dist/index.d.ts +31 -7
- package/dist/index.js +287 -18
- package/dist/index.js.map +1 -1
- package/dist/live-client.browser.global.js +287 -18
- package/dist/live-client.browser.global.js.map +1 -1
- package/package.json +42 -39
package/dist/index.d.ts
CHANGED
|
@@ -46,6 +46,7 @@ declare class LiveConnection {
|
|
|
46
46
|
private heartbeatInterval;
|
|
47
47
|
private componentCallbacks;
|
|
48
48
|
private binaryCallbacks;
|
|
49
|
+
private roomBinaryHandlers;
|
|
49
50
|
private pendingRequests;
|
|
50
51
|
private stateListeners;
|
|
51
52
|
private _state;
|
|
@@ -74,8 +75,10 @@ declare class LiveConnection {
|
|
|
74
75
|
sendMessageAndWait(message: WebSocketMessage, timeout?: number): Promise<WebSocketResponse>;
|
|
75
76
|
/** Send binary data and wait for response (for file uploads) */
|
|
76
77
|
sendBinaryAndWait(data: ArrayBuffer, requestId: string, timeout?: number): Promise<WebSocketResponse>;
|
|
77
|
-
/** Parse and route
|
|
78
|
+
/** Parse and route binary frames (state delta, room events, room state) */
|
|
78
79
|
private handleBinaryMessage;
|
|
80
|
+
/** Register a handler for binary room frames (0x02 / 0x03). Returns unsubscribe. */
|
|
81
|
+
registerRoomBinaryHandler(callback: (frame: Uint8Array) => void): () => void;
|
|
79
82
|
/** Register a binary message handler for a component */
|
|
80
83
|
registerBinaryHandler(componentId: string, callback: (payload: Uint8Array) => void): () => void;
|
|
81
84
|
/** Register a component message callback */
|
|
@@ -177,6 +180,12 @@ declare class LiveComponentHandle<TState extends Record<string, any> = Record<st
|
|
|
177
180
|
|
|
178
181
|
type EventHandler<T = any> = (data: T) => void;
|
|
179
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;
|
|
180
189
|
/** Message from client to server */
|
|
181
190
|
interface RoomClientMessage {
|
|
182
191
|
type: 'ROOM_JOIN' | 'ROOM_LEAVE' | 'ROOM_EMIT' | 'ROOM_STATE_GET' | 'ROOM_STATE_SET';
|
|
@@ -196,7 +205,7 @@ interface RoomServerMessage {
|
|
|
196
205
|
timestamp: number;
|
|
197
206
|
}
|
|
198
207
|
/** Interface of an individual room handle */
|
|
199
|
-
|
|
208
|
+
type RoomHandle<TState = any, TEvents extends Record<string, any> = Record<string, any>> = {
|
|
200
209
|
readonly id: string;
|
|
201
210
|
readonly joined: boolean;
|
|
202
211
|
readonly state: TState;
|
|
@@ -206,10 +215,16 @@ interface RoomHandle<TState = any, TEvents extends Record<string, any> = Record<
|
|
|
206
215
|
on: <K extends keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>) => Unsubscribe;
|
|
207
216
|
onSystem: (event: string, handler: EventHandler) => Unsubscribe;
|
|
208
217
|
setState: (updates: Partial<TState>) => void;
|
|
209
|
-
}
|
|
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>;
|
|
210
223
|
/** Proxy interface for $room - callable as function or object */
|
|
211
|
-
|
|
212
|
-
|
|
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>>;
|
|
213
228
|
readonly id: string | null;
|
|
214
229
|
readonly joined: boolean;
|
|
215
230
|
readonly state: TState;
|
|
@@ -219,13 +234,15 @@ interface RoomProxy<TState = any, TEvents extends Record<string, any> = Record<s
|
|
|
219
234
|
on: <K extends keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>) => Unsubscribe;
|
|
220
235
|
onSystem: (event: string, handler: EventHandler) => Unsubscribe;
|
|
221
236
|
setState: (updates: Partial<TState>) => void;
|
|
222
|
-
}
|
|
237
|
+
} & RoomStateFields<TState>;
|
|
223
238
|
interface RoomManagerOptions {
|
|
224
239
|
componentId: string | null;
|
|
225
240
|
defaultRoom?: string;
|
|
226
241
|
sendMessage: (msg: any) => void;
|
|
227
242
|
sendMessageAndWait: (msg: any, timeout?: number) => Promise<any>;
|
|
228
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;
|
|
229
246
|
}
|
|
230
247
|
/** Client-side room manager. Framework-agnostic. */
|
|
231
248
|
declare class RoomManager<TState = any, TEvents extends Record<string, any> = Record<string, any>> {
|
|
@@ -236,8 +253,15 @@ declare class RoomManager<TState = any, TEvents extends Record<string, any> = Re
|
|
|
236
253
|
private sendMessage;
|
|
237
254
|
private sendMessageAndWait;
|
|
238
255
|
private globalUnsubscribe;
|
|
256
|
+
private binaryUnsubscribe;
|
|
257
|
+
private onBinaryMessage;
|
|
258
|
+
private onMessageFactory;
|
|
239
259
|
constructor(options: RoomManagerOptions);
|
|
260
|
+
/** Re-subscribe message and binary handlers (needed after destroy/remount in React Strict Mode) */
|
|
261
|
+
resubscribe(): void;
|
|
240
262
|
private handleServerMessage;
|
|
263
|
+
/** Handle binary room frames (0x02 ROOM_EVENT, 0x03 ROOM_STATE) */
|
|
264
|
+
private handleBinaryFrame;
|
|
241
265
|
private getOrCreateRoom;
|
|
242
266
|
/** Create handle for a specific room (cached) */
|
|
243
267
|
createHandle(roomId: string): RoomHandle<TState, TEvents>;
|
|
@@ -247,7 +271,7 @@ declare class RoomManager<TState = any, TEvents extends Record<string, any> = Re
|
|
|
247
271
|
getJoinedRooms(): string[];
|
|
248
272
|
/** Update componentId (when component mounts) */
|
|
249
273
|
setComponentId(id: string | null): void;
|
|
250
|
-
/** Cleanup */
|
|
274
|
+
/** Cleanup — unsubscribes handlers but keeps factory refs for resubscribe() */
|
|
251
275
|
destroy(): void;
|
|
252
276
|
}
|
|
253
277
|
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ var LiveConnection = class {
|
|
|
7
7
|
heartbeatInterval = null;
|
|
8
8
|
componentCallbacks = /* @__PURE__ */ new Map();
|
|
9
9
|
binaryCallbacks = /* @__PURE__ */ new Map();
|
|
10
|
+
roomBinaryHandlers = /* @__PURE__ */ new Set();
|
|
10
11
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
11
12
|
stateListeners = /* @__PURE__ */ new Set();
|
|
12
13
|
_state = {
|
|
@@ -298,18 +299,30 @@ var LiveConnection = class {
|
|
|
298
299
|
}
|
|
299
300
|
});
|
|
300
301
|
}
|
|
301
|
-
/** Parse and route
|
|
302
|
+
/** Parse and route binary frames (state delta, room events, room state) */
|
|
302
303
|
handleBinaryMessage(buffer) {
|
|
303
|
-
if (buffer.length < 3
|
|
304
|
-
const
|
|
305
|
-
if (
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
callback(
|
|
304
|
+
if (buffer.length < 3) return;
|
|
305
|
+
const frameType = buffer[0];
|
|
306
|
+
if (frameType === 1) {
|
|
307
|
+
const idLen = buffer[1];
|
|
308
|
+
if (buffer.length < 2 + idLen) return;
|
|
309
|
+
const componentId = new TextDecoder().decode(buffer.subarray(2, 2 + idLen));
|
|
310
|
+
const payload = buffer.subarray(2 + idLen);
|
|
311
|
+
const callback = this.binaryCallbacks.get(componentId);
|
|
312
|
+
if (callback) callback(payload);
|
|
313
|
+
} else if (frameType === 2 || frameType === 3) {
|
|
314
|
+
for (const callback of this.roomBinaryHandlers) {
|
|
315
|
+
callback(buffer);
|
|
316
|
+
}
|
|
311
317
|
}
|
|
312
318
|
}
|
|
319
|
+
/** Register a handler for binary room frames (0x02 / 0x03). Returns unsubscribe. */
|
|
320
|
+
registerRoomBinaryHandler(callback) {
|
|
321
|
+
this.roomBinaryHandlers.add(callback);
|
|
322
|
+
return () => {
|
|
323
|
+
this.roomBinaryHandlers.delete(callback);
|
|
324
|
+
};
|
|
325
|
+
}
|
|
313
326
|
/** Register a binary message handler for a component */
|
|
314
327
|
registerBinaryHandler(componentId, callback) {
|
|
315
328
|
this.binaryCallbacks.set(componentId, callback);
|
|
@@ -353,6 +366,7 @@ var LiveConnection = class {
|
|
|
353
366
|
this.disconnect();
|
|
354
367
|
this.componentCallbacks.clear();
|
|
355
368
|
this.binaryCallbacks.clear();
|
|
369
|
+
this.roomBinaryHandlers.clear();
|
|
356
370
|
for (const [, req] of this.pendingRequests) {
|
|
357
371
|
clearTimeout(req.timeout);
|
|
358
372
|
req.reject(new Error("Connection destroyed"));
|
|
@@ -363,6 +377,25 @@ var LiveConnection = class {
|
|
|
363
377
|
};
|
|
364
378
|
|
|
365
379
|
// src/component.ts
|
|
380
|
+
function isPlainObject(v) {
|
|
381
|
+
return v !== null && typeof v === "object" && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype;
|
|
382
|
+
}
|
|
383
|
+
function deepMerge(target, source, seen) {
|
|
384
|
+
if (!seen) seen = /* @__PURE__ */ new Set();
|
|
385
|
+
if (seen.has(source)) return target;
|
|
386
|
+
seen.add(source);
|
|
387
|
+
const result = { ...target };
|
|
388
|
+
for (const key of Object.keys(source)) {
|
|
389
|
+
const newVal = source[key];
|
|
390
|
+
const oldVal = result[key];
|
|
391
|
+
if (isPlainObject(oldVal) && isPlainObject(newVal)) {
|
|
392
|
+
result[key] = deepMerge(oldVal, newVal, seen);
|
|
393
|
+
} else {
|
|
394
|
+
result[key] = newVal;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return result;
|
|
398
|
+
}
|
|
366
399
|
var LiveComponentHandle = class {
|
|
367
400
|
connection;
|
|
368
401
|
componentName;
|
|
@@ -551,7 +584,7 @@ var LiveComponentHandle = class {
|
|
|
551
584
|
return this.connection.registerBinaryHandler(this._componentId, (payload) => {
|
|
552
585
|
try {
|
|
553
586
|
const delta = decoder(payload);
|
|
554
|
-
this._state =
|
|
587
|
+
this._state = deepMerge(this._state, delta);
|
|
555
588
|
this.notifyStateChange(this._state, delta);
|
|
556
589
|
} catch (e) {
|
|
557
590
|
console.error("Binary decode error:", e);
|
|
@@ -574,7 +607,7 @@ var LiveComponentHandle = class {
|
|
|
574
607
|
case "STATE_UPDATE": {
|
|
575
608
|
const newState = msg.payload?.state;
|
|
576
609
|
if (newState) {
|
|
577
|
-
this._state =
|
|
610
|
+
this._state = deepMerge(this._state, newState);
|
|
578
611
|
this.notifyStateChange(this._state, null);
|
|
579
612
|
}
|
|
580
613
|
break;
|
|
@@ -582,7 +615,7 @@ var LiveComponentHandle = class {
|
|
|
582
615
|
case "STATE_DELTA": {
|
|
583
616
|
const delta = msg.payload?.delta;
|
|
584
617
|
if (delta) {
|
|
585
|
-
this._state =
|
|
618
|
+
this._state = deepMerge(this._state, delta);
|
|
586
619
|
this.notifyStateChange(this._state, delta);
|
|
587
620
|
}
|
|
588
621
|
break;
|
|
@@ -624,6 +657,180 @@ var LiveComponentHandle = class {
|
|
|
624
657
|
};
|
|
625
658
|
|
|
626
659
|
// src/rooms.ts
|
|
660
|
+
function isPlainObject2(v) {
|
|
661
|
+
return v !== null && typeof v === "object" && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype;
|
|
662
|
+
}
|
|
663
|
+
function deepMerge2(target, source, seen) {
|
|
664
|
+
if (!seen) seen = /* @__PURE__ */ new Set();
|
|
665
|
+
if (seen.has(source)) return target;
|
|
666
|
+
seen.add(source);
|
|
667
|
+
const result = { ...target };
|
|
668
|
+
for (const key of Object.keys(source)) {
|
|
669
|
+
const newVal = source[key];
|
|
670
|
+
const oldVal = result[key];
|
|
671
|
+
if (isPlainObject2(oldVal) && isPlainObject2(newVal)) {
|
|
672
|
+
result[key] = deepMerge2(oldVal, newVal, seen);
|
|
673
|
+
} else {
|
|
674
|
+
result[key] = newVal;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return result;
|
|
678
|
+
}
|
|
679
|
+
var BINARY_ROOM_EVENT = 2;
|
|
680
|
+
var BINARY_ROOM_STATE = 3;
|
|
681
|
+
var _decoder = new TextDecoder();
|
|
682
|
+
function msgpackDecode(buf) {
|
|
683
|
+
return _decodeAt(buf, 0).value;
|
|
684
|
+
}
|
|
685
|
+
function _decodeAt(buf, offset) {
|
|
686
|
+
if (offset >= buf.length) return { value: null, offset };
|
|
687
|
+
const byte = buf[offset];
|
|
688
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
689
|
+
if (byte < 128) return { value: byte, offset: offset + 1 };
|
|
690
|
+
if (byte >= 128 && byte <= 143) return _decodeMap(buf, offset + 1, byte & 15);
|
|
691
|
+
if (byte >= 144 && byte <= 159) return _decodeArr(buf, offset + 1, byte & 15);
|
|
692
|
+
if (byte >= 160 && byte <= 191) {
|
|
693
|
+
const len = byte & 31;
|
|
694
|
+
return { value: _decoder.decode(buf.subarray(offset + 1, offset + 1 + len)), offset: offset + 1 + len };
|
|
695
|
+
}
|
|
696
|
+
if (byte >= 224) return { value: byte - 256, offset: offset + 1 };
|
|
697
|
+
switch (byte) {
|
|
698
|
+
case 192:
|
|
699
|
+
return { value: null, offset: offset + 1 };
|
|
700
|
+
case 194:
|
|
701
|
+
return { value: false, offset: offset + 1 };
|
|
702
|
+
case 195:
|
|
703
|
+
return { value: true, offset: offset + 1 };
|
|
704
|
+
case 196: {
|
|
705
|
+
const l = buf[offset + 1];
|
|
706
|
+
return { value: buf.slice(offset + 2, offset + 2 + l), offset: offset + 2 + l };
|
|
707
|
+
}
|
|
708
|
+
case 197: {
|
|
709
|
+
const l = view.getUint16(offset + 1, false);
|
|
710
|
+
return { value: buf.slice(offset + 3, offset + 3 + l), offset: offset + 3 + l };
|
|
711
|
+
}
|
|
712
|
+
case 198: {
|
|
713
|
+
const l = view.getUint32(offset + 1, false);
|
|
714
|
+
return { value: buf.slice(offset + 5, offset + 5 + l), offset: offset + 5 + l };
|
|
715
|
+
}
|
|
716
|
+
case 203:
|
|
717
|
+
return { value: view.getFloat64(offset + 1, false), offset: offset + 9 };
|
|
718
|
+
case 204:
|
|
719
|
+
return { value: buf[offset + 1], offset: offset + 2 };
|
|
720
|
+
case 205:
|
|
721
|
+
return { value: view.getUint16(offset + 1, false), offset: offset + 3 };
|
|
722
|
+
case 206:
|
|
723
|
+
return { value: view.getUint32(offset + 1, false), offset: offset + 5 };
|
|
724
|
+
case 208:
|
|
725
|
+
return { value: view.getInt8(offset + 1), offset: offset + 2 };
|
|
726
|
+
case 209:
|
|
727
|
+
return { value: view.getInt16(offset + 1, false), offset: offset + 3 };
|
|
728
|
+
case 210:
|
|
729
|
+
return { value: view.getInt32(offset + 1, false), offset: offset + 5 };
|
|
730
|
+
case 217: {
|
|
731
|
+
const l = buf[offset + 1];
|
|
732
|
+
return { value: _decoder.decode(buf.subarray(offset + 2, offset + 2 + l)), offset: offset + 2 + l };
|
|
733
|
+
}
|
|
734
|
+
case 218: {
|
|
735
|
+
const l = view.getUint16(offset + 1, false);
|
|
736
|
+
return { value: _decoder.decode(buf.subarray(offset + 3, offset + 3 + l)), offset: offset + 3 + l };
|
|
737
|
+
}
|
|
738
|
+
case 219: {
|
|
739
|
+
const l = view.getUint32(offset + 1, false);
|
|
740
|
+
return { value: _decoder.decode(buf.subarray(offset + 5, offset + 5 + l)), offset: offset + 5 + l };
|
|
741
|
+
}
|
|
742
|
+
case 220:
|
|
743
|
+
return _decodeArr(buf, offset + 3, view.getUint16(offset + 1, false));
|
|
744
|
+
case 221:
|
|
745
|
+
return _decodeArr(buf, offset + 5, view.getUint32(offset + 1, false));
|
|
746
|
+
case 222:
|
|
747
|
+
return _decodeMap(buf, offset + 3, view.getUint16(offset + 1, false));
|
|
748
|
+
case 223:
|
|
749
|
+
return _decodeMap(buf, offset + 5, view.getUint32(offset + 1, false));
|
|
750
|
+
}
|
|
751
|
+
return { value: null, offset: offset + 1 };
|
|
752
|
+
}
|
|
753
|
+
function _decodeArr(buf, offset, count) {
|
|
754
|
+
const arr = new Array(count);
|
|
755
|
+
for (let i = 0; i < count; i++) {
|
|
756
|
+
const r = _decodeAt(buf, offset);
|
|
757
|
+
arr[i] = r.value;
|
|
758
|
+
offset = r.offset;
|
|
759
|
+
}
|
|
760
|
+
return { value: arr, offset };
|
|
761
|
+
}
|
|
762
|
+
function _decodeMap(buf, offset, count) {
|
|
763
|
+
const obj = {};
|
|
764
|
+
for (let i = 0; i < count; i++) {
|
|
765
|
+
const k = _decodeAt(buf, offset);
|
|
766
|
+
offset = k.offset;
|
|
767
|
+
const v = _decodeAt(buf, offset);
|
|
768
|
+
offset = v.offset;
|
|
769
|
+
obj[String(k.value)] = v.value;
|
|
770
|
+
}
|
|
771
|
+
return { value: obj, offset };
|
|
772
|
+
}
|
|
773
|
+
function parseRoomFrame(buf) {
|
|
774
|
+
if (buf.length < 6) return null;
|
|
775
|
+
let offset = 0;
|
|
776
|
+
const frameType = buf[offset++];
|
|
777
|
+
const compIdLen = buf[offset++];
|
|
778
|
+
if (offset + compIdLen > buf.length) return null;
|
|
779
|
+
const componentId = _decoder.decode(buf.subarray(offset, offset + compIdLen));
|
|
780
|
+
offset += compIdLen;
|
|
781
|
+
const roomIdLen = buf[offset++];
|
|
782
|
+
if (offset + roomIdLen > buf.length) return null;
|
|
783
|
+
const roomId = _decoder.decode(buf.subarray(offset, offset + roomIdLen));
|
|
784
|
+
offset += roomIdLen;
|
|
785
|
+
if (offset + 2 > buf.length) return null;
|
|
786
|
+
const eventLen = buf[offset] << 8 | buf[offset + 1];
|
|
787
|
+
offset += 2;
|
|
788
|
+
if (offset + eventLen > buf.length) return null;
|
|
789
|
+
const event = _decoder.decode(buf.subarray(offset, offset + eventLen));
|
|
790
|
+
offset += eventLen;
|
|
791
|
+
return { frameType, componentId, roomId, event, payload: buf.subarray(offset) };
|
|
792
|
+
}
|
|
793
|
+
var ROOM_RESERVED_KEYS = /* @__PURE__ */ new Set([
|
|
794
|
+
"id",
|
|
795
|
+
"joined",
|
|
796
|
+
"state",
|
|
797
|
+
"join",
|
|
798
|
+
"leave",
|
|
799
|
+
"emit",
|
|
800
|
+
"on",
|
|
801
|
+
"onSystem",
|
|
802
|
+
"setState",
|
|
803
|
+
"call",
|
|
804
|
+
"apply",
|
|
805
|
+
"bind",
|
|
806
|
+
"prototype",
|
|
807
|
+
"length",
|
|
808
|
+
"name",
|
|
809
|
+
"arguments",
|
|
810
|
+
"caller",
|
|
811
|
+
Symbol.toPrimitive,
|
|
812
|
+
Symbol.toStringTag,
|
|
813
|
+
Symbol.hasInstance
|
|
814
|
+
]);
|
|
815
|
+
function wrapWithStateProxy(target, getState, setStateFn) {
|
|
816
|
+
return new Proxy(target, {
|
|
817
|
+
get(obj, prop, receiver) {
|
|
818
|
+
if (ROOM_RESERVED_KEYS.has(prop) || typeof prop === "symbol") {
|
|
819
|
+
return Reflect.get(obj, prop, receiver);
|
|
820
|
+
}
|
|
821
|
+
const desc = Object.getOwnPropertyDescriptor(obj, prop);
|
|
822
|
+
if (desc) return Reflect.get(obj, prop, receiver);
|
|
823
|
+
if (prop in obj) return Reflect.get(obj, prop, receiver);
|
|
824
|
+
const st = getState();
|
|
825
|
+
return st?.[prop];
|
|
826
|
+
},
|
|
827
|
+
set(_obj, prop, value) {
|
|
828
|
+
if (typeof prop === "symbol") return false;
|
|
829
|
+
setStateFn({ [prop]: value });
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
}
|
|
627
834
|
var RoomManager = class {
|
|
628
835
|
componentId;
|
|
629
836
|
defaultRoom;
|
|
@@ -632,12 +839,29 @@ var RoomManager = class {
|
|
|
632
839
|
sendMessage;
|
|
633
840
|
sendMessageAndWait;
|
|
634
841
|
globalUnsubscribe = null;
|
|
842
|
+
binaryUnsubscribe = null;
|
|
843
|
+
onBinaryMessage = null;
|
|
844
|
+
onMessageFactory = null;
|
|
635
845
|
constructor(options) {
|
|
636
846
|
this.componentId = options.componentId;
|
|
637
847
|
this.defaultRoom = options.defaultRoom || null;
|
|
638
848
|
this.sendMessage = options.sendMessage;
|
|
639
849
|
this.sendMessageAndWait = options.sendMessageAndWait;
|
|
850
|
+
this.onBinaryMessage = options.onBinaryMessage ?? null;
|
|
851
|
+
this.onMessageFactory = options.onMessage;
|
|
640
852
|
this.globalUnsubscribe = options.onMessage((msg) => this.handleServerMessage(msg));
|
|
853
|
+
if (options.onBinaryMessage) {
|
|
854
|
+
this.binaryUnsubscribe = options.onBinaryMessage((frame) => this.handleBinaryFrame(frame));
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
/** Re-subscribe message and binary handlers (needed after destroy/remount in React Strict Mode) */
|
|
858
|
+
resubscribe() {
|
|
859
|
+
if (!this.globalUnsubscribe && this.onMessageFactory) {
|
|
860
|
+
this.globalUnsubscribe = this.onMessageFactory((msg) => this.handleServerMessage(msg));
|
|
861
|
+
}
|
|
862
|
+
if (!this.binaryUnsubscribe && this.onBinaryMessage) {
|
|
863
|
+
this.binaryUnsubscribe = this.onBinaryMessage((frame) => this.handleBinaryFrame(frame));
|
|
864
|
+
}
|
|
641
865
|
}
|
|
642
866
|
handleServerMessage(msg) {
|
|
643
867
|
if (msg.componentId !== this.componentId) return;
|
|
@@ -659,10 +883,11 @@ var RoomManager = class {
|
|
|
659
883
|
break;
|
|
660
884
|
}
|
|
661
885
|
case "ROOM_STATE": {
|
|
662
|
-
|
|
886
|
+
const stateChanges = msg.data?.state ?? msg.data;
|
|
887
|
+
room.state = deepMerge2(room.state, stateChanges);
|
|
663
888
|
const stateHandlers = room.handlers.get("$state:change");
|
|
664
889
|
if (stateHandlers) {
|
|
665
|
-
for (const handler of stateHandlers) handler(
|
|
890
|
+
for (const handler of stateHandlers) handler(stateChanges);
|
|
666
891
|
}
|
|
667
892
|
break;
|
|
668
893
|
}
|
|
@@ -675,6 +900,34 @@ var RoomManager = class {
|
|
|
675
900
|
break;
|
|
676
901
|
}
|
|
677
902
|
}
|
|
903
|
+
/** Handle binary room frames (0x02 ROOM_EVENT, 0x03 ROOM_STATE) */
|
|
904
|
+
handleBinaryFrame(frame) {
|
|
905
|
+
const parsed = parseRoomFrame(frame);
|
|
906
|
+
if (!parsed) return;
|
|
907
|
+
if (parsed.componentId !== this.componentId) return;
|
|
908
|
+
const room = this.rooms.get(parsed.roomId);
|
|
909
|
+
if (!room) return;
|
|
910
|
+
const data = msgpackDecode(parsed.payload);
|
|
911
|
+
if (parsed.frameType === BINARY_ROOM_EVENT) {
|
|
912
|
+
const handlers = room.handlers.get(parsed.event);
|
|
913
|
+
if (handlers) {
|
|
914
|
+
for (const handler of handlers) {
|
|
915
|
+
try {
|
|
916
|
+
handler(data);
|
|
917
|
+
} catch (error) {
|
|
918
|
+
console.error(`[Room:${parsed.roomId}] Handler error for '${parsed.event}':`, error);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
} else if (parsed.frameType === BINARY_ROOM_STATE) {
|
|
923
|
+
const stateChanges = data?.state ?? data;
|
|
924
|
+
room.state = deepMerge2(room.state, stateChanges);
|
|
925
|
+
const stateHandlers = room.handlers.get("$state:change");
|
|
926
|
+
if (stateHandlers) {
|
|
927
|
+
for (const handler of stateHandlers) handler(stateChanges);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
678
931
|
getOrCreateRoom(roomId) {
|
|
679
932
|
if (!this.rooms.has(roomId)) {
|
|
680
933
|
this.rooms.set(roomId, {
|
|
@@ -755,7 +1008,7 @@ var RoomManager = class {
|
|
|
755
1008
|
},
|
|
756
1009
|
setState: (updates) => {
|
|
757
1010
|
if (!this.componentId) return;
|
|
758
|
-
room.state =
|
|
1011
|
+
room.state = deepMerge2(room.state, updates);
|
|
759
1012
|
this.sendMessage({
|
|
760
1013
|
type: "ROOM_STATE_SET",
|
|
761
1014
|
componentId: this.componentId,
|
|
@@ -765,8 +1018,13 @@ var RoomManager = class {
|
|
|
765
1018
|
});
|
|
766
1019
|
}
|
|
767
1020
|
};
|
|
768
|
-
|
|
769
|
-
|
|
1021
|
+
const proxied = wrapWithStateProxy(
|
|
1022
|
+
handle,
|
|
1023
|
+
() => room.state,
|
|
1024
|
+
(updates) => handle.setState(updates)
|
|
1025
|
+
);
|
|
1026
|
+
this.handles.set(roomId, proxied);
|
|
1027
|
+
return proxied;
|
|
770
1028
|
}
|
|
771
1029
|
/** Create the $room proxy */
|
|
772
1030
|
createProxy() {
|
|
@@ -816,6 +1074,14 @@ var RoomManager = class {
|
|
|
816
1074
|
}
|
|
817
1075
|
}
|
|
818
1076
|
});
|
|
1077
|
+
if (this.defaultRoom && defaultHandle) {
|
|
1078
|
+
const room = this.getOrCreateRoom(this.defaultRoom);
|
|
1079
|
+
return wrapWithStateProxy(
|
|
1080
|
+
proxyFn,
|
|
1081
|
+
() => room.state,
|
|
1082
|
+
(updates) => defaultHandle.setState(updates)
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
819
1085
|
return proxyFn;
|
|
820
1086
|
}
|
|
821
1087
|
/** List of rooms currently joined */
|
|
@@ -830,9 +1096,12 @@ var RoomManager = class {
|
|
|
830
1096
|
setComponentId(id) {
|
|
831
1097
|
this.componentId = id;
|
|
832
1098
|
}
|
|
833
|
-
/** Cleanup */
|
|
1099
|
+
/** Cleanup — unsubscribes handlers but keeps factory refs for resubscribe() */
|
|
834
1100
|
destroy() {
|
|
835
1101
|
this.globalUnsubscribe?.();
|
|
1102
|
+
this.globalUnsubscribe = null;
|
|
1103
|
+
this.binaryUnsubscribe?.();
|
|
1104
|
+
this.binaryUnsubscribe = null;
|
|
836
1105
|
for (const [, room] of this.rooms) {
|
|
837
1106
|
room.handlers.clear();
|
|
838
1107
|
}
|