@fluxstack/live 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.js CHANGED
@@ -331,6 +331,11 @@ function deduplicateDeltas(messages) {
331
331
  }
332
332
  return result;
333
333
  }
334
+ function sendBinaryImmediate(ws, data) {
335
+ if (ws && ws.readyState === 1) {
336
+ ws.send(data);
337
+ }
338
+ }
334
339
  function sendImmediate(ws, data) {
335
340
  if (ws && ws.readyState === 1) {
336
341
  ws.send(data);
@@ -374,22 +379,7 @@ function shouldLog(componentId, category) {
374
379
  if (cfg === true) return true;
375
380
  return cfg.includes(category);
376
381
  }
377
- var _debugger = null;
378
- function _setLoggerDebugger(dbg) {
379
- _debugger = dbg;
380
- }
381
- function emitToDebugger(category, level, componentId, message, args) {
382
- if (!_debugger?.enabled) return;
383
- const data = { category, level, message };
384
- if (args.length === 1 && typeof args[0] === "object" && args[0] !== null) {
385
- data.details = args[0];
386
- } else if (args.length > 0) {
387
- data.details = args;
388
- }
389
- _debugger.emit("LOG", componentId, null, data);
390
- }
391
382
  function liveLog(category, componentId, message, ...args) {
392
- emitToDebugger(category, "info", componentId, message, args);
393
383
  if (shouldLog(componentId, category)) {
394
384
  if (args.length > 0) {
395
385
  console.log(message, ...args);
@@ -399,7 +389,6 @@ function liveLog(category, componentId, message, ...args) {
399
389
  }
400
390
  }
401
391
  function liveWarn(category, componentId, message, ...args) {
402
- emitToDebugger(category, "warn", componentId, message, args);
403
392
  if (shouldLog(componentId, category)) {
404
393
  if (args.length > 0) {
405
394
  console.warn(message, ...args);
@@ -409,9 +398,383 @@ function liveWarn(category, componentId, message, ...args) {
409
398
  }
410
399
  }
411
400
 
401
+ // src/utils/deepDiff.ts
402
+ function isPlainObject(v) {
403
+ return v !== null && typeof v === "object" && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype;
404
+ }
405
+ function computeDeepDiff(prev, next, depth = 0, maxDepth = 3, seen) {
406
+ if (depth > maxDepth) return prev === next ? null : next;
407
+ if (!seen) seen = /* @__PURE__ */ new Set();
408
+ if (seen.has(next)) return prev === next ? null : next;
409
+ seen.add(next);
410
+ let result = null;
411
+ for (const key of Object.keys(next)) {
412
+ const oldVal = prev[key];
413
+ const newVal = next[key];
414
+ if (oldVal === newVal) continue;
415
+ if (isPlainObject(oldVal) && isPlainObject(newVal)) {
416
+ const nested = computeDeepDiff(oldVal, newVal, depth + 1, maxDepth, seen);
417
+ if (nested !== null) {
418
+ result ??= {};
419
+ result[key] = nested;
420
+ }
421
+ } else {
422
+ result ??= {};
423
+ result[key] = newVal;
424
+ }
425
+ }
426
+ return result;
427
+ }
428
+ function deepAssign(target, source, seen) {
429
+ if (!seen) seen = /* @__PURE__ */ new Set();
430
+ if (seen.has(source)) return;
431
+ seen.add(source);
432
+ for (const key of Object.keys(source)) {
433
+ if (isPlainObject(target[key]) && isPlainObject(source[key])) {
434
+ deepAssign(target[key], source[key], seen);
435
+ } else {
436
+ target[key] = source[key];
437
+ }
438
+ }
439
+ }
440
+
441
+ // src/rooms/RoomCodec.ts
442
+ var BINARY_ROOM_EVENT = 2;
443
+ var BINARY_ROOM_STATE = 3;
444
+ var encoder = new TextEncoder();
445
+ var decoder = new TextDecoder();
446
+ function msgpackEncode(value) {
447
+ const parts = [];
448
+ encode(value, parts);
449
+ let totalLen = 0;
450
+ for (const p of parts) totalLen += p.length;
451
+ const result = new Uint8Array(totalLen);
452
+ let offset = 0;
453
+ for (const p of parts) {
454
+ result.set(p, offset);
455
+ offset += p.length;
456
+ }
457
+ return result;
458
+ }
459
+ function encode(value, parts) {
460
+ if (value === null || value === void 0) {
461
+ parts.push(new Uint8Array([192]));
462
+ return;
463
+ }
464
+ if (typeof value === "boolean") {
465
+ parts.push(new Uint8Array([value ? 195 : 194]));
466
+ return;
467
+ }
468
+ if (typeof value === "number") {
469
+ if (Number.isInteger(value)) {
470
+ encodeInt(value, parts);
471
+ } else {
472
+ const buf = new Uint8Array(9);
473
+ buf[0] = 203;
474
+ new DataView(buf.buffer).setFloat64(1, value, false);
475
+ parts.push(buf);
476
+ }
477
+ return;
478
+ }
479
+ if (typeof value === "string") {
480
+ const encoded = encoder.encode(value);
481
+ const len = encoded.length;
482
+ if (len < 32) {
483
+ parts.push(new Uint8Array([160 | len]));
484
+ } else if (len < 256) {
485
+ parts.push(new Uint8Array([217, len]));
486
+ } else if (len < 65536) {
487
+ parts.push(new Uint8Array([218, len >> 8, len & 255]));
488
+ } else {
489
+ parts.push(new Uint8Array([219, len >> 24 & 255, len >> 16 & 255, len >> 8 & 255, len & 255]));
490
+ }
491
+ parts.push(encoded);
492
+ return;
493
+ }
494
+ if (value instanceof Uint8Array) {
495
+ const len = value.length;
496
+ if (len < 256) {
497
+ parts.push(new Uint8Array([196, len]));
498
+ } else if (len < 65536) {
499
+ parts.push(new Uint8Array([197, len >> 8, len & 255]));
500
+ } else {
501
+ parts.push(new Uint8Array([198, len >> 24 & 255, len >> 16 & 255, len >> 8 & 255, len & 255]));
502
+ }
503
+ parts.push(value);
504
+ return;
505
+ }
506
+ if (Array.isArray(value)) {
507
+ const len = value.length;
508
+ if (len < 16) {
509
+ parts.push(new Uint8Array([144 | len]));
510
+ } else if (len < 65536) {
511
+ parts.push(new Uint8Array([220, len >> 8, len & 255]));
512
+ } else {
513
+ parts.push(new Uint8Array([221, len >> 24 & 255, len >> 16 & 255, len >> 8 & 255, len & 255]));
514
+ }
515
+ for (const item of value) {
516
+ encode(item, parts);
517
+ }
518
+ return;
519
+ }
520
+ if (typeof value === "object") {
521
+ const keys = Object.keys(value);
522
+ const len = keys.length;
523
+ if (len < 16) {
524
+ parts.push(new Uint8Array([128 | len]));
525
+ } else if (len < 65536) {
526
+ parts.push(new Uint8Array([222, len >> 8, len & 255]));
527
+ } else {
528
+ parts.push(new Uint8Array([223, len >> 24 & 255, len >> 16 & 255, len >> 8 & 255, len & 255]));
529
+ }
530
+ for (const key of keys) {
531
+ encode(key, parts);
532
+ encode(value[key], parts);
533
+ }
534
+ return;
535
+ }
536
+ parts.push(new Uint8Array([192]));
537
+ }
538
+ function encodeInt(value, parts) {
539
+ if (value >= 0) {
540
+ if (value < 128) {
541
+ parts.push(new Uint8Array([value]));
542
+ } else if (value < 256) {
543
+ parts.push(new Uint8Array([204, value]));
544
+ } else if (value < 65536) {
545
+ parts.push(new Uint8Array([205, value >> 8, value & 255]));
546
+ } else if (value < 4294967296) {
547
+ parts.push(new Uint8Array([206, value >> 24 & 255, value >> 16 & 255, value >> 8 & 255, value & 255]));
548
+ } else {
549
+ const buf = new Uint8Array(9);
550
+ buf[0] = 203;
551
+ new DataView(buf.buffer).setFloat64(1, value, false);
552
+ parts.push(buf);
553
+ }
554
+ } else {
555
+ if (value >= -32) {
556
+ parts.push(new Uint8Array([value & 255]));
557
+ } else if (value >= -128) {
558
+ parts.push(new Uint8Array([208, value & 255]));
559
+ } else if (value >= -32768) {
560
+ const buf = new Uint8Array(3);
561
+ buf[0] = 209;
562
+ new DataView(buf.buffer).setInt16(1, value, false);
563
+ parts.push(buf);
564
+ } else if (value >= -2147483648) {
565
+ const buf = new Uint8Array(5);
566
+ buf[0] = 210;
567
+ new DataView(buf.buffer).setInt32(1, value, false);
568
+ parts.push(buf);
569
+ } else {
570
+ const buf = new Uint8Array(9);
571
+ buf[0] = 203;
572
+ new DataView(buf.buffer).setFloat64(1, value, false);
573
+ parts.push(buf);
574
+ }
575
+ }
576
+ }
577
+ function msgpackDecode(buf) {
578
+ const result = decodeAt(buf, 0);
579
+ return result.value;
580
+ }
581
+ function decodeAt(buf, offset) {
582
+ if (offset >= buf.length) return { value: null, offset };
583
+ const byte = buf[offset];
584
+ const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
585
+ if (byte < 128) return { value: byte, offset: offset + 1 };
586
+ if (byte >= 128 && byte <= 143) return decodeMap(buf, offset + 1, byte & 15);
587
+ if (byte >= 144 && byte <= 159) return decodeArray(buf, offset + 1, byte & 15);
588
+ if (byte >= 160 && byte <= 191) {
589
+ const len = byte & 31;
590
+ const str = decoder.decode(buf.subarray(offset + 1, offset + 1 + len));
591
+ return { value: str, offset: offset + 1 + len };
592
+ }
593
+ if (byte >= 224) return { value: byte - 256, offset: offset + 1 };
594
+ switch (byte) {
595
+ // nil
596
+ case 192:
597
+ return { value: null, offset: offset + 1 };
598
+ // false
599
+ case 194:
600
+ return { value: false, offset: offset + 1 };
601
+ // true
602
+ case 195:
603
+ return { value: true, offset: offset + 1 };
604
+ // bin 8
605
+ case 196: {
606
+ const len = buf[offset + 1];
607
+ return { value: buf.slice(offset + 2, offset + 2 + len), offset: offset + 2 + len };
608
+ }
609
+ // bin 16
610
+ case 197: {
611
+ const len = view.getUint16(offset + 1, false);
612
+ return { value: buf.slice(offset + 3, offset + 3 + len), offset: offset + 3 + len };
613
+ }
614
+ // bin 32
615
+ case 198: {
616
+ const len = view.getUint32(offset + 1, false);
617
+ return { value: buf.slice(offset + 5, offset + 5 + len), offset: offset + 5 + len };
618
+ }
619
+ // float 64
620
+ case 203:
621
+ return { value: view.getFloat64(offset + 1, false), offset: offset + 9 };
622
+ // uint 8
623
+ case 204:
624
+ return { value: buf[offset + 1], offset: offset + 2 };
625
+ // uint 16
626
+ case 205:
627
+ return { value: view.getUint16(offset + 1, false), offset: offset + 3 };
628
+ // uint 32
629
+ case 206:
630
+ return { value: view.getUint32(offset + 1, false), offset: offset + 5 };
631
+ // int 8
632
+ case 208:
633
+ return { value: view.getInt8(offset + 1), offset: offset + 2 };
634
+ // int 16
635
+ case 209:
636
+ return { value: view.getInt16(offset + 1, false), offset: offset + 3 };
637
+ // int 32
638
+ case 210:
639
+ return { value: view.getInt32(offset + 1, false), offset: offset + 5 };
640
+ // str 8
641
+ case 217: {
642
+ const len = buf[offset + 1];
643
+ return { value: decoder.decode(buf.subarray(offset + 2, offset + 2 + len)), offset: offset + 2 + len };
644
+ }
645
+ // str 16
646
+ case 218: {
647
+ const len = view.getUint16(offset + 1, false);
648
+ return { value: decoder.decode(buf.subarray(offset + 3, offset + 3 + len)), offset: offset + 3 + len };
649
+ }
650
+ // str 32
651
+ case 219: {
652
+ const len = view.getUint32(offset + 1, false);
653
+ return { value: decoder.decode(buf.subarray(offset + 5, offset + 5 + len)), offset: offset + 5 + len };
654
+ }
655
+ // array 16
656
+ case 220:
657
+ return decodeArray(buf, offset + 3, view.getUint16(offset + 1, false));
658
+ // array 32
659
+ case 221:
660
+ return decodeArray(buf, offset + 5, view.getUint32(offset + 1, false));
661
+ // map 16
662
+ case 222:
663
+ return decodeMap(buf, offset + 3, view.getUint16(offset + 1, false));
664
+ // map 32
665
+ case 223:
666
+ return decodeMap(buf, offset + 5, view.getUint32(offset + 1, false));
667
+ }
668
+ return { value: null, offset: offset + 1 };
669
+ }
670
+ function decodeArray(buf, offset, count) {
671
+ const arr = new Array(count);
672
+ for (let i = 0; i < count; i++) {
673
+ const result = decodeAt(buf, offset);
674
+ arr[i] = result.value;
675
+ offset = result.offset;
676
+ }
677
+ return { value: arr, offset };
678
+ }
679
+ function decodeMap(buf, offset, count) {
680
+ const obj = {};
681
+ for (let i = 0; i < count; i++) {
682
+ const keyResult = decodeAt(buf, offset);
683
+ offset = keyResult.offset;
684
+ const valResult = decodeAt(buf, offset);
685
+ offset = valResult.offset;
686
+ obj[String(keyResult.value)] = valResult.value;
687
+ }
688
+ return { value: obj, offset };
689
+ }
690
+ var msgpackCodec = {
691
+ encode: msgpackEncode,
692
+ decode: msgpackDecode
693
+ };
694
+ var jsonCodec = {
695
+ encode(data) {
696
+ return encoder.encode(JSON.stringify(data));
697
+ },
698
+ decode(buf) {
699
+ return JSON.parse(decoder.decode(buf));
700
+ }
701
+ };
702
+ function resolveCodec(option) {
703
+ if (!option || option === "msgpack") return msgpackCodec;
704
+ if (option === "json") return jsonCodec;
705
+ return option;
706
+ }
707
+ var textEncoder = encoder;
708
+ function buildRoomFrame(frameType, componentId, roomId, event, payload) {
709
+ const compIdBytes = textEncoder.encode(componentId);
710
+ const roomIdBytes = textEncoder.encode(roomId);
711
+ const eventBytes = textEncoder.encode(event);
712
+ const totalLen = 1 + 1 + compIdBytes.length + 1 + roomIdBytes.length + 2 + eventBytes.length + payload.length;
713
+ const frame = new Uint8Array(totalLen);
714
+ let offset = 0;
715
+ frame[offset++] = frameType;
716
+ frame[offset++] = compIdBytes.length;
717
+ frame.set(compIdBytes, offset);
718
+ offset += compIdBytes.length;
719
+ frame[offset++] = roomIdBytes.length;
720
+ frame.set(roomIdBytes, offset);
721
+ offset += roomIdBytes.length;
722
+ frame[offset++] = eventBytes.length >> 8 & 255;
723
+ frame[offset++] = eventBytes.length & 255;
724
+ frame.set(eventBytes, offset);
725
+ offset += eventBytes.length;
726
+ frame.set(payload, offset);
727
+ return frame;
728
+ }
729
+ function buildRoomFrameTail(roomId, event, payload) {
730
+ const roomIdBytes = textEncoder.encode(roomId);
731
+ const eventBytes = textEncoder.encode(event);
732
+ const tailLen = 1 + roomIdBytes.length + 2 + eventBytes.length + payload.length;
733
+ const tail = new Uint8Array(tailLen);
734
+ let offset = 0;
735
+ tail[offset++] = roomIdBytes.length;
736
+ tail.set(roomIdBytes, offset);
737
+ offset += roomIdBytes.length;
738
+ tail[offset++] = eventBytes.length >> 8 & 255;
739
+ tail[offset++] = eventBytes.length & 255;
740
+ tail.set(eventBytes, offset);
741
+ offset += eventBytes.length;
742
+ tail.set(payload, offset);
743
+ return tail;
744
+ }
745
+ function prependMemberHeader(frameType, componentId, tail) {
746
+ const compIdBytes = textEncoder.encode(componentId);
747
+ const frame = new Uint8Array(1 + 1 + compIdBytes.length + tail.length);
748
+ frame[0] = frameType;
749
+ frame[1] = compIdBytes.length;
750
+ frame.set(compIdBytes, 2);
751
+ frame.set(tail, 2 + compIdBytes.length);
752
+ return frame;
753
+ }
754
+ function parseRoomFrame(buf) {
755
+ if (buf.length < 6) return null;
756
+ let offset = 0;
757
+ const frameType = buf[offset++];
758
+ const compIdLen = buf[offset++];
759
+ if (offset + compIdLen > buf.length) return null;
760
+ const componentId = decoder.decode(buf.subarray(offset, offset + compIdLen));
761
+ offset += compIdLen;
762
+ const roomIdLen = buf[offset++];
763
+ if (offset + roomIdLen > buf.length) return null;
764
+ const roomId = decoder.decode(buf.subarray(offset, offset + roomIdLen));
765
+ offset += roomIdLen;
766
+ if (offset + 2 > buf.length) return null;
767
+ const eventLen = buf[offset] << 8 | buf[offset + 1];
768
+ offset += 2;
769
+ if (offset + eventLen > buf.length) return null;
770
+ const event = decoder.decode(buf.subarray(offset, offset + eventLen));
771
+ offset += eventLen;
772
+ const payload = buf.subarray(offset);
773
+ return { frameType, componentId, roomId, event, payload };
774
+ }
775
+
412
776
  // src/rooms/LiveRoomManager.ts
413
777
  var LiveRoomManager = class {
414
- // componentId -> roomIds
415
778
  /**
416
779
  * @param roomEvents - Local server-side event bus
417
780
  * @param pubsub - Optional cross-instance pub/sub adapter (e.g. Redis).
@@ -424,26 +787,79 @@ var LiveRoomManager = class {
424
787
  }
425
788
  rooms = /* @__PURE__ */ new Map();
426
789
  componentRooms = /* @__PURE__ */ new Map();
790
+ // componentId -> roomIds
791
+ /** Room registry for LiveRoom class lookup. Set by LiveServer. */
792
+ roomRegistry;
427
793
  /**
428
- * Component joins a room
794
+ * Component joins a room.
795
+ * @param options.deepDiff - Enable/disable deep diff for this room's state. Default: true
796
+ * @param joinContext - Optional context for LiveRoom lifecycle hooks (userId, payload)
429
797
  */
430
- joinRoom(componentId, roomId, ws, initialState) {
798
+ joinRoom(componentId, roomId, ws, initialState, options, joinContext) {
431
799
  if (!roomId || !ROOM_NAME_REGEX.test(roomId)) {
432
800
  throw new Error("Invalid room name. Must be 1-64 alphanumeric characters, hyphens, underscores, dots, or colons.");
433
801
  }
434
802
  const now = Date.now();
435
803
  let room = this.rooms.get(roomId);
804
+ let isNewRoom = false;
436
805
  if (!room) {
437
- room = {
438
- id: roomId,
439
- state: initialState || {},
440
- members: /* @__PURE__ */ new Map(),
441
- createdAt: now,
442
- lastActivity: now
443
- };
806
+ isNewRoom = true;
807
+ const roomClass = this.roomRegistry?.resolveFromId(roomId);
808
+ if (roomClass) {
809
+ const instance = new roomClass(roomId, this);
810
+ const opts = roomClass.$options ?? {};
811
+ room = {
812
+ id: roomId,
813
+ state: instance.state,
814
+ members: /* @__PURE__ */ new Map(),
815
+ createdAt: now,
816
+ lastActivity: now,
817
+ deepDiff: opts.deepDiff ?? true,
818
+ deepDiffDepth: opts.deepDiffDepth ?? 3,
819
+ serverOnlyState: true,
820
+ // LiveRoom-backed rooms are always server-only
821
+ instance,
822
+ codec: resolveCodec(opts.codec)
823
+ };
824
+ } else {
825
+ room = {
826
+ id: roomId,
827
+ state: initialState || {},
828
+ members: /* @__PURE__ */ new Map(),
829
+ createdAt: now,
830
+ lastActivity: now,
831
+ deepDiff: options?.deepDiff ?? true,
832
+ deepDiffDepth: options?.deepDiffDepth ?? 3,
833
+ serverOnlyState: options?.serverOnlyState ?? false,
834
+ codec: null
835
+ };
836
+ }
444
837
  this.rooms.set(roomId, room);
445
838
  liveLog("rooms", componentId, `Room '${roomId}' created`);
446
839
  }
840
+ if (room.instance) {
841
+ const ctor = room.instance.constructor;
842
+ const maxMembers = ctor.$options?.maxMembers;
843
+ if (maxMembers && room.members.size >= maxMembers) {
844
+ return { rejected: true, reason: "Room is full" };
845
+ }
846
+ }
847
+ if (room.instance) {
848
+ const result = room.instance.onJoin({
849
+ componentId,
850
+ userId: joinContext?.userId,
851
+ payload: joinContext?.payload
852
+ });
853
+ if (result === false) {
854
+ if (isNewRoom) {
855
+ this.rooms.delete(roomId);
856
+ }
857
+ return { rejected: true, reason: "Join rejected by room" };
858
+ }
859
+ }
860
+ if (isNewRoom && room.instance) {
861
+ room.instance.onCreate();
862
+ }
447
863
  room.members.set(componentId, {
448
864
  componentId,
449
865
  ws,
@@ -475,10 +891,17 @@ var LiveRoomManager = class {
475
891
  }
476
892
  /**
477
893
  * Component leaves a room
894
+ * @param leaveReason - Why the component is leaving. Default: 'leave'
478
895
  */
479
- leaveRoom(componentId, roomId) {
896
+ leaveRoom(componentId, roomId, leaveReason = "leave") {
480
897
  const room = this.rooms.get(roomId);
481
898
  if (!room) return;
899
+ if (room.instance) {
900
+ room.instance.onLeave({
901
+ componentId,
902
+ reason: leaveReason
903
+ });
904
+ }
482
905
  room.members.delete(componentId);
483
906
  const now = Date.now();
484
907
  room.lastActivity = now;
@@ -502,6 +925,10 @@ var LiveRoomManager = class {
502
925
  setTimeout(() => {
503
926
  const currentRoom = this.rooms.get(roomId);
504
927
  if (currentRoom && currentRoom.members.size === 0) {
928
+ if (currentRoom.instance) {
929
+ const result = currentRoom.instance.onDestroy();
930
+ if (result === false) return;
931
+ }
505
932
  this.rooms.delete(roomId);
506
933
  liveLog("rooms", null, `Room '${roomId}' destroyed (empty)`);
507
934
  }
@@ -510,7 +937,7 @@ var LiveRoomManager = class {
510
937
  }
511
938
  /**
512
939
  * Component disconnects - leave all rooms.
513
- * Batches removals: removes member from all rooms first,
940
+ * Batches removals: calls onLeave hooks, removes member from all rooms,
514
941
  * then sends leave notifications in bulk.
515
942
  */
516
943
  cleanupComponent(componentId) {
@@ -521,6 +948,12 @@ var LiveRoomManager = class {
521
948
  for (const roomId of roomIds) {
522
949
  const room = this.rooms.get(roomId);
523
950
  if (!room) continue;
951
+ if (room.instance) {
952
+ room.instance.onLeave({
953
+ componentId,
954
+ reason: "disconnect"
955
+ });
956
+ }
524
957
  room.members.delete(componentId);
525
958
  room.lastActivity = now;
526
959
  const memberCount = room.members.size;
@@ -530,7 +963,12 @@ var LiveRoomManager = class {
530
963
  setTimeout(() => {
531
964
  const currentRoom = this.rooms.get(roomId);
532
965
  if (currentRoom && currentRoom.members.size === 0) {
966
+ if (currentRoom.instance) {
967
+ const result = currentRoom.instance.onDestroy();
968
+ if (result === false) return;
969
+ }
533
970
  this.rooms.delete(roomId);
971
+ liveLog("rooms", null, `Room '${roomId}' destroyed (empty)`);
534
972
  }
535
973
  }, 5 * 60 * 1e3);
536
974
  }
@@ -551,13 +989,19 @@ var LiveRoomManager = class {
551
989
  this.componentRooms.delete(componentId);
552
990
  }
553
991
  /**
554
- * Emit event to all members in a room
992
+ * Emit event to all members in a room.
993
+ * For LiveRoom-backed rooms, calls onEvent() hook before broadcasting.
555
994
  */
556
995
  emitToRoom(roomId, event, data, excludeComponentId) {
557
996
  const room = this.rooms.get(roomId);
558
997
  if (!room) return 0;
559
998
  const now = Date.now();
560
999
  room.lastActivity = now;
1000
+ if (room.instance) {
1001
+ room.instance.onEvent(event, data, {
1002
+ componentId: excludeComponentId ?? ""
1003
+ });
1004
+ }
561
1005
  this.roomEvents.emit("room", roomId, event, data, excludeComponentId);
562
1006
  this.pubsub?.publish(roomId, event, data)?.catch(() => {
563
1007
  });
@@ -572,12 +1016,35 @@ var LiveRoomManager = class {
572
1016
  }
573
1017
  /**
574
1018
  * Update room state.
575
- * Mutates state in-place with Object.assign to avoid full-object spread.
1019
+ * When deepDiff is enabled (default), deep-diffs plain objects to send only changed fields.
1020
+ * When disabled, uses shallow diff (reference equality) like classic behavior.
576
1021
  */
577
1022
  setRoomState(roomId, updates, excludeComponentId) {
578
1023
  const room = this.rooms.get(roomId);
579
1024
  if (!room) return;
580
- Object.assign(room.state, updates);
1025
+ let actualChanges;
1026
+ if (room.deepDiff) {
1027
+ const diff = computeDeepDiff(
1028
+ room.state,
1029
+ updates,
1030
+ 0,
1031
+ room.deepDiffDepth
1032
+ );
1033
+ if (diff === null) return;
1034
+ actualChanges = diff;
1035
+ deepAssign(room.state, actualChanges);
1036
+ } else {
1037
+ actualChanges = {};
1038
+ let hasChanges = false;
1039
+ for (const key of Object.keys(updates)) {
1040
+ if (room.state[key] !== updates[key]) {
1041
+ actualChanges[key] = updates[key];
1042
+ hasChanges = true;
1043
+ }
1044
+ }
1045
+ if (!hasChanges) return;
1046
+ Object.assign(room.state, actualChanges);
1047
+ }
581
1048
  if (room.stateSize === void 0) {
582
1049
  const fullJson = JSON.stringify(room.state);
583
1050
  room.stateSize = fullJson.length;
@@ -585,7 +1052,7 @@ var LiveRoomManager = class {
585
1052
  throw new Error("Room state exceeds maximum size limit");
586
1053
  }
587
1054
  } else {
588
- const deltaSize = JSON.stringify(updates).length;
1055
+ const deltaSize = JSON.stringify(actualChanges).length;
589
1056
  room.stateSize += deltaSize;
590
1057
  if (room.stateSize > MAX_ROOM_STATE_SIZE) {
591
1058
  const precise = JSON.stringify(room.state).length;
@@ -597,14 +1064,14 @@ var LiveRoomManager = class {
597
1064
  }
598
1065
  const now = Date.now();
599
1066
  room.lastActivity = now;
600
- this.pubsub?.publishStateChange(roomId, updates)?.catch(() => {
1067
+ this.pubsub?.publishStateChange(roomId, actualChanges)?.catch(() => {
601
1068
  });
602
1069
  this.broadcastToRoom(roomId, {
603
1070
  type: "ROOM_STATE",
604
1071
  componentId: "",
605
1072
  roomId,
606
1073
  event: "$state:update",
607
- data: { state: updates },
1074
+ data: { state: actualChanges },
608
1075
  timestamp: now
609
1076
  }, excludeComponentId);
610
1077
  }
@@ -616,27 +1083,39 @@ var LiveRoomManager = class {
616
1083
  }
617
1084
  /**
618
1085
  * Broadcast to all members in a room.
619
- * Serializes the message ONCE and sends the same string to all members.
1086
+ *
1087
+ * When the room has a binary codec (LiveRoom-backed), builds a binary frame
1088
+ * once (encode payload + frame tail), then prepends per-member componentId header.
1089
+ *
1090
+ * When no codec (legacy rooms), uses JSON with serialize-once optimization:
1091
+ * builds the JSON string template once, then inserts each member's componentId.
620
1092
  */
621
1093
  broadcastToRoom(roomId, message, excludeComponentId) {
622
1094
  const room = this.rooms.get(roomId);
623
1095
  if (!room || room.members.size === 0) return 0;
624
- const serialized = JSON.stringify(message);
625
1096
  let sent = 0;
626
- if (excludeComponentId) {
627
- for (const [componentId, member] of room.members) {
628
- if (componentId === excludeComponentId) continue;
629
- if (member.ws.readyState === 1) {
630
- queuePreSerialized(member.ws, serialized);
631
- sent++;
632
- }
1097
+ if (room.codec) {
1098
+ const frameType = message.type === "ROOM_EVENT" || message.type === "ROOM_SYSTEM" ? BINARY_ROOM_EVENT : BINARY_ROOM_STATE;
1099
+ const event = message.event ?? "";
1100
+ const payload = room.codec.encode(message.data);
1101
+ const tail = buildRoomFrameTail(roomId, event, payload);
1102
+ for (const [memberComponentId, member] of room.members) {
1103
+ if (memberComponentId === excludeComponentId) continue;
1104
+ if (member.ws.readyState !== 1) continue;
1105
+ const frame = prependMemberHeader(frameType, memberComponentId, tail);
1106
+ sendBinaryImmediate(member.ws, frame);
1107
+ sent++;
633
1108
  }
634
1109
  } else {
635
- for (const member of room.members.values()) {
636
- if (member.ws.readyState === 1) {
637
- queuePreSerialized(member.ws, serialized);
638
- sent++;
639
- }
1110
+ const { componentId: _, ...rest } = message;
1111
+ const jsonBody = JSON.stringify(rest);
1112
+ const prefix = '{"componentId":"';
1113
+ const suffix = '",' + jsonBody.slice(1);
1114
+ for (const [memberComponentId, member] of room.members) {
1115
+ if (memberComponentId === excludeComponentId) continue;
1116
+ if (member.ws.readyState !== 1) continue;
1117
+ queuePreSerialized(member.ws, prefix + memberComponentId + suffix);
1118
+ sent++;
640
1119
  }
641
1120
  }
642
1121
  return sent;
@@ -647,12 +1126,31 @@ var LiveRoomManager = class {
647
1126
  isInRoom(componentId, roomId) {
648
1127
  return this.rooms.get(roomId)?.members.has(componentId) ?? false;
649
1128
  }
1129
+ /**
1130
+ * Check if room state is server-only (no client writes)
1131
+ */
1132
+ isServerOnlyState(roomId) {
1133
+ return this.rooms.get(roomId)?.serverOnlyState ?? false;
1134
+ }
650
1135
  /**
651
1136
  * Get rooms for a component
652
1137
  */
653
1138
  getComponentRooms(componentId) {
654
1139
  return Array.from(this.componentRooms.get(componentId) || []);
655
1140
  }
1141
+ /**
1142
+ * Get member count for a room
1143
+ */
1144
+ getMemberCount(roomId) {
1145
+ return this.rooms.get(roomId)?.members.size ?? 0;
1146
+ }
1147
+ /**
1148
+ * Get the LiveRoom instance for a room (if backed by a typed room class).
1149
+ * Used by ComponentRoomProxy to expose custom methods.
1150
+ */
1151
+ getRoomInstance(roomId) {
1152
+ return this.rooms.get(roomId)?.instance;
1153
+ }
656
1154
  /**
657
1155
  * Get statistics
658
1156
  */
@@ -672,276 +1170,6 @@ var LiveRoomManager = class {
672
1170
  }
673
1171
  };
674
1172
 
675
- // src/debug/LiveDebugger.ts
676
- var MAX_EVENTS = 500;
677
- var MAX_STATE_SIZE = 5e4;
678
- var LiveDebugger = class {
679
- events = [];
680
- componentSnapshots = /* @__PURE__ */ new Map();
681
- debugClients = /* @__PURE__ */ new Set();
682
- _enabled = false;
683
- startTime = Date.now();
684
- eventCounter = 0;
685
- constructor(enabled = false) {
686
- this._enabled = enabled;
687
- }
688
- get enabled() {
689
- return this._enabled;
690
- }
691
- set enabled(value) {
692
- this._enabled = value;
693
- }
694
- // ===== Event Emission =====
695
- emit(type, componentId, componentName, data = {}) {
696
- if (!this._enabled) return;
697
- const event = {
698
- id: `dbg-${++this.eventCounter}`,
699
- timestamp: Date.now(),
700
- type,
701
- componentId,
702
- componentName,
703
- data: this.sanitizeData(data)
704
- };
705
- this.events.push(event);
706
- if (this.events.length > MAX_EVENTS) {
707
- this.events.shift();
708
- }
709
- if (componentId) {
710
- this.updateSnapshot(event);
711
- }
712
- this.broadcastEvent(event);
713
- }
714
- // ===== Component Tracking =====
715
- trackComponentMount(componentId, componentName, initialState, room, debugLabel) {
716
- if (!this._enabled) return;
717
- const snapshot = {
718
- componentId,
719
- componentName,
720
- debugLabel,
721
- state: this.sanitizeState(initialState),
722
- rooms: room ? [room] : [],
723
- mountedAt: Date.now(),
724
- lastActivity: Date.now(),
725
- actionCount: 0,
726
- stateChangeCount: 0,
727
- errorCount: 0
728
- };
729
- this.componentSnapshots.set(componentId, snapshot);
730
- this.emit("COMPONENT_MOUNT", componentId, componentName, {
731
- initialState: snapshot.state,
732
- room: room ?? null,
733
- debugLabel: debugLabel ?? null
734
- });
735
- }
736
- trackComponentUnmount(componentId) {
737
- if (!this._enabled) return;
738
- const snapshot = this.componentSnapshots.get(componentId);
739
- const componentName = snapshot?.componentName ?? null;
740
- this.emit("COMPONENT_UNMOUNT", componentId, componentName, {
741
- lifetime: snapshot ? Date.now() - snapshot.mountedAt : 0,
742
- totalActions: snapshot?.actionCount ?? 0,
743
- totalStateChanges: snapshot?.stateChangeCount ?? 0,
744
- totalErrors: snapshot?.errorCount ?? 0
745
- });
746
- this.componentSnapshots.delete(componentId);
747
- }
748
- trackStateChange(componentId, delta, fullState, source = "setState") {
749
- if (!this._enabled) return;
750
- const snapshot = this.componentSnapshots.get(componentId);
751
- if (snapshot) {
752
- snapshot.state = this.sanitizeState(fullState);
753
- snapshot.stateChangeCount++;
754
- snapshot.lastActivity = Date.now();
755
- }
756
- this.emit("STATE_CHANGE", componentId, snapshot?.componentName ?? null, {
757
- delta,
758
- fullState: this.sanitizeState(fullState),
759
- source
760
- });
761
- }
762
- trackActionCall(componentId, action, payload) {
763
- if (!this._enabled) return;
764
- const snapshot = this.componentSnapshots.get(componentId);
765
- if (snapshot) {
766
- snapshot.actionCount++;
767
- snapshot.lastActivity = Date.now();
768
- }
769
- this.emit("ACTION_CALL", componentId, snapshot?.componentName ?? null, {
770
- action,
771
- payload: this.sanitizeData({ payload }).payload
772
- });
773
- }
774
- trackActionResult(componentId, action, result, duration) {
775
- if (!this._enabled) return;
776
- const snapshot = this.componentSnapshots.get(componentId);
777
- this.emit("ACTION_RESULT", componentId, snapshot?.componentName ?? null, {
778
- action,
779
- result: this.sanitizeData({ result }).result,
780
- duration
781
- });
782
- }
783
- trackActionError(componentId, action, error, duration) {
784
- if (!this._enabled) return;
785
- const snapshot = this.componentSnapshots.get(componentId);
786
- if (snapshot) {
787
- snapshot.errorCount++;
788
- }
789
- this.emit("ACTION_ERROR", componentId, snapshot?.componentName ?? null, {
790
- action,
791
- error,
792
- duration
793
- });
794
- }
795
- trackRoomJoin(componentId, roomId) {
796
- if (!this._enabled) return;
797
- const snapshot = this.componentSnapshots.get(componentId);
798
- if (snapshot && !snapshot.rooms.includes(roomId)) {
799
- snapshot.rooms.push(roomId);
800
- }
801
- this.emit("ROOM_JOIN", componentId, snapshot?.componentName ?? null, { roomId });
802
- }
803
- trackRoomLeave(componentId, roomId) {
804
- if (!this._enabled) return;
805
- const snapshot = this.componentSnapshots.get(componentId);
806
- if (snapshot) {
807
- snapshot.rooms = snapshot.rooms.filter((r) => r !== roomId);
808
- }
809
- this.emit("ROOM_LEAVE", componentId, snapshot?.componentName ?? null, { roomId });
810
- }
811
- trackRoomEmit(componentId, roomId, event, data) {
812
- if (!this._enabled) return;
813
- const snapshot = this.componentSnapshots.get(componentId);
814
- this.emit("ROOM_EMIT", componentId, snapshot?.componentName ?? null, {
815
- roomId,
816
- event,
817
- data: this.sanitizeData({ data }).data
818
- });
819
- }
820
- trackConnection(connectionId) {
821
- if (!this._enabled) return;
822
- this.emit("WS_CONNECT", null, null, { connectionId });
823
- }
824
- trackDisconnection(connectionId, componentCount) {
825
- if (!this._enabled) return;
826
- this.emit("WS_DISCONNECT", null, null, { connectionId, componentCount });
827
- }
828
- trackError(componentId, error, context) {
829
- if (!this._enabled) return;
830
- const snapshot = componentId ? this.componentSnapshots.get(componentId) : null;
831
- if (snapshot) {
832
- snapshot.errorCount++;
833
- }
834
- this.emit("ERROR", componentId, snapshot?.componentName ?? null, {
835
- error,
836
- ...context
837
- });
838
- }
839
- // ===== Debug Client Management =====
840
- registerDebugClient(ws) {
841
- if (!this._enabled) {
842
- const disabled = {
843
- type: "DEBUG_DISABLED",
844
- enabled: false,
845
- timestamp: Date.now()
846
- };
847
- ws.send(JSON.stringify(disabled));
848
- ws.close();
849
- return;
850
- }
851
- this.debugClients.add(ws);
852
- const welcome = {
853
- type: "DEBUG_WELCOME",
854
- enabled: true,
855
- snapshot: this.getSnapshot(),
856
- timestamp: Date.now()
857
- };
858
- ws.send(JSON.stringify(welcome));
859
- for (const event of this.events.slice(-100)) {
860
- const msg = {
861
- type: "DEBUG_EVENT",
862
- event,
863
- timestamp: Date.now()
864
- };
865
- ws.send(JSON.stringify(msg));
866
- }
867
- }
868
- unregisterDebugClient(ws) {
869
- this.debugClients.delete(ws);
870
- }
871
- // ===== Snapshot =====
872
- getSnapshot() {
873
- return {
874
- components: Array.from(this.componentSnapshots.values()),
875
- connections: this.debugClients.size,
876
- uptime: Date.now() - this.startTime,
877
- totalEvents: this.eventCounter
878
- };
879
- }
880
- getComponentState(componentId) {
881
- return this.componentSnapshots.get(componentId) ?? null;
882
- }
883
- getEvents(filter) {
884
- let filtered = this.events;
885
- if (filter?.componentId) {
886
- filtered = filtered.filter((e) => e.componentId === filter.componentId);
887
- }
888
- if (filter?.type) {
889
- filtered = filtered.filter((e) => e.type === filter.type);
890
- }
891
- const limit = filter?.limit ?? 100;
892
- return filtered.slice(-limit);
893
- }
894
- clearEvents() {
895
- this.events = [];
896
- }
897
- // ===== Internal =====
898
- broadcastEvent(event) {
899
- if (this.debugClients.size === 0) return;
900
- const msg = {
901
- type: "DEBUG_EVENT",
902
- event,
903
- timestamp: Date.now()
904
- };
905
- const json = JSON.stringify(msg);
906
- for (const client of this.debugClients) {
907
- try {
908
- client.send(json);
909
- } catch {
910
- this.debugClients.delete(client);
911
- }
912
- }
913
- }
914
- sanitizeData(data) {
915
- try {
916
- const json = JSON.stringify(data);
917
- if (json.length > MAX_STATE_SIZE) {
918
- return { _truncated: true, _size: json.length, _preview: json.slice(0, 500) + "..." };
919
- }
920
- return JSON.parse(json);
921
- } catch {
922
- return { _serialization_error: true };
923
- }
924
- }
925
- sanitizeState(state) {
926
- try {
927
- const json = JSON.stringify(state);
928
- if (json.length > MAX_STATE_SIZE) {
929
- return { _truncated: true, _size: json.length };
930
- }
931
- return JSON.parse(json);
932
- } catch {
933
- return { _serialization_error: true };
934
- }
935
- }
936
- updateSnapshot(event) {
937
- if (!event.componentId) return;
938
- const snapshot = this.componentSnapshots.get(event.componentId);
939
- if (snapshot) {
940
- snapshot.lastActivity = event.timestamp;
941
- }
942
- }
943
- };
944
-
945
1173
  // src/auth/LiveAuthContext.ts
946
1174
  var AuthenticatedContext = class {
947
1175
  authenticated = true;
@@ -2021,18 +2249,20 @@ var ComponentStateManager = class {
2021
2249
  _proxyState;
2022
2250
  _inStateChange = false;
2023
2251
  _idBytes = null;
2252
+ _deepDiff;
2253
+ _deepDiffDepth;
2024
2254
  componentId;
2025
2255
  ws;
2026
2256
  emitFn;
2027
2257
  onStateChangeFn;
2028
- _debugger;
2029
2258
  constructor(opts) {
2030
2259
  this.componentId = opts.componentId;
2031
2260
  this.ws = opts.ws;
2032
2261
  this.emitFn = opts.emitFn;
2033
2262
  this.onStateChangeFn = opts.onStateChangeFn;
2034
- this._debugger = opts.debugger ?? null;
2035
- this._state = opts.initialState;
2263
+ this._deepDiff = opts.deepDiff ?? false;
2264
+ this._deepDiffDepth = opts.deepDiffDepth ?? 3;
2265
+ this._state = this._deepDiff ? structuredClone(opts.initialState) : opts.initialState;
2036
2266
  this._proxyState = this.createStateProxy(this._state);
2037
2267
  }
2038
2268
  get rawState() {
@@ -2064,12 +2294,6 @@ var ComponentStateManager = class {
2064
2294
  self._inStateChange = false;
2065
2295
  }
2066
2296
  }
2067
- self._debugger?.trackStateChange(
2068
- self.componentId,
2069
- changes,
2070
- target,
2071
- "proxy"
2072
- );
2073
2297
  }
2074
2298
  return true;
2075
2299
  },
@@ -2080,16 +2304,34 @@ var ComponentStateManager = class {
2080
2304
  }
2081
2305
  setState(updates) {
2082
2306
  const newUpdates = typeof updates === "function" ? updates(this._state) : updates;
2083
- const actualChanges = {};
2084
- let hasChanges = false;
2085
- for (const key of Object.keys(newUpdates)) {
2086
- if (this._state[key] !== newUpdates[key]) {
2087
- actualChanges[key] = newUpdates[key];
2088
- hasChanges = true;
2307
+ let actualChanges;
2308
+ let hasChanges;
2309
+ if (this._deepDiff) {
2310
+ const diff = computeDeepDiff(
2311
+ this._state,
2312
+ newUpdates,
2313
+ 0,
2314
+ this._deepDiffDepth
2315
+ );
2316
+ if (diff === null) return;
2317
+ actualChanges = diff;
2318
+ hasChanges = true;
2319
+ } else {
2320
+ actualChanges = {};
2321
+ hasChanges = false;
2322
+ for (const key of Object.keys(newUpdates)) {
2323
+ if (this._state[key] !== newUpdates[key]) {
2324
+ actualChanges[key] = newUpdates[key];
2325
+ hasChanges = true;
2326
+ }
2089
2327
  }
2090
2328
  }
2091
2329
  if (!hasChanges) return;
2092
- Object.assign(this._state, actualChanges);
2330
+ if (this._deepDiff) {
2331
+ deepAssign(this._state, actualChanges);
2332
+ } else {
2333
+ Object.assign(this._state, actualChanges);
2334
+ }
2093
2335
  this.emitFn("STATE_DELTA", { delta: actualChanges });
2094
2336
  if (!this._inStateChange) {
2095
2337
  this._inStateChange = true;
@@ -2101,14 +2343,8 @@ var ComponentStateManager = class {
2101
2343
  this._inStateChange = false;
2102
2344
  }
2103
2345
  }
2104
- this._debugger?.trackStateChange(
2105
- this.componentId,
2106
- actualChanges,
2107
- this._state,
2108
- "setState"
2109
- );
2110
2346
  }
2111
- sendBinaryDelta(delta, encoder) {
2347
+ sendBinaryDelta(delta, encoder2) {
2112
2348
  const actualChanges = {};
2113
2349
  let hasChanges = false;
2114
2350
  for (const key of Object.keys(delta)) {
@@ -2119,7 +2355,7 @@ var ComponentStateManager = class {
2119
2355
  }
2120
2356
  if (!hasChanges) return;
2121
2357
  Object.assign(this._state, actualChanges);
2122
- const payload = encoder(actualChanges);
2358
+ const payload = encoder2(actualChanges);
2123
2359
  if (!this._idBytes) {
2124
2360
  this._idBytes = new TextEncoder().encode(this.componentId);
2125
2361
  }
@@ -2278,7 +2514,6 @@ var BLOCKED_ACTIONS = /* @__PURE__ */ new Set([
2278
2514
  var ActionSecurityManager = class {
2279
2515
  _actionCalls = /* @__PURE__ */ new Map();
2280
2516
  async validateAndExecute(action, payload, ctx) {
2281
- const actionStart = Date.now();
2282
2517
  const { component, componentClass, componentId } = ctx;
2283
2518
  try {
2284
2519
  if (BLOCKED_ACTIONS.has(action)) {
@@ -2332,12 +2567,10 @@ var ActionSecurityManager = class {
2332
2567
  }
2333
2568
  payload = result2.data ?? payload;
2334
2569
  }
2335
- ctx.debugger?.trackActionCall(componentId, action, payload);
2336
2570
  let hookResult;
2337
2571
  try {
2338
2572
  hookResult = await component.onAction(action, payload);
2339
2573
  } catch (hookError) {
2340
- ctx.debugger?.trackActionError(componentId, action, hookError.message, Date.now() - actionStart);
2341
2574
  ctx.emitFn("ERROR", {
2342
2575
  action,
2343
2576
  error: `Action '${action}' failed pre-validation`
@@ -2345,15 +2578,12 @@ var ActionSecurityManager = class {
2345
2578
  throw hookError;
2346
2579
  }
2347
2580
  if (hookResult === false) {
2348
- ctx.debugger?.trackActionError(componentId, action, "Action cancelled", Date.now() - actionStart);
2349
2581
  throw new Error(`Action '${action}' was cancelled`);
2350
2582
  }
2351
2583
  const result = await method.call(component, payload);
2352
- ctx.debugger?.trackActionResult(componentId, action, result, Date.now() - actionStart);
2353
2584
  return result;
2354
2585
  } catch (error) {
2355
2586
  if (!error.message?.includes("was cancelled") && !error.message?.includes("pre-validation")) {
2356
- ctx.debugger?.trackActionError(componentId, action, error.message, Date.now() - actionStart);
2357
2587
  ctx.emitFn("ERROR", {
2358
2588
  action,
2359
2589
  error: error.message
@@ -2365,6 +2595,66 @@ var ActionSecurityManager = class {
2365
2595
  };
2366
2596
 
2367
2597
  // src/component/managers/ComponentRoomProxy.ts
2598
+ var RESERVED_KEYS = /* @__PURE__ */ new Set([
2599
+ "id",
2600
+ "state",
2601
+ "join",
2602
+ "leave",
2603
+ "emit",
2604
+ "on",
2605
+ "setState",
2606
+ // Function internals (proxy wraps a function)
2607
+ "call",
2608
+ "apply",
2609
+ "bind",
2610
+ "prototype",
2611
+ "length",
2612
+ "name",
2613
+ "arguments",
2614
+ "caller",
2615
+ // Symbol keys
2616
+ Symbol.toPrimitive,
2617
+ Symbol.toStringTag,
2618
+ Symbol.hasInstance
2619
+ ]);
2620
+ var TYPED_RESERVED_KEYS = /* @__PURE__ */ new Set([
2621
+ "id",
2622
+ "state",
2623
+ "meta",
2624
+ "join",
2625
+ "leave",
2626
+ "emit",
2627
+ "on",
2628
+ "setState",
2629
+ "memberCount",
2630
+ // Proxy internals
2631
+ "then",
2632
+ "toJSON",
2633
+ "valueOf",
2634
+ "toString",
2635
+ Symbol.toPrimitive,
2636
+ Symbol.toStringTag,
2637
+ Symbol.hasInstance
2638
+ ]);
2639
+ function wrapWithStateProxy(target, getState, setState) {
2640
+ return new Proxy(target, {
2641
+ get(obj, prop, receiver) {
2642
+ if (RESERVED_KEYS.has(prop) || typeof prop === "symbol") {
2643
+ return Reflect.get(obj, prop, receiver);
2644
+ }
2645
+ const desc = Object.getOwnPropertyDescriptor(obj, prop);
2646
+ if (desc) return Reflect.get(obj, prop, receiver);
2647
+ if (prop in obj) return Reflect.get(obj, prop, receiver);
2648
+ const st = getState();
2649
+ return st?.[prop];
2650
+ },
2651
+ set(_obj, prop, value) {
2652
+ if (typeof prop === "symbol") return false;
2653
+ setState({ [prop]: value });
2654
+ return true;
2655
+ }
2656
+ });
2657
+ }
2368
2658
  var ComponentRoomProxy = class {
2369
2659
  roomEventUnsubscribers = [];
2370
2660
  joinedRooms = /* @__PURE__ */ new Set();
@@ -2377,18 +2667,22 @@ var ComponentRoomProxy = class {
2377
2667
  componentId;
2378
2668
  ws;
2379
2669
  getCtx;
2380
- _debugger;
2381
2670
  setStateFn;
2671
+ _deepDiff;
2672
+ _deepDiffDepth;
2673
+ _serverOnlyState;
2382
2674
  constructor(rctx) {
2383
2675
  this.componentId = rctx.componentId;
2384
2676
  this.ws = rctx.ws;
2385
2677
  this.room = rctx.defaultRoom;
2386
2678
  this.getCtx = rctx.getCtx;
2387
- this._debugger = rctx.debugger ?? null;
2388
2679
  this.setStateFn = rctx.setStateFn;
2680
+ this._deepDiff = rctx.deepDiff ?? true;
2681
+ this._deepDiffDepth = rctx.deepDiffDepth;
2682
+ this._serverOnlyState = rctx.serverOnlyState ?? false;
2389
2683
  if (this.room) {
2390
2684
  this.joinedRooms.add(this.room);
2391
- this.ctx.roomManager.joinRoom(this.componentId, this.room, this.ws);
2685
+ this.ctx.roomManager.joinRoom(this.componentId, this.room, this.ws, void 0, { deepDiff: this._deepDiff, deepDiffDepth: this._deepDiffDepth, serverOnlyState: this._serverOnlyState });
2392
2686
  }
2393
2687
  }
2394
2688
  /** Lazy context resolution — cached after first access */
@@ -2416,7 +2710,7 @@ var ComponentRoomProxy = class {
2416
2710
  if (self.joinedRooms.has(roomId)) return;
2417
2711
  self.joinedRooms.add(roomId);
2418
2712
  self._roomsCache = null;
2419
- self.ctx.roomManager.joinRoom(self.componentId, roomId, self.ws, initialState);
2713
+ self.ctx.roomManager.joinRoom(self.componentId, roomId, self.ws, initialState, { deepDiff: self._deepDiff, deepDiffDepth: self._deepDiffDepth, serverOnlyState: self._serverOnlyState });
2420
2714
  },
2421
2715
  leave: () => {
2422
2716
  if (!self.joinedRooms.has(roomId)) return;
@@ -2442,10 +2736,20 @@ var ComponentRoomProxy = class {
2442
2736
  self.ctx.roomManager.setRoomState(roomId, updates, self.componentId);
2443
2737
  }
2444
2738
  };
2445
- this.roomHandles.set(roomId, handle);
2446
- return handle;
2739
+ const proxied = wrapWithStateProxy(
2740
+ handle,
2741
+ () => self.ctx.roomManager.getRoomState(roomId),
2742
+ (updates) => self.ctx.roomManager.setRoomState(roomId, updates, self.componentId)
2743
+ );
2744
+ this.roomHandles.set(roomId, proxied);
2745
+ return proxied;
2447
2746
  };
2448
- const proxyFn = ((roomId) => createHandle(roomId));
2747
+ const proxyFn = ((roomIdOrClass, instanceId) => {
2748
+ if (typeof roomIdOrClass === "function" && instanceId !== void 0) {
2749
+ return self.$typedRoom(roomIdOrClass, instanceId);
2750
+ }
2751
+ return createHandle(roomIdOrClass);
2752
+ });
2449
2753
  const defaultHandle = this.room ? createHandle(this.room) : null;
2450
2754
  Object.defineProperties(proxyFn, {
2451
2755
  id: { get: () => self.room },
@@ -2481,8 +2785,113 @@ var ComponentRoomProxy = class {
2481
2785
  }
2482
2786
  }
2483
2787
  });
2484
- this._roomProxy = proxyFn;
2485
- return proxyFn;
2788
+ const defaultRoom = this.room;
2789
+ const wrapped = defaultRoom ? wrapWithStateProxy(
2790
+ proxyFn,
2791
+ () => self.ctx.roomManager.getRoomState(defaultRoom),
2792
+ (updates) => self.ctx.roomManager.setRoomState(defaultRoom, updates, self.componentId)
2793
+ ) : proxyFn;
2794
+ this._roomProxy = wrapped;
2795
+ return wrapped;
2796
+ }
2797
+ /**
2798
+ * Get a typed room handle backed by a LiveRoom class.
2799
+ *
2800
+ * Usage: `this.$room(ChatRoom, 'lobby')` → typed handle with custom methods
2801
+ *
2802
+ * The returned handle exposes:
2803
+ * - `.id`, `.state`, `.meta`, `.memberCount` — framework properties
2804
+ * - `.join(payload?)`, `.leave()`, `.emit()`, `.on()`, `.setState()` — framework API
2805
+ * - Any custom method defined on the LiveRoom subclass (e.g. `.addMessage()`, `.ban()`)
2806
+ *
2807
+ * The compound room ID is `${roomClass.roomName}:${instanceId}`.
2808
+ */
2809
+ $typedRoom(roomClass, instanceId) {
2810
+ const roomId = `${roomClass.roomName}:${instanceId}`;
2811
+ const self = this;
2812
+ const cached = this.roomHandles.get(roomId);
2813
+ if (cached) return cached;
2814
+ const handle = {
2815
+ get id() {
2816
+ return roomId;
2817
+ },
2818
+ get state() {
2819
+ const instance = self.ctx.roomManager.getRoomInstance?.(roomId);
2820
+ return instance ? instance.state : self.ctx.roomManager.getRoomState(roomId);
2821
+ },
2822
+ get meta() {
2823
+ const instance = self.ctx.roomManager.getRoomInstance?.(roomId);
2824
+ if (!instance) throw new Error(`Room '${roomId}' not found or not backed by a LiveRoom class`);
2825
+ return instance.meta;
2826
+ },
2827
+ get memberCount() {
2828
+ return self.ctx.roomManager.getMemberCount?.(roomId) ?? 0;
2829
+ },
2830
+ join: (payload) => {
2831
+ if (self.joinedRooms.has(roomId)) return {};
2832
+ const result = self.ctx.roomManager.joinRoom(
2833
+ self.componentId,
2834
+ roomId,
2835
+ self.ws,
2836
+ void 0,
2837
+ void 0,
2838
+ { userId: void 0, payload }
2839
+ );
2840
+ if ("rejected" in result && result.rejected) {
2841
+ return result;
2842
+ }
2843
+ self.joinedRooms.add(roomId);
2844
+ self._roomsCache = null;
2845
+ sendImmediate(self.ws, JSON.stringify({
2846
+ type: "ROOM_JOINED",
2847
+ componentId: self.componentId,
2848
+ roomId,
2849
+ event: "$room:joined",
2850
+ data: { state: result.state },
2851
+ timestamp: Date.now()
2852
+ }));
2853
+ return {};
2854
+ },
2855
+ leave: () => {
2856
+ if (!self.joinedRooms.has(roomId)) return;
2857
+ self.joinedRooms.delete(roomId);
2858
+ self._roomsCache = null;
2859
+ self.ctx.roomManager.leaveRoom(self.componentId, roomId, "leave");
2860
+ },
2861
+ emit: ((event, data) => {
2862
+ return self.ctx.roomManager.emitToRoom(roomId, event, data, self.componentId);
2863
+ }),
2864
+ on: (event, handler) => {
2865
+ const unsubscribe = self.ctx.roomEvents.on(
2866
+ "room",
2867
+ roomId,
2868
+ event,
2869
+ self.componentId,
2870
+ handler
2871
+ );
2872
+ self.roomEventUnsubscribers.push(unsubscribe);
2873
+ return unsubscribe;
2874
+ },
2875
+ setState: (updates) => {
2876
+ self.ctx.roomManager.setRoomState(roomId, updates, self.componentId);
2877
+ }
2878
+ };
2879
+ const proxied = new Proxy(handle, {
2880
+ get(obj, prop, receiver) {
2881
+ if (TYPED_RESERVED_KEYS.has(prop) || typeof prop === "symbol") {
2882
+ return Reflect.get(obj, prop, receiver);
2883
+ }
2884
+ if (prop in obj) return Reflect.get(obj, prop, receiver);
2885
+ const instance = self.ctx.roomManager.getRoomInstance?.(roomId);
2886
+ if (instance && prop in instance) {
2887
+ const val = instance[prop];
2888
+ return typeof val === "function" ? val.bind(instance) : val;
2889
+ }
2890
+ return void 0;
2891
+ }
2892
+ });
2893
+ this.roomHandles.set(roomId, proxied);
2894
+ return proxied;
2486
2895
  }
2487
2896
  get $rooms() {
2488
2897
  if (this._roomsCache) return this._roomsCache;
@@ -2500,7 +2909,6 @@ var ComponentRoomProxy = class {
2500
2909
  const excludeId = notifySelf ? void 0 : this.componentId;
2501
2910
  const notified = this.ctx.roomEvents.emit(this.roomType, this.room, event, data, excludeId);
2502
2911
  liveLog("rooms", this.componentId, `[${this.componentId}] Room event '${event}' -> ${notified} components`);
2503
- this._debugger?.trackRoomEmit(this.componentId, this.room, event, data);
2504
2912
  return notified;
2505
2913
  }
2506
2914
  onRoomEvent(event, handler) {
@@ -2546,10 +2954,6 @@ var ComponentRoomProxy = class {
2546
2954
  };
2547
2955
 
2548
2956
  // src/component/LiveComponent.ts
2549
- var _liveDebugger = null;
2550
- function _setLiveDebugger(dbg) {
2551
- _liveDebugger = dbg;
2552
- }
2553
2957
  var LiveComponent = class {
2554
2958
  /** Component name for registry lookup - must be defined in subclasses */
2555
2959
  static componentName;
@@ -2599,6 +3003,13 @@ var LiveComponent = class {
2599
3003
  * All clients share the same state.
2600
3004
  */
2601
3005
  static singleton;
3006
+ /**
3007
+ * Component behavior options.
3008
+ *
3009
+ * @example
3010
+ * static $options = { deepDiff: true }
3011
+ */
3012
+ static $options;
2602
3013
  id;
2603
3014
  state;
2604
3015
  // Proxy wrapper (getter delegates to _stateManager)
@@ -2642,7 +3053,8 @@ var LiveComponent = class {
2642
3053
  ws: this.ws,
2643
3054
  emitFn: (type, payload) => this._messaging.emit(type, payload),
2644
3055
  onStateChangeFn: (changes) => this.onStateChange(changes),
2645
- debugger: _liveDebugger
3056
+ deepDiff: ctor.$options?.deepDiff ?? false,
3057
+ deepDiffDepth: ctor.$options?.deepDiffDepth
2646
3058
  });
2647
3059
  this.state = this._stateManager.proxyState;
2648
3060
  this._actionSecurity = new ActionSecurityManager();
@@ -2651,8 +3063,10 @@ var LiveComponent = class {
2651
3063
  ws: this.ws,
2652
3064
  defaultRoom: this.room,
2653
3065
  getCtx: () => getLiveComponentContext(),
2654
- debugger: _liveDebugger,
2655
- setStateFn: (updates) => this.setState(updates)
3066
+ setStateFn: (updates) => this.setState(updates),
3067
+ deepDiff: ctor.$options?.roomDeepDiff,
3068
+ deepDiffDepth: ctor.$options?.deepDiffDepth,
3069
+ serverOnlyState: ctor.$options?.serverOnlyRoomState
2656
3070
  });
2657
3071
  this._stateManager.applyDirectAccessors(this, this.constructor);
2658
3072
  }
@@ -2665,6 +3079,14 @@ var LiveComponent = class {
2665
3079
  // ========================================
2666
3080
  // $room - Unified Room System
2667
3081
  // ========================================
3082
+ /**
3083
+ * Unified room accessor.
3084
+ *
3085
+ * Usage:
3086
+ * - `this.$room` — default room handle (legacy)
3087
+ * - `this.$room('roomId')` — untyped room handle (legacy)
3088
+ * - `this.$room(ChatRoom, 'lobby')` — typed handle with custom methods
3089
+ */
2668
3090
  get $room() {
2669
3091
  return this._roomProxyManager.$room;
2670
3092
  }
@@ -2746,8 +3168,8 @@ var LiveComponent = class {
2746
3168
  * as a binary frame: [0x01][idLen:u8][id_bytes:utf8][payload_bytes].
2747
3169
  * Bypasses the JSON batcher — ideal for high-frequency updates.
2748
3170
  */
2749
- sendBinaryDelta(delta, encoder) {
2750
- this._stateManager.sendBinaryDelta(delta, encoder);
3171
+ sendBinaryDelta(delta, encoder2) {
3172
+ this._stateManager.sendBinaryDelta(delta, encoder2);
2751
3173
  }
2752
3174
  setValue(payload) {
2753
3175
  return this._stateManager.setValue(payload);
@@ -2760,8 +3182,7 @@ var LiveComponent = class {
2760
3182
  component: this,
2761
3183
  componentClass: this.constructor,
2762
3184
  componentId: this.id,
2763
- emitFn: (type, p) => this.emit(type, p),
2764
- debugger: _liveDebugger
3185
+ emitFn: (type, p) => this.emit(type, p)
2765
3186
  });
2766
3187
  }
2767
3188
  // ========================================
@@ -2828,16 +3249,13 @@ var ComponentRegistry = class {
2828
3249
  remoteSingletons = /* @__PURE__ */ new Map();
2829
3250
  cluster;
2830
3251
  authManager;
2831
- debugger;
2832
3252
  stateSignature;
2833
3253
  performanceMonitor;
2834
3254
  constructor(deps) {
2835
3255
  this.authManager = deps.authManager;
2836
- this.debugger = deps.debugger;
2837
3256
  this.stateSignature = deps.stateSignature;
2838
3257
  this.performanceMonitor = deps.performanceMonitor;
2839
3258
  this.cluster = deps.cluster;
2840
- _setLiveDebugger(deps.debugger);
2841
3259
  this.setupHealthMonitoring();
2842
3260
  this.setupClusterHandlers();
2843
3261
  }
@@ -3142,13 +3560,6 @@ var ComponentRegistry = class {
3142
3560
  ;
3143
3561
  component.emit("ERROR", { action: "onMount", error: `Mount initialization failed: ${err?.message || err}` });
3144
3562
  }
3145
- this.debugger.trackComponentMount(
3146
- component.id,
3147
- componentName,
3148
- component.getSerializableState(),
3149
- options?.room,
3150
- options?.debugLabel
3151
- );
3152
3563
  return { componentId: component.id, initialState: component.getSerializableState(), signedState };
3153
3564
  } catch (error) {
3154
3565
  console.error(`Failed to mount component ${componentName}:`, error);
@@ -3291,7 +3702,6 @@ var ComponentRegistry = class {
3291
3702
  } else {
3292
3703
  if (this.removeSingletonConnection(componentId, void 0, "unmount")) return;
3293
3704
  }
3294
- this.debugger.trackComponentUnmount(componentId);
3295
3705
  component.destroy?.();
3296
3706
  this.unsubscribeFromAllRooms(componentId);
3297
3707
  this.components.delete(componentId);
@@ -3638,12 +4048,146 @@ function sanitizePayload(value, depth = 0) {
3638
4048
  return value;
3639
4049
  }
3640
4050
 
4051
+ // src/rooms/LiveRoom.ts
4052
+ var LiveRoom = class {
4053
+ /** Unique room type name. Used as prefix in compound room IDs (e.g. "chat:lobby"). */
4054
+ static roomName;
4055
+ /** Initial public state template. Cloned per room instance. */
4056
+ static defaultState = {};
4057
+ /** Initial private metadata template. Cloned per room instance. */
4058
+ static defaultMeta = {};
4059
+ /** Room-level options */
4060
+ static $options;
4061
+ /** The unique room instance identifier (e.g. "chat:lobby") */
4062
+ id;
4063
+ /** Public state — synced to all connected clients via setState(). */
4064
+ state;
4065
+ /** Private metadata — NEVER leaves the server. Mutate directly. */
4066
+ meta;
4067
+ /** @internal Reference to the room manager for broadcasting */
4068
+ _manager;
4069
+ constructor(id, manager) {
4070
+ const ctor = this.constructor;
4071
+ this.id = id;
4072
+ this._manager = manager;
4073
+ this.state = structuredClone(ctor.defaultState ?? {});
4074
+ this.meta = structuredClone(ctor.defaultMeta ?? {});
4075
+ }
4076
+ // ===== Framework Methods =====
4077
+ /**
4078
+ * Update public state and broadcast changes to all room members.
4079
+ * Uses deep diff by default — only changed fields are sent over the wire.
4080
+ */
4081
+ setState(updates) {
4082
+ this._manager.setRoomState(this.id, updates);
4083
+ }
4084
+ /**
4085
+ * Emit a typed event to all members in this room.
4086
+ * @returns Number of members notified
4087
+ */
4088
+ emit(event, data) {
4089
+ return this._manager.emitToRoom(this.id, event, data);
4090
+ }
4091
+ /** Get current member count */
4092
+ get memberCount() {
4093
+ return this._manager.getMemberCount?.(this.id) ?? 0;
4094
+ }
4095
+ // ===== Lifecycle Hooks (override in subclass) =====
4096
+ /**
4097
+ * Called when a component attempts to join this room.
4098
+ * Return false to reject the join.
4099
+ */
4100
+ onJoin(_ctx2) {
4101
+ }
4102
+ /**
4103
+ * Called after a component leaves this room.
4104
+ */
4105
+ onLeave(_ctx2) {
4106
+ }
4107
+ /**
4108
+ * Called when an event is emitted to this room.
4109
+ * Can intercept/validate events before broadcasting.
4110
+ */
4111
+ onEvent(_event, _data, _ctx2) {
4112
+ }
4113
+ /**
4114
+ * Called once when the room is first created (first member joins).
4115
+ */
4116
+ onCreate() {
4117
+ }
4118
+ /**
4119
+ * Called when the last member leaves and the room is about to be destroyed.
4120
+ * Return false to keep the room alive (e.g., persist state).
4121
+ */
4122
+ onDestroy() {
4123
+ }
4124
+ };
4125
+
4126
+ // src/rooms/RoomRegistry.ts
4127
+ var RoomRegistry = class {
4128
+ rooms = /* @__PURE__ */ new Map();
4129
+ /**
4130
+ * Register a LiveRoom subclass.
4131
+ * @throws If the class doesn't define a static roomName
4132
+ */
4133
+ register(roomClass) {
4134
+ const name = roomClass.roomName;
4135
+ if (!name) {
4136
+ throw new Error("LiveRoom subclass must define static roomName");
4137
+ }
4138
+ if (this.rooms.has(name)) {
4139
+ throw new Error(`LiveRoom '${name}' is already registered`);
4140
+ }
4141
+ this.rooms.set(name, roomClass);
4142
+ }
4143
+ /**
4144
+ * Get a registered room class by name.
4145
+ */
4146
+ get(name) {
4147
+ return this.rooms.get(name);
4148
+ }
4149
+ /**
4150
+ * Check if a room class is registered.
4151
+ */
4152
+ has(name) {
4153
+ return this.rooms.has(name);
4154
+ }
4155
+ /**
4156
+ * Resolve a compound room ID (e.g. "chat:lobby") to its registered class.
4157
+ * Returns undefined if the prefix doesn't match any registered room.
4158
+ */
4159
+ resolveFromId(roomId) {
4160
+ const colonIdx = roomId.indexOf(":");
4161
+ if (colonIdx === -1) return void 0;
4162
+ const prefix = roomId.substring(0, colonIdx);
4163
+ return this.rooms.get(prefix);
4164
+ }
4165
+ /**
4166
+ * Get all registered room names.
4167
+ */
4168
+ getRegisteredNames() {
4169
+ return Array.from(this.rooms.keys());
4170
+ }
4171
+ /**
4172
+ * Check if a value is a LiveRoom subclass.
4173
+ */
4174
+ static isLiveRoomClass(cls) {
4175
+ if (typeof cls !== "function" || !cls.prototype) return false;
4176
+ if (typeof cls.roomName !== "string") return false;
4177
+ let proto = Object.getPrototypeOf(cls.prototype);
4178
+ while (proto) {
4179
+ if (proto.constructor === LiveRoom) return true;
4180
+ proto = Object.getPrototypeOf(proto);
4181
+ }
4182
+ return false;
4183
+ }
4184
+ };
4185
+
3641
4186
  // src/server/LiveServer.ts
3642
4187
  var LiveServer = class {
3643
4188
  // Public singletons (accessible for transport adapters & advanced usage)
3644
4189
  roomEvents;
3645
4190
  roomManager;
3646
- debugger;
3647
4191
  authManager;
3648
4192
  stateSignature;
3649
4193
  performanceMonitor;
@@ -3651,6 +4195,7 @@ var LiveServer = class {
3651
4195
  connectionManager;
3652
4196
  registry;
3653
4197
  rateLimiter;
4198
+ roomRegistry;
3654
4199
  transport;
3655
4200
  options;
3656
4201
  constructor(options) {
@@ -3658,25 +4203,28 @@ var LiveServer = class {
3658
4203
  this.transport = options.transport;
3659
4204
  this.roomEvents = new RoomEventBus();
3660
4205
  this.roomManager = new LiveRoomManager(this.roomEvents, options.roomPubSub);
3661
- this.debugger = new LiveDebugger(options.debug ?? false);
3662
4206
  this.authManager = new LiveAuthManager();
3663
4207
  this.stateSignature = new StateSignatureManager(options.stateSignature);
3664
4208
  this.performanceMonitor = new PerformanceMonitor(options.performance);
3665
4209
  this.fileUploadManager = new FileUploadManager(options.fileUpload);
3666
4210
  this.connectionManager = new WebSocketConnectionManager(options.connection);
3667
4211
  this.rateLimiter = new RateLimiterRegistry(options.rateLimitMaxTokens, options.rateLimitRefillRate);
4212
+ this.roomRegistry = new RoomRegistry();
4213
+ this.roomManager.roomRegistry = this.roomRegistry;
4214
+ if (options.rooms) {
4215
+ for (const roomClass of options.rooms) {
4216
+ this.roomRegistry.register(roomClass);
4217
+ }
4218
+ }
3668
4219
  this.registry = new ComponentRegistry({
3669
4220
  authManager: this.authManager,
3670
- debugger: this.debugger,
3671
4221
  stateSignature: this.stateSignature,
3672
4222
  performanceMonitor: this.performanceMonitor,
3673
4223
  cluster: options.cluster
3674
4224
  });
3675
- _setLoggerDebugger(this.debugger);
3676
4225
  setLiveComponentContext({
3677
4226
  roomEvents: this.roomEvents,
3678
- roomManager: this.roomManager,
3679
- debugger: this.debugger
4227
+ roomManager: this.roomManager
3680
4228
  });
3681
4229
  }
3682
4230
  /**
@@ -3686,6 +4234,14 @@ var LiveServer = class {
3686
4234
  this.authManager.register(provider);
3687
4235
  return this;
3688
4236
  }
4237
+ /**
4238
+ * Register a LiveRoom class.
4239
+ * Can be called before start() to register room types dynamically.
4240
+ */
4241
+ useRoom(roomClass) {
4242
+ this.roomRegistry.register(roomClass);
4243
+ return this;
4244
+ }
3689
4245
  /**
3690
4246
  * Start the LiveServer: register WS + HTTP handlers on the transport.
3691
4247
  */
@@ -3745,7 +4301,6 @@ var LiveServer = class {
3745
4301
  origin
3746
4302
  };
3747
4303
  this.connectionManager.registerConnection(ws, connectionId);
3748
- this.debugger.trackConnection(connectionId);
3749
4304
  sendImmediate(ws, JSON.stringify({
3750
4305
  type: "CONNECTION_ESTABLISHED",
3751
4306
  connectionId,
@@ -3803,7 +4358,7 @@ var LiveServer = class {
3803
4358
  return;
3804
4359
  }
3805
4360
  if (message.type === "ROOM_JOIN" || message.type === "ROOM_LEAVE" || message.type === "ROOM_EMIT" || message.type === "ROOM_STATE_SET" || message.type === "ROOM_STATE_GET") {
3806
- this.handleRoomMessage(ws, message);
4361
+ await this.handleRoomMessage(ws, message);
3807
4362
  return;
3808
4363
  }
3809
4364
  if (message.type === "FILE_UPLOAD_START") {
@@ -3882,18 +4437,27 @@ var LiveServer = class {
3882
4437
  this.connectionManager.cleanupConnection(connectionId);
3883
4438
  this.rateLimiter.remove(connectionId);
3884
4439
  }
3885
- this.debugger.trackDisconnection(connectionId || "", componentCount);
3886
4440
  liveLog("websocket", null, `Connection closed: ${connectionId} (${componentCount} components)`);
3887
4441
  }
3888
4442
  handleError(ws, error) {
3889
4443
  console.error(`[LiveServer] WebSocket error:`, error.message);
3890
4444
  }
3891
4445
  // ===== Room Message Router =====
3892
- handleRoomMessage(ws, message) {
4446
+ async handleRoomMessage(ws, message) {
3893
4447
  const { componentId } = message;
3894
4448
  const roomId = message.roomId || message.payload?.roomId;
3895
4449
  switch (message.type) {
3896
4450
  case "ROOM_JOIN": {
4451
+ if (this.roomRegistry.resolveFromId(roomId)) {
4452
+ sendImmediate(ws, JSON.stringify({
4453
+ type: "ERROR",
4454
+ componentId,
4455
+ error: "Room requires server-side join via component action",
4456
+ requestId: message.requestId,
4457
+ timestamp: Date.now()
4458
+ }));
4459
+ break;
4460
+ }
3897
4461
  const connRooms = ws.data?.rooms;
3898
4462
  if (connRooms && connRooms.size >= MAX_ROOMS_PER_CONNECTION) {
3899
4463
  sendImmediate(ws, JSON.stringify({
@@ -3905,7 +4469,34 @@ var LiveServer = class {
3905
4469
  }));
3906
4470
  break;
3907
4471
  }
4472
+ if (this.authManager.hasProviders()) {
4473
+ const authContext = ws.data?.authContext;
4474
+ const authResult = await this.authManager.authorizeRoom(
4475
+ authContext || ANONYMOUS_CONTEXT,
4476
+ roomId
4477
+ );
4478
+ if (!authResult.allowed) {
4479
+ sendImmediate(ws, JSON.stringify({
4480
+ type: "ERROR",
4481
+ componentId,
4482
+ error: authResult.reason || "Room access denied",
4483
+ requestId: message.requestId,
4484
+ timestamp: Date.now()
4485
+ }));
4486
+ break;
4487
+ }
4488
+ }
3908
4489
  const result = this.roomManager.joinRoom(componentId, roomId, ws, message.payload?.initialState);
4490
+ if ("rejected" in result && result.rejected) {
4491
+ sendImmediate(ws, JSON.stringify({
4492
+ type: "ERROR",
4493
+ componentId,
4494
+ error: result.reason,
4495
+ requestId: message.requestId,
4496
+ timestamp: Date.now()
4497
+ }));
4498
+ break;
4499
+ }
3909
4500
  if (!ws.data.rooms) ws.data.rooms = /* @__PURE__ */ new Set();
3910
4501
  ws.data.rooms.add(roomId);
3911
4502
  sendImmediate(ws, JSON.stringify({
@@ -3928,13 +4519,55 @@ var LiveServer = class {
3928
4519
  timestamp: Date.now()
3929
4520
  }));
3930
4521
  break;
3931
- case "ROOM_EMIT":
4522
+ case "ROOM_EMIT": {
4523
+ if (!this.roomManager.isInRoom(componentId, roomId)) {
4524
+ sendImmediate(ws, JSON.stringify({
4525
+ type: "ERROR",
4526
+ componentId,
4527
+ error: "Not a member of this room",
4528
+ requestId: message.requestId,
4529
+ timestamp: Date.now()
4530
+ }));
4531
+ break;
4532
+ }
3932
4533
  this.roomManager.emitToRoom(roomId, message.payload?.event, message.payload?.data, componentId);
3933
4534
  break;
3934
- case "ROOM_STATE_SET":
4535
+ }
4536
+ case "ROOM_STATE_SET": {
4537
+ if (!this.roomManager.isInRoom(componentId, roomId)) {
4538
+ sendImmediate(ws, JSON.stringify({
4539
+ type: "ERROR",
4540
+ componentId,
4541
+ error: "Not a member of this room",
4542
+ requestId: message.requestId,
4543
+ timestamp: Date.now()
4544
+ }));
4545
+ break;
4546
+ }
4547
+ if (this.roomManager.isServerOnlyState(roomId)) {
4548
+ sendImmediate(ws, JSON.stringify({
4549
+ type: "ERROR",
4550
+ componentId,
4551
+ error: "Room state is server-only",
4552
+ requestId: message.requestId,
4553
+ timestamp: Date.now()
4554
+ }));
4555
+ break;
4556
+ }
3935
4557
  this.roomManager.setRoomState(roomId, message.payload?.state, componentId);
3936
4558
  break;
4559
+ }
3937
4560
  case "ROOM_STATE_GET": {
4561
+ if (!this.roomManager.isInRoom(componentId, roomId)) {
4562
+ sendImmediate(ws, JSON.stringify({
4563
+ type: "ERROR",
4564
+ componentId,
4565
+ error: "Not a member of this room",
4566
+ requestId: message.requestId,
4567
+ timestamp: Date.now()
4568
+ }));
4569
+ break;
4570
+ }
3938
4571
  const state = this.roomManager.getRoomState(roomId);
3939
4572
  sendImmediate(ws, JSON.stringify({
3940
4573
  type: "ROOM_STATE",
@@ -4189,6 +4822,6 @@ var InMemoryRoomAdapter = class {
4189
4822
  }
4190
4823
  };
4191
4824
 
4192
- export { ANONYMOUS_CONTEXT, AnonymousContext, AuthenticatedContext, ComponentRegistry, ConnectionRateLimiter, DEFAULT_CHUNK_SIZE, DEFAULT_WS_PATH, EMIT_OVERRIDE_KEY, FileUploadManager, InMemoryRoomAdapter, LiveAuthManager, LiveComponent, LiveDebugger, LiveRoomManager, LiveServer, PROTOCOL_VERSION, PerformanceMonitor, RateLimiterRegistry, RoomEventBus, RoomStateManager, StateSignatureManager, WebSocketConnectionManager, createTypedRoomEventBus, createTypedRoomState, decodeBinaryChunk, encodeBinaryChunk, getLiveComponentContext, liveLog, liveWarn, queueWsMessage, registerComponentLogging, sendImmediate, setLiveComponentContext, unregisterComponentLogging };
4825
+ export { ANONYMOUS_CONTEXT, AnonymousContext, AuthenticatedContext, BINARY_ROOM_EVENT, BINARY_ROOM_STATE, ComponentRegistry, ConnectionRateLimiter, DEFAULT_CHUNK_SIZE, DEFAULT_WS_PATH, EMIT_OVERRIDE_KEY, FileUploadManager, InMemoryRoomAdapter, LiveAuthManager, LiveComponent, LiveRoom, LiveRoomManager, LiveServer, PROTOCOL_VERSION, PerformanceMonitor, RateLimiterRegistry, RoomEventBus, RoomRegistry, RoomStateManager, StateSignatureManager, WebSocketConnectionManager, buildRoomFrame, buildRoomFrameTail, createTypedRoomEventBus, createTypedRoomState, decodeBinaryChunk, encodeBinaryChunk, getLiveComponentContext, jsonCodec, liveLog, liveWarn, msgpackCodec, parseRoomFrame, prependMemberHeader, queueWsMessage, registerComponentLogging, resolveCodec, sendBinaryImmediate, sendImmediate, setLiveComponentContext, unregisterComponentLogging };
4193
4826
  //# sourceMappingURL=index.js.map
4194
4827
  //# sourceMappingURL=index.js.map