@fluxstack/live-client 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/README.md +179 -0
- package/dist/index.d.ts +126 -7
- package/dist/index.js +422 -14
- package/dist/index.js.map +1 -1
- package/dist/live-client.browser.global.js +422 -14
- package/dist/live-client.browser.global.js.map +1 -1
- package/package.json +42 -39
package/dist/index.js
CHANGED
|
@@ -6,6 +6,8 @@ var LiveConnection = class {
|
|
|
6
6
|
reconnectTimeout = null;
|
|
7
7
|
heartbeatInterval = null;
|
|
8
8
|
componentCallbacks = /* @__PURE__ */ new Map();
|
|
9
|
+
binaryCallbacks = /* @__PURE__ */ new Map();
|
|
10
|
+
roomBinaryHandlers = /* @__PURE__ */ new Set();
|
|
9
11
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
10
12
|
stateListeners = /* @__PURE__ */ new Set();
|
|
11
13
|
_state = {
|
|
@@ -86,6 +88,7 @@ var LiveConnection = class {
|
|
|
86
88
|
this.log("Connecting...", { url });
|
|
87
89
|
try {
|
|
88
90
|
const ws = new WebSocket(url);
|
|
91
|
+
ws.binaryType = "arraybuffer";
|
|
89
92
|
this.ws = ws;
|
|
90
93
|
ws.onopen = () => {
|
|
91
94
|
this.log("Connected");
|
|
@@ -94,19 +97,34 @@ var LiveConnection = class {
|
|
|
94
97
|
this.startHeartbeat();
|
|
95
98
|
};
|
|
96
99
|
ws.onmessage = (event) => {
|
|
100
|
+
if (event.data instanceof ArrayBuffer) {
|
|
101
|
+
this.handleBinaryMessage(new Uint8Array(event.data));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
97
104
|
try {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
105
|
+
const parsed = JSON.parse(event.data);
|
|
106
|
+
if (Array.isArray(parsed)) {
|
|
107
|
+
for (const msg of parsed) {
|
|
108
|
+
this.log("Received", { type: msg.type, componentId: msg.componentId });
|
|
109
|
+
this.handleMessage(msg);
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
this.log("Received", { type: parsed.type, componentId: parsed.componentId });
|
|
113
|
+
this.handleMessage(parsed);
|
|
114
|
+
}
|
|
101
115
|
} catch {
|
|
102
116
|
this.log("Failed to parse message");
|
|
103
117
|
this.setState({ error: "Failed to parse message" });
|
|
104
118
|
}
|
|
105
119
|
};
|
|
106
|
-
ws.onclose = () => {
|
|
107
|
-
this.log("Disconnected");
|
|
120
|
+
ws.onclose = (event) => {
|
|
121
|
+
this.log("Disconnected", { code: event.code, reason: event.reason });
|
|
108
122
|
this.setState({ connected: false, connecting: false, connectionId: null });
|
|
109
123
|
this.stopHeartbeat();
|
|
124
|
+
if (event.code === 4003) {
|
|
125
|
+
this.setState({ error: "Connection rejected: origin not allowed" });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
110
128
|
this.attemptReconnect();
|
|
111
129
|
};
|
|
112
130
|
ws.onerror = () => {
|
|
@@ -281,6 +299,37 @@ var LiveConnection = class {
|
|
|
281
299
|
}
|
|
282
300
|
});
|
|
283
301
|
}
|
|
302
|
+
/** Parse and route binary frames (state delta, room events, room state) */
|
|
303
|
+
handleBinaryMessage(buffer) {
|
|
304
|
+
if (buffer.length < 3) return;
|
|
305
|
+
const frameType = buffer[0];
|
|
306
|
+
if (frameType === 1) {
|
|
307
|
+
const idLen = buffer[1];
|
|
308
|
+
if (buffer.length < 2 + idLen) return;
|
|
309
|
+
const componentId = new TextDecoder().decode(buffer.subarray(2, 2 + idLen));
|
|
310
|
+
const payload = buffer.subarray(2 + idLen);
|
|
311
|
+
const callback = this.binaryCallbacks.get(componentId);
|
|
312
|
+
if (callback) callback(payload);
|
|
313
|
+
} else if (frameType === 2 || frameType === 3) {
|
|
314
|
+
for (const callback of this.roomBinaryHandlers) {
|
|
315
|
+
callback(buffer);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/** Register a handler for binary room frames (0x02 / 0x03). Returns unsubscribe. */
|
|
320
|
+
registerRoomBinaryHandler(callback) {
|
|
321
|
+
this.roomBinaryHandlers.add(callback);
|
|
322
|
+
return () => {
|
|
323
|
+
this.roomBinaryHandlers.delete(callback);
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
/** Register a binary message handler for a component */
|
|
327
|
+
registerBinaryHandler(componentId, callback) {
|
|
328
|
+
this.binaryCallbacks.set(componentId, callback);
|
|
329
|
+
return () => {
|
|
330
|
+
this.binaryCallbacks.delete(componentId);
|
|
331
|
+
};
|
|
332
|
+
}
|
|
284
333
|
/** Register a component message callback */
|
|
285
334
|
registerComponent(componentId, callback) {
|
|
286
335
|
this.log("Registering component", componentId);
|
|
@@ -316,6 +365,8 @@ var LiveConnection = class {
|
|
|
316
365
|
destroy() {
|
|
317
366
|
this.disconnect();
|
|
318
367
|
this.componentCallbacks.clear();
|
|
368
|
+
this.binaryCallbacks.clear();
|
|
369
|
+
this.roomBinaryHandlers.clear();
|
|
319
370
|
for (const [, req] of this.pendingRequests) {
|
|
320
371
|
clearTimeout(req.timeout);
|
|
321
372
|
req.reject(new Error("Connection destroyed"));
|
|
@@ -326,6 +377,25 @@ var LiveConnection = class {
|
|
|
326
377
|
};
|
|
327
378
|
|
|
328
379
|
// src/component.ts
|
|
380
|
+
function isPlainObject(v) {
|
|
381
|
+
return v !== null && typeof v === "object" && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype;
|
|
382
|
+
}
|
|
383
|
+
function deepMerge(target, source, seen) {
|
|
384
|
+
if (!seen) seen = /* @__PURE__ */ new Set();
|
|
385
|
+
if (seen.has(source)) return target;
|
|
386
|
+
seen.add(source);
|
|
387
|
+
const result = { ...target };
|
|
388
|
+
for (const key of Object.keys(source)) {
|
|
389
|
+
const newVal = source[key];
|
|
390
|
+
const oldVal = result[key];
|
|
391
|
+
if (isPlainObject(oldVal) && isPlainObject(newVal)) {
|
|
392
|
+
result[key] = deepMerge(oldVal, newVal, seen);
|
|
393
|
+
} else {
|
|
394
|
+
result[key] = newVal;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return result;
|
|
398
|
+
}
|
|
329
399
|
var LiveComponentHandle = class {
|
|
330
400
|
connection;
|
|
331
401
|
componentName;
|
|
@@ -474,6 +544,21 @@ var LiveComponentHandle = class {
|
|
|
474
544
|
}
|
|
475
545
|
return response.result;
|
|
476
546
|
}
|
|
547
|
+
/**
|
|
548
|
+
* Fire an action without waiting for a response (fire-and-forget).
|
|
549
|
+
* Useful for high-frequency operations like game input where the
|
|
550
|
+
* server doesn't need to send back a result.
|
|
551
|
+
*/
|
|
552
|
+
fire(action, payload = {}) {
|
|
553
|
+
if (!this._mounted || !this._componentId) return;
|
|
554
|
+
this.connection.sendMessage({
|
|
555
|
+
type: "CALL_ACTION",
|
|
556
|
+
componentId: this._componentId,
|
|
557
|
+
action,
|
|
558
|
+
payload,
|
|
559
|
+
expectResponse: false
|
|
560
|
+
});
|
|
561
|
+
}
|
|
477
562
|
// ── State ──
|
|
478
563
|
/**
|
|
479
564
|
* Subscribe to state changes.
|
|
@@ -486,6 +571,26 @@ var LiveComponentHandle = class {
|
|
|
486
571
|
this.stateListeners.delete(callback);
|
|
487
572
|
};
|
|
488
573
|
}
|
|
574
|
+
/**
|
|
575
|
+
* Register a binary decoder for this component.
|
|
576
|
+
* When the server sends a BINARY_STATE_DELTA frame targeting this component,
|
|
577
|
+
* the decoder converts the raw payload into a delta object which is merged into state.
|
|
578
|
+
* Returns an unsubscribe function.
|
|
579
|
+
*/
|
|
580
|
+
setBinaryDecoder(decoder) {
|
|
581
|
+
if (!this._componentId) {
|
|
582
|
+
throw new Error("Component must be mounted before setting binary decoder");
|
|
583
|
+
}
|
|
584
|
+
return this.connection.registerBinaryHandler(this._componentId, (payload) => {
|
|
585
|
+
try {
|
|
586
|
+
const delta = decoder(payload);
|
|
587
|
+
this._state = deepMerge(this._state, delta);
|
|
588
|
+
this.notifyStateChange(this._state, delta);
|
|
589
|
+
} catch (e) {
|
|
590
|
+
console.error("Binary decode error:", e);
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
}
|
|
489
594
|
/**
|
|
490
595
|
* Subscribe to errors.
|
|
491
596
|
* Returns an unsubscribe function.
|
|
@@ -502,7 +607,7 @@ var LiveComponentHandle = class {
|
|
|
502
607
|
case "STATE_UPDATE": {
|
|
503
608
|
const newState = msg.payload?.state;
|
|
504
609
|
if (newState) {
|
|
505
|
-
this._state =
|
|
610
|
+
this._state = deepMerge(this._state, newState);
|
|
506
611
|
this.notifyStateChange(this._state, null);
|
|
507
612
|
}
|
|
508
613
|
break;
|
|
@@ -510,7 +615,7 @@ var LiveComponentHandle = class {
|
|
|
510
615
|
case "STATE_DELTA": {
|
|
511
616
|
const delta = msg.payload?.delta;
|
|
512
617
|
if (delta) {
|
|
513
|
-
this._state =
|
|
618
|
+
this._state = deepMerge(this._state, delta);
|
|
514
619
|
this.notifyStateChange(this._state, delta);
|
|
515
620
|
}
|
|
516
621
|
break;
|
|
@@ -552,6 +657,180 @@ var LiveComponentHandle = class {
|
|
|
552
657
|
};
|
|
553
658
|
|
|
554
659
|
// src/rooms.ts
|
|
660
|
+
function isPlainObject2(v) {
|
|
661
|
+
return v !== null && typeof v === "object" && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype;
|
|
662
|
+
}
|
|
663
|
+
function deepMerge2(target, source, seen) {
|
|
664
|
+
if (!seen) seen = /* @__PURE__ */ new Set();
|
|
665
|
+
if (seen.has(source)) return target;
|
|
666
|
+
seen.add(source);
|
|
667
|
+
const result = { ...target };
|
|
668
|
+
for (const key of Object.keys(source)) {
|
|
669
|
+
const newVal = source[key];
|
|
670
|
+
const oldVal = result[key];
|
|
671
|
+
if (isPlainObject2(oldVal) && isPlainObject2(newVal)) {
|
|
672
|
+
result[key] = deepMerge2(oldVal, newVal, seen);
|
|
673
|
+
} else {
|
|
674
|
+
result[key] = newVal;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return result;
|
|
678
|
+
}
|
|
679
|
+
var BINARY_ROOM_EVENT = 2;
|
|
680
|
+
var BINARY_ROOM_STATE = 3;
|
|
681
|
+
var _decoder = new TextDecoder();
|
|
682
|
+
function msgpackDecode(buf) {
|
|
683
|
+
return _decodeAt(buf, 0).value;
|
|
684
|
+
}
|
|
685
|
+
function _decodeAt(buf, offset) {
|
|
686
|
+
if (offset >= buf.length) return { value: null, offset };
|
|
687
|
+
const byte = buf[offset];
|
|
688
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
689
|
+
if (byte < 128) return { value: byte, offset: offset + 1 };
|
|
690
|
+
if (byte >= 128 && byte <= 143) return _decodeMap(buf, offset + 1, byte & 15);
|
|
691
|
+
if (byte >= 144 && byte <= 159) return _decodeArr(buf, offset + 1, byte & 15);
|
|
692
|
+
if (byte >= 160 && byte <= 191) {
|
|
693
|
+
const len = byte & 31;
|
|
694
|
+
return { value: _decoder.decode(buf.subarray(offset + 1, offset + 1 + len)), offset: offset + 1 + len };
|
|
695
|
+
}
|
|
696
|
+
if (byte >= 224) return { value: byte - 256, offset: offset + 1 };
|
|
697
|
+
switch (byte) {
|
|
698
|
+
case 192:
|
|
699
|
+
return { value: null, offset: offset + 1 };
|
|
700
|
+
case 194:
|
|
701
|
+
return { value: false, offset: offset + 1 };
|
|
702
|
+
case 195:
|
|
703
|
+
return { value: true, offset: offset + 1 };
|
|
704
|
+
case 196: {
|
|
705
|
+
const l = buf[offset + 1];
|
|
706
|
+
return { value: buf.slice(offset + 2, offset + 2 + l), offset: offset + 2 + l };
|
|
707
|
+
}
|
|
708
|
+
case 197: {
|
|
709
|
+
const l = view.getUint16(offset + 1, false);
|
|
710
|
+
return { value: buf.slice(offset + 3, offset + 3 + l), offset: offset + 3 + l };
|
|
711
|
+
}
|
|
712
|
+
case 198: {
|
|
713
|
+
const l = view.getUint32(offset + 1, false);
|
|
714
|
+
return { value: buf.slice(offset + 5, offset + 5 + l), offset: offset + 5 + l };
|
|
715
|
+
}
|
|
716
|
+
case 203:
|
|
717
|
+
return { value: view.getFloat64(offset + 1, false), offset: offset + 9 };
|
|
718
|
+
case 204:
|
|
719
|
+
return { value: buf[offset + 1], offset: offset + 2 };
|
|
720
|
+
case 205:
|
|
721
|
+
return { value: view.getUint16(offset + 1, false), offset: offset + 3 };
|
|
722
|
+
case 206:
|
|
723
|
+
return { value: view.getUint32(offset + 1, false), offset: offset + 5 };
|
|
724
|
+
case 208:
|
|
725
|
+
return { value: view.getInt8(offset + 1), offset: offset + 2 };
|
|
726
|
+
case 209:
|
|
727
|
+
return { value: view.getInt16(offset + 1, false), offset: offset + 3 };
|
|
728
|
+
case 210:
|
|
729
|
+
return { value: view.getInt32(offset + 1, false), offset: offset + 5 };
|
|
730
|
+
case 217: {
|
|
731
|
+
const l = buf[offset + 1];
|
|
732
|
+
return { value: _decoder.decode(buf.subarray(offset + 2, offset + 2 + l)), offset: offset + 2 + l };
|
|
733
|
+
}
|
|
734
|
+
case 218: {
|
|
735
|
+
const l = view.getUint16(offset + 1, false);
|
|
736
|
+
return { value: _decoder.decode(buf.subarray(offset + 3, offset + 3 + l)), offset: offset + 3 + l };
|
|
737
|
+
}
|
|
738
|
+
case 219: {
|
|
739
|
+
const l = view.getUint32(offset + 1, false);
|
|
740
|
+
return { value: _decoder.decode(buf.subarray(offset + 5, offset + 5 + l)), offset: offset + 5 + l };
|
|
741
|
+
}
|
|
742
|
+
case 220:
|
|
743
|
+
return _decodeArr(buf, offset + 3, view.getUint16(offset + 1, false));
|
|
744
|
+
case 221:
|
|
745
|
+
return _decodeArr(buf, offset + 5, view.getUint32(offset + 1, false));
|
|
746
|
+
case 222:
|
|
747
|
+
return _decodeMap(buf, offset + 3, view.getUint16(offset + 1, false));
|
|
748
|
+
case 223:
|
|
749
|
+
return _decodeMap(buf, offset + 5, view.getUint32(offset + 1, false));
|
|
750
|
+
}
|
|
751
|
+
return { value: null, offset: offset + 1 };
|
|
752
|
+
}
|
|
753
|
+
function _decodeArr(buf, offset, count) {
|
|
754
|
+
const arr = new Array(count);
|
|
755
|
+
for (let i = 0; i < count; i++) {
|
|
756
|
+
const r = _decodeAt(buf, offset);
|
|
757
|
+
arr[i] = r.value;
|
|
758
|
+
offset = r.offset;
|
|
759
|
+
}
|
|
760
|
+
return { value: arr, offset };
|
|
761
|
+
}
|
|
762
|
+
function _decodeMap(buf, offset, count) {
|
|
763
|
+
const obj = {};
|
|
764
|
+
for (let i = 0; i < count; i++) {
|
|
765
|
+
const k = _decodeAt(buf, offset);
|
|
766
|
+
offset = k.offset;
|
|
767
|
+
const v = _decodeAt(buf, offset);
|
|
768
|
+
offset = v.offset;
|
|
769
|
+
obj[String(k.value)] = v.value;
|
|
770
|
+
}
|
|
771
|
+
return { value: obj, offset };
|
|
772
|
+
}
|
|
773
|
+
function parseRoomFrame(buf) {
|
|
774
|
+
if (buf.length < 6) return null;
|
|
775
|
+
let offset = 0;
|
|
776
|
+
const frameType = buf[offset++];
|
|
777
|
+
const compIdLen = buf[offset++];
|
|
778
|
+
if (offset + compIdLen > buf.length) return null;
|
|
779
|
+
const componentId = _decoder.decode(buf.subarray(offset, offset + compIdLen));
|
|
780
|
+
offset += compIdLen;
|
|
781
|
+
const roomIdLen = buf[offset++];
|
|
782
|
+
if (offset + roomIdLen > buf.length) return null;
|
|
783
|
+
const roomId = _decoder.decode(buf.subarray(offset, offset + roomIdLen));
|
|
784
|
+
offset += roomIdLen;
|
|
785
|
+
if (offset + 2 > buf.length) return null;
|
|
786
|
+
const eventLen = buf[offset] << 8 | buf[offset + 1];
|
|
787
|
+
offset += 2;
|
|
788
|
+
if (offset + eventLen > buf.length) return null;
|
|
789
|
+
const event = _decoder.decode(buf.subarray(offset, offset + eventLen));
|
|
790
|
+
offset += eventLen;
|
|
791
|
+
return { frameType, componentId, roomId, event, payload: buf.subarray(offset) };
|
|
792
|
+
}
|
|
793
|
+
var ROOM_RESERVED_KEYS = /* @__PURE__ */ new Set([
|
|
794
|
+
"id",
|
|
795
|
+
"joined",
|
|
796
|
+
"state",
|
|
797
|
+
"join",
|
|
798
|
+
"leave",
|
|
799
|
+
"emit",
|
|
800
|
+
"on",
|
|
801
|
+
"onSystem",
|
|
802
|
+
"setState",
|
|
803
|
+
"call",
|
|
804
|
+
"apply",
|
|
805
|
+
"bind",
|
|
806
|
+
"prototype",
|
|
807
|
+
"length",
|
|
808
|
+
"name",
|
|
809
|
+
"arguments",
|
|
810
|
+
"caller",
|
|
811
|
+
Symbol.toPrimitive,
|
|
812
|
+
Symbol.toStringTag,
|
|
813
|
+
Symbol.hasInstance
|
|
814
|
+
]);
|
|
815
|
+
function wrapWithStateProxy(target, getState, setStateFn) {
|
|
816
|
+
return new Proxy(target, {
|
|
817
|
+
get(obj, prop, receiver) {
|
|
818
|
+
if (ROOM_RESERVED_KEYS.has(prop) || typeof prop === "symbol") {
|
|
819
|
+
return Reflect.get(obj, prop, receiver);
|
|
820
|
+
}
|
|
821
|
+
const desc = Object.getOwnPropertyDescriptor(obj, prop);
|
|
822
|
+
if (desc) return Reflect.get(obj, prop, receiver);
|
|
823
|
+
if (prop in obj) return Reflect.get(obj, prop, receiver);
|
|
824
|
+
const st = getState();
|
|
825
|
+
return st?.[prop];
|
|
826
|
+
},
|
|
827
|
+
set(_obj, prop, value) {
|
|
828
|
+
if (typeof prop === "symbol") return false;
|
|
829
|
+
setStateFn({ [prop]: value });
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
}
|
|
555
834
|
var RoomManager = class {
|
|
556
835
|
componentId;
|
|
557
836
|
defaultRoom;
|
|
@@ -560,12 +839,29 @@ var RoomManager = class {
|
|
|
560
839
|
sendMessage;
|
|
561
840
|
sendMessageAndWait;
|
|
562
841
|
globalUnsubscribe = null;
|
|
842
|
+
binaryUnsubscribe = null;
|
|
843
|
+
onBinaryMessage = null;
|
|
844
|
+
onMessageFactory = null;
|
|
563
845
|
constructor(options) {
|
|
564
846
|
this.componentId = options.componentId;
|
|
565
847
|
this.defaultRoom = options.defaultRoom || null;
|
|
566
848
|
this.sendMessage = options.sendMessage;
|
|
567
849
|
this.sendMessageAndWait = options.sendMessageAndWait;
|
|
850
|
+
this.onBinaryMessage = options.onBinaryMessage ?? null;
|
|
851
|
+
this.onMessageFactory = options.onMessage;
|
|
568
852
|
this.globalUnsubscribe = options.onMessage((msg) => this.handleServerMessage(msg));
|
|
853
|
+
if (options.onBinaryMessage) {
|
|
854
|
+
this.binaryUnsubscribe = options.onBinaryMessage((frame) => this.handleBinaryFrame(frame));
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
/** Re-subscribe message and binary handlers (needed after destroy/remount in React Strict Mode) */
|
|
858
|
+
resubscribe() {
|
|
859
|
+
if (!this.globalUnsubscribe && this.onMessageFactory) {
|
|
860
|
+
this.globalUnsubscribe = this.onMessageFactory((msg) => this.handleServerMessage(msg));
|
|
861
|
+
}
|
|
862
|
+
if (!this.binaryUnsubscribe && this.onBinaryMessage) {
|
|
863
|
+
this.binaryUnsubscribe = this.onBinaryMessage((frame) => this.handleBinaryFrame(frame));
|
|
864
|
+
}
|
|
569
865
|
}
|
|
570
866
|
handleServerMessage(msg) {
|
|
571
867
|
if (msg.componentId !== this.componentId) return;
|
|
@@ -587,10 +883,11 @@ var RoomManager = class {
|
|
|
587
883
|
break;
|
|
588
884
|
}
|
|
589
885
|
case "ROOM_STATE": {
|
|
590
|
-
|
|
886
|
+
const stateChanges = msg.data?.state ?? msg.data;
|
|
887
|
+
room.state = deepMerge2(room.state, stateChanges);
|
|
591
888
|
const stateHandlers = room.handlers.get("$state:change");
|
|
592
889
|
if (stateHandlers) {
|
|
593
|
-
for (const handler of stateHandlers) handler(
|
|
890
|
+
for (const handler of stateHandlers) handler(stateChanges);
|
|
594
891
|
}
|
|
595
892
|
break;
|
|
596
893
|
}
|
|
@@ -603,6 +900,34 @@ var RoomManager = class {
|
|
|
603
900
|
break;
|
|
604
901
|
}
|
|
605
902
|
}
|
|
903
|
+
/** Handle binary room frames (0x02 ROOM_EVENT, 0x03 ROOM_STATE) */
|
|
904
|
+
handleBinaryFrame(frame) {
|
|
905
|
+
const parsed = parseRoomFrame(frame);
|
|
906
|
+
if (!parsed) return;
|
|
907
|
+
if (parsed.componentId !== this.componentId) return;
|
|
908
|
+
const room = this.rooms.get(parsed.roomId);
|
|
909
|
+
if (!room) return;
|
|
910
|
+
const data = msgpackDecode(parsed.payload);
|
|
911
|
+
if (parsed.frameType === BINARY_ROOM_EVENT) {
|
|
912
|
+
const handlers = room.handlers.get(parsed.event);
|
|
913
|
+
if (handlers) {
|
|
914
|
+
for (const handler of handlers) {
|
|
915
|
+
try {
|
|
916
|
+
handler(data);
|
|
917
|
+
} catch (error) {
|
|
918
|
+
console.error(`[Room:${parsed.roomId}] Handler error for '${parsed.event}':`, error);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
} else if (parsed.frameType === BINARY_ROOM_STATE) {
|
|
923
|
+
const stateChanges = data?.state ?? data;
|
|
924
|
+
room.state = deepMerge2(room.state, stateChanges);
|
|
925
|
+
const stateHandlers = room.handlers.get("$state:change");
|
|
926
|
+
if (stateHandlers) {
|
|
927
|
+
for (const handler of stateHandlers) handler(stateChanges);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
606
931
|
getOrCreateRoom(roomId) {
|
|
607
932
|
if (!this.rooms.has(roomId)) {
|
|
608
933
|
this.rooms.set(roomId, {
|
|
@@ -683,7 +1008,7 @@ var RoomManager = class {
|
|
|
683
1008
|
},
|
|
684
1009
|
setState: (updates) => {
|
|
685
1010
|
if (!this.componentId) return;
|
|
686
|
-
room.state =
|
|
1011
|
+
room.state = deepMerge2(room.state, updates);
|
|
687
1012
|
this.sendMessage({
|
|
688
1013
|
type: "ROOM_STATE_SET",
|
|
689
1014
|
componentId: this.componentId,
|
|
@@ -693,8 +1018,13 @@ var RoomManager = class {
|
|
|
693
1018
|
});
|
|
694
1019
|
}
|
|
695
1020
|
};
|
|
696
|
-
|
|
697
|
-
|
|
1021
|
+
const proxied = wrapWithStateProxy(
|
|
1022
|
+
handle,
|
|
1023
|
+
() => room.state,
|
|
1024
|
+
(updates) => handle.setState(updates)
|
|
1025
|
+
);
|
|
1026
|
+
this.handles.set(roomId, proxied);
|
|
1027
|
+
return proxied;
|
|
698
1028
|
}
|
|
699
1029
|
/** Create the $room proxy */
|
|
700
1030
|
createProxy() {
|
|
@@ -744,6 +1074,14 @@ var RoomManager = class {
|
|
|
744
1074
|
}
|
|
745
1075
|
}
|
|
746
1076
|
});
|
|
1077
|
+
if (this.defaultRoom && defaultHandle) {
|
|
1078
|
+
const room = this.getOrCreateRoom(this.defaultRoom);
|
|
1079
|
+
return wrapWithStateProxy(
|
|
1080
|
+
proxyFn,
|
|
1081
|
+
() => room.state,
|
|
1082
|
+
(updates) => defaultHandle.setState(updates)
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
747
1085
|
return proxyFn;
|
|
748
1086
|
}
|
|
749
1087
|
/** List of rooms currently joined */
|
|
@@ -758,9 +1096,12 @@ var RoomManager = class {
|
|
|
758
1096
|
setComponentId(id) {
|
|
759
1097
|
this.componentId = id;
|
|
760
1098
|
}
|
|
761
|
-
/** Cleanup */
|
|
1099
|
+
/** Cleanup — unsubscribes handlers but keeps factory refs for resubscribe() */
|
|
762
1100
|
destroy() {
|
|
763
1101
|
this.globalUnsubscribe?.();
|
|
1102
|
+
this.globalUnsubscribe = null;
|
|
1103
|
+
this.binaryUnsubscribe?.();
|
|
1104
|
+
this.binaryUnsubscribe = null;
|
|
764
1105
|
for (const [, room] of this.rooms) {
|
|
765
1106
|
room.handlers.clear();
|
|
766
1107
|
}
|
|
@@ -1133,6 +1474,70 @@ var StateValidator = class {
|
|
|
1133
1474
|
};
|
|
1134
1475
|
}
|
|
1135
1476
|
};
|
|
1477
|
+
|
|
1478
|
+
// src/index.ts
|
|
1479
|
+
var _sharedConnection = null;
|
|
1480
|
+
var _sharedConnectionUrl = null;
|
|
1481
|
+
var _statusListeners = /* @__PURE__ */ new Set();
|
|
1482
|
+
function getOrCreateConnection(url) {
|
|
1483
|
+
const resolvedUrl = url ?? `ws://${typeof location !== "undefined" ? location.host : "localhost:3000"}/api/live/ws`;
|
|
1484
|
+
if (_sharedConnection && _sharedConnectionUrl === resolvedUrl) {
|
|
1485
|
+
return _sharedConnection;
|
|
1486
|
+
}
|
|
1487
|
+
if (_sharedConnection) {
|
|
1488
|
+
_sharedConnection.destroy();
|
|
1489
|
+
}
|
|
1490
|
+
_sharedConnection = new LiveConnection({ url: resolvedUrl });
|
|
1491
|
+
_sharedConnectionUrl = resolvedUrl;
|
|
1492
|
+
_sharedConnection.onStateChange((state) => {
|
|
1493
|
+
for (const cb of _statusListeners) {
|
|
1494
|
+
cb(state.connected);
|
|
1495
|
+
}
|
|
1496
|
+
});
|
|
1497
|
+
return _sharedConnection;
|
|
1498
|
+
}
|
|
1499
|
+
function useLive(componentName, initialState, options = {}) {
|
|
1500
|
+
const { url, room, userId, autoMount = true, debug = false } = options;
|
|
1501
|
+
const connection = getOrCreateConnection(url);
|
|
1502
|
+
const handle = new LiveComponentHandle(connection, componentName, {
|
|
1503
|
+
initialState,
|
|
1504
|
+
room,
|
|
1505
|
+
userId,
|
|
1506
|
+
autoMount,
|
|
1507
|
+
debug
|
|
1508
|
+
});
|
|
1509
|
+
return {
|
|
1510
|
+
call: (action, payload) => handle.call(action, payload ?? {}),
|
|
1511
|
+
on: (callback) => handle.onStateChange(callback),
|
|
1512
|
+
onError: (callback) => handle.onError(callback),
|
|
1513
|
+
get state() {
|
|
1514
|
+
return handle.state;
|
|
1515
|
+
},
|
|
1516
|
+
get mounted() {
|
|
1517
|
+
return handle.mounted;
|
|
1518
|
+
},
|
|
1519
|
+
get componentId() {
|
|
1520
|
+
return handle.componentId;
|
|
1521
|
+
},
|
|
1522
|
+
get error() {
|
|
1523
|
+
return handle.error;
|
|
1524
|
+
},
|
|
1525
|
+
destroy: () => handle.destroy(),
|
|
1526
|
+
handle
|
|
1527
|
+
};
|
|
1528
|
+
}
|
|
1529
|
+
function onConnectionChange(callback) {
|
|
1530
|
+
_statusListeners.add(callback);
|
|
1531
|
+
if (_sharedConnection) {
|
|
1532
|
+
callback(_sharedConnection.state.connected);
|
|
1533
|
+
}
|
|
1534
|
+
return () => {
|
|
1535
|
+
_statusListeners.delete(callback);
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
function getConnection(url) {
|
|
1539
|
+
return getOrCreateConnection(url);
|
|
1540
|
+
}
|
|
1136
1541
|
export {
|
|
1137
1542
|
AdaptiveChunkSizer,
|
|
1138
1543
|
ChunkedUploader,
|
|
@@ -1142,7 +1547,10 @@ export {
|
|
|
1142
1547
|
StateValidator,
|
|
1143
1548
|
clearPersistedState,
|
|
1144
1549
|
createBinaryChunkMessage,
|
|
1550
|
+
getConnection,
|
|
1145
1551
|
getPersistedState,
|
|
1146
|
-
|
|
1552
|
+
onConnectionChange,
|
|
1553
|
+
persistState,
|
|
1554
|
+
useLive
|
|
1147
1555
|
};
|
|
1148
1556
|
//# sourceMappingURL=index.js.map
|