@fluxstack/live 0.1.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
@@ -1,4 +1,4 @@
1
- import { randomBytes, createCipheriv, createDecipheriv, createHmac } from 'crypto';
1
+ import { randomBytes, createHmac, createCipheriv, createDecipheriv, scryptSync } from 'crypto';
2
2
  import { gzipSync, gunzipSync } from 'zlib';
3
3
  import { EventEmitter } from 'events';
4
4
 
@@ -12,6 +12,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
12
12
  // src/rooms/RoomEventBus.ts
13
13
  function createTypedRoomEventBus() {
14
14
  const subscriptions = /* @__PURE__ */ new Map();
15
+ const componentIndex = /* @__PURE__ */ new Map();
15
16
  const getKey = (roomType, roomId, event) => `${roomType}:${roomId}:${event}`;
16
17
  const getRoomKey = (roomType, roomId) => `${roomType}:${roomId}`;
17
18
  return {
@@ -28,6 +29,10 @@ function createTypedRoomEventBus() {
28
29
  componentId
29
30
  };
30
31
  subscriptions.get(key).add(subscription);
32
+ if (!componentIndex.has(componentId)) {
33
+ componentIndex.set(componentId, /* @__PURE__ */ new Set());
34
+ }
35
+ componentIndex.get(componentId).add(key);
31
36
  return () => {
32
37
  subscriptions.get(key)?.delete(subscription);
33
38
  if (subscriptions.get(key)?.size === 0) {
@@ -52,8 +57,12 @@ function createTypedRoomEventBus() {
52
57
  return notified;
53
58
  },
54
59
  unsubscribeAll(componentId) {
60
+ const keys = componentIndex.get(componentId);
61
+ if (!keys) return 0;
55
62
  let removed = 0;
56
- for (const [key, subs] of subscriptions) {
63
+ for (const key of keys) {
64
+ const subs = subscriptions.get(key);
65
+ if (!subs) continue;
57
66
  for (const sub of subs) {
58
67
  if (sub.componentId === componentId) {
59
68
  subs.delete(sub);
@@ -64,6 +73,7 @@ function createTypedRoomEventBus() {
64
73
  subscriptions.delete(key);
65
74
  }
66
75
  }
76
+ componentIndex.delete(componentId);
67
77
  return removed;
68
78
  },
69
79
  clearRoom(roomType, roomId) {
@@ -71,7 +81,13 @@ function createTypedRoomEventBus() {
71
81
  let removed = 0;
72
82
  for (const key of subscriptions.keys()) {
73
83
  if (key.startsWith(prefix)) {
74
- removed += subscriptions.get(key)?.size ?? 0;
84
+ const subs = subscriptions.get(key);
85
+ if (subs) {
86
+ for (const sub of subs) {
87
+ componentIndex.get(sub.componentId)?.delete(key);
88
+ }
89
+ removed += subs.size;
90
+ }
75
91
  subscriptions.delete(key);
76
92
  }
77
93
  }
@@ -96,6 +112,8 @@ function createTypedRoomEventBus() {
96
112
  }
97
113
  var RoomEventBus = class {
98
114
  subscriptions = /* @__PURE__ */ new Map();
115
+ /** Reverse index: componentId -> Set of subscription keys for O(1) unsubscribeAll */
116
+ componentIndex = /* @__PURE__ */ new Map();
99
117
  getKey(roomType, roomId, event) {
100
118
  return `${roomType}:${roomId}:${event}`;
101
119
  }
@@ -106,6 +124,10 @@ var RoomEventBus = class {
106
124
  }
107
125
  const subscription = { roomType, roomId, event, handler, componentId };
108
126
  this.subscriptions.get(key).add(subscription);
127
+ if (!this.componentIndex.has(componentId)) {
128
+ this.componentIndex.set(componentId, /* @__PURE__ */ new Set());
129
+ }
130
+ this.componentIndex.get(componentId).add(key);
109
131
  return () => {
110
132
  this.subscriptions.get(key)?.delete(subscription);
111
133
  if (this.subscriptions.get(key)?.size === 0) {
@@ -130,8 +152,12 @@ var RoomEventBus = class {
130
152
  return notified;
131
153
  }
132
154
  unsubscribeAll(componentId) {
155
+ const keys = this.componentIndex.get(componentId);
156
+ if (!keys) return 0;
133
157
  let removed = 0;
134
- for (const [key, subs] of this.subscriptions) {
158
+ for (const key of keys) {
159
+ const subs = this.subscriptions.get(key);
160
+ if (!subs) continue;
135
161
  for (const sub of subs) {
136
162
  if (sub.componentId === componentId) {
137
163
  subs.delete(sub);
@@ -142,6 +168,7 @@ var RoomEventBus = class {
142
168
  this.subscriptions.delete(key);
143
169
  }
144
170
  }
171
+ this.componentIndex.delete(componentId);
145
172
  return removed;
146
173
  }
147
174
  clearRoom(roomType, roomId) {
@@ -149,7 +176,13 @@ var RoomEventBus = class {
149
176
  let removed = 0;
150
177
  for (const key of this.subscriptions.keys()) {
151
178
  if (key.startsWith(prefix)) {
152
- removed += this.subscriptions.get(key)?.size ?? 0;
179
+ const subs = this.subscriptions.get(key);
180
+ if (subs) {
181
+ for (const sub of subs) {
182
+ this.componentIndex.get(sub.componentId)?.delete(key);
183
+ }
184
+ removed += subs.size;
185
+ }
153
186
  this.subscriptions.delete(key);
154
187
  }
155
188
  }
@@ -172,6 +205,143 @@ var RoomEventBus = class {
172
205
  }
173
206
  };
174
207
 
208
+ // src/protocol/constants.ts
209
+ var PROTOCOL_VERSION = 1;
210
+ var DEFAULT_WS_PATH = "/api/live/ws";
211
+ var DEFAULT_CHUNK_SIZE = 64 * 1024;
212
+ var DEFAULT_RATE_LIMIT_MAX_TOKENS = 100;
213
+ var DEFAULT_RATE_LIMIT_REFILL_RATE = 50;
214
+ var MAX_MESSAGE_SIZE = 100 * 1024 * 1024;
215
+ var MAX_ROOM_STATE_SIZE = 10 * 1024 * 1024;
216
+ var MAX_ROOMS_PER_CONNECTION = 50;
217
+ var ROOM_NAME_REGEX = /^[a-zA-Z0-9_:.-]{1,64}$/;
218
+ var MAX_QUEUE_SIZE = 1e3;
219
+
220
+ // src/transport/WsSendBatcher.ts
221
+ var wsQueues = /* @__PURE__ */ new WeakMap();
222
+ var scheduledFlushes = /* @__PURE__ */ new WeakSet();
223
+ var pendingWs = [];
224
+ var globalFlushScheduled = false;
225
+ function scheduleWs(ws) {
226
+ if (!scheduledFlushes.has(ws)) {
227
+ scheduledFlushes.add(ws);
228
+ pendingWs.push(ws);
229
+ if (!globalFlushScheduled) {
230
+ globalFlushScheduled = true;
231
+ queueMicrotask(flushAll);
232
+ }
233
+ }
234
+ }
235
+ function queueWsMessage(ws, message) {
236
+ if (!ws || ws.readyState !== 1) return;
237
+ let queue = wsQueues.get(ws);
238
+ if (!queue) {
239
+ queue = [];
240
+ wsQueues.set(ws, queue);
241
+ }
242
+ if (queue.length >= MAX_QUEUE_SIZE) {
243
+ queue.shift();
244
+ }
245
+ queue.push(message);
246
+ scheduleWs(ws);
247
+ }
248
+ function queuePreSerialized(ws, serialized) {
249
+ if (!ws || ws.readyState !== 1) return;
250
+ let queue = wsQueues.get(ws);
251
+ if (!queue) {
252
+ queue = [];
253
+ wsQueues.set(ws, queue);
254
+ }
255
+ if (queue.length >= MAX_QUEUE_SIZE) {
256
+ queue.shift();
257
+ }
258
+ queue.push(serialized);
259
+ scheduleWs(ws);
260
+ }
261
+ function flushAll() {
262
+ globalFlushScheduled = false;
263
+ const connections = pendingWs;
264
+ pendingWs = [];
265
+ for (const ws of connections) {
266
+ scheduledFlushes.delete(ws);
267
+ const queue = wsQueues.get(ws);
268
+ if (!queue || queue.length === 0) continue;
269
+ wsQueues.set(ws, []);
270
+ if (ws.readyState !== 1) continue;
271
+ try {
272
+ if (queue.length === 1) {
273
+ const item = queue[0];
274
+ if (typeof item === "string") {
275
+ ws.send(item);
276
+ } else {
277
+ ws.send(JSON.stringify(item));
278
+ }
279
+ } else {
280
+ const objects = [];
281
+ const preSerialized = [];
282
+ for (const item of queue) {
283
+ if (typeof item === "string") {
284
+ preSerialized.push(item);
285
+ } else {
286
+ objects.push(item);
287
+ }
288
+ }
289
+ if (objects.length === 0) {
290
+ ws.send("[" + preSerialized.join(",") + "]");
291
+ } else if (preSerialized.length === 0) {
292
+ const deduped = deduplicateDeltas(objects);
293
+ ws.send(JSON.stringify(deduped));
294
+ } else {
295
+ const deduped = deduplicateDeltas(objects);
296
+ let result = "[";
297
+ for (let i = 0; i < deduped.length; i++) {
298
+ if (i > 0) result += ",";
299
+ result += JSON.stringify(deduped[i]);
300
+ }
301
+ for (const ps of preSerialized) {
302
+ result += "," + ps;
303
+ }
304
+ result += "]";
305
+ ws.send(result);
306
+ }
307
+ }
308
+ } catch {
309
+ }
310
+ }
311
+ }
312
+ function deduplicateDeltas(messages) {
313
+ const deltaIndices = /* @__PURE__ */ new Map();
314
+ const result = [];
315
+ for (const msg of messages) {
316
+ if (msg.type === "STATE_DELTA" && msg.componentId && msg.payload?.delta) {
317
+ const existing = deltaIndices.get(msg.componentId);
318
+ if (existing !== void 0) {
319
+ const target = result[existing];
320
+ target.payload = {
321
+ delta: { ...target.payload.delta, ...msg.payload.delta }
322
+ };
323
+ target.timestamp = msg.timestamp;
324
+ } else {
325
+ deltaIndices.set(msg.componentId, result.length);
326
+ result.push({ ...msg, payload: { delta: { ...msg.payload.delta } } });
327
+ }
328
+ } else {
329
+ result.push(msg);
330
+ }
331
+ }
332
+ return result;
333
+ }
334
+ function sendBinaryImmediate(ws, data) {
335
+ if (ws && ws.readyState === 1) {
336
+ ws.send(data);
337
+ }
338
+ }
339
+ function sendImmediate(ws, data) {
340
+ if (ws && ws.readyState === 1) {
341
+ ws.send(data);
342
+ }
343
+ }
344
+
175
345
  // src/debug/LiveLogger.ts
176
346
  var componentConfigs = /* @__PURE__ */ new Map();
177
347
  var globalConfigParsed = false;
@@ -209,22 +379,7 @@ function shouldLog(componentId, category) {
209
379
  if (cfg === true) return true;
210
380
  return cfg.includes(category);
211
381
  }
212
- var _debugger = null;
213
- function _setLoggerDebugger(dbg) {
214
- _debugger = dbg;
215
- }
216
- function emitToDebugger(category, level, componentId, message, args) {
217
- if (!_debugger?.enabled) return;
218
- const data = { category, level, message };
219
- if (args.length === 1 && typeof args[0] === "object" && args[0] !== null) {
220
- data.details = args[0];
221
- } else if (args.length > 0) {
222
- data.details = args;
223
- }
224
- _debugger.emit("LOG", componentId, null, data);
225
- }
226
382
  function liveLog(category, componentId, message, ...args) {
227
- emitToDebugger(category, "info", componentId, message, args);
228
383
  if (shouldLog(componentId, category)) {
229
384
  if (args.length > 0) {
230
385
  console.log(message, ...args);
@@ -234,7 +389,6 @@ function liveLog(category, componentId, message, ...args) {
234
389
  }
235
390
  }
236
391
  function liveWarn(category, componentId, message, ...args) {
237
- emitToDebugger(category, "warn", componentId, message, args);
238
392
  if (shouldLog(componentId, category)) {
239
393
  if (args.length > 0) {
240
394
  console.warn(message, ...args);
@@ -244,51 +398,482 @@ function liveWarn(category, componentId, message, ...args) {
244
398
  }
245
399
  }
246
400
 
247
- // src/protocol/constants.ts
248
- var PROTOCOL_VERSION = 1;
249
- var DEFAULT_WS_PATH = "/api/live/ws";
250
- var DEFAULT_CHUNK_SIZE = 64 * 1024;
251
- var DEFAULT_RATE_LIMIT_MAX_TOKENS = 100;
252
- var DEFAULT_RATE_LIMIT_REFILL_RATE = 50;
253
- var MAX_ROOM_STATE_SIZE = 10 * 1024 * 1024;
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
+ }
254
775
 
255
776
  // src/rooms/LiveRoomManager.ts
256
777
  var LiveRoomManager = class {
257
- // componentId -> roomIds
258
- constructor(roomEvents) {
778
+ /**
779
+ * @param roomEvents - Local server-side event bus
780
+ * @param pubsub - Optional cross-instance pub/sub adapter (e.g. Redis).
781
+ * When provided, room events/state/membership are propagated
782
+ * to other server instances in the background.
783
+ */
784
+ constructor(roomEvents, pubsub) {
259
785
  this.roomEvents = roomEvents;
786
+ this.pubsub = pubsub;
260
787
  }
261
788
  rooms = /* @__PURE__ */ new Map();
262
789
  componentRooms = /* @__PURE__ */ new Map();
790
+ // componentId -> roomIds
791
+ /** Room registry for LiveRoom class lookup. Set by LiveServer. */
792
+ roomRegistry;
263
793
  /**
264
- * 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)
265
797
  */
266
- joinRoom(componentId, roomId, ws, initialState) {
267
- if (!roomId || !/^[a-zA-Z0-9_:.-]{1,64}$/.test(roomId)) {
798
+ joinRoom(componentId, roomId, ws, initialState, options, joinContext) {
799
+ if (!roomId || !ROOM_NAME_REGEX.test(roomId)) {
268
800
  throw new Error("Invalid room name. Must be 1-64 alphanumeric characters, hyphens, underscores, dots, or colons.");
269
801
  }
270
- if (!this.rooms.has(roomId)) {
271
- this.rooms.set(roomId, {
272
- id: roomId,
273
- state: initialState || {},
274
- members: /* @__PURE__ */ new Map(),
275
- createdAt: Date.now(),
276
- lastActivity: Date.now()
277
- });
802
+ const now = Date.now();
803
+ let room = this.rooms.get(roomId);
804
+ let isNewRoom = false;
805
+ if (!room) {
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
+ }
837
+ this.rooms.set(roomId, room);
278
838
  liveLog("rooms", componentId, `Room '${roomId}' created`);
279
839
  }
280
- const room = this.rooms.get(roomId);
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
+ }
281
863
  room.members.set(componentId, {
282
864
  componentId,
283
865
  ws,
284
- joinedAt: Date.now()
866
+ joinedAt: now
285
867
  });
286
- room.lastActivity = Date.now();
287
- if (!this.componentRooms.has(componentId)) {
288
- this.componentRooms.set(componentId, /* @__PURE__ */ new Set());
289
- }
290
- this.componentRooms.get(componentId).add(roomId);
291
- liveLog("rooms", componentId, `Component '${componentId}' joined room '${roomId}' (${room.members.size} members)`);
868
+ room.lastActivity = now;
869
+ let compRooms = this.componentRooms.get(componentId);
870
+ if (!compRooms) {
871
+ compRooms = /* @__PURE__ */ new Set();
872
+ this.componentRooms.set(componentId, compRooms);
873
+ }
874
+ compRooms.add(roomId);
875
+ const memberCount = room.members.size;
876
+ liveLog("rooms", componentId, `Component '${componentId}' joined room '${roomId}' (${memberCount} members)`);
292
877
  this.broadcastToRoom(roomId, {
293
878
  type: "ROOM_SYSTEM",
294
879
  componentId,
@@ -296,22 +881,33 @@ var LiveRoomManager = class {
296
881
  event: "$sub:join",
297
882
  data: {
298
883
  subscriberId: componentId,
299
- count: room.members.size
884
+ count: memberCount
300
885
  },
301
- timestamp: Date.now()
886
+ timestamp: now
302
887
  }, componentId);
888
+ this.pubsub?.publishMembership(roomId, "join", componentId)?.catch(() => {
889
+ });
303
890
  return { state: room.state };
304
891
  }
305
892
  /**
306
893
  * Component leaves a room
894
+ * @param leaveReason - Why the component is leaving. Default: 'leave'
307
895
  */
308
- leaveRoom(componentId, roomId) {
896
+ leaveRoom(componentId, roomId, leaveReason = "leave") {
309
897
  const room = this.rooms.get(roomId);
310
898
  if (!room) return;
899
+ if (room.instance) {
900
+ room.instance.onLeave({
901
+ componentId,
902
+ reason: leaveReason
903
+ });
904
+ }
311
905
  room.members.delete(componentId);
312
- room.lastActivity = Date.now();
906
+ const now = Date.now();
907
+ room.lastActivity = now;
313
908
  this.componentRooms.get(componentId)?.delete(roomId);
314
- liveLog("rooms", componentId, `Component '${componentId}' left room '${roomId}' (${room.members.size} members)`);
909
+ const memberCount = room.members.size;
910
+ liveLog("rooms", componentId, `Component '${componentId}' left room '${roomId}' (${memberCount} members)`);
315
911
  this.broadcastToRoom(roomId, {
316
912
  type: "ROOM_SYSTEM",
317
913
  componentId,
@@ -319,14 +915,20 @@ var LiveRoomManager = class {
319
915
  event: "$sub:leave",
320
916
  data: {
321
917
  subscriberId: componentId,
322
- count: room.members.size
918
+ count: memberCount
323
919
  },
324
- timestamp: Date.now()
920
+ timestamp: now
325
921
  });
326
- if (room.members.size === 0) {
922
+ this.pubsub?.publishMembership(roomId, "leave", componentId)?.catch(() => {
923
+ });
924
+ if (memberCount === 0) {
327
925
  setTimeout(() => {
328
926
  const currentRoom = this.rooms.get(roomId);
329
927
  if (currentRoom && currentRoom.members.size === 0) {
928
+ if (currentRoom.instance) {
929
+ const result = currentRoom.instance.onDestroy();
930
+ if (result === false) return;
931
+ }
330
932
  this.rooms.delete(roomId);
331
933
  liveLog("rooms", null, `Room '${roomId}' destroyed (empty)`);
332
934
  }
@@ -334,53 +936,143 @@ var LiveRoomManager = class {
334
936
  }
335
937
  }
336
938
  /**
337
- * Component disconnects - leave all rooms
939
+ * Component disconnects - leave all rooms.
940
+ * Batches removals: calls onLeave hooks, removes member from all rooms,
941
+ * then sends leave notifications in bulk.
338
942
  */
339
943
  cleanupComponent(componentId) {
340
- const rooms = this.componentRooms.get(componentId);
341
- if (!rooms) return;
342
- for (const roomId of rooms) {
343
- this.leaveRoom(componentId, roomId);
944
+ const roomIds = this.componentRooms.get(componentId);
945
+ if (!roomIds || roomIds.size === 0) return;
946
+ const now = Date.now();
947
+ const notifications = [];
948
+ for (const roomId of roomIds) {
949
+ const room = this.rooms.get(roomId);
950
+ if (!room) continue;
951
+ if (room.instance) {
952
+ room.instance.onLeave({
953
+ componentId,
954
+ reason: "disconnect"
955
+ });
956
+ }
957
+ room.members.delete(componentId);
958
+ room.lastActivity = now;
959
+ const memberCount = room.members.size;
960
+ if (memberCount > 0) {
961
+ notifications.push({ roomId, count: memberCount });
962
+ } else {
963
+ setTimeout(() => {
964
+ const currentRoom = this.rooms.get(roomId);
965
+ if (currentRoom && currentRoom.members.size === 0) {
966
+ if (currentRoom.instance) {
967
+ const result = currentRoom.instance.onDestroy();
968
+ if (result === false) return;
969
+ }
970
+ this.rooms.delete(roomId);
971
+ liveLog("rooms", null, `Room '${roomId}' destroyed (empty)`);
972
+ }
973
+ }, 5 * 60 * 1e3);
974
+ }
975
+ }
976
+ for (const { roomId, count } of notifications) {
977
+ this.broadcastToRoom(roomId, {
978
+ type: "ROOM_SYSTEM",
979
+ componentId,
980
+ roomId,
981
+ event: "$sub:leave",
982
+ data: {
983
+ subscriberId: componentId,
984
+ count
985
+ },
986
+ timestamp: now
987
+ });
344
988
  }
345
989
  this.componentRooms.delete(componentId);
346
990
  }
347
991
  /**
348
- * 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.
349
994
  */
350
995
  emitToRoom(roomId, event, data, excludeComponentId) {
351
996
  const room = this.rooms.get(roomId);
352
997
  if (!room) return 0;
353
- room.lastActivity = Date.now();
998
+ const now = Date.now();
999
+ room.lastActivity = now;
1000
+ if (room.instance) {
1001
+ room.instance.onEvent(event, data, {
1002
+ componentId: excludeComponentId ?? ""
1003
+ });
1004
+ }
354
1005
  this.roomEvents.emit("room", roomId, event, data, excludeComponentId);
1006
+ this.pubsub?.publish(roomId, event, data)?.catch(() => {
1007
+ });
355
1008
  return this.broadcastToRoom(roomId, {
356
1009
  type: "ROOM_EVENT",
357
1010
  componentId: "",
358
1011
  roomId,
359
1012
  event,
360
1013
  data,
361
- timestamp: Date.now()
1014
+ timestamp: now
362
1015
  }, excludeComponentId);
363
1016
  }
364
1017
  /**
365
- * Update room state
1018
+ * Update room state.
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.
366
1021
  */
367
1022
  setRoomState(roomId, updates, excludeComponentId) {
368
1023
  const room = this.rooms.get(roomId);
369
1024
  if (!room) return;
370
- const newState = { ...room.state, ...updates };
371
- const stateSize = Buffer.byteLength(JSON.stringify(newState), "utf8");
372
- if (stateSize > MAX_ROOM_STATE_SIZE) {
373
- throw new Error("Room state exceeds maximum size limit");
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
+ }
1048
+ if (room.stateSize === void 0) {
1049
+ const fullJson = JSON.stringify(room.state);
1050
+ room.stateSize = fullJson.length;
1051
+ if (room.stateSize > MAX_ROOM_STATE_SIZE) {
1052
+ throw new Error("Room state exceeds maximum size limit");
1053
+ }
1054
+ } else {
1055
+ const deltaSize = JSON.stringify(actualChanges).length;
1056
+ room.stateSize += deltaSize;
1057
+ if (room.stateSize > MAX_ROOM_STATE_SIZE) {
1058
+ const precise = JSON.stringify(room.state).length;
1059
+ room.stateSize = precise;
1060
+ if (precise > MAX_ROOM_STATE_SIZE) {
1061
+ throw new Error("Room state exceeds maximum size limit");
1062
+ }
1063
+ }
374
1064
  }
375
- room.state = newState;
376
- room.lastActivity = Date.now();
1065
+ const now = Date.now();
1066
+ room.lastActivity = now;
1067
+ this.pubsub?.publishStateChange(roomId, actualChanges)?.catch(() => {
1068
+ });
377
1069
  this.broadcastToRoom(roomId, {
378
1070
  type: "ROOM_STATE",
379
1071
  componentId: "",
380
1072
  roomId,
381
1073
  event: "$state:update",
382
- data: { state: updates },
383
- timestamp: Date.now()
1074
+ data: { state: actualChanges },
1075
+ timestamp: now
384
1076
  }, excludeComponentId);
385
1077
  }
386
1078
  /**
@@ -390,24 +1082,40 @@ var LiveRoomManager = class {
390
1082
  return this.rooms.get(roomId)?.state || {};
391
1083
  }
392
1084
  /**
393
- * Broadcast to all members in a room
1085
+ * Broadcast to all members in a room.
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.
394
1092
  */
395
1093
  broadcastToRoom(roomId, message, excludeComponentId) {
396
1094
  const room = this.rooms.get(roomId);
397
- if (!room) return 0;
1095
+ if (!room || room.members.size === 0) return 0;
398
1096
  let sent = 0;
399
- for (const [componentId, member] of room.members) {
400
- if (excludeComponentId && componentId === excludeComponentId) continue;
401
- try {
402
- if (member.ws && member.ws.readyState === 1) {
403
- member.ws.send(JSON.stringify({
404
- ...message,
405
- componentId
406
- }));
407
- sent++;
408
- }
409
- } catch (error) {
410
- console.error(`Failed to send to ${componentId}:`, error);
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++;
1108
+ }
1109
+ } else {
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++;
411
1119
  }
412
1120
  }
413
1121
  return sent;
@@ -418,12 +1126,31 @@ var LiveRoomManager = class {
418
1126
  isInRoom(componentId, roomId) {
419
1127
  return this.rooms.get(roomId)?.members.has(componentId) ?? false;
420
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
+ }
421
1135
  /**
422
1136
  * Get rooms for a component
423
1137
  */
424
1138
  getComponentRooms(componentId) {
425
1139
  return Array.from(this.componentRooms.get(componentId) || []);
426
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
+ }
427
1154
  /**
428
1155
  * Get statistics
429
1156
  */
@@ -443,329 +1170,59 @@ var LiveRoomManager = class {
443
1170
  }
444
1171
  };
445
1172
 
446
- // src/debug/LiveDebugger.ts
447
- var MAX_EVENTS = 500;
448
- var MAX_STATE_SIZE = 5e4;
449
- var LiveDebugger = class {
450
- events = [];
451
- componentSnapshots = /* @__PURE__ */ new Map();
452
- debugClients = /* @__PURE__ */ new Set();
453
- _enabled = false;
454
- startTime = Date.now();
455
- eventCounter = 0;
456
- constructor(enabled = false) {
457
- this._enabled = enabled;
458
- }
459
- get enabled() {
460
- return this._enabled;
461
- }
462
- set enabled(value) {
463
- this._enabled = value;
464
- }
465
- // ===== Event Emission =====
466
- emit(type, componentId, componentName, data = {}) {
467
- if (!this._enabled) return;
468
- const event = {
469
- id: `dbg-${++this.eventCounter}`,
470
- timestamp: Date.now(),
471
- type,
472
- componentId,
473
- componentName,
474
- data: this.sanitizeData(data)
475
- };
476
- this.events.push(event);
477
- if (this.events.length > MAX_EVENTS) {
478
- this.events.shift();
479
- }
480
- if (componentId) {
481
- this.updateSnapshot(event);
482
- }
483
- this.broadcastEvent(event);
1173
+ // src/auth/LiveAuthContext.ts
1174
+ var AuthenticatedContext = class {
1175
+ authenticated = true;
1176
+ user;
1177
+ token;
1178
+ authenticatedAt;
1179
+ constructor(user, token) {
1180
+ this.user = user;
1181
+ this.token = token;
1182
+ this.authenticatedAt = Date.now();
484
1183
  }
485
- // ===== Component Tracking =====
486
- trackComponentMount(componentId, componentName, initialState, room, debugLabel) {
487
- if (!this._enabled) return;
488
- const snapshot = {
489
- componentId,
490
- componentName,
491
- debugLabel,
492
- state: this.sanitizeState(initialState),
493
- rooms: room ? [room] : [],
494
- mountedAt: Date.now(),
495
- lastActivity: Date.now(),
496
- actionCount: 0,
497
- stateChangeCount: 0,
498
- errorCount: 0
499
- };
500
- this.componentSnapshots.set(componentId, snapshot);
501
- this.emit("COMPONENT_MOUNT", componentId, componentName, {
502
- initialState: snapshot.state,
503
- room: room ?? null,
504
- debugLabel: debugLabel ?? null
505
- });
1184
+ hasRole(role) {
1185
+ return this.user.roles?.includes(role) ?? false;
506
1186
  }
507
- trackComponentUnmount(componentId) {
508
- if (!this._enabled) return;
509
- const snapshot = this.componentSnapshots.get(componentId);
510
- const componentName = snapshot?.componentName ?? null;
511
- this.emit("COMPONENT_UNMOUNT", componentId, componentName, {
512
- lifetime: snapshot ? Date.now() - snapshot.mountedAt : 0,
513
- totalActions: snapshot?.actionCount ?? 0,
514
- totalStateChanges: snapshot?.stateChangeCount ?? 0,
515
- totalErrors: snapshot?.errorCount ?? 0
516
- });
517
- this.componentSnapshots.delete(componentId);
518
- }
519
- trackStateChange(componentId, delta, fullState, source = "setState") {
520
- if (!this._enabled) return;
521
- const snapshot = this.componentSnapshots.get(componentId);
522
- if (snapshot) {
523
- snapshot.state = this.sanitizeState(fullState);
524
- snapshot.stateChangeCount++;
525
- snapshot.lastActivity = Date.now();
526
- }
527
- this.emit("STATE_CHANGE", componentId, snapshot?.componentName ?? null, {
528
- delta,
529
- fullState: this.sanitizeState(fullState),
530
- source
531
- });
1187
+ hasAnyRole(roles) {
1188
+ if (!this.user.roles?.length) return false;
1189
+ return roles.some((role) => this.user.roles.includes(role));
532
1190
  }
533
- trackActionCall(componentId, action, payload) {
534
- if (!this._enabled) return;
535
- const snapshot = this.componentSnapshots.get(componentId);
536
- if (snapshot) {
537
- snapshot.actionCount++;
538
- snapshot.lastActivity = Date.now();
539
- }
540
- this.emit("ACTION_CALL", componentId, snapshot?.componentName ?? null, {
541
- action,
542
- payload: this.sanitizeData({ payload }).payload
543
- });
1191
+ hasAllRoles(roles) {
1192
+ if (!this.user.roles?.length) return roles.length === 0;
1193
+ return roles.every((role) => this.user.roles.includes(role));
544
1194
  }
545
- trackActionResult(componentId, action, result, duration) {
546
- if (!this._enabled) return;
547
- const snapshot = this.componentSnapshots.get(componentId);
548
- this.emit("ACTION_RESULT", componentId, snapshot?.componentName ?? null, {
549
- action,
550
- result: this.sanitizeData({ result }).result,
551
- duration
552
- });
1195
+ hasPermission(permission) {
1196
+ return this.user.permissions?.includes(permission) ?? false;
553
1197
  }
554
- trackActionError(componentId, action, error, duration) {
555
- if (!this._enabled) return;
556
- const snapshot = this.componentSnapshots.get(componentId);
557
- if (snapshot) {
558
- snapshot.errorCount++;
559
- }
560
- this.emit("ACTION_ERROR", componentId, snapshot?.componentName ?? null, {
561
- action,
562
- error,
563
- duration
564
- });
1198
+ hasAllPermissions(permissions) {
1199
+ if (!this.user.permissions?.length) return permissions.length === 0;
1200
+ return permissions.every((perm) => this.user.permissions.includes(perm));
565
1201
  }
566
- trackRoomJoin(componentId, roomId) {
567
- if (!this._enabled) return;
568
- const snapshot = this.componentSnapshots.get(componentId);
569
- if (snapshot && !snapshot.rooms.includes(roomId)) {
570
- snapshot.rooms.push(roomId);
571
- }
572
- this.emit("ROOM_JOIN", componentId, snapshot?.componentName ?? null, { roomId });
1202
+ hasAnyPermission(permissions) {
1203
+ if (!this.user.permissions?.length) return false;
1204
+ return permissions.some((perm) => this.user.permissions.includes(perm));
573
1205
  }
574
- trackRoomLeave(componentId, roomId) {
575
- if (!this._enabled) return;
576
- const snapshot = this.componentSnapshots.get(componentId);
577
- if (snapshot) {
578
- snapshot.rooms = snapshot.rooms.filter((r) => r !== roomId);
579
- }
580
- this.emit("ROOM_LEAVE", componentId, snapshot?.componentName ?? null, { roomId });
1206
+ };
1207
+ var AnonymousContext = class {
1208
+ authenticated = false;
1209
+ user = void 0;
1210
+ token = void 0;
1211
+ authenticatedAt = void 0;
1212
+ hasRole() {
1213
+ return false;
581
1214
  }
582
- trackRoomEmit(componentId, roomId, event, data) {
583
- if (!this._enabled) return;
584
- const snapshot = this.componentSnapshots.get(componentId);
585
- this.emit("ROOM_EMIT", componentId, snapshot?.componentName ?? null, {
586
- roomId,
587
- event,
588
- data: this.sanitizeData({ data }).data
589
- });
1215
+ hasAnyRole() {
1216
+ return false;
590
1217
  }
591
- trackConnection(connectionId) {
592
- if (!this._enabled) return;
593
- this.emit("WS_CONNECT", null, null, { connectionId });
1218
+ hasAllRoles() {
1219
+ return false;
594
1220
  }
595
- trackDisconnection(connectionId, componentCount) {
596
- if (!this._enabled) return;
597
- this.emit("WS_DISCONNECT", null, null, { connectionId, componentCount });
1221
+ hasPermission() {
1222
+ return false;
598
1223
  }
599
- trackError(componentId, error, context) {
600
- if (!this._enabled) return;
601
- const snapshot = componentId ? this.componentSnapshots.get(componentId) : null;
602
- if (snapshot) {
603
- snapshot.errorCount++;
604
- }
605
- this.emit("ERROR", componentId, snapshot?.componentName ?? null, {
606
- error,
607
- ...context
608
- });
609
- }
610
- // ===== Debug Client Management =====
611
- registerDebugClient(ws) {
612
- if (!this._enabled) {
613
- const disabled = {
614
- type: "DEBUG_DISABLED",
615
- enabled: false,
616
- timestamp: Date.now()
617
- };
618
- ws.send(JSON.stringify(disabled));
619
- ws.close();
620
- return;
621
- }
622
- this.debugClients.add(ws);
623
- const welcome = {
624
- type: "DEBUG_WELCOME",
625
- enabled: true,
626
- snapshot: this.getSnapshot(),
627
- timestamp: Date.now()
628
- };
629
- ws.send(JSON.stringify(welcome));
630
- for (const event of this.events.slice(-100)) {
631
- const msg = {
632
- type: "DEBUG_EVENT",
633
- event,
634
- timestamp: Date.now()
635
- };
636
- ws.send(JSON.stringify(msg));
637
- }
638
- }
639
- unregisterDebugClient(ws) {
640
- this.debugClients.delete(ws);
641
- }
642
- // ===== Snapshot =====
643
- getSnapshot() {
644
- return {
645
- components: Array.from(this.componentSnapshots.values()),
646
- connections: this.debugClients.size,
647
- uptime: Date.now() - this.startTime,
648
- totalEvents: this.eventCounter
649
- };
650
- }
651
- getComponentState(componentId) {
652
- return this.componentSnapshots.get(componentId) ?? null;
653
- }
654
- getEvents(filter) {
655
- let filtered = this.events;
656
- if (filter?.componentId) {
657
- filtered = filtered.filter((e) => e.componentId === filter.componentId);
658
- }
659
- if (filter?.type) {
660
- filtered = filtered.filter((e) => e.type === filter.type);
661
- }
662
- const limit = filter?.limit ?? 100;
663
- return filtered.slice(-limit);
664
- }
665
- clearEvents() {
666
- this.events = [];
667
- }
668
- // ===== Internal =====
669
- broadcastEvent(event) {
670
- if (this.debugClients.size === 0) return;
671
- const msg = {
672
- type: "DEBUG_EVENT",
673
- event,
674
- timestamp: Date.now()
675
- };
676
- const json = JSON.stringify(msg);
677
- for (const client of this.debugClients) {
678
- try {
679
- client.send(json);
680
- } catch {
681
- this.debugClients.delete(client);
682
- }
683
- }
684
- }
685
- sanitizeData(data) {
686
- try {
687
- const json = JSON.stringify(data);
688
- if (json.length > MAX_STATE_SIZE) {
689
- return { _truncated: true, _size: json.length, _preview: json.slice(0, 500) + "..." };
690
- }
691
- return JSON.parse(json);
692
- } catch {
693
- return { _serialization_error: true };
694
- }
695
- }
696
- sanitizeState(state) {
697
- try {
698
- const json = JSON.stringify(state);
699
- if (json.length > MAX_STATE_SIZE) {
700
- return { _truncated: true, _size: json.length };
701
- }
702
- return JSON.parse(json);
703
- } catch {
704
- return { _serialization_error: true };
705
- }
706
- }
707
- updateSnapshot(event) {
708
- if (!event.componentId) return;
709
- const snapshot = this.componentSnapshots.get(event.componentId);
710
- if (snapshot) {
711
- snapshot.lastActivity = event.timestamp;
712
- }
713
- }
714
- };
715
-
716
- // src/auth/LiveAuthContext.ts
717
- var AuthenticatedContext = class {
718
- authenticated = true;
719
- user;
720
- token;
721
- authenticatedAt;
722
- constructor(user, token) {
723
- this.user = user;
724
- this.token = token;
725
- this.authenticatedAt = Date.now();
726
- }
727
- hasRole(role) {
728
- return this.user.roles?.includes(role) ?? false;
729
- }
730
- hasAnyRole(roles) {
731
- if (!this.user.roles?.length) return false;
732
- return roles.some((role) => this.user.roles.includes(role));
733
- }
734
- hasAllRoles(roles) {
735
- if (!this.user.roles?.length) return roles.length === 0;
736
- return roles.every((role) => this.user.roles.includes(role));
737
- }
738
- hasPermission(permission) {
739
- return this.user.permissions?.includes(permission) ?? false;
740
- }
741
- hasAllPermissions(permissions) {
742
- if (!this.user.permissions?.length) return permissions.length === 0;
743
- return permissions.every((perm) => this.user.permissions.includes(perm));
744
- }
745
- hasAnyPermission(permissions) {
746
- if (!this.user.permissions?.length) return false;
747
- return permissions.some((perm) => this.user.permissions.includes(perm));
748
- }
749
- };
750
- var AnonymousContext = class {
751
- authenticated = false;
752
- user = void 0;
753
- token = void 0;
754
- authenticatedAt = void 0;
755
- hasRole() {
756
- return false;
757
- }
758
- hasAnyRole() {
759
- return false;
760
- }
761
- hasAllRoles() {
762
- return false;
763
- }
764
- hasPermission() {
765
- return false;
766
- }
767
- hasAllPermissions() {
768
- return false;
1224
+ hasAllPermissions() {
1225
+ return false;
769
1226
  }
770
1227
  hasAnyPermission() {
771
1228
  return false;
@@ -853,15 +1310,21 @@ var LiveAuthManager = class {
853
1310
  providersToTry.push(provider);
854
1311
  }
855
1312
  }
1313
+ const errors = [];
856
1314
  for (const provider of providersToTry) {
857
1315
  try {
858
1316
  const context = await provider.authenticate(credentials);
859
1317
  if (context && context.authenticated) {
860
1318
  return context;
861
1319
  }
862
- } catch {
1320
+ } catch (error) {
1321
+ console.warn(`[Auth] Provider '${provider.name}' threw during authentication:`, error.message);
1322
+ errors.push({ provider: provider.name, error });
863
1323
  }
864
1324
  }
1325
+ if (errors.length > 0) {
1326
+ console.warn(`[Auth] All ${providersToTry.length} provider(s) failed. Errors: ${errors.map((e) => `${e.provider}: ${e.error.message}`).join("; ")}`);
1327
+ }
865
1328
  return ANONYMOUS_CONTEXT;
866
1329
  }
867
1330
  /**
@@ -959,10 +1422,13 @@ var StateSignatureManager = class {
959
1422
  secret;
960
1423
  previousSecrets = [];
961
1424
  rotationTimer;
962
- usedNonces = /* @__PURE__ */ new Set();
963
- nonceCleanupTimer;
964
1425
  stateBackups = /* @__PURE__ */ new Map();
965
1426
  config;
1427
+ encryptionSalt;
1428
+ cachedEncryptionKey = null;
1429
+ /** Replay detection: nonce → timestamp when it was first seen. Cleaned every 60s. */
1430
+ usedNonces = /* @__PURE__ */ new Map();
1431
+ nonceCleanupTimer;
966
1432
  constructor(config = {}) {
967
1433
  const defaultSecret = typeof process !== "undefined" ? process.env?.LIVE_STATE_SECRET : void 0;
968
1434
  this.config = {
@@ -972,23 +1438,65 @@ var StateSignatureManager = class {
972
1438
  compressionEnabled: config.compressionEnabled ?? true,
973
1439
  encryptionEnabled: config.encryptionEnabled ?? false,
974
1440
  nonceEnabled: config.nonceEnabled ?? false,
975
- maxStateAge: config.maxStateAge ?? 7 * 24 * 60 * 60 * 1e3,
1441
+ maxStateAge: config.maxStateAge ?? 30 * 60 * 1e3,
976
1442
  backupEnabled: config.backupEnabled ?? true,
977
- maxBackups: config.maxBackups ?? 3
1443
+ maxBackups: config.maxBackups ?? 3,
1444
+ nonceTTL: config.nonceTTL ?? 5 * 60 * 1e3
978
1445
  };
979
1446
  if (!this.config.secret) {
980
1447
  this.config.secret = randomBytes(32).toString("hex");
981
1448
  liveWarn("state", null, "No LIVE_STATE_SECRET provided. Using random key (state will not persist across restarts).");
982
1449
  }
983
1450
  this.secret = Buffer.from(this.config.secret, "utf-8");
1451
+ this.encryptionSalt = randomBytes(16);
984
1452
  if (this.config.rotationEnabled) {
985
1453
  this.setupKeyRotation();
986
1454
  }
987
1455
  if (this.config.nonceEnabled) {
988
- this.nonceCleanupTimer = setInterval(() => this.cleanupNonces(), 60 * 60 * 1e3);
1456
+ this.nonceCleanupTimer = setInterval(() => this.cleanupNonces(), this.config.nonceTTL + 10 * 1e3);
1457
+ }
1458
+ }
1459
+ /**
1460
+ * Generate a hybrid nonce: `timestamp:random:HMAC(timestamp:random, secret)`
1461
+ * Self-validating via HMAC, unique via random bytes, replay-tracked via Map.
1462
+ */
1463
+ generateNonce() {
1464
+ const ts = Date.now().toString();
1465
+ const rand = randomBytes(8).toString("hex");
1466
+ const payload = `${ts}:${rand}`;
1467
+ const mac = createHmac("sha256", this.secret).update(payload).digest("hex").slice(0, 16);
1468
+ return `${ts}:${rand}:${mac}`;
1469
+ }
1470
+ /**
1471
+ * Validate a hybrid nonce: check format, HMAC, and TTL.
1472
+ */
1473
+ validateNonce(nonce) {
1474
+ const parts = nonce.split(":");
1475
+ if (parts.length !== 3) return { valid: false, error: "Malformed nonce" };
1476
+ const [ts, rand, mac] = parts;
1477
+ const timestamp = Number(ts);
1478
+ if (isNaN(timestamp)) return { valid: false, error: "Malformed nonce timestamp" };
1479
+ const age = Date.now() - timestamp;
1480
+ if (age > this.config.nonceTTL) {
1481
+ return { valid: false, error: "Nonce expired" };
1482
+ }
1483
+ if (age < -3e4) {
1484
+ return { valid: false, error: "Nonce timestamp in the future" };
1485
+ }
1486
+ const payload = `${ts}:${rand}`;
1487
+ const expectedMac = createHmac("sha256", this.secret).update(payload).digest("hex").slice(0, 16);
1488
+ if (this.timingSafeEqual(mac, expectedMac)) {
1489
+ return { valid: true };
1490
+ }
1491
+ for (const prevSecret of this.previousSecrets) {
1492
+ const prevMac = createHmac("sha256", prevSecret).update(payload).digest("hex").slice(0, 16);
1493
+ if (this.timingSafeEqual(mac, prevMac)) {
1494
+ return { valid: true };
1495
+ }
989
1496
  }
1497
+ return { valid: false, error: "Invalid nonce signature" };
990
1498
  }
991
- async signState(componentId, state, version, options) {
1499
+ signState(componentId, state, version, options) {
992
1500
  let dataStr = JSON.stringify(state);
993
1501
  let compressed = false;
994
1502
  let encrypted = false;
@@ -1009,7 +1517,7 @@ var StateSignatureManager = class {
1009
1517
  dataStr = iv.toString("base64") + ":" + encryptedData;
1010
1518
  encrypted = true;
1011
1519
  }
1012
- const nonce = this.config.nonceEnabled ? randomBytes(16).toString("hex") : void 0;
1520
+ const nonce = this.config.nonceEnabled ? this.generateNonce() : void 0;
1013
1521
  const signedState = {
1014
1522
  data: dataStr,
1015
1523
  signature: "",
@@ -1026,26 +1534,34 @@ var StateSignatureManager = class {
1026
1534
  }
1027
1535
  return signedState;
1028
1536
  }
1029
- async validateState(signedState) {
1537
+ validateState(signedState, options) {
1030
1538
  try {
1031
1539
  const age = Date.now() - signedState.timestamp;
1032
1540
  if (age > this.config.maxStateAge) {
1033
1541
  return { valid: false, error: "State expired" };
1034
1542
  }
1035
- if (signedState.nonce && this.config.nonceEnabled) {
1543
+ if (signedState.nonce && this.config.nonceEnabled && !options?.skipNonce) {
1544
+ const nonceResult = this.validateNonce(signedState.nonce);
1545
+ if (!nonceResult.valid) {
1546
+ return { valid: false, error: nonceResult.error };
1547
+ }
1036
1548
  if (this.usedNonces.has(signedState.nonce)) {
1037
- return { valid: false, error: "Nonce already used (replay attempt)" };
1549
+ return { valid: false, error: "Nonce already used" };
1038
1550
  }
1039
1551
  }
1040
1552
  const expectedSig = this.computeSignature(signedState);
1041
1553
  if (this.timingSafeEqual(signedState.signature, expectedSig)) {
1042
- if (signedState.nonce) this.usedNonces.add(signedState.nonce);
1554
+ if (signedState.nonce && this.config.nonceEnabled) {
1555
+ this.usedNonces.set(signedState.nonce, Date.now());
1556
+ }
1043
1557
  return { valid: true };
1044
1558
  }
1045
1559
  for (const prevSecret of this.previousSecrets) {
1046
1560
  const prevSig = this.computeSignatureWithKey(signedState, prevSecret);
1047
1561
  if (this.timingSafeEqual(signedState.signature, prevSig)) {
1048
- if (signedState.nonce) this.usedNonces.add(signedState.nonce);
1562
+ if (signedState.nonce && this.config.nonceEnabled) {
1563
+ this.usedNonces.set(signedState.nonce, Date.now());
1564
+ }
1049
1565
  return { valid: true };
1050
1566
  }
1051
1567
  }
@@ -1054,7 +1570,7 @@ var StateSignatureManager = class {
1054
1570
  return { valid: false, error: error.message };
1055
1571
  }
1056
1572
  }
1057
- async extractData(signedState) {
1573
+ extractData(signedState) {
1058
1574
  let dataStr = signedState.data;
1059
1575
  if (signedState.encrypted) {
1060
1576
  const [ivB64, encryptedData] = dataStr.split(":");
@@ -1107,7 +1623,9 @@ var StateSignatureManager = class {
1107
1623
  }
1108
1624
  }
1109
1625
  deriveEncryptionKey() {
1110
- return createHmac("sha256", this.secret).update("encryption-key-derivation").digest();
1626
+ if (this.cachedEncryptionKey) return this.cachedEncryptionKey;
1627
+ this.cachedEncryptionKey = scryptSync(this.secret, this.encryptionSalt, 32);
1628
+ return this.cachedEncryptionKey;
1111
1629
  }
1112
1630
  setupKeyRotation() {
1113
1631
  this.rotationTimer = setInterval(() => {
@@ -1116,12 +1634,15 @@ var StateSignatureManager = class {
1116
1634
  this.previousSecrets.pop();
1117
1635
  }
1118
1636
  this.secret = randomBytes(32);
1637
+ this.cachedEncryptionKey = null;
1119
1638
  liveLog("state", null, "Key rotation completed");
1120
1639
  }, this.config.rotationInterval);
1121
1640
  }
1641
+ /** Remove nonces older than nonceTTL + 10s from the replay detection map. */
1122
1642
  cleanupNonces() {
1123
- if (this.usedNonces.size > 1e5) {
1124
- this.usedNonces.clear();
1643
+ const cutoff = Date.now() - (this.config.nonceTTL + 10 * 1e3);
1644
+ for (const [nonce, ts] of this.usedNonces) {
1645
+ if (ts < cutoff) this.usedNonces.delete(nonce);
1125
1646
  }
1126
1647
  }
1127
1648
  shutdown() {
@@ -1542,6 +2063,8 @@ var WebSocketConnectionManager = class extends EventEmitter {
1542
2063
  connections = /* @__PURE__ */ new Map();
1543
2064
  connectionMetrics = /* @__PURE__ */ new Map();
1544
2065
  connectionPools = /* @__PURE__ */ new Map();
2066
+ /** Reverse index: connectionId -> Set of poolIds for O(1) cleanup */
2067
+ connectionPoolIndex = /* @__PURE__ */ new Map();
1545
2068
  messageQueues = /* @__PURE__ */ new Map();
1546
2069
  healthCheckTimer;
1547
2070
  heartbeatTimer;
@@ -1596,6 +2119,10 @@ var WebSocketConnectionManager = class extends EventEmitter {
1596
2119
  this.connectionPools.set(poolId, /* @__PURE__ */ new Set());
1597
2120
  }
1598
2121
  this.connectionPools.get(poolId).add(connectionId);
2122
+ if (!this.connectionPoolIndex.has(connectionId)) {
2123
+ this.connectionPoolIndex.set(connectionId, /* @__PURE__ */ new Set());
2124
+ }
2125
+ this.connectionPoolIndex.get(connectionId).add(poolId);
1599
2126
  }
1600
2127
  removeFromPool(connectionId, poolId) {
1601
2128
  const pool = this.connectionPools.get(poolId);
@@ -1603,15 +2130,22 @@ var WebSocketConnectionManager = class extends EventEmitter {
1603
2130
  pool.delete(connectionId);
1604
2131
  if (pool.size === 0) this.connectionPools.delete(poolId);
1605
2132
  }
2133
+ this.connectionPoolIndex.get(connectionId)?.delete(poolId);
1606
2134
  }
1607
2135
  cleanupConnection(connectionId) {
1608
2136
  this.connections.delete(connectionId);
1609
2137
  this.connectionMetrics.delete(connectionId);
1610
2138
  this.messageQueues.delete(connectionId);
1611
- for (const [poolId, pool] of this.connectionPools) {
1612
- if (pool.has(connectionId)) {
1613
- this.removeFromPool(connectionId, poolId);
2139
+ const poolIds = this.connectionPoolIndex.get(connectionId);
2140
+ if (poolIds) {
2141
+ for (const poolId of poolIds) {
2142
+ const pool = this.connectionPools.get(poolId);
2143
+ if (pool) {
2144
+ pool.delete(connectionId);
2145
+ if (pool.size === 0) this.connectionPools.delete(poolId);
2146
+ }
1614
2147
  }
2148
+ this.connectionPoolIndex.delete(connectionId);
1615
2149
  }
1616
2150
  }
1617
2151
  getConnectionMetrics(connectionId) {
@@ -1693,6 +2227,7 @@ var WebSocketConnectionManager = class extends EventEmitter {
1693
2227
  this.connections.clear();
1694
2228
  this.connectionMetrics.clear();
1695
2229
  this.connectionPools.clear();
2230
+ this.connectionPoolIndex.clear();
1696
2231
  this.messageQueues.clear();
1697
2232
  }
1698
2233
  };
@@ -1707,116 +2242,39 @@ function getLiveComponentContext() {
1707
2242
  return _ctx;
1708
2243
  }
1709
2244
 
1710
- // src/component/LiveComponent.ts
1711
- var EMIT_OVERRIDE_KEY = /* @__PURE__ */ Symbol.for("fluxstack:emitOverride");
1712
- var _liveDebugger = null;
1713
- function _setLiveDebugger(dbg) {
1714
- _liveDebugger = dbg;
1715
- }
1716
- var LiveComponent = class _LiveComponent {
1717
- /** Component name for registry lookup - must be defined in subclasses */
1718
- static componentName;
1719
- /** Default state - must be defined in subclasses */
1720
- static defaultState;
1721
- /**
1722
- * Per-component logging control. Silent by default.
1723
- *
1724
- * @example
1725
- * static logging = true // all categories
1726
- * static logging = ['lifecycle', 'messages'] // specific categories
1727
- */
1728
- static logging;
1729
- /**
1730
- * Component-level auth configuration.
1731
- */
1732
- static auth;
1733
- /**
1734
- * Per-action auth configuration.
1735
- */
1736
- static actionAuth;
1737
- /**
1738
- * Data that survives HMR reloads.
1739
- */
1740
- static persistent;
1741
- /**
1742
- * When true, only ONE server-side instance exists for this component.
1743
- * All clients share the same state.
1744
- */
1745
- static singleton;
1746
- id;
2245
+ // src/component/managers/ComponentStateManager.ts
2246
+ var _forbiddenSetCache = /* @__PURE__ */ new WeakMap();
2247
+ var ComponentStateManager = class {
1747
2248
  _state;
1748
- state;
1749
- // Proxy wrapper
1750
- ws;
1751
- room;
1752
- userId;
1753
- broadcastToRoom = () => {
1754
- };
1755
- // Server-only private state (NEVER sent to client)
1756
- _privateState = {};
1757
- // Auth context (injected by registry during mount)
1758
- _authContext = ANONYMOUS_CONTEXT;
1759
- // Room event subscriptions (cleaned up on destroy)
1760
- roomEventUnsubscribers = [];
1761
- joinedRooms = /* @__PURE__ */ new Set();
1762
- // Room type for typed events (override in subclass)
1763
- roomType = "default";
1764
- // Cached room handles
1765
- roomHandles = /* @__PURE__ */ new Map();
1766
- // Guard against infinite recursion in onStateChange
2249
+ _proxyState;
1767
2250
  _inStateChange = false;
1768
- // Singleton emit override
1769
- [EMIT_OVERRIDE_KEY] = null;
1770
- constructor(initialState, ws, options) {
1771
- this.id = this.generateId();
1772
- const ctor = this.constructor;
1773
- this._state = { ...ctor.defaultState, ...initialState };
1774
- this.state = this.createStateProxy(this._state);
1775
- this.ws = ws;
1776
- this.room = options?.room;
1777
- this.userId = options?.userId;
1778
- if (this.room) {
1779
- this.joinedRooms.add(this.room);
1780
- const ctx = getLiveComponentContext();
1781
- ctx.roomManager.joinRoom(this.id, this.room, this.ws);
1782
- }
1783
- this.createDirectStateAccessors();
1784
- }
1785
- // Create getters/setters for each state property directly on `this`
1786
- createDirectStateAccessors() {
1787
- const forbidden = /* @__PURE__ */ new Set([
1788
- ...Object.keys(this),
1789
- ...Object.getOwnPropertyNames(Object.getPrototypeOf(this)),
1790
- "state",
1791
- "_state",
1792
- "ws",
1793
- "id",
1794
- "room",
1795
- "userId",
1796
- "broadcastToRoom",
1797
- "$private",
1798
- "_privateState",
1799
- "$room",
1800
- "$rooms",
1801
- "roomType",
1802
- "roomHandles",
1803
- "joinedRooms",
1804
- "roomEventUnsubscribers"
1805
- ]);
1806
- for (const key of Object.keys(this._state)) {
1807
- if (!forbidden.has(key)) {
1808
- Object.defineProperty(this, key, {
1809
- get: () => this._state[key],
1810
- set: (value) => {
1811
- this.state[key] = value;
1812
- },
1813
- enumerable: true,
1814
- configurable: true
1815
- });
1816
- }
1817
- }
2251
+ _idBytes = null;
2252
+ _deepDiff;
2253
+ _deepDiffDepth;
2254
+ componentId;
2255
+ ws;
2256
+ emitFn;
2257
+ onStateChangeFn;
2258
+ constructor(opts) {
2259
+ this.componentId = opts.componentId;
2260
+ this.ws = opts.ws;
2261
+ this.emitFn = opts.emitFn;
2262
+ this.onStateChangeFn = opts.onStateChangeFn;
2263
+ this._deepDiff = opts.deepDiff ?? false;
2264
+ this._deepDiffDepth = opts.deepDiffDepth ?? 3;
2265
+ this._state = this._deepDiff ? structuredClone(opts.initialState) : opts.initialState;
2266
+ this._proxyState = this.createStateProxy(this._state);
2267
+ }
2268
+ get rawState() {
2269
+ return this._state;
2270
+ }
2271
+ get proxyState() {
2272
+ return this._proxyState;
2273
+ }
2274
+ /** Guard flag — prevents infinite recursion in onStateChange */
2275
+ get inStateChange() {
2276
+ return this._inStateChange;
1818
2277
  }
1819
- // Create a Proxy that auto-emits STATE_DELTA on any mutation
1820
2278
  createStateProxy(state) {
1821
2279
  const self = this;
1822
2280
  return new Proxy(state, {
@@ -1825,23 +2283,17 @@ var LiveComponent = class _LiveComponent {
1825
2283
  if (oldValue !== value) {
1826
2284
  target[prop] = value;
1827
2285
  const changes = { [prop]: value };
1828
- self.emit("STATE_DELTA", { delta: changes });
2286
+ self.emitFn("STATE_DELTA", { delta: changes });
1829
2287
  if (!self._inStateChange) {
1830
2288
  self._inStateChange = true;
1831
2289
  try {
1832
- self.onStateChange(changes);
2290
+ self.onStateChangeFn(changes);
1833
2291
  } catch (err) {
1834
- console.error(`[${self.id}] onStateChange error:`, err?.message || err);
2292
+ console.error(`[${self.componentId}] onStateChange error:`, err?.message || err);
1835
2293
  } finally {
1836
2294
  self._inStateChange = false;
1837
2295
  }
1838
2296
  }
1839
- _liveDebugger?.trackStateChange(
1840
- self.id,
1841
- changes,
1842
- target,
1843
- "proxy"
1844
- );
1845
2297
  }
1846
2298
  return true;
1847
2299
  },
@@ -1850,76 +2302,459 @@ var LiveComponent = class _LiveComponent {
1850
2302
  }
1851
2303
  });
1852
2304
  }
1853
- // ========================================
1854
- // $private - Server-Only State
1855
- // ========================================
1856
- get $private() {
1857
- return this._privateState;
2305
+ setState(updates) {
2306
+ const newUpdates = typeof updates === "function" ? updates(this._state) : updates;
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
+ }
2327
+ }
2328
+ }
2329
+ if (!hasChanges) return;
2330
+ if (this._deepDiff) {
2331
+ deepAssign(this._state, actualChanges);
2332
+ } else {
2333
+ Object.assign(this._state, actualChanges);
2334
+ }
2335
+ this.emitFn("STATE_DELTA", { delta: actualChanges });
2336
+ if (!this._inStateChange) {
2337
+ this._inStateChange = true;
2338
+ try {
2339
+ this.onStateChangeFn(actualChanges);
2340
+ } catch (err) {
2341
+ console.error(`[${this.componentId}] onStateChange error:`, err?.message || err);
2342
+ } finally {
2343
+ this._inStateChange = false;
2344
+ }
2345
+ }
1858
2346
  }
1859
- // ========================================
1860
- // $room - Unified Room System
1861
- // ========================================
1862
- get $room() {
1863
- const self = this;
1864
- const ctx = getLiveComponentContext();
1865
- const createHandle = (roomId) => {
1866
- if (this.roomHandles.has(roomId)) {
1867
- return this.roomHandles.get(roomId);
2347
+ sendBinaryDelta(delta, encoder2) {
2348
+ const actualChanges = {};
2349
+ let hasChanges = false;
2350
+ for (const key of Object.keys(delta)) {
2351
+ if (this._state[key] !== delta[key]) {
2352
+ actualChanges[key] = delta[key];
2353
+ hasChanges = true;
1868
2354
  }
1869
- const handle = {
1870
- get id() {
1871
- return roomId;
1872
- },
1873
- get state() {
1874
- return ctx.roomManager.getRoomState(roomId);
1875
- },
1876
- join: (initialState) => {
1877
- if (self.joinedRooms.has(roomId)) return;
1878
- self.joinedRooms.add(roomId);
1879
- ctx.roomManager.joinRoom(self.id, roomId, self.ws, initialState);
1880
- try {
1881
- self.onRoomJoin(roomId);
1882
- } catch (err) {
1883
- console.error(`[${self.id}] onRoomJoin error:`, err?.message || err);
1884
- }
1885
- },
1886
- leave: () => {
1887
- if (!self.joinedRooms.has(roomId)) return;
1888
- self.joinedRooms.delete(roomId);
1889
- ctx.roomManager.leaveRoom(self.id, roomId);
1890
- try {
1891
- self.onRoomLeave(roomId);
1892
- } catch (err) {
1893
- console.error(`[${self.id}] onRoomLeave error:`, err?.message || err);
1894
- }
1895
- },
1896
- emit: (event, data) => {
1897
- return ctx.roomManager.emitToRoom(roomId, event, data, self.id);
1898
- },
1899
- on: (event, handler) => {
1900
- const unsubscribe = ctx.roomEvents.on(
1901
- "room",
1902
- roomId,
1903
- event,
1904
- self.id,
1905
- handler
1906
- );
1907
- self.roomEventUnsubscribers.push(unsubscribe);
1908
- return unsubscribe;
1909
- },
1910
- setState: (updates) => {
1911
- ctx.roomManager.setRoomState(roomId, updates, self.id);
1912
- }
1913
- };
1914
- this.roomHandles.set(roomId, handle);
1915
- return handle;
1916
- };
1917
- const proxyFn = ((roomId) => createHandle(roomId));
1918
- const defaultHandle = this.room ? createHandle(this.room) : null;
1919
- Object.defineProperties(proxyFn, {
1920
- id: { get: () => self.room },
1921
- state: { get: () => defaultHandle?.state ?? {} },
1922
- join: {
2355
+ }
2356
+ if (!hasChanges) return;
2357
+ Object.assign(this._state, actualChanges);
2358
+ const payload = encoder2(actualChanges);
2359
+ if (!this._idBytes) {
2360
+ this._idBytes = new TextEncoder().encode(this.componentId);
2361
+ }
2362
+ const idBytes = this._idBytes;
2363
+ const frame = new Uint8Array(1 + 1 + idBytes.length + payload.length);
2364
+ frame[0] = 1;
2365
+ frame[1] = idBytes.length;
2366
+ frame.set(idBytes, 2);
2367
+ frame.set(payload, 2 + idBytes.length);
2368
+ if (this.ws && this.ws.readyState === 1) {
2369
+ this.ws.send(frame);
2370
+ }
2371
+ }
2372
+ setValue(payload) {
2373
+ const { key, value } = payload;
2374
+ const update = { [key]: value };
2375
+ this.setState(update);
2376
+ return { success: true, key, value };
2377
+ }
2378
+ getSerializableState() {
2379
+ return this._proxyState;
2380
+ }
2381
+ /**
2382
+ * Create getters/setters for each state property directly on `target`.
2383
+ * This allows `this.count` instead of `this.state.count` in subclasses.
2384
+ */
2385
+ applyDirectAccessors(target, constructorFn) {
2386
+ let forbidden = _forbiddenSetCache.get(constructorFn);
2387
+ if (!forbidden) {
2388
+ forbidden = /* @__PURE__ */ new Set([
2389
+ ...Object.keys(target),
2390
+ ...Object.getOwnPropertyNames(Object.getPrototypeOf(target)),
2391
+ "state",
2392
+ "_state",
2393
+ "ws",
2394
+ "id",
2395
+ "room",
2396
+ "userId",
2397
+ "broadcastToRoom",
2398
+ "$private",
2399
+ "_privateState",
2400
+ "$room",
2401
+ "$rooms",
2402
+ "roomType",
2403
+ "roomHandles",
2404
+ "joinedRooms",
2405
+ "roomEventUnsubscribers",
2406
+ // Internal manager fields
2407
+ "_stateManager",
2408
+ "_roomProxyManager",
2409
+ "_actionSecurity",
2410
+ "_messaging"
2411
+ ]);
2412
+ _forbiddenSetCache.set(constructorFn, forbidden);
2413
+ }
2414
+ for (const key of Object.keys(this._state)) {
2415
+ if (!forbidden.has(key)) {
2416
+ Object.defineProperty(target, key, {
2417
+ get: () => this._state[key],
2418
+ set: (value) => {
2419
+ this._proxyState[key] = value;
2420
+ },
2421
+ enumerable: true,
2422
+ configurable: true
2423
+ });
2424
+ }
2425
+ }
2426
+ }
2427
+ /** Release cached resources */
2428
+ cleanup() {
2429
+ this._idBytes = null;
2430
+ }
2431
+ };
2432
+
2433
+ // src/component/managers/ComponentMessaging.ts
2434
+ var EMIT_OVERRIDE_KEY = /* @__PURE__ */ Symbol.for("fluxstack:emitOverride");
2435
+ var ComponentMessaging = class {
2436
+ constructor(ctx) {
2437
+ this.ctx = ctx;
2438
+ }
2439
+ emit(type, payload) {
2440
+ const override = this.ctx.getEmitOverride();
2441
+ if (override) {
2442
+ override(type, payload);
2443
+ return;
2444
+ }
2445
+ const message = {
2446
+ type,
2447
+ componentId: this.ctx.componentId,
2448
+ payload,
2449
+ timestamp: Date.now(),
2450
+ userId: this.ctx.getUserId(),
2451
+ room: this.ctx.getRoom()
2452
+ };
2453
+ if (this.ctx.ws) {
2454
+ queueWsMessage(this.ctx.ws, message);
2455
+ }
2456
+ }
2457
+ broadcast(type, payload, excludeCurrentUser = false) {
2458
+ const room = this.ctx.getRoom();
2459
+ if (!room) {
2460
+ liveWarn("rooms", this.ctx.componentId, `[${this.ctx.componentId}] Cannot broadcast '${type}' - no room set`);
2461
+ return;
2462
+ }
2463
+ const message = {
2464
+ type,
2465
+ payload,
2466
+ room,
2467
+ excludeUser: excludeCurrentUser ? this.ctx.getUserId() : void 0
2468
+ };
2469
+ liveLog("rooms", this.ctx.componentId, `[${this.ctx.componentId}] Broadcasting '${type}' to room '${room}'`);
2470
+ this.ctx.getBroadcastToRoom()(message);
2471
+ }
2472
+ };
2473
+
2474
+ // src/component/managers/ActionSecurityManager.ts
2475
+ var BLOCKED_ACTIONS = /* @__PURE__ */ new Set([
2476
+ "constructor",
2477
+ "destroy",
2478
+ "executeAction",
2479
+ "getSerializableState",
2480
+ "onMount",
2481
+ "onDestroy",
2482
+ "onConnect",
2483
+ "onDisconnect",
2484
+ "onStateChange",
2485
+ "onRoomJoin",
2486
+ "onRoomLeave",
2487
+ "onRehydrate",
2488
+ "onAction",
2489
+ "onClientJoin",
2490
+ "onClientLeave",
2491
+ "setState",
2492
+ "sendBinaryDelta",
2493
+ "emit",
2494
+ "broadcast",
2495
+ "broadcastToRoom",
2496
+ "createStateProxy",
2497
+ "createDirectStateAccessors",
2498
+ "generateId",
2499
+ "setAuthContext",
2500
+ "_resetAuthContext",
2501
+ "$auth",
2502
+ "$private",
2503
+ "_privateState",
2504
+ "$persistent",
2505
+ "_inStateChange",
2506
+ "$room",
2507
+ "$rooms",
2508
+ "subscribeToRoom",
2509
+ "unsubscribeFromRoom",
2510
+ "emitRoomEvent",
2511
+ "onRoomEvent",
2512
+ "emitRoomEventWithState"
2513
+ ]);
2514
+ var ActionSecurityManager = class {
2515
+ _actionCalls = /* @__PURE__ */ new Map();
2516
+ async validateAndExecute(action, payload, ctx) {
2517
+ const { component, componentClass, componentId } = ctx;
2518
+ try {
2519
+ if (BLOCKED_ACTIONS.has(action)) {
2520
+ throw new Error(`Action '${action}' is not callable`);
2521
+ }
2522
+ if (action.startsWith("_") || action.startsWith("#")) {
2523
+ throw new Error(`Action '${action}' is not callable`);
2524
+ }
2525
+ const publicActions = componentClass.publicActions;
2526
+ if (!publicActions) {
2527
+ console.warn(`[SECURITY] Component '${componentClass.componentName || componentClass.name}' has no publicActions defined. All remote actions are blocked.`);
2528
+ throw new Error(`Action '${action}' is not callable - component has no publicActions defined`);
2529
+ }
2530
+ if (!publicActions.includes(action)) {
2531
+ const methodExists = typeof component[action] === "function";
2532
+ if (methodExists) {
2533
+ const name = componentClass.componentName || componentClass.name;
2534
+ throw new Error(
2535
+ `Action '${action}' exists on '${name}' but is not listed in publicActions. Add it to: static publicActions = [..., '${action}']`
2536
+ );
2537
+ }
2538
+ throw new Error(`Action '${action}' is not callable`);
2539
+ }
2540
+ const method = component[action];
2541
+ if (typeof method !== "function") {
2542
+ throw new Error(`Action '${action}' not found on component`);
2543
+ }
2544
+ if (Object.prototype.hasOwnProperty.call(Object.prototype, action)) {
2545
+ throw new Error(`Action '${action}' is not callable`);
2546
+ }
2547
+ const rateLimit = componentClass.actionRateLimit;
2548
+ if (rateLimit) {
2549
+ const now = Date.now();
2550
+ const key = rateLimit.perAction ? action : "*";
2551
+ let entry = this._actionCalls.get(key);
2552
+ if (!entry || now - entry.windowStart >= rateLimit.windowMs) {
2553
+ entry = { count: 0, windowStart: now };
2554
+ this._actionCalls.set(key, entry);
2555
+ }
2556
+ entry.count++;
2557
+ if (entry.count > rateLimit.maxCalls) {
2558
+ throw new Error(`Action rate limit exceeded (max ${rateLimit.maxCalls} calls per ${rateLimit.windowMs}ms)`);
2559
+ }
2560
+ }
2561
+ const schemas = componentClass.actionSchemas;
2562
+ if (schemas && schemas[action]) {
2563
+ const result2 = schemas[action].safeParse(payload);
2564
+ if (!result2.success) {
2565
+ const errorMsg = result2.error?.message || result2.error?.issues?.map((i) => i.message).join(", ") || "Invalid payload";
2566
+ throw new Error(`Action '${action}' payload validation failed: ${errorMsg}`);
2567
+ }
2568
+ payload = result2.data ?? payload;
2569
+ }
2570
+ let hookResult;
2571
+ try {
2572
+ hookResult = await component.onAction(action, payload);
2573
+ } catch (hookError) {
2574
+ ctx.emitFn("ERROR", {
2575
+ action,
2576
+ error: `Action '${action}' failed pre-validation`
2577
+ });
2578
+ throw hookError;
2579
+ }
2580
+ if (hookResult === false) {
2581
+ throw new Error(`Action '${action}' was cancelled`);
2582
+ }
2583
+ const result = await method.call(component, payload);
2584
+ return result;
2585
+ } catch (error) {
2586
+ if (!error.message?.includes("was cancelled") && !error.message?.includes("pre-validation")) {
2587
+ ctx.emitFn("ERROR", {
2588
+ action,
2589
+ error: error.message
2590
+ });
2591
+ }
2592
+ throw error;
2593
+ }
2594
+ }
2595
+ };
2596
+
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
+ }
2658
+ var ComponentRoomProxy = class {
2659
+ roomEventUnsubscribers = [];
2660
+ joinedRooms = /* @__PURE__ */ new Set();
2661
+ roomHandles = /* @__PURE__ */ new Map();
2662
+ _roomProxy = null;
2663
+ _roomsCache = null;
2664
+ _cachedCtx = null;
2665
+ roomType = "default";
2666
+ room;
2667
+ componentId;
2668
+ ws;
2669
+ getCtx;
2670
+ setStateFn;
2671
+ _deepDiff;
2672
+ _deepDiffDepth;
2673
+ _serverOnlyState;
2674
+ constructor(rctx) {
2675
+ this.componentId = rctx.componentId;
2676
+ this.ws = rctx.ws;
2677
+ this.room = rctx.defaultRoom;
2678
+ this.getCtx = rctx.getCtx;
2679
+ this.setStateFn = rctx.setStateFn;
2680
+ this._deepDiff = rctx.deepDiff ?? true;
2681
+ this._deepDiffDepth = rctx.deepDiffDepth;
2682
+ this._serverOnlyState = rctx.serverOnlyState ?? false;
2683
+ if (this.room) {
2684
+ this.joinedRooms.add(this.room);
2685
+ this.ctx.roomManager.joinRoom(this.componentId, this.room, this.ws, void 0, { deepDiff: this._deepDiff, deepDiffDepth: this._deepDiffDepth, serverOnlyState: this._serverOnlyState });
2686
+ }
2687
+ }
2688
+ /** Lazy context resolution — cached after first access */
2689
+ get ctx() {
2690
+ if (!this._cachedCtx) {
2691
+ this._cachedCtx = this.getCtx();
2692
+ }
2693
+ return this._cachedCtx;
2694
+ }
2695
+ get $room() {
2696
+ if (this._roomProxy) return this._roomProxy;
2697
+ const self = this;
2698
+ const createHandle = (roomId) => {
2699
+ if (this.roomHandles.has(roomId)) {
2700
+ return this.roomHandles.get(roomId);
2701
+ }
2702
+ const handle = {
2703
+ get id() {
2704
+ return roomId;
2705
+ },
2706
+ get state() {
2707
+ return self.ctx.roomManager.getRoomState(roomId);
2708
+ },
2709
+ join: (initialState) => {
2710
+ if (self.joinedRooms.has(roomId)) return;
2711
+ self.joinedRooms.add(roomId);
2712
+ self._roomsCache = null;
2713
+ self.ctx.roomManager.joinRoom(self.componentId, roomId, self.ws, initialState, { deepDiff: self._deepDiff, deepDiffDepth: self._deepDiffDepth, serverOnlyState: self._serverOnlyState });
2714
+ },
2715
+ leave: () => {
2716
+ if (!self.joinedRooms.has(roomId)) return;
2717
+ self.joinedRooms.delete(roomId);
2718
+ self._roomsCache = null;
2719
+ self.ctx.roomManager.leaveRoom(self.componentId, roomId);
2720
+ },
2721
+ emit: (event, data) => {
2722
+ return self.ctx.roomManager.emitToRoom(roomId, event, data, self.componentId);
2723
+ },
2724
+ on: (event, handler) => {
2725
+ const unsubscribe = self.ctx.roomEvents.on(
2726
+ "room",
2727
+ roomId,
2728
+ event,
2729
+ self.componentId,
2730
+ handler
2731
+ );
2732
+ self.roomEventUnsubscribers.push(unsubscribe);
2733
+ return unsubscribe;
2734
+ },
2735
+ setState: (updates) => {
2736
+ self.ctx.roomManager.setRoomState(roomId, updates, self.componentId);
2737
+ }
2738
+ };
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;
2746
+ };
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
+ });
2753
+ const defaultHandle = this.room ? createHandle(this.room) : null;
2754
+ Object.defineProperties(proxyFn, {
2755
+ id: { get: () => self.room },
2756
+ state: { get: () => defaultHandle?.state ?? {} },
2757
+ join: {
1923
2758
  value: (initialState) => {
1924
2759
  if (!defaultHandle) throw new Error("No default room set");
1925
2760
  defaultHandle.join(initialState);
@@ -1950,13 +2785,317 @@ var LiveComponent = class _LiveComponent {
1950
2785
  }
1951
2786
  }
1952
2787
  });
1953
- 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;
2895
+ }
2896
+ get $rooms() {
2897
+ if (this._roomsCache) return this._roomsCache;
2898
+ this._roomsCache = Array.from(this.joinedRooms);
2899
+ return this._roomsCache;
2900
+ }
2901
+ getJoinedRooms() {
2902
+ return this.joinedRooms;
2903
+ }
2904
+ emitRoomEvent(event, data, notifySelf = false) {
2905
+ if (!this.room) {
2906
+ liveWarn("rooms", this.componentId, `[${this.componentId}] Cannot emit room event '${event}' - no room set`);
2907
+ return 0;
2908
+ }
2909
+ const excludeId = notifySelf ? void 0 : this.componentId;
2910
+ const notified = this.ctx.roomEvents.emit(this.roomType, this.room, event, data, excludeId);
2911
+ liveLog("rooms", this.componentId, `[${this.componentId}] Room event '${event}' -> ${notified} components`);
2912
+ return notified;
2913
+ }
2914
+ onRoomEvent(event, handler) {
2915
+ if (!this.room) {
2916
+ liveWarn("rooms", this.componentId, `[${this.componentId}] Cannot subscribe to room event '${event}' - no room set`);
2917
+ return;
2918
+ }
2919
+ const unsubscribe = this.ctx.roomEvents.on(
2920
+ this.roomType,
2921
+ this.room,
2922
+ event,
2923
+ this.componentId,
2924
+ handler
2925
+ );
2926
+ this.roomEventUnsubscribers.push(unsubscribe);
2927
+ liveLog("rooms", this.componentId, `[${this.componentId}] Subscribed to room event '${event}'`);
2928
+ }
2929
+ emitRoomEventWithState(event, data, stateUpdates) {
2930
+ this.setStateFn(stateUpdates);
2931
+ return this.emitRoomEvent(event, data, false);
2932
+ }
2933
+ subscribeToRoom(roomId) {
2934
+ this.room = roomId;
2935
+ }
2936
+ unsubscribeFromRoom() {
2937
+ this.room = void 0;
2938
+ }
2939
+ destroy() {
2940
+ for (const unsubscribe of this.roomEventUnsubscribers) {
2941
+ unsubscribe();
2942
+ }
2943
+ this.roomEventUnsubscribers = [];
2944
+ if (this.joinedRooms.size > 0 && this._cachedCtx) {
2945
+ for (const roomId of this.joinedRooms) {
2946
+ this._cachedCtx.roomManager.leaveRoom(this.componentId, roomId);
2947
+ }
2948
+ }
2949
+ this.joinedRooms.clear();
2950
+ this.roomHandles.clear();
2951
+ this._roomProxy = null;
2952
+ this._roomsCache = null;
2953
+ }
2954
+ };
2955
+
2956
+ // src/component/LiveComponent.ts
2957
+ var LiveComponent = class {
2958
+ /** Component name for registry lookup - must be defined in subclasses */
2959
+ static componentName;
2960
+ /** Default state - must be defined in subclasses */
2961
+ static defaultState;
2962
+ /**
2963
+ * Per-component logging control. Silent by default.
2964
+ *
2965
+ * @example
2966
+ * static logging = true // all categories
2967
+ * static logging = ['lifecycle', 'messages'] // specific categories
2968
+ */
2969
+ static logging;
2970
+ /**
2971
+ * Component-level auth configuration.
2972
+ */
2973
+ static auth;
2974
+ /**
2975
+ * Per-action auth configuration.
2976
+ */
2977
+ static actionAuth;
2978
+ /**
2979
+ * Zod schemas for action payload validation.
2980
+ * When defined, payloads are validated before the action method is called.
2981
+ *
2982
+ * @example
2983
+ * static actionSchemas = {
2984
+ * sendMessage: z.object({ text: z.string().max(500) }),
2985
+ * updatePosition: z.object({ x: z.number(), y: z.number() }),
2986
+ * }
2987
+ */
2988
+ static actionSchemas;
2989
+ /**
2990
+ * Rate limit for action execution.
2991
+ * Prevents clients from spamming expensive operations.
2992
+ *
2993
+ * @example
2994
+ * static actionRateLimit = { maxCalls: 10, windowMs: 1000, perAction: true }
2995
+ */
2996
+ static actionRateLimit;
2997
+ /**
2998
+ * Data that survives HMR reloads.
2999
+ */
3000
+ static persistent;
3001
+ /**
3002
+ * When true, only ONE server-side instance exists for this component.
3003
+ * All clients share the same state.
3004
+ */
3005
+ static singleton;
3006
+ /**
3007
+ * Component behavior options.
3008
+ *
3009
+ * @example
3010
+ * static $options = { deepDiff: true }
3011
+ */
3012
+ static $options;
3013
+ id;
3014
+ state;
3015
+ // Proxy wrapper (getter delegates to _stateManager)
3016
+ ws;
3017
+ room;
3018
+ userId;
3019
+ broadcastToRoom = () => {
3020
+ };
3021
+ // Server-only private state (NEVER sent to client)
3022
+ _privateState = {};
3023
+ // Auth context (injected by registry during mount, immutable after first set)
3024
+ _authContext = ANONYMOUS_CONTEXT;
3025
+ _authContextSet = false;
3026
+ // Room type for typed events (override in subclass)
3027
+ roomType = "default";
3028
+ // Singleton emit override
3029
+ [EMIT_OVERRIDE_KEY] = null;
3030
+ // ===== Internal Managers (composition) =====
3031
+ _stateManager;
3032
+ _messaging;
3033
+ _actionSecurity;
3034
+ _roomProxyManager;
3035
+ static publicActions;
3036
+ constructor(initialState, ws, options) {
3037
+ this.id = this.generateId();
3038
+ const ctor = this.constructor;
3039
+ this.ws = ws;
3040
+ this.room = options?.room;
3041
+ this.userId = options?.userId;
3042
+ this._messaging = new ComponentMessaging({
3043
+ componentId: this.id,
3044
+ ws: this.ws,
3045
+ getUserId: () => this.userId,
3046
+ getRoom: () => this.room,
3047
+ getBroadcastToRoom: () => this.broadcastToRoom,
3048
+ getEmitOverride: () => this[EMIT_OVERRIDE_KEY]
3049
+ });
3050
+ this._stateManager = new ComponentStateManager({
3051
+ componentId: this.id,
3052
+ initialState: { ...ctor.defaultState, ...initialState },
3053
+ ws: this.ws,
3054
+ emitFn: (type, payload) => this._messaging.emit(type, payload),
3055
+ onStateChangeFn: (changes) => this.onStateChange(changes),
3056
+ deepDiff: ctor.$options?.deepDiff ?? false,
3057
+ deepDiffDepth: ctor.$options?.deepDiffDepth
3058
+ });
3059
+ this.state = this._stateManager.proxyState;
3060
+ this._actionSecurity = new ActionSecurityManager();
3061
+ this._roomProxyManager = new ComponentRoomProxy({
3062
+ componentId: this.id,
3063
+ ws: this.ws,
3064
+ defaultRoom: this.room,
3065
+ getCtx: () => getLiveComponentContext(),
3066
+ setStateFn: (updates) => this.setState(updates),
3067
+ deepDiff: ctor.$options?.roomDeepDiff,
3068
+ deepDiffDepth: ctor.$options?.deepDiffDepth,
3069
+ serverOnlyState: ctor.$options?.serverOnlyRoomState
3070
+ });
3071
+ this._stateManager.applyDirectAccessors(this, this.constructor);
3072
+ }
3073
+ // ========================================
3074
+ // $private - Server-Only State
3075
+ // ========================================
3076
+ get $private() {
3077
+ return this._privateState;
3078
+ }
3079
+ // ========================================
3080
+ // $room - Unified Room System
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
+ */
3090
+ get $room() {
3091
+ return this._roomProxyManager.$room;
1954
3092
  }
1955
3093
  /**
1956
- * List of room IDs this component is participating in
3094
+ * List of room IDs this component is participating in.
3095
+ * Cached — invalidated on join/leave.
1957
3096
  */
1958
3097
  get $rooms() {
1959
- return Array.from(this.joinedRooms);
3098
+ return this._roomProxyManager.$rooms;
1960
3099
  }
1961
3100
  // ========================================
1962
3101
  // $auth - Authentication Context
@@ -1964,13 +3103,22 @@ var LiveComponent = class _LiveComponent {
1964
3103
  get $auth() {
1965
3104
  return this._authContext;
1966
3105
  }
1967
- /** @internal */
3106
+ /** @internal - Immutable after first set to prevent privilege escalation */
1968
3107
  setAuthContext(context) {
3108
+ if (this._authContextSet) {
3109
+ throw new Error("Auth context is immutable after initial set");
3110
+ }
1969
3111
  this._authContext = context;
3112
+ this._authContextSet = true;
1970
3113
  if (context.authenticated && context.user?.id && !this.userId) {
1971
3114
  this.userId = context.user.id;
1972
3115
  }
1973
3116
  }
3117
+ /** @internal - Reset auth context (for registry use in reconnection) */
3118
+ _resetAuthContext() {
3119
+ this._authContextSet = false;
3120
+ this._authContext = ANONYMOUS_CONTEXT;
3121
+ }
1974
3122
  // ========================================
1975
3123
  // $persistent - HMR-Safe State
1976
3124
  // ========================================
@@ -2006,224 +3154,64 @@ var LiveComponent = class _LiveComponent {
2006
3154
  }
2007
3155
  onClientJoin(connectionId, connectionCount) {
2008
3156
  }
2009
- onClientLeave(connectionId, connectionCount) {
2010
- }
2011
- // ========================================
2012
- // State Management
2013
- // ========================================
2014
- setState(updates) {
2015
- const newUpdates = typeof updates === "function" ? updates(this._state) : updates;
2016
- const actualChanges = {};
2017
- let hasChanges = false;
2018
- for (const key of Object.keys(newUpdates)) {
2019
- if (this._state[key] !== newUpdates[key]) {
2020
- actualChanges[key] = newUpdates[key];
2021
- hasChanges = true;
2022
- }
2023
- }
2024
- if (!hasChanges) return;
2025
- Object.assign(this._state, actualChanges);
2026
- this.emit("STATE_DELTA", { delta: actualChanges });
2027
- if (!this._inStateChange) {
2028
- this._inStateChange = true;
2029
- try {
2030
- this.onStateChange(actualChanges);
2031
- } catch (err) {
2032
- console.error(`[${this.id}] onStateChange error:`, err?.message || err);
2033
- } finally {
2034
- this._inStateChange = false;
2035
- }
2036
- }
2037
- _liveDebugger?.trackStateChange(
2038
- this.id,
2039
- actualChanges,
2040
- this._state,
2041
- "setState"
2042
- );
2043
- }
2044
- async setValue(payload) {
2045
- const { key, value } = payload;
2046
- const update = { [key]: value };
2047
- this.setState(update);
2048
- return { success: true, key, value };
2049
- }
2050
- // ========================================
2051
- // Action Security
2052
- // ========================================
2053
- static publicActions;
2054
- static BLOCKED_ACTIONS = /* @__PURE__ */ new Set([
2055
- "constructor",
2056
- "destroy",
2057
- "executeAction",
2058
- "getSerializableState",
2059
- "onMount",
2060
- "onDestroy",
2061
- "onConnect",
2062
- "onDisconnect",
2063
- "onStateChange",
2064
- "onRoomJoin",
2065
- "onRoomLeave",
2066
- "onRehydrate",
2067
- "onAction",
2068
- "onClientJoin",
2069
- "onClientLeave",
2070
- "setState",
2071
- "emit",
2072
- "broadcast",
2073
- "broadcastToRoom",
2074
- "createStateProxy",
2075
- "createDirectStateAccessors",
2076
- "generateId",
2077
- "setAuthContext",
2078
- "$auth",
2079
- "$private",
2080
- "_privateState",
2081
- "$persistent",
2082
- "_inStateChange",
2083
- "$room",
2084
- "$rooms",
2085
- "subscribeToRoom",
2086
- "unsubscribeFromRoom",
2087
- "emitRoomEvent",
2088
- "onRoomEvent",
2089
- "emitRoomEventWithState"
2090
- ]);
2091
- async executeAction(action, payload) {
2092
- const actionStart = Date.now();
2093
- try {
2094
- if (_LiveComponent.BLOCKED_ACTIONS.has(action)) {
2095
- throw new Error(`Action '${action}' is not callable`);
2096
- }
2097
- if (action.startsWith("_") || action.startsWith("#")) {
2098
- throw new Error(`Action '${action}' is not callable`);
2099
- }
2100
- const componentClass = this.constructor;
2101
- const publicActions = componentClass.publicActions;
2102
- if (!publicActions) {
2103
- console.warn(`[SECURITY] Component '${componentClass.componentName || componentClass.name}' has no publicActions defined. All remote actions are blocked.`);
2104
- throw new Error(`Action '${action}' is not callable - component has no publicActions defined`);
2105
- }
2106
- if (!publicActions.includes(action)) {
2107
- const methodExists = typeof this[action] === "function";
2108
- if (methodExists) {
2109
- const name = componentClass.componentName || componentClass.name;
2110
- throw new Error(
2111
- `Action '${action}' exists on '${name}' but is not listed in publicActions. Add it to: static publicActions = [..., '${action}']`
2112
- );
2113
- }
2114
- throw new Error(`Action '${action}' is not callable`);
2115
- }
2116
- const method = this[action];
2117
- if (typeof method !== "function") {
2118
- throw new Error(`Action '${action}' not found on component`);
2119
- }
2120
- if (Object.prototype.hasOwnProperty.call(Object.prototype, action)) {
2121
- throw new Error(`Action '${action}' is not callable`);
2122
- }
2123
- _liveDebugger?.trackActionCall(this.id, action, payload);
2124
- let hookResult;
2125
- try {
2126
- hookResult = await this.onAction(action, payload);
2127
- } catch (hookError) {
2128
- _liveDebugger?.trackActionError(this.id, action, hookError.message, Date.now() - actionStart);
2129
- this.emit("ERROR", {
2130
- action,
2131
- error: `Action '${action}' failed pre-validation`
2132
- });
2133
- throw hookError;
2134
- }
2135
- if (hookResult === false) {
2136
- _liveDebugger?.trackActionError(this.id, action, "Action cancelled", Date.now() - actionStart);
2137
- throw new Error(`Action '${action}' was cancelled`);
2138
- }
2139
- const result = await method.call(this, payload);
2140
- _liveDebugger?.trackActionResult(this.id, action, result, Date.now() - actionStart);
2141
- return result;
2142
- } catch (error) {
2143
- if (!error.message?.includes("was cancelled") && !error.message?.includes("pre-validation")) {
2144
- _liveDebugger?.trackActionError(this.id, action, error.message, Date.now() - actionStart);
2145
- this.emit("ERROR", {
2146
- action,
2147
- error: error.message
2148
- });
2149
- }
2150
- throw error;
2151
- }
3157
+ onClientLeave(connectionId, connectionCount) {
2152
3158
  }
2153
3159
  // ========================================
2154
- // Messaging
3160
+ // State Management (delegates to _stateManager)
2155
3161
  // ========================================
2156
- emit(type, payload) {
2157
- const override = this[EMIT_OVERRIDE_KEY];
2158
- if (override) {
2159
- override(type, payload);
2160
- return;
2161
- }
2162
- const message = {
2163
- type,
3162
+ setState(updates) {
3163
+ this._stateManager.setState(updates);
3164
+ }
3165
+ /**
3166
+ * Send a binary-encoded state delta directly over WebSocket.
3167
+ * Updates internal state (same as setState) then sends the encoder's output
3168
+ * as a binary frame: [0x01][idLen:u8][id_bytes:utf8][payload_bytes].
3169
+ * Bypasses the JSON batcher — ideal for high-frequency updates.
3170
+ */
3171
+ sendBinaryDelta(delta, encoder2) {
3172
+ this._stateManager.sendBinaryDelta(delta, encoder2);
3173
+ }
3174
+ setValue(payload) {
3175
+ return this._stateManager.setValue(payload);
3176
+ }
3177
+ // ========================================
3178
+ // Action Execution (delegates to _actionSecurity)
3179
+ // ========================================
3180
+ async executeAction(action, payload) {
3181
+ return this._actionSecurity.validateAndExecute(action, payload, {
3182
+ component: this,
3183
+ componentClass: this.constructor,
2164
3184
  componentId: this.id,
2165
- payload,
2166
- timestamp: Date.now(),
2167
- userId: this.userId,
2168
- room: this.room
2169
- };
2170
- if (this.ws && this.ws.send) {
2171
- this.ws.send(JSON.stringify(message));
2172
- }
3185
+ emitFn: (type, p) => this.emit(type, p)
3186
+ });
3187
+ }
3188
+ // ========================================
3189
+ // Messaging (delegates to _messaging)
3190
+ // ========================================
3191
+ emit(type, payload) {
3192
+ this._messaging.emit(type, payload);
2173
3193
  }
2174
3194
  broadcast(type, payload, excludeCurrentUser = false) {
2175
- if (!this.room) {
2176
- liveWarn("rooms", this.id, `[${this.id}] Cannot broadcast '${type}' - no room set`);
2177
- return;
2178
- }
2179
- const message = {
2180
- type,
2181
- payload,
2182
- room: this.room,
2183
- excludeUser: excludeCurrentUser ? this.userId : void 0
2184
- };
2185
- liveLog("rooms", this.id, `[${this.id}] Broadcasting '${type}' to room '${this.room}'`);
2186
- this.broadcastToRoom(message);
3195
+ this._messaging.broadcast(type, payload, excludeCurrentUser);
2187
3196
  }
2188
3197
  // ========================================
2189
- // Room Events - Internal Server Events
3198
+ // Room Events (delegates to _roomProxyManager)
2190
3199
  // ========================================
2191
3200
  emitRoomEvent(event, data, notifySelf = false) {
2192
- if (!this.room) {
2193
- liveWarn("rooms", this.id, `[${this.id}] Cannot emit room event '${event}' - no room set`);
2194
- return 0;
2195
- }
2196
- const ctx = getLiveComponentContext();
2197
- const excludeId = notifySelf ? void 0 : this.id;
2198
- const notified = ctx.roomEvents.emit(this.roomType, this.room, event, data, excludeId);
2199
- liveLog("rooms", this.id, `[${this.id}] Room event '${event}' -> ${notified} components`);
2200
- _liveDebugger?.trackRoomEmit(this.id, this.room, event, data);
2201
- return notified;
3201
+ return this._roomProxyManager.emitRoomEvent(event, data, notifySelf);
2202
3202
  }
2203
3203
  onRoomEvent(event, handler) {
2204
- if (!this.room) {
2205
- liveWarn("rooms", this.id, `[${this.id}] Cannot subscribe to room event '${event}' - no room set`);
2206
- return;
2207
- }
2208
- const ctx = getLiveComponentContext();
2209
- const unsubscribe = ctx.roomEvents.on(
2210
- this.roomType,
2211
- this.room,
2212
- event,
2213
- this.id,
2214
- handler
2215
- );
2216
- this.roomEventUnsubscribers.push(unsubscribe);
2217
- liveLog("rooms", this.id, `[${this.id}] Subscribed to room event '${event}'`);
3204
+ this._roomProxyManager.onRoomEvent(event, handler);
2218
3205
  }
2219
3206
  emitRoomEventWithState(event, data, stateUpdates) {
2220
- this.setState(stateUpdates);
2221
- return this.emitRoomEvent(event, data, false);
3207
+ return this._roomProxyManager.emitRoomEventWithState(event, data, stateUpdates);
2222
3208
  }
2223
- async subscribeToRoom(roomId) {
3209
+ subscribeToRoom(roomId) {
3210
+ this._roomProxyManager.subscribeToRoom(roomId);
2224
3211
  this.room = roomId;
2225
3212
  }
2226
- async unsubscribeFromRoom() {
3213
+ unsubscribeFromRoom() {
3214
+ this._roomProxyManager.unsubscribeFromRoom();
2227
3215
  this.room = void 0;
2228
3216
  }
2229
3217
  // ========================================
@@ -2238,21 +3226,13 @@ var LiveComponent = class _LiveComponent {
2238
3226
  } catch (err) {
2239
3227
  console.error(`[${this.id}] onDestroy error:`, err?.message || err);
2240
3228
  }
2241
- for (const unsubscribe of this.roomEventUnsubscribers) {
2242
- unsubscribe();
2243
- }
2244
- this.roomEventUnsubscribers = [];
2245
- const ctx = getLiveComponentContext();
2246
- for (const roomId of this.joinedRooms) {
2247
- ctx.roomManager.leaveRoom(this.id, roomId);
2248
- }
2249
- this.joinedRooms.clear();
2250
- this.roomHandles.clear();
3229
+ this._roomProxyManager.destroy();
3230
+ this._stateManager.cleanup();
2251
3231
  this._privateState = {};
2252
- this.unsubscribeFromRoom();
3232
+ this.room = void 0;
2253
3233
  }
2254
3234
  getSerializableState() {
2255
- return this.state;
3235
+ return this._stateManager.getSerializableState();
2256
3236
  }
2257
3237
  };
2258
3238
 
@@ -2266,17 +3246,80 @@ var ComponentRegistry = class {
2266
3246
  autoDiscoveredComponents = /* @__PURE__ */ new Map();
2267
3247
  healthCheckInterval;
2268
3248
  singletons = /* @__PURE__ */ new Map();
3249
+ remoteSingletons = /* @__PURE__ */ new Map();
3250
+ cluster;
2269
3251
  authManager;
2270
- debugger;
2271
3252
  stateSignature;
2272
3253
  performanceMonitor;
2273
3254
  constructor(deps) {
2274
3255
  this.authManager = deps.authManager;
2275
- this.debugger = deps.debugger;
2276
3256
  this.stateSignature = deps.stateSignature;
2277
3257
  this.performanceMonitor = deps.performanceMonitor;
2278
- _setLiveDebugger(deps.debugger);
3258
+ this.cluster = deps.cluster;
2279
3259
  this.setupHealthMonitoring();
3260
+ this.setupClusterHandlers();
3261
+ }
3262
+ /** Set up handlers for incoming cluster messages (deltas, forwarded actions). */
3263
+ setupClusterHandlers() {
3264
+ if (!this.cluster) return;
3265
+ this.cluster.onDelta((componentId, componentName, delta, sourceInstanceId) => {
3266
+ const remote = this.remoteSingletons.get(componentName);
3267
+ if (!remote || remote.componentId !== componentId) return;
3268
+ if (delta && remote.lastState) {
3269
+ Object.assign(remote.lastState, delta);
3270
+ }
3271
+ const message = JSON.stringify({
3272
+ type: "STATE_DELTA",
3273
+ componentId,
3274
+ payload: { delta },
3275
+ timestamp: Date.now()
3276
+ });
3277
+ const dead = [];
3278
+ for (const [connId, ws] of remote.connections) {
3279
+ if (ws.readyState === 1) {
3280
+ try {
3281
+ ws.send(message);
3282
+ } catch {
3283
+ dead.push(connId);
3284
+ }
3285
+ } else {
3286
+ dead.push(connId);
3287
+ }
3288
+ }
3289
+ for (const connId of dead) remote.connections.delete(connId);
3290
+ });
3291
+ this.cluster.onOwnershipLost((componentName) => {
3292
+ const singleton = this.singletons.get(componentName);
3293
+ if (!singleton) return;
3294
+ this.cluster.saveSingletonState(componentName, singleton.instance.getSerializableState()).catch(() => {
3295
+ });
3296
+ const errorMsg = JSON.stringify({
3297
+ type: "ERROR",
3298
+ componentId: singleton.instance.id,
3299
+ payload: { error: "OWNERSHIP_LOST: singleton moved to another server" },
3300
+ timestamp: Date.now()
3301
+ });
3302
+ for (const [, ws] of singleton.connections) {
3303
+ try {
3304
+ ws.send(errorMsg);
3305
+ } catch {
3306
+ }
3307
+ }
3308
+ this.cleanupComponent(singleton.instance.id);
3309
+ this.singletons.delete(componentName);
3310
+ });
3311
+ this.cluster.onActionForward(async (request) => {
3312
+ try {
3313
+ const stillOwner = await this.cluster.verifySingletonOwnership(request.componentName);
3314
+ if (!stillOwner) {
3315
+ return { success: false, error: "OWNERSHIP_LOST: this instance no longer owns the singleton", requestId: request.requestId };
3316
+ }
3317
+ const result = await this.executeAction(request.componentId, request.action, request.payload);
3318
+ return { success: true, result, requestId: request.requestId };
3319
+ } catch (error) {
3320
+ return { success: false, error: error.message, requestId: request.requestId };
3321
+ }
3322
+ });
2280
3323
  }
2281
3324
  setupHealthMonitoring() {
2282
3325
  this.healthCheckInterval = setInterval(() => this.performHealthChecks(), 3e4);
@@ -2360,6 +3403,7 @@ var ComponentRegistry = class {
2360
3403
  const authResult = this.authManager.authorizeComponent(authContext, componentAuth);
2361
3404
  if (!authResult.allowed) throw new Error(`AUTH_DENIED: ${authResult.reason}`);
2362
3405
  const isSingleton = ComponentClass.singleton === true;
3406
+ let clusterSingletonId = null;
2363
3407
  if (isSingleton) {
2364
3408
  const existing = this.singletons.get(componentName);
2365
3409
  if (existing) {
@@ -2367,11 +3411,11 @@ var ComponentRegistry = class {
2367
3411
  existing.connections.set(connId, ws);
2368
3412
  this.ensureWsData(ws, options?.userId);
2369
3413
  ws.data.components.set(existing.instance.id, existing.instance);
2370
- const signedState2 = await this.stateSignature.signState(existing.instance.id, {
3414
+ const signedState2 = this.stateSignature.signState(existing.instance.id, {
2371
3415
  ...existing.instance.getSerializableState(),
2372
3416
  __componentName: componentName
2373
3417
  }, 1, { compress: true, backup: true });
2374
- ws.send(JSON.stringify({
3418
+ sendImmediate(ws, JSON.stringify({
2375
3419
  type: "STATE_UPDATE",
2376
3420
  componentId: existing.instance.id,
2377
3421
  payload: { state: existing.instance.getSerializableState(), signedState: signedState2 },
@@ -2383,9 +3427,57 @@ var ComponentRegistry = class {
2383
3427
  }
2384
3428
  return { componentId: existing.instance.id, initialState: existing.instance.getSerializableState(), signedState: signedState2 };
2385
3429
  }
3430
+ const existingRemote = this.remoteSingletons.get(componentName);
3431
+ if (existingRemote) {
3432
+ const connId = ws.data?.connectionId || `ws-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3433
+ this.ensureWsData(ws, options?.userId);
3434
+ existingRemote.connections.set(connId, ws);
3435
+ sendImmediate(ws, JSON.stringify({
3436
+ type: "STATE_UPDATE",
3437
+ componentId: existingRemote.componentId,
3438
+ payload: { state: existingRemote.lastState },
3439
+ timestamp: Date.now()
3440
+ }));
3441
+ return { componentId: existingRemote.componentId, initialState: existingRemote.lastState, signedState: null };
3442
+ }
3443
+ if (this.cluster) {
3444
+ clusterSingletonId = `live-${crypto.randomUUID()}`;
3445
+ const claim = await this.cluster.claimSingleton(componentName, clusterSingletonId);
3446
+ if (!claim.claimed) {
3447
+ clusterSingletonId = null;
3448
+ const owner = await this.cluster.getSingletonOwner(componentName);
3449
+ if (owner) {
3450
+ const stored = await this.cluster.loadState(owner.componentId);
3451
+ const connId = ws.data?.connectionId || `ws-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3452
+ this.ensureWsData(ws, options?.userId);
3453
+ const remote = {
3454
+ componentName,
3455
+ componentId: owner.componentId,
3456
+ ownerInstanceId: owner.instanceId,
3457
+ lastState: stored?.state || {},
3458
+ connections: /* @__PURE__ */ new Map([[connId, ws]])
3459
+ };
3460
+ this.remoteSingletons.set(componentName, remote);
3461
+ sendImmediate(ws, JSON.stringify({
3462
+ type: "STATE_UPDATE",
3463
+ componentId: owner.componentId,
3464
+ payload: { state: remote.lastState },
3465
+ timestamp: Date.now()
3466
+ }));
3467
+ return { componentId: owner.componentId, initialState: remote.lastState, signedState: null };
3468
+ }
3469
+ }
3470
+ if (claim.recoveredState) {
3471
+ props = { ...props, ...claim.recoveredState };
3472
+ }
3473
+ }
2386
3474
  }
2387
3475
  const component = new ComponentClass({ ...initialState, ...props }, ws, options);
2388
3476
  component.setAuthContext(authContext);
3477
+ if (clusterSingletonId) {
3478
+ ;
3479
+ component.id = clusterSingletonId;
3480
+ }
2389
3481
  component.broadcastToRoom = (message) => {
2390
3482
  this.broadcastToRoom(message, component.id);
2391
3483
  };
@@ -2401,6 +3493,13 @@ var ComponentRegistry = class {
2401
3493
  const connections = /* @__PURE__ */ new Map();
2402
3494
  connections.set(connId, ws);
2403
3495
  this.singletons.set(componentName, { instance: component, connections });
3496
+ if (this.cluster) {
3497
+ this.cluster.saveState(component.id, componentName, component.getSerializableState()).catch(() => {
3498
+ });
3499
+ this.cluster.saveSingletonState(componentName, component.getSerializableState()).catch(() => {
3500
+ });
3501
+ }
3502
+ ;
2404
3503
  component[EMIT_OVERRIDE_KEY] = (type, payload) => {
2405
3504
  const message = {
2406
3505
  type,
@@ -2423,6 +3522,14 @@ var ComponentRegistry = class {
2423
3522
  }
2424
3523
  for (const cId of dead) singleton.connections.delete(cId);
2425
3524
  }
3525
+ if (this.cluster && type === "STATE_DELTA" && payload?.delta) {
3526
+ this.cluster.publishDelta(component.id, componentName, payload.delta).catch(() => {
3527
+ });
3528
+ this.cluster.saveState(component.id, componentName, component.getSerializableState()).catch(() => {
3529
+ });
3530
+ this.cluster.saveSingletonState(componentName, component.getSerializableState()).catch(() => {
3531
+ });
3532
+ }
2426
3533
  };
2427
3534
  try {
2428
3535
  component.onClientJoin(connId, 1);
@@ -2435,7 +3542,7 @@ var ComponentRegistry = class {
2435
3542
  registerComponentLogging(component.id, ComponentClass.logging);
2436
3543
  this.performanceMonitor.initializeComponent(component.id, componentName);
2437
3544
  this.performanceMonitor.recordRenderTime(component.id, renderTime);
2438
- const signedState = await this.stateSignature.signState(component.id, {
3545
+ const signedState = this.stateSignature.signState(component.id, {
2439
3546
  ...component.getSerializableState(),
2440
3547
  __componentName: componentName
2441
3548
  }, 1, { compress: true, backup: true });
@@ -2453,13 +3560,6 @@ var ComponentRegistry = class {
2453
3560
  ;
2454
3561
  component.emit("ERROR", { action: "onMount", error: `Mount initialization failed: ${err?.message || err}` });
2455
3562
  }
2456
- this.debugger.trackComponentMount(
2457
- component.id,
2458
- componentName,
2459
- component.getSerializableState(),
2460
- options?.room,
2461
- options?.debugLabel
2462
- );
2463
3563
  return { componentId: component.id, initialState: component.getSerializableState(), signedState };
2464
3564
  } catch (error) {
2465
3565
  console.error(`Failed to mount component ${componentName}:`, error);
@@ -2468,7 +3568,7 @@ var ComponentRegistry = class {
2468
3568
  }
2469
3569
  async rehydrateComponent(componentId, componentName, signedState, ws, options) {
2470
3570
  try {
2471
- const validation = await this.stateSignature.validateState(signedState);
3571
+ const validation = this.stateSignature.validateState(signedState, { skipNonce: true });
2472
3572
  if (!validation.valid) return { success: false, error: validation.error || "Invalid state signature" };
2473
3573
  const definition = this.definitions.get(componentName);
2474
3574
  let ComponentClass = null;
@@ -2491,8 +3591,8 @@ var ComponentRegistry = class {
2491
3591
  const componentAuth = ComponentClass.auth;
2492
3592
  const authResult = this.authManager.authorizeComponent(authContext, componentAuth);
2493
3593
  if (!authResult.allowed) return { success: false, error: `AUTH_DENIED: ${authResult.reason}` };
2494
- const clientState = await this.stateSignature.extractData(signedState);
2495
- if (clientState.__componentName && clientState.__componentName !== componentName) {
3594
+ const clientState = this.stateSignature.extractData(signedState);
3595
+ if (!clientState.__componentName || clientState.__componentName !== componentName) {
2496
3596
  return { success: false, error: "Component class mismatch - state tampering detected" };
2497
3597
  }
2498
3598
  const { __componentName, ...cleanState } = clientState;
@@ -2505,7 +3605,7 @@ var ComponentRegistry = class {
2505
3605
  this.ensureWsData(ws, options?.userId);
2506
3606
  ws.data.components.set(component.id, component);
2507
3607
  registerComponentLogging(component.id, ComponentClass.logging);
2508
- const newSignedState = await this.stateSignature.signState(
3608
+ const newSignedState = this.stateSignature.signState(
2509
3609
  component.id,
2510
3610
  { ...component.getSerializableState(), __componentName: componentName },
2511
3611
  signedState.version + 1
@@ -2554,20 +3654,39 @@ var ComponentRegistry = class {
2554
3654
  if (singleton.instance.id !== componentId) continue;
2555
3655
  if (connId) singleton.connections.delete(connId);
2556
3656
  if (singleton.connections.size === 0) {
3657
+ const finalState = singleton.instance.getSerializableState();
2557
3658
  try {
2558
3659
  singleton.instance.onDisconnect();
2559
3660
  } catch {
2560
3661
  }
2561
3662
  this.cleanupComponent(componentId);
2562
3663
  this.singletons.delete(name);
3664
+ if (this.cluster) {
3665
+ this.cluster.saveSingletonState(name, finalState).then(() => this.cluster.releaseSingleton(name)).then(() => this.cluster.deleteState(componentId)).catch(() => {
3666
+ });
3667
+ }
3668
+ }
3669
+ return true;
3670
+ }
3671
+ for (const [name, remote] of this.remoteSingletons) {
3672
+ if (remote.componentId !== componentId) continue;
3673
+ if (connId) remote.connections.delete(connId);
3674
+ if (remote.connections.size === 0) {
3675
+ this.remoteSingletons.delete(name);
2563
3676
  }
2564
3677
  return true;
2565
3678
  }
2566
3679
  return false;
2567
3680
  }
2568
- async unmountComponent(componentId, ws) {
3681
+ unmountComponent(componentId, ws) {
2569
3682
  const component = this.components.get(componentId);
2570
- if (!component) return;
3683
+ if (!component) {
3684
+ if (ws) {
3685
+ const connId = ws.data?.connectionId;
3686
+ this.removeSingletonConnection(componentId, connId, "unmount");
3687
+ }
3688
+ return;
3689
+ }
2571
3690
  if (ws) {
2572
3691
  const connId = ws.data?.connectionId;
2573
3692
  ws.data?.components?.delete(componentId);
@@ -2583,7 +3702,6 @@ var ComponentRegistry = class {
2583
3702
  } else {
2584
3703
  if (this.removeSingletonConnection(componentId, void 0, "unmount")) return;
2585
3704
  }
2586
- this.debugger.trackComponentUnmount(componentId);
2587
3705
  component.destroy?.();
2588
3706
  this.unsubscribeFromAllRooms(componentId);
2589
3707
  this.components.delete(componentId);
@@ -2596,6 +3714,13 @@ var ComponentRegistry = class {
2596
3714
  }
2597
3715
  return null;
2598
3716
  }
3717
+ /** Find a remote singleton entry by componentId. */
3718
+ findRemoteSingleton(componentId) {
3719
+ for (const [, entry] of this.remoteSingletons) {
3720
+ if (entry.componentId === componentId) return entry;
3721
+ }
3722
+ return null;
3723
+ }
2599
3724
  async executeAction(componentId, action, payload) {
2600
3725
  const component = this.components.get(componentId);
2601
3726
  if (!component) throw new Error(`COMPONENT_REHYDRATION_REQUIRED:${componentId}`);
@@ -2646,7 +3771,7 @@ var ComponentRegistry = class {
2646
3771
  const component = this.components.get(componentId);
2647
3772
  if (message.excludeUser && component?.userId === message.excludeUser) continue;
2648
3773
  const ws = this.wsConnections.get(componentId);
2649
- if (ws && ws.send) ws.send(JSON.stringify(broadcastMessage));
3774
+ if (ws) queueWsMessage(ws, broadcastMessage);
2650
3775
  }
2651
3776
  }
2652
3777
  async handleMessage(ws, message) {
@@ -2661,9 +3786,26 @@ var ComponentRegistry = class {
2661
3786
  });
2662
3787
  return { success: true, result: mountResult };
2663
3788
  case "COMPONENT_UNMOUNT":
2664
- await this.unmountComponent(message.componentId, ws);
3789
+ this.unmountComponent(message.componentId, ws);
2665
3790
  return { success: true };
2666
- case "CALL_ACTION":
3791
+ case "CALL_ACTION": {
3792
+ const remoteSingleton = this.findRemoteSingleton(message.componentId);
3793
+ if (remoteSingleton && this.cluster) {
3794
+ const requestId = `req-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3795
+ const request = {
3796
+ sourceInstanceId: this.cluster.instanceId,
3797
+ targetInstanceId: remoteSingleton.ownerInstanceId,
3798
+ componentId: remoteSingleton.componentId,
3799
+ componentName: remoteSingleton.componentName,
3800
+ action: message.action,
3801
+ payload: message.payload,
3802
+ requestId
3803
+ };
3804
+ const response = await this.cluster.forwardAction(request);
3805
+ if (!response.success) throw new Error(response.error || "Remote action failed");
3806
+ if (message.expectResponse) return { success: true, result: response.result };
3807
+ return null;
3808
+ }
2667
3809
  this.recordComponentMetrics(message.componentId, void 0, message.action);
2668
3810
  const actionStart = Date.now();
2669
3811
  try {
@@ -2675,6 +3817,7 @@ var ComponentRegistry = class {
2675
3817
  this.performanceMonitor.recordActionTime(message.componentId, message.action, Date.now() - actionStart, error);
2676
3818
  throw error;
2677
3819
  }
3820
+ }
2678
3821
  case "PROPERTY_UPDATE":
2679
3822
  this.updateProperty(message.componentId, message.property, message.payload.value);
2680
3823
  return { success: true };
@@ -2702,6 +3845,14 @@ var ComponentRegistry = class {
2702
3845
  this.cleanupComponent(componentId);
2703
3846
  }
2704
3847
  }
3848
+ if (connId) {
3849
+ for (const [name, remote] of this.remoteSingletons) {
3850
+ remote.connections.delete(connId);
3851
+ if (remote.connections.size === 0) {
3852
+ this.remoteSingletons.delete(name);
3853
+ }
3854
+ }
3855
+ }
2705
3856
  ws.data.components.clear();
2706
3857
  }
2707
3858
  getStats() {
@@ -2713,6 +3864,9 @@ var ComponentRegistry = class {
2713
3864
  singletons: Object.fromEntries(
2714
3865
  Array.from(this.singletons.entries()).map(([name, s]) => [name, { componentId: s.instance.id, connections: s.connections.size }])
2715
3866
  ),
3867
+ remoteSingletons: Object.fromEntries(
3868
+ Array.from(this.remoteSingletons.entries()).map(([name, r]) => [name, { componentId: r.componentId, ownerInstanceId: r.ownerInstanceId, connections: r.connections.size }])
3869
+ ),
2716
3870
  roomDetails: Object.fromEntries(
2717
3871
  Array.from(this.rooms.entries()).map(([roomId, components]) => [roomId, components.size])
2718
3872
  )
@@ -2770,7 +3924,7 @@ var ComponentRegistry = class {
2770
3924
  metadata.healthStatus = metadata.metrics.errorCount > 5 ? "unhealthy" : "degraded";
2771
3925
  }
2772
3926
  }
2773
- async performHealthChecks() {
3927
+ performHealthChecks() {
2774
3928
  for (const [componentId, metadata] of this.metadata) {
2775
3929
  if (!this.components.get(componentId)) continue;
2776
3930
  if (metadata.metrics.errorCount > 10) metadata.healthStatus = "unhealthy";
@@ -2796,6 +3950,7 @@ var ComponentRegistry = class {
2796
3950
  cleanup() {
2797
3951
  if (this.healthCheckInterval) clearInterval(this.healthCheckInterval);
2798
3952
  this.singletons.clear();
3953
+ this.remoteSingletons.clear();
2799
3954
  for (const [componentId] of this.components) this.cleanupComponent(componentId);
2800
3955
  }
2801
3956
  };
@@ -2874,12 +4029,165 @@ function decodeBinaryChunk(raw) {
2874
4029
  return { header, data };
2875
4030
  }
2876
4031
 
4032
+ // src/security/sanitize.ts
4033
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
4034
+ var MAX_DEPTH = 10;
4035
+ function sanitizePayload(value, depth = 0) {
4036
+ if (depth > MAX_DEPTH) return value;
4037
+ if (Array.isArray(value)) {
4038
+ return value.map((item) => sanitizePayload(item, depth + 1));
4039
+ }
4040
+ if (value !== null && typeof value === "object") {
4041
+ const clean = {};
4042
+ for (const key of Object.keys(value)) {
4043
+ if (DANGEROUS_KEYS.has(key)) continue;
4044
+ clean[key] = sanitizePayload(value[key], depth + 1);
4045
+ }
4046
+ return clean;
4047
+ }
4048
+ return value;
4049
+ }
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
+
2877
4186
  // src/server/LiveServer.ts
2878
4187
  var LiveServer = class {
2879
4188
  // Public singletons (accessible for transport adapters & advanced usage)
2880
4189
  roomEvents;
2881
4190
  roomManager;
2882
- debugger;
2883
4191
  authManager;
2884
4192
  stateSignature;
2885
4193
  performanceMonitor;
@@ -2887,31 +4195,36 @@ var LiveServer = class {
2887
4195
  connectionManager;
2888
4196
  registry;
2889
4197
  rateLimiter;
4198
+ roomRegistry;
2890
4199
  transport;
2891
4200
  options;
2892
4201
  constructor(options) {
2893
4202
  this.options = options;
2894
4203
  this.transport = options.transport;
2895
4204
  this.roomEvents = new RoomEventBus();
2896
- this.roomManager = new LiveRoomManager(this.roomEvents);
2897
- this.debugger = new LiveDebugger(options.debug ?? false);
4205
+ this.roomManager = new LiveRoomManager(this.roomEvents, options.roomPubSub);
2898
4206
  this.authManager = new LiveAuthManager();
2899
4207
  this.stateSignature = new StateSignatureManager(options.stateSignature);
2900
4208
  this.performanceMonitor = new PerformanceMonitor(options.performance);
2901
4209
  this.fileUploadManager = new FileUploadManager(options.fileUpload);
2902
4210
  this.connectionManager = new WebSocketConnectionManager(options.connection);
2903
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
+ }
2904
4219
  this.registry = new ComponentRegistry({
2905
4220
  authManager: this.authManager,
2906
- debugger: this.debugger,
2907
4221
  stateSignature: this.stateSignature,
2908
- performanceMonitor: this.performanceMonitor
4222
+ performanceMonitor: this.performanceMonitor,
4223
+ cluster: options.cluster
2909
4224
  });
2910
- _setLoggerDebugger(this.debugger);
2911
4225
  setLiveComponentContext({
2912
4226
  roomEvents: this.roomEvents,
2913
- roomManager: this.roomManager,
2914
- debugger: this.debugger
4227
+ roomManager: this.roomManager
2915
4228
  });
2916
4229
  }
2917
4230
  /**
@@ -2921,6 +4234,14 @@ var LiveServer = class {
2921
4234
  this.authManager.register(provider);
2922
4235
  return this;
2923
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
+ }
2924
4245
  /**
2925
4246
  * Start the LiveServer: register WS + HTTP handlers on the transport.
2926
4247
  */
@@ -2940,10 +4261,13 @@ var LiveServer = class {
2940
4261
  const prefix = this.options.httpPrefix ?? "/api/live";
2941
4262
  await this.transport.registerHttpRoutes(this.buildHttpRoutes(prefix));
2942
4263
  }
4264
+ if (this.options.cluster) {
4265
+ await this.options.cluster.start();
4266
+ }
2943
4267
  if (this.transport.start) {
2944
4268
  await this.transport.start();
2945
4269
  }
2946
- liveLog("lifecycle", null, `LiveServer started (ws: ${wsConfig.path})`);
4270
+ liveLog("lifecycle", null, `LiveServer started (ws: ${wsConfig.path}${this.options.cluster ? ", cluster: enabled" : ""})`);
2947
4271
  }
2948
4272
  /**
2949
4273
  * Graceful shutdown.
@@ -2953,21 +4277,31 @@ var LiveServer = class {
2953
4277
  this.connectionManager.shutdown();
2954
4278
  this.fileUploadManager.shutdown();
2955
4279
  this.stateSignature.shutdown();
4280
+ if (this.options.cluster) await this.options.cluster.shutdown();
2956
4281
  if (this.transport.shutdown) await this.transport.shutdown();
2957
4282
  liveLog("lifecycle", null, "LiveServer shut down");
2958
4283
  }
2959
4284
  // ===== WebSocket Handlers =====
2960
4285
  handleOpen(ws) {
4286
+ const origin = ws.data?.origin;
4287
+ const allowedOrigins = this.options.allowedOrigins;
4288
+ if (allowedOrigins && allowedOrigins.length > 0) {
4289
+ if (!origin || !allowedOrigins.includes(origin)) {
4290
+ liveLog("websocket", null, `Connection rejected: origin '${origin || "none"}' not in allowedOrigins`);
4291
+ ws.close(4003, "Origin not allowed");
4292
+ return;
4293
+ }
4294
+ }
2961
4295
  const connectionId = `ws-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2962
4296
  ws.data = {
2963
4297
  connectionId,
2964
4298
  components: /* @__PURE__ */ new Map(),
2965
4299
  subscriptions: /* @__PURE__ */ new Set(),
2966
- connectedAt: /* @__PURE__ */ new Date()
4300
+ connectedAt: /* @__PURE__ */ new Date(),
4301
+ origin
2967
4302
  };
2968
4303
  this.connectionManager.registerConnection(ws, connectionId);
2969
- this.debugger.trackConnection(connectionId);
2970
- ws.send(JSON.stringify({
4304
+ sendImmediate(ws, JSON.stringify({
2971
4305
  type: "CONNECTION_ESTABLISHED",
2972
4306
  connectionId,
2973
4307
  timestamp: Date.now()
@@ -2979,7 +4313,7 @@ var LiveServer = class {
2979
4313
  if (connectionId) {
2980
4314
  const limiter = this.rateLimiter.get(connectionId);
2981
4315
  if (!limiter.tryConsume()) {
2982
- ws.send(JSON.stringify({ type: "ERROR", error: "Rate limit exceeded", timestamp: Date.now() }));
4316
+ sendImmediate(ws, JSON.stringify({ type: "ERROR", error: "Rate limit exceeded", timestamp: Date.now() }));
2983
4317
  return;
2984
4318
  }
2985
4319
  }
@@ -2989,26 +4323,33 @@ var LiveServer = class {
2989
4323
  if (header.type === "FILE_UPLOAD_CHUNK") {
2990
4324
  const chunkMessage = { ...header, data: "" };
2991
4325
  const progress = await this.fileUploadManager.receiveChunk(chunkMessage, data);
2992
- if (progress) ws.send(JSON.stringify(progress));
4326
+ if (progress) sendImmediate(ws, JSON.stringify(progress));
2993
4327
  }
2994
4328
  } catch (error) {
2995
- ws.send(JSON.stringify({ type: "ERROR", error: error.message, timestamp: Date.now() }));
4329
+ sendImmediate(ws, JSON.stringify({ type: "ERROR", error: error.message, timestamp: Date.now() }));
2996
4330
  }
2997
4331
  return;
2998
4332
  }
4333
+ const str = typeof rawMessage === "string" ? rawMessage : new TextDecoder().decode(rawMessage);
4334
+ if (str.length > MAX_MESSAGE_SIZE) {
4335
+ sendImmediate(ws, JSON.stringify({ type: "ERROR", error: "Message too large", timestamp: Date.now() }));
4336
+ return;
4337
+ }
2999
4338
  let message;
3000
4339
  try {
3001
- const str = typeof rawMessage === "string" ? rawMessage : new TextDecoder().decode(rawMessage);
3002
4340
  message = JSON.parse(str);
3003
4341
  } catch {
3004
- ws.send(JSON.stringify({ type: "ERROR", error: "Invalid JSON", timestamp: Date.now() }));
4342
+ sendImmediate(ws, JSON.stringify({ type: "ERROR", error: "Invalid JSON", timestamp: Date.now() }));
3005
4343
  return;
3006
4344
  }
4345
+ if (message.payload) {
4346
+ message.payload = sanitizePayload(message.payload);
4347
+ }
3007
4348
  try {
3008
4349
  if (message.type === "AUTH") {
3009
4350
  const authContext = await this.authManager.authenticate(message.payload || {});
3010
4351
  if (ws.data) ws.data.authContext = authContext;
3011
- ws.send(JSON.stringify({
4352
+ sendImmediate(ws, JSON.stringify({
3012
4353
  type: "AUTH_RESPONSE",
3013
4354
  success: authContext.authenticated,
3014
4355
  payload: authContext.authenticated ? { userId: authContext.user?.id } : { error: "Authentication failed" },
@@ -3022,7 +4363,7 @@ var LiveServer = class {
3022
4363
  }
3023
4364
  if (message.type === "FILE_UPLOAD_START") {
3024
4365
  const result2 = await this.fileUploadManager.startUpload(message, ws.data?.userId);
3025
- ws.send(JSON.stringify({
4366
+ sendImmediate(ws, JSON.stringify({
3026
4367
  type: "FILE_UPLOAD_START_RESPONSE",
3027
4368
  componentId: message.componentId,
3028
4369
  uploadId: message.payload?.uploadId,
@@ -3035,12 +4376,12 @@ var LiveServer = class {
3035
4376
  }
3036
4377
  if (message.type === "FILE_UPLOAD_CHUNK") {
3037
4378
  const progress = await this.fileUploadManager.receiveChunk(message);
3038
- if (progress) ws.send(JSON.stringify(progress));
4379
+ if (progress) sendImmediate(ws, JSON.stringify(progress));
3039
4380
  return;
3040
4381
  }
3041
4382
  if (message.type === "FILE_UPLOAD_COMPLETE") {
3042
4383
  const result2 = await this.fileUploadManager.completeUpload(message);
3043
- ws.send(JSON.stringify(result2));
4384
+ sendImmediate(ws, JSON.stringify(result2));
3044
4385
  return;
3045
4386
  }
3046
4387
  if (message.type === "COMPONENT_REHYDRATE") {
@@ -3051,7 +4392,7 @@ var LiveServer = class {
3051
4392
  ws,
3052
4393
  { room: message.payload.room, userId: message.userId }
3053
4394
  );
3054
- ws.send(JSON.stringify({
4395
+ sendImmediate(ws, JSON.stringify({
3055
4396
  type: "COMPONENT_REHYDRATED",
3056
4397
  componentId: message.componentId,
3057
4398
  success: result2.success,
@@ -3075,10 +4416,10 @@ var LiveServer = class {
3075
4416
  responseId: message.responseId,
3076
4417
  timestamp: Date.now()
3077
4418
  };
3078
- ws.send(JSON.stringify(response));
4419
+ sendImmediate(ws, JSON.stringify(response));
3079
4420
  }
3080
4421
  } catch (error) {
3081
- ws.send(JSON.stringify({
4422
+ sendImmediate(ws, JSON.stringify({
3082
4423
  type: "ERROR",
3083
4424
  componentId: message.componentId,
3084
4425
  error: error.message,
@@ -3096,7 +4437,6 @@ var LiveServer = class {
3096
4437
  this.connectionManager.cleanupConnection(connectionId);
3097
4438
  this.rateLimiter.remove(connectionId);
3098
4439
  }
3099
- this.debugger.trackDisconnection(connectionId || "", componentCount);
3100
4440
  liveLog("websocket", null, `Connection closed: ${connectionId} (${componentCount} components)`);
3101
4441
  }
3102
4442
  handleError(ws, error) {
@@ -3108,8 +4448,58 @@ var LiveServer = class {
3108
4448
  const roomId = message.roomId || message.payload?.roomId;
3109
4449
  switch (message.type) {
3110
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
+ }
4461
+ const connRooms = ws.data?.rooms;
4462
+ if (connRooms && connRooms.size >= MAX_ROOMS_PER_CONNECTION) {
4463
+ sendImmediate(ws, JSON.stringify({
4464
+ type: "ERROR",
4465
+ componentId,
4466
+ error: "Room limit exceeded",
4467
+ requestId: message.requestId,
4468
+ timestamp: Date.now()
4469
+ }));
4470
+ break;
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
+ }
3111
4489
  const result = this.roomManager.joinRoom(componentId, roomId, ws, message.payload?.initialState);
3112
- ws.send(JSON.stringify({
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
+ }
4500
+ if (!ws.data.rooms) ws.data.rooms = /* @__PURE__ */ new Set();
4501
+ ws.data.rooms.add(roomId);
4502
+ sendImmediate(ws, JSON.stringify({
3113
4503
  type: "ROOM_JOINED",
3114
4504
  componentId,
3115
4505
  payload: { roomId, state: result.state },
@@ -3120,7 +4510,8 @@ var LiveServer = class {
3120
4510
  }
3121
4511
  case "ROOM_LEAVE":
3122
4512
  this.roomManager.leaveRoom(componentId, roomId);
3123
- ws.send(JSON.stringify({
4513
+ ws.data?.rooms?.delete(roomId);
4514
+ sendImmediate(ws, JSON.stringify({
3124
4515
  type: "ROOM_LEFT",
3125
4516
  componentId,
3126
4517
  payload: { roomId },
@@ -3128,15 +4519,57 @@ var LiveServer = class {
3128
4519
  timestamp: Date.now()
3129
4520
  }));
3130
4521
  break;
3131
- 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
+ }
3132
4533
  this.roomManager.emitToRoom(roomId, message.payload?.event, message.payload?.data, componentId);
3133
4534
  break;
3134
- 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
+ }
3135
4557
  this.roomManager.setRoomState(roomId, message.payload?.state, componentId);
3136
4558
  break;
4559
+ }
3137
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
+ }
3138
4571
  const state = this.roomManager.getRoomState(roomId);
3139
- ws.send(JSON.stringify({
4572
+ sendImmediate(ws, JSON.stringify({
3140
4573
  type: "ROOM_STATE",
3141
4574
  componentId,
3142
4575
  payload: { roomId, state },
@@ -3153,7 +4586,7 @@ var LiveServer = class {
3153
4586
  {
3154
4587
  method: "GET",
3155
4588
  path: `${prefix}/stats`,
3156
- handler: async () => ({
4589
+ handler: () => ({
3157
4590
  body: {
3158
4591
  components: this.registry.getStats(),
3159
4592
  rooms: this.roomManager.getStats(),
@@ -3167,7 +4600,7 @@ var LiveServer = class {
3167
4600
  {
3168
4601
  method: "GET",
3169
4602
  path: `${prefix}/components`,
3170
- handler: async () => ({
4603
+ handler: () => ({
3171
4604
  body: { names: this.registry.getRegisteredComponentNames() }
3172
4605
  }),
3173
4606
  metadata: { summary: "List registered component names", tags: ["live"] }
@@ -3175,7 +4608,7 @@ var LiveServer = class {
3175
4608
  {
3176
4609
  method: "POST",
3177
4610
  path: `${prefix}/rooms/:roomId/messages`,
3178
- handler: async (req) => {
4611
+ handler: (req) => {
3179
4612
  const roomId = req.params.roomId;
3180
4613
  this.roomManager.emitToRoom(roomId, "message:new", req.body);
3181
4614
  return { body: { success: true, roomId } };
@@ -3185,7 +4618,7 @@ var LiveServer = class {
3185
4618
  {
3186
4619
  method: "POST",
3187
4620
  path: `${prefix}/rooms/:roomId/emit`,
3188
- handler: async (req) => {
4621
+ handler: (req) => {
3189
4622
  const roomId = req.params.roomId;
3190
4623
  const { event, data } = req.body;
3191
4624
  this.roomManager.emitToRoom(roomId, event, data);
@@ -3329,6 +4762,66 @@ var RoomStateManager = class {
3329
4762
  }
3330
4763
  };
3331
4764
 
3332
- export { ANONYMOUS_CONTEXT, AnonymousContext, AuthenticatedContext, ComponentRegistry, ConnectionRateLimiter, DEFAULT_CHUNK_SIZE, DEFAULT_WS_PATH, FileUploadManager, LiveAuthManager, LiveComponent, LiveDebugger, LiveRoomManager, LiveServer, PROTOCOL_VERSION, PerformanceMonitor, RateLimiterRegistry, RoomEventBus, RoomStateManager, StateSignatureManager, WebSocketConnectionManager, createTypedRoomEventBus, createTypedRoomState, decodeBinaryChunk, encodeBinaryChunk, getLiveComponentContext, liveLog, liveWarn, registerComponentLogging, setLiveComponentContext, unregisterComponentLogging };
4765
+ // src/rooms/InMemoryRoomAdapter.ts
4766
+ var InMemoryRoomAdapter = class {
4767
+ rooms = /* @__PURE__ */ new Map();
4768
+ // ===== IRoomStorageAdapter =====
4769
+ async getOrCreateRoom(roomId, initialState) {
4770
+ const existing = this.rooms.get(roomId);
4771
+ if (existing) {
4772
+ return { state: existing.state, created: false };
4773
+ }
4774
+ const now = Date.now();
4775
+ const data = {
4776
+ state: initialState ?? {},
4777
+ createdAt: now,
4778
+ lastUpdate: now
4779
+ };
4780
+ this.rooms.set(roomId, data);
4781
+ return { state: data.state, created: true };
4782
+ }
4783
+ async getState(roomId) {
4784
+ return this.rooms.get(roomId)?.state ?? {};
4785
+ }
4786
+ async updateState(roomId, updates) {
4787
+ const room = this.rooms.get(roomId);
4788
+ if (room) {
4789
+ Object.assign(room.state, updates);
4790
+ room.lastUpdate = Date.now();
4791
+ }
4792
+ }
4793
+ async hasRoom(roomId) {
4794
+ return this.rooms.has(roomId);
4795
+ }
4796
+ async deleteRoom(roomId) {
4797
+ return this.rooms.delete(roomId);
4798
+ }
4799
+ async getStats() {
4800
+ const rooms = {};
4801
+ for (const [id, data] of this.rooms) {
4802
+ rooms[id] = {
4803
+ createdAt: data.createdAt,
4804
+ lastUpdate: data.lastUpdate,
4805
+ stateKeys: Object.keys(data.state)
4806
+ };
4807
+ }
4808
+ return { totalRooms: this.rooms.size, rooms };
4809
+ }
4810
+ // ===== IRoomPubSubAdapter =====
4811
+ // No-ops for single-instance: all events are already propagated locally
4812
+ // by RoomEventBus and LiveRoomManager's broadcastToRoom().
4813
+ async publish(_roomId, _event, _data) {
4814
+ }
4815
+ async subscribe(_roomId, _handler) {
4816
+ return () => {
4817
+ };
4818
+ }
4819
+ async publishMembership(_roomId, _action, _componentId) {
4820
+ }
4821
+ async publishStateChange(_roomId, _updates) {
4822
+ }
4823
+ };
4824
+
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 };
3333
4826
  //# sourceMappingURL=index.js.map
3334
4827
  //# sourceMappingURL=index.js.map