@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.
@@ -48,6 +48,7 @@ var FluxstackLive = (() => {
48
48
  __publicField(this, "heartbeatInterval", null);
49
49
  __publicField(this, "componentCallbacks", /* @__PURE__ */ new Map());
50
50
  __publicField(this, "binaryCallbacks", /* @__PURE__ */ new Map());
51
+ __publicField(this, "roomBinaryHandlers", /* @__PURE__ */ new Set());
51
52
  __publicField(this, "pendingRequests", /* @__PURE__ */ new Map());
52
53
  __publicField(this, "stateListeners", /* @__PURE__ */ new Set());
53
54
  __publicField(this, "_state", {
@@ -338,18 +339,30 @@ var FluxstackLive = (() => {
338
339
  }
339
340
  });
340
341
  }
341
- /** Parse and route a binary BINARY_STATE_DELTA frame */
342
+ /** Parse and route binary frames (state delta, room events, room state) */
342
343
  handleBinaryMessage(buffer) {
343
- if (buffer.length < 3 || buffer[0] !== 1) return;
344
- const idLen = buffer[1];
345
- if (buffer.length < 2 + idLen) return;
346
- const componentId = new TextDecoder().decode(buffer.subarray(2, 2 + idLen));
347
- const payload = buffer.subarray(2 + idLen);
348
- const callback = this.binaryCallbacks.get(componentId);
349
- if (callback) {
350
- callback(payload);
344
+ if (buffer.length < 3) return;
345
+ const frameType = buffer[0];
346
+ if (frameType === 1) {
347
+ const idLen = buffer[1];
348
+ if (buffer.length < 2 + idLen) return;
349
+ const componentId = new TextDecoder().decode(buffer.subarray(2, 2 + idLen));
350
+ const payload = buffer.subarray(2 + idLen);
351
+ const callback = this.binaryCallbacks.get(componentId);
352
+ if (callback) callback(payload);
353
+ } else if (frameType === 2 || frameType === 3) {
354
+ for (const callback of this.roomBinaryHandlers) {
355
+ callback(buffer);
356
+ }
351
357
  }
352
358
  }
359
+ /** Register a handler for binary room frames (0x02 / 0x03). Returns unsubscribe. */
360
+ registerRoomBinaryHandler(callback) {
361
+ this.roomBinaryHandlers.add(callback);
362
+ return () => {
363
+ this.roomBinaryHandlers.delete(callback);
364
+ };
365
+ }
353
366
  /** Register a binary message handler for a component */
354
367
  registerBinaryHandler(componentId, callback) {
355
368
  this.binaryCallbacks.set(componentId, callback);
@@ -393,6 +406,7 @@ var FluxstackLive = (() => {
393
406
  this.disconnect();
394
407
  this.componentCallbacks.clear();
395
408
  this.binaryCallbacks.clear();
409
+ this.roomBinaryHandlers.clear();
396
410
  for (const [, req] of this.pendingRequests) {
397
411
  clearTimeout(req.timeout);
398
412
  req.reject(new Error("Connection destroyed"));
@@ -403,6 +417,25 @@ var FluxstackLive = (() => {
403
417
  };
404
418
 
405
419
  // src/component.ts
420
+ function isPlainObject(v) {
421
+ return v !== null && typeof v === "object" && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype;
422
+ }
423
+ function deepMerge(target, source, seen) {
424
+ if (!seen) seen = /* @__PURE__ */ new Set();
425
+ if (seen.has(source)) return target;
426
+ seen.add(source);
427
+ const result = { ...target };
428
+ for (const key of Object.keys(source)) {
429
+ const newVal = source[key];
430
+ const oldVal = result[key];
431
+ if (isPlainObject(oldVal) && isPlainObject(newVal)) {
432
+ result[key] = deepMerge(oldVal, newVal, seen);
433
+ } else {
434
+ result[key] = newVal;
435
+ }
436
+ }
437
+ return result;
438
+ }
406
439
  var LiveComponentHandle = class {
407
440
  constructor(connection, componentName, options = {}) {
408
441
  __publicField(this, "connection");
@@ -591,7 +624,7 @@ var FluxstackLive = (() => {
591
624
  return this.connection.registerBinaryHandler(this._componentId, (payload) => {
592
625
  try {
593
626
  const delta = decoder(payload);
594
- this._state = { ...this._state, ...delta };
627
+ this._state = deepMerge(this._state, delta);
595
628
  this.notifyStateChange(this._state, delta);
596
629
  } catch (e) {
597
630
  console.error("Binary decode error:", e);
@@ -614,7 +647,7 @@ var FluxstackLive = (() => {
614
647
  case "STATE_UPDATE": {
615
648
  const newState = msg.payload?.state;
616
649
  if (newState) {
617
- this._state = { ...this._state, ...newState };
650
+ this._state = deepMerge(this._state, newState);
618
651
  this.notifyStateChange(this._state, null);
619
652
  }
620
653
  break;
@@ -622,7 +655,7 @@ var FluxstackLive = (() => {
622
655
  case "STATE_DELTA": {
623
656
  const delta = msg.payload?.delta;
624
657
  if (delta) {
625
- this._state = { ...this._state, ...delta };
658
+ this._state = deepMerge(this._state, delta);
626
659
  this.notifyStateChange(this._state, delta);
627
660
  }
628
661
  break;
@@ -664,6 +697,180 @@ var FluxstackLive = (() => {
664
697
  };
665
698
 
666
699
  // src/rooms.ts
700
+ function isPlainObject2(v) {
701
+ return v !== null && typeof v === "object" && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype;
702
+ }
703
+ function deepMerge2(target, source, seen) {
704
+ if (!seen) seen = /* @__PURE__ */ new Set();
705
+ if (seen.has(source)) return target;
706
+ seen.add(source);
707
+ const result = { ...target };
708
+ for (const key of Object.keys(source)) {
709
+ const newVal = source[key];
710
+ const oldVal = result[key];
711
+ if (isPlainObject2(oldVal) && isPlainObject2(newVal)) {
712
+ result[key] = deepMerge2(oldVal, newVal, seen);
713
+ } else {
714
+ result[key] = newVal;
715
+ }
716
+ }
717
+ return result;
718
+ }
719
+ var BINARY_ROOM_EVENT = 2;
720
+ var BINARY_ROOM_STATE = 3;
721
+ var _decoder = new TextDecoder();
722
+ function msgpackDecode(buf) {
723
+ return _decodeAt(buf, 0).value;
724
+ }
725
+ function _decodeAt(buf, offset) {
726
+ if (offset >= buf.length) return { value: null, offset };
727
+ const byte = buf[offset];
728
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
729
+ if (byte < 128) return { value: byte, offset: offset + 1 };
730
+ if (byte >= 128 && byte <= 143) return _decodeMap(buf, offset + 1, byte & 15);
731
+ if (byte >= 144 && byte <= 159) return _decodeArr(buf, offset + 1, byte & 15);
732
+ if (byte >= 160 && byte <= 191) {
733
+ const len = byte & 31;
734
+ return { value: _decoder.decode(buf.subarray(offset + 1, offset + 1 + len)), offset: offset + 1 + len };
735
+ }
736
+ if (byte >= 224) return { value: byte - 256, offset: offset + 1 };
737
+ switch (byte) {
738
+ case 192:
739
+ return { value: null, offset: offset + 1 };
740
+ case 194:
741
+ return { value: false, offset: offset + 1 };
742
+ case 195:
743
+ return { value: true, offset: offset + 1 };
744
+ case 196: {
745
+ const l = buf[offset + 1];
746
+ return { value: buf.slice(offset + 2, offset + 2 + l), offset: offset + 2 + l };
747
+ }
748
+ case 197: {
749
+ const l = view.getUint16(offset + 1, false);
750
+ return { value: buf.slice(offset + 3, offset + 3 + l), offset: offset + 3 + l };
751
+ }
752
+ case 198: {
753
+ const l = view.getUint32(offset + 1, false);
754
+ return { value: buf.slice(offset + 5, offset + 5 + l), offset: offset + 5 + l };
755
+ }
756
+ case 203:
757
+ return { value: view.getFloat64(offset + 1, false), offset: offset + 9 };
758
+ case 204:
759
+ return { value: buf[offset + 1], offset: offset + 2 };
760
+ case 205:
761
+ return { value: view.getUint16(offset + 1, false), offset: offset + 3 };
762
+ case 206:
763
+ return { value: view.getUint32(offset + 1, false), offset: offset + 5 };
764
+ case 208:
765
+ return { value: view.getInt8(offset + 1), offset: offset + 2 };
766
+ case 209:
767
+ return { value: view.getInt16(offset + 1, false), offset: offset + 3 };
768
+ case 210:
769
+ return { value: view.getInt32(offset + 1, false), offset: offset + 5 };
770
+ case 217: {
771
+ const l = buf[offset + 1];
772
+ return { value: _decoder.decode(buf.subarray(offset + 2, offset + 2 + l)), offset: offset + 2 + l };
773
+ }
774
+ case 218: {
775
+ const l = view.getUint16(offset + 1, false);
776
+ return { value: _decoder.decode(buf.subarray(offset + 3, offset + 3 + l)), offset: offset + 3 + l };
777
+ }
778
+ case 219: {
779
+ const l = view.getUint32(offset + 1, false);
780
+ return { value: _decoder.decode(buf.subarray(offset + 5, offset + 5 + l)), offset: offset + 5 + l };
781
+ }
782
+ case 220:
783
+ return _decodeArr(buf, offset + 3, view.getUint16(offset + 1, false));
784
+ case 221:
785
+ return _decodeArr(buf, offset + 5, view.getUint32(offset + 1, false));
786
+ case 222:
787
+ return _decodeMap(buf, offset + 3, view.getUint16(offset + 1, false));
788
+ case 223:
789
+ return _decodeMap(buf, offset + 5, view.getUint32(offset + 1, false));
790
+ }
791
+ return { value: null, offset: offset + 1 };
792
+ }
793
+ function _decodeArr(buf, offset, count) {
794
+ const arr = new Array(count);
795
+ for (let i = 0; i < count; i++) {
796
+ const r = _decodeAt(buf, offset);
797
+ arr[i] = r.value;
798
+ offset = r.offset;
799
+ }
800
+ return { value: arr, offset };
801
+ }
802
+ function _decodeMap(buf, offset, count) {
803
+ const obj = {};
804
+ for (let i = 0; i < count; i++) {
805
+ const k = _decodeAt(buf, offset);
806
+ offset = k.offset;
807
+ const v = _decodeAt(buf, offset);
808
+ offset = v.offset;
809
+ obj[String(k.value)] = v.value;
810
+ }
811
+ return { value: obj, offset };
812
+ }
813
+ function parseRoomFrame(buf) {
814
+ if (buf.length < 6) return null;
815
+ let offset = 0;
816
+ const frameType = buf[offset++];
817
+ const compIdLen = buf[offset++];
818
+ if (offset + compIdLen > buf.length) return null;
819
+ const componentId = _decoder.decode(buf.subarray(offset, offset + compIdLen));
820
+ offset += compIdLen;
821
+ const roomIdLen = buf[offset++];
822
+ if (offset + roomIdLen > buf.length) return null;
823
+ const roomId = _decoder.decode(buf.subarray(offset, offset + roomIdLen));
824
+ offset += roomIdLen;
825
+ if (offset + 2 > buf.length) return null;
826
+ const eventLen = buf[offset] << 8 | buf[offset + 1];
827
+ offset += 2;
828
+ if (offset + eventLen > buf.length) return null;
829
+ const event = _decoder.decode(buf.subarray(offset, offset + eventLen));
830
+ offset += eventLen;
831
+ return { frameType, componentId, roomId, event, payload: buf.subarray(offset) };
832
+ }
833
+ var ROOM_RESERVED_KEYS = /* @__PURE__ */ new Set([
834
+ "id",
835
+ "joined",
836
+ "state",
837
+ "join",
838
+ "leave",
839
+ "emit",
840
+ "on",
841
+ "onSystem",
842
+ "setState",
843
+ "call",
844
+ "apply",
845
+ "bind",
846
+ "prototype",
847
+ "length",
848
+ "name",
849
+ "arguments",
850
+ "caller",
851
+ Symbol.toPrimitive,
852
+ Symbol.toStringTag,
853
+ Symbol.hasInstance
854
+ ]);
855
+ function wrapWithStateProxy(target, getState, setStateFn) {
856
+ return new Proxy(target, {
857
+ get(obj, prop, receiver) {
858
+ if (ROOM_RESERVED_KEYS.has(prop) || typeof prop === "symbol") {
859
+ return Reflect.get(obj, prop, receiver);
860
+ }
861
+ const desc = Object.getOwnPropertyDescriptor(obj, prop);
862
+ if (desc) return Reflect.get(obj, prop, receiver);
863
+ if (prop in obj) return Reflect.get(obj, prop, receiver);
864
+ const st = getState();
865
+ return st?.[prop];
866
+ },
867
+ set(_obj, prop, value) {
868
+ if (typeof prop === "symbol") return false;
869
+ setStateFn({ [prop]: value });
870
+ return true;
871
+ }
872
+ });
873
+ }
667
874
  var RoomManager = class {
668
875
  constructor(options) {
669
876
  __publicField(this, "componentId");
@@ -673,11 +880,28 @@ var FluxstackLive = (() => {
673
880
  __publicField(this, "sendMessage");
674
881
  __publicField(this, "sendMessageAndWait");
675
882
  __publicField(this, "globalUnsubscribe", null);
883
+ __publicField(this, "binaryUnsubscribe", null);
884
+ __publicField(this, "onBinaryMessage", null);
885
+ __publicField(this, "onMessageFactory", null);
676
886
  this.componentId = options.componentId;
677
887
  this.defaultRoom = options.defaultRoom || null;
678
888
  this.sendMessage = options.sendMessage;
679
889
  this.sendMessageAndWait = options.sendMessageAndWait;
890
+ this.onBinaryMessage = options.onBinaryMessage ?? null;
891
+ this.onMessageFactory = options.onMessage;
680
892
  this.globalUnsubscribe = options.onMessage((msg) => this.handleServerMessage(msg));
893
+ if (options.onBinaryMessage) {
894
+ this.binaryUnsubscribe = options.onBinaryMessage((frame) => this.handleBinaryFrame(frame));
895
+ }
896
+ }
897
+ /** Re-subscribe message and binary handlers (needed after destroy/remount in React Strict Mode) */
898
+ resubscribe() {
899
+ if (!this.globalUnsubscribe && this.onMessageFactory) {
900
+ this.globalUnsubscribe = this.onMessageFactory((msg) => this.handleServerMessage(msg));
901
+ }
902
+ if (!this.binaryUnsubscribe && this.onBinaryMessage) {
903
+ this.binaryUnsubscribe = this.onBinaryMessage((frame) => this.handleBinaryFrame(frame));
904
+ }
681
905
  }
682
906
  handleServerMessage(msg) {
683
907
  if (msg.componentId !== this.componentId) return;
@@ -699,10 +923,11 @@ var FluxstackLive = (() => {
699
923
  break;
700
924
  }
701
925
  case "ROOM_STATE": {
702
- room.state = { ...room.state, ...msg.data };
926
+ const stateChanges = msg.data?.state ?? msg.data;
927
+ room.state = deepMerge2(room.state, stateChanges);
703
928
  const stateHandlers = room.handlers.get("$state:change");
704
929
  if (stateHandlers) {
705
- for (const handler of stateHandlers) handler(msg.data);
930
+ for (const handler of stateHandlers) handler(stateChanges);
706
931
  }
707
932
  break;
708
933
  }
@@ -715,6 +940,34 @@ var FluxstackLive = (() => {
715
940
  break;
716
941
  }
717
942
  }
943
+ /** Handle binary room frames (0x02 ROOM_EVENT, 0x03 ROOM_STATE) */
944
+ handleBinaryFrame(frame) {
945
+ const parsed = parseRoomFrame(frame);
946
+ if (!parsed) return;
947
+ if (parsed.componentId !== this.componentId) return;
948
+ const room = this.rooms.get(parsed.roomId);
949
+ if (!room) return;
950
+ const data = msgpackDecode(parsed.payload);
951
+ if (parsed.frameType === BINARY_ROOM_EVENT) {
952
+ const handlers = room.handlers.get(parsed.event);
953
+ if (handlers) {
954
+ for (const handler of handlers) {
955
+ try {
956
+ handler(data);
957
+ } catch (error) {
958
+ console.error(`[Room:${parsed.roomId}] Handler error for '${parsed.event}':`, error);
959
+ }
960
+ }
961
+ }
962
+ } else if (parsed.frameType === BINARY_ROOM_STATE) {
963
+ const stateChanges = data?.state ?? data;
964
+ room.state = deepMerge2(room.state, stateChanges);
965
+ const stateHandlers = room.handlers.get("$state:change");
966
+ if (stateHandlers) {
967
+ for (const handler of stateHandlers) handler(stateChanges);
968
+ }
969
+ }
970
+ }
718
971
  getOrCreateRoom(roomId) {
719
972
  if (!this.rooms.has(roomId)) {
720
973
  this.rooms.set(roomId, {
@@ -795,7 +1048,7 @@ var FluxstackLive = (() => {
795
1048
  },
796
1049
  setState: (updates) => {
797
1050
  if (!this.componentId) return;
798
- room.state = { ...room.state, ...updates };
1051
+ room.state = deepMerge2(room.state, updates);
799
1052
  this.sendMessage({
800
1053
  type: "ROOM_STATE_SET",
801
1054
  componentId: this.componentId,
@@ -805,8 +1058,13 @@ var FluxstackLive = (() => {
805
1058
  });
806
1059
  }
807
1060
  };
808
- this.handles.set(roomId, handle);
809
- return handle;
1061
+ const proxied = wrapWithStateProxy(
1062
+ handle,
1063
+ () => room.state,
1064
+ (updates) => handle.setState(updates)
1065
+ );
1066
+ this.handles.set(roomId, proxied);
1067
+ return proxied;
810
1068
  }
811
1069
  /** Create the $room proxy */
812
1070
  createProxy() {
@@ -856,6 +1114,14 @@ var FluxstackLive = (() => {
856
1114
  }
857
1115
  }
858
1116
  });
1117
+ if (this.defaultRoom && defaultHandle) {
1118
+ const room = this.getOrCreateRoom(this.defaultRoom);
1119
+ return wrapWithStateProxy(
1120
+ proxyFn,
1121
+ () => room.state,
1122
+ (updates) => defaultHandle.setState(updates)
1123
+ );
1124
+ }
859
1125
  return proxyFn;
860
1126
  }
861
1127
  /** List of rooms currently joined */
@@ -870,9 +1136,12 @@ var FluxstackLive = (() => {
870
1136
  setComponentId(id) {
871
1137
  this.componentId = id;
872
1138
  }
873
- /** Cleanup */
1139
+ /** Cleanup — unsubscribes handlers but keeps factory refs for resubscribe() */
874
1140
  destroy() {
875
1141
  this.globalUnsubscribe?.();
1142
+ this.globalUnsubscribe = null;
1143
+ this.binaryUnsubscribe?.();
1144
+ this.binaryUnsubscribe = null;
876
1145
  for (const [, room] of this.rooms) {
877
1146
  room.handlers.clear();
878
1147
  }