@fluxstack/live-client 0.2.0 → 0.3.1

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 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 a binary BINARY_STATE_DELTA frame */
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
- interface RoomHandle<TState = any, TEvents extends Record<string, any> = Record<string, any>> {
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
- interface RoomProxy<TState = any, TEvents extends Record<string, any> = Record<string, any>> {
212
- (roomId: string): RoomHandle<TState, TEvents>;
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 a binary BINARY_STATE_DELTA frame */
302
+ /** Parse and route binary frames (state delta, room events, room state) */
302
303
  handleBinaryMessage(buffer) {
303
- if (buffer.length < 3 || buffer[0] !== 1) return;
304
- const idLen = buffer[1];
305
- if (buffer.length < 2 + idLen) return;
306
- const componentId = new TextDecoder().decode(buffer.subarray(2, 2 + idLen));
307
- const payload = buffer.subarray(2 + idLen);
308
- const callback = this.binaryCallbacks.get(componentId);
309
- if (callback) {
310
- callback(payload);
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 = { ...this._state, ...delta };
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 = { ...this._state, ...newState };
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 = { ...this._state, ...delta };
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
- room.state = { ...room.state, ...msg.data };
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(msg.data);
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 = { ...room.state, ...updates };
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
- this.handles.set(roomId, handle);
769
- return handle;
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
  }