@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.d.ts +314 -125
- package/dist/index.js +1026 -393
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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:
|
|
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
|
-
*
|
|
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 (
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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.
|
|
2035
|
-
this.
|
|
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
|
-
|
|
2084
|
-
let hasChanges
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
2446
|
-
|
|
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 = ((
|
|
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
|
-
|
|
2485
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2655
|
-
|
|
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,
|
|
2750
|
-
this._stateManager.sendBinaryDelta(delta,
|
|
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
|
-
|
|
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,
|
|
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
|