@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
|
@@ -31,8 +31,11 @@ var FluxstackLive = (() => {
|
|
|
31
31
|
StateValidator: () => StateValidator,
|
|
32
32
|
clearPersistedState: () => clearPersistedState,
|
|
33
33
|
createBinaryChunkMessage: () => createBinaryChunkMessage,
|
|
34
|
+
getConnection: () => getConnection,
|
|
34
35
|
getPersistedState: () => getPersistedState,
|
|
35
|
-
|
|
36
|
+
onConnectionChange: () => onConnectionChange,
|
|
37
|
+
persistState: () => persistState,
|
|
38
|
+
useLive: () => useLive
|
|
36
39
|
});
|
|
37
40
|
|
|
38
41
|
// src/connection.ts
|
|
@@ -44,6 +47,8 @@ var FluxstackLive = (() => {
|
|
|
44
47
|
__publicField(this, "reconnectTimeout", null);
|
|
45
48
|
__publicField(this, "heartbeatInterval", null);
|
|
46
49
|
__publicField(this, "componentCallbacks", /* @__PURE__ */ new Map());
|
|
50
|
+
__publicField(this, "binaryCallbacks", /* @__PURE__ */ new Map());
|
|
51
|
+
__publicField(this, "roomBinaryHandlers", /* @__PURE__ */ new Set());
|
|
47
52
|
__publicField(this, "pendingRequests", /* @__PURE__ */ new Map());
|
|
48
53
|
__publicField(this, "stateListeners", /* @__PURE__ */ new Set());
|
|
49
54
|
__publicField(this, "_state", {
|
|
@@ -123,6 +128,7 @@ var FluxstackLive = (() => {
|
|
|
123
128
|
this.log("Connecting...", { url });
|
|
124
129
|
try {
|
|
125
130
|
const ws = new WebSocket(url);
|
|
131
|
+
ws.binaryType = "arraybuffer";
|
|
126
132
|
this.ws = ws;
|
|
127
133
|
ws.onopen = () => {
|
|
128
134
|
this.log("Connected");
|
|
@@ -131,19 +137,34 @@ var FluxstackLive = (() => {
|
|
|
131
137
|
this.startHeartbeat();
|
|
132
138
|
};
|
|
133
139
|
ws.onmessage = (event) => {
|
|
140
|
+
if (event.data instanceof ArrayBuffer) {
|
|
141
|
+
this.handleBinaryMessage(new Uint8Array(event.data));
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
134
144
|
try {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
145
|
+
const parsed = JSON.parse(event.data);
|
|
146
|
+
if (Array.isArray(parsed)) {
|
|
147
|
+
for (const msg of parsed) {
|
|
148
|
+
this.log("Received", { type: msg.type, componentId: msg.componentId });
|
|
149
|
+
this.handleMessage(msg);
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
this.log("Received", { type: parsed.type, componentId: parsed.componentId });
|
|
153
|
+
this.handleMessage(parsed);
|
|
154
|
+
}
|
|
138
155
|
} catch {
|
|
139
156
|
this.log("Failed to parse message");
|
|
140
157
|
this.setState({ error: "Failed to parse message" });
|
|
141
158
|
}
|
|
142
159
|
};
|
|
143
|
-
ws.onclose = () => {
|
|
144
|
-
this.log("Disconnected");
|
|
160
|
+
ws.onclose = (event) => {
|
|
161
|
+
this.log("Disconnected", { code: event.code, reason: event.reason });
|
|
145
162
|
this.setState({ connected: false, connecting: false, connectionId: null });
|
|
146
163
|
this.stopHeartbeat();
|
|
164
|
+
if (event.code === 4003) {
|
|
165
|
+
this.setState({ error: "Connection rejected: origin not allowed" });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
147
168
|
this.attemptReconnect();
|
|
148
169
|
};
|
|
149
170
|
ws.onerror = () => {
|
|
@@ -318,6 +339,37 @@ var FluxstackLive = (() => {
|
|
|
318
339
|
}
|
|
319
340
|
});
|
|
320
341
|
}
|
|
342
|
+
/** Parse and route binary frames (state delta, room events, room state) */
|
|
343
|
+
handleBinaryMessage(buffer) {
|
|
344
|
+
if (buffer.length < 3) return;
|
|
345
|
+
const frameType = buffer[0];
|
|
346
|
+
if (frameType === 1) {
|
|
347
|
+
const idLen = buffer[1];
|
|
348
|
+
if (buffer.length < 2 + idLen) return;
|
|
349
|
+
const componentId = new TextDecoder().decode(buffer.subarray(2, 2 + idLen));
|
|
350
|
+
const payload = buffer.subarray(2 + idLen);
|
|
351
|
+
const callback = this.binaryCallbacks.get(componentId);
|
|
352
|
+
if (callback) callback(payload);
|
|
353
|
+
} else if (frameType === 2 || frameType === 3) {
|
|
354
|
+
for (const callback of this.roomBinaryHandlers) {
|
|
355
|
+
callback(buffer);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/** Register a handler for binary room frames (0x02 / 0x03). Returns unsubscribe. */
|
|
360
|
+
registerRoomBinaryHandler(callback) {
|
|
361
|
+
this.roomBinaryHandlers.add(callback);
|
|
362
|
+
return () => {
|
|
363
|
+
this.roomBinaryHandlers.delete(callback);
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
/** Register a binary message handler for a component */
|
|
367
|
+
registerBinaryHandler(componentId, callback) {
|
|
368
|
+
this.binaryCallbacks.set(componentId, callback);
|
|
369
|
+
return () => {
|
|
370
|
+
this.binaryCallbacks.delete(componentId);
|
|
371
|
+
};
|
|
372
|
+
}
|
|
321
373
|
/** Register a component message callback */
|
|
322
374
|
registerComponent(componentId, callback) {
|
|
323
375
|
this.log("Registering component", componentId);
|
|
@@ -353,6 +405,8 @@ var FluxstackLive = (() => {
|
|
|
353
405
|
destroy() {
|
|
354
406
|
this.disconnect();
|
|
355
407
|
this.componentCallbacks.clear();
|
|
408
|
+
this.binaryCallbacks.clear();
|
|
409
|
+
this.roomBinaryHandlers.clear();
|
|
356
410
|
for (const [, req] of this.pendingRequests) {
|
|
357
411
|
clearTimeout(req.timeout);
|
|
358
412
|
req.reject(new Error("Connection destroyed"));
|
|
@@ -363,6 +417,25 @@ var FluxstackLive = (() => {
|
|
|
363
417
|
};
|
|
364
418
|
|
|
365
419
|
// src/component.ts
|
|
420
|
+
function isPlainObject(v) {
|
|
421
|
+
return v !== null && typeof v === "object" && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype;
|
|
422
|
+
}
|
|
423
|
+
function deepMerge(target, source, seen) {
|
|
424
|
+
if (!seen) seen = /* @__PURE__ */ new Set();
|
|
425
|
+
if (seen.has(source)) return target;
|
|
426
|
+
seen.add(source);
|
|
427
|
+
const result = { ...target };
|
|
428
|
+
for (const key of Object.keys(source)) {
|
|
429
|
+
const newVal = source[key];
|
|
430
|
+
const oldVal = result[key];
|
|
431
|
+
if (isPlainObject(oldVal) && isPlainObject(newVal)) {
|
|
432
|
+
result[key] = deepMerge(oldVal, newVal, seen);
|
|
433
|
+
} else {
|
|
434
|
+
result[key] = newVal;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return result;
|
|
438
|
+
}
|
|
366
439
|
var LiveComponentHandle = class {
|
|
367
440
|
constructor(connection, componentName, options = {}) {
|
|
368
441
|
__publicField(this, "connection");
|
|
@@ -511,6 +584,21 @@ var FluxstackLive = (() => {
|
|
|
511
584
|
}
|
|
512
585
|
return response.result;
|
|
513
586
|
}
|
|
587
|
+
/**
|
|
588
|
+
* Fire an action without waiting for a response (fire-and-forget).
|
|
589
|
+
* Useful for high-frequency operations like game input where the
|
|
590
|
+
* server doesn't need to send back a result.
|
|
591
|
+
*/
|
|
592
|
+
fire(action, payload = {}) {
|
|
593
|
+
if (!this._mounted || !this._componentId) return;
|
|
594
|
+
this.connection.sendMessage({
|
|
595
|
+
type: "CALL_ACTION",
|
|
596
|
+
componentId: this._componentId,
|
|
597
|
+
action,
|
|
598
|
+
payload,
|
|
599
|
+
expectResponse: false
|
|
600
|
+
});
|
|
601
|
+
}
|
|
514
602
|
// ── State ──
|
|
515
603
|
/**
|
|
516
604
|
* Subscribe to state changes.
|
|
@@ -523,6 +611,26 @@ var FluxstackLive = (() => {
|
|
|
523
611
|
this.stateListeners.delete(callback);
|
|
524
612
|
};
|
|
525
613
|
}
|
|
614
|
+
/**
|
|
615
|
+
* Register a binary decoder for this component.
|
|
616
|
+
* When the server sends a BINARY_STATE_DELTA frame targeting this component,
|
|
617
|
+
* the decoder converts the raw payload into a delta object which is merged into state.
|
|
618
|
+
* Returns an unsubscribe function.
|
|
619
|
+
*/
|
|
620
|
+
setBinaryDecoder(decoder) {
|
|
621
|
+
if (!this._componentId) {
|
|
622
|
+
throw new Error("Component must be mounted before setting binary decoder");
|
|
623
|
+
}
|
|
624
|
+
return this.connection.registerBinaryHandler(this._componentId, (payload) => {
|
|
625
|
+
try {
|
|
626
|
+
const delta = decoder(payload);
|
|
627
|
+
this._state = deepMerge(this._state, delta);
|
|
628
|
+
this.notifyStateChange(this._state, delta);
|
|
629
|
+
} catch (e) {
|
|
630
|
+
console.error("Binary decode error:", e);
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
}
|
|
526
634
|
/**
|
|
527
635
|
* Subscribe to errors.
|
|
528
636
|
* Returns an unsubscribe function.
|
|
@@ -539,7 +647,7 @@ var FluxstackLive = (() => {
|
|
|
539
647
|
case "STATE_UPDATE": {
|
|
540
648
|
const newState = msg.payload?.state;
|
|
541
649
|
if (newState) {
|
|
542
|
-
this._state =
|
|
650
|
+
this._state = deepMerge(this._state, newState);
|
|
543
651
|
this.notifyStateChange(this._state, null);
|
|
544
652
|
}
|
|
545
653
|
break;
|
|
@@ -547,7 +655,7 @@ var FluxstackLive = (() => {
|
|
|
547
655
|
case "STATE_DELTA": {
|
|
548
656
|
const delta = msg.payload?.delta;
|
|
549
657
|
if (delta) {
|
|
550
|
-
this._state =
|
|
658
|
+
this._state = deepMerge(this._state, delta);
|
|
551
659
|
this.notifyStateChange(this._state, delta);
|
|
552
660
|
}
|
|
553
661
|
break;
|
|
@@ -589,6 +697,180 @@ var FluxstackLive = (() => {
|
|
|
589
697
|
};
|
|
590
698
|
|
|
591
699
|
// src/rooms.ts
|
|
700
|
+
function isPlainObject2(v) {
|
|
701
|
+
return v !== null && typeof v === "object" && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype;
|
|
702
|
+
}
|
|
703
|
+
function deepMerge2(target, source, seen) {
|
|
704
|
+
if (!seen) seen = /* @__PURE__ */ new Set();
|
|
705
|
+
if (seen.has(source)) return target;
|
|
706
|
+
seen.add(source);
|
|
707
|
+
const result = { ...target };
|
|
708
|
+
for (const key of Object.keys(source)) {
|
|
709
|
+
const newVal = source[key];
|
|
710
|
+
const oldVal = result[key];
|
|
711
|
+
if (isPlainObject2(oldVal) && isPlainObject2(newVal)) {
|
|
712
|
+
result[key] = deepMerge2(oldVal, newVal, seen);
|
|
713
|
+
} else {
|
|
714
|
+
result[key] = newVal;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return result;
|
|
718
|
+
}
|
|
719
|
+
var BINARY_ROOM_EVENT = 2;
|
|
720
|
+
var BINARY_ROOM_STATE = 3;
|
|
721
|
+
var _decoder = new TextDecoder();
|
|
722
|
+
function msgpackDecode(buf) {
|
|
723
|
+
return _decodeAt(buf, 0).value;
|
|
724
|
+
}
|
|
725
|
+
function _decodeAt(buf, offset) {
|
|
726
|
+
if (offset >= buf.length) return { value: null, offset };
|
|
727
|
+
const byte = buf[offset];
|
|
728
|
+
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
729
|
+
if (byte < 128) return { value: byte, offset: offset + 1 };
|
|
730
|
+
if (byte >= 128 && byte <= 143) return _decodeMap(buf, offset + 1, byte & 15);
|
|
731
|
+
if (byte >= 144 && byte <= 159) return _decodeArr(buf, offset + 1, byte & 15);
|
|
732
|
+
if (byte >= 160 && byte <= 191) {
|
|
733
|
+
const len = byte & 31;
|
|
734
|
+
return { value: _decoder.decode(buf.subarray(offset + 1, offset + 1 + len)), offset: offset + 1 + len };
|
|
735
|
+
}
|
|
736
|
+
if (byte >= 224) return { value: byte - 256, offset: offset + 1 };
|
|
737
|
+
switch (byte) {
|
|
738
|
+
case 192:
|
|
739
|
+
return { value: null, offset: offset + 1 };
|
|
740
|
+
case 194:
|
|
741
|
+
return { value: false, offset: offset + 1 };
|
|
742
|
+
case 195:
|
|
743
|
+
return { value: true, offset: offset + 1 };
|
|
744
|
+
case 196: {
|
|
745
|
+
const l = buf[offset + 1];
|
|
746
|
+
return { value: buf.slice(offset + 2, offset + 2 + l), offset: offset + 2 + l };
|
|
747
|
+
}
|
|
748
|
+
case 197: {
|
|
749
|
+
const l = view.getUint16(offset + 1, false);
|
|
750
|
+
return { value: buf.slice(offset + 3, offset + 3 + l), offset: offset + 3 + l };
|
|
751
|
+
}
|
|
752
|
+
case 198: {
|
|
753
|
+
const l = view.getUint32(offset + 1, false);
|
|
754
|
+
return { value: buf.slice(offset + 5, offset + 5 + l), offset: offset + 5 + l };
|
|
755
|
+
}
|
|
756
|
+
case 203:
|
|
757
|
+
return { value: view.getFloat64(offset + 1, false), offset: offset + 9 };
|
|
758
|
+
case 204:
|
|
759
|
+
return { value: buf[offset + 1], offset: offset + 2 };
|
|
760
|
+
case 205:
|
|
761
|
+
return { value: view.getUint16(offset + 1, false), offset: offset + 3 };
|
|
762
|
+
case 206:
|
|
763
|
+
return { value: view.getUint32(offset + 1, false), offset: offset + 5 };
|
|
764
|
+
case 208:
|
|
765
|
+
return { value: view.getInt8(offset + 1), offset: offset + 2 };
|
|
766
|
+
case 209:
|
|
767
|
+
return { value: view.getInt16(offset + 1, false), offset: offset + 3 };
|
|
768
|
+
case 210:
|
|
769
|
+
return { value: view.getInt32(offset + 1, false), offset: offset + 5 };
|
|
770
|
+
case 217: {
|
|
771
|
+
const l = buf[offset + 1];
|
|
772
|
+
return { value: _decoder.decode(buf.subarray(offset + 2, offset + 2 + l)), offset: offset + 2 + l };
|
|
773
|
+
}
|
|
774
|
+
case 218: {
|
|
775
|
+
const l = view.getUint16(offset + 1, false);
|
|
776
|
+
return { value: _decoder.decode(buf.subarray(offset + 3, offset + 3 + l)), offset: offset + 3 + l };
|
|
777
|
+
}
|
|
778
|
+
case 219: {
|
|
779
|
+
const l = view.getUint32(offset + 1, false);
|
|
780
|
+
return { value: _decoder.decode(buf.subarray(offset + 5, offset + 5 + l)), offset: offset + 5 + l };
|
|
781
|
+
}
|
|
782
|
+
case 220:
|
|
783
|
+
return _decodeArr(buf, offset + 3, view.getUint16(offset + 1, false));
|
|
784
|
+
case 221:
|
|
785
|
+
return _decodeArr(buf, offset + 5, view.getUint32(offset + 1, false));
|
|
786
|
+
case 222:
|
|
787
|
+
return _decodeMap(buf, offset + 3, view.getUint16(offset + 1, false));
|
|
788
|
+
case 223:
|
|
789
|
+
return _decodeMap(buf, offset + 5, view.getUint32(offset + 1, false));
|
|
790
|
+
}
|
|
791
|
+
return { value: null, offset: offset + 1 };
|
|
792
|
+
}
|
|
793
|
+
function _decodeArr(buf, offset, count) {
|
|
794
|
+
const arr = new Array(count);
|
|
795
|
+
for (let i = 0; i < count; i++) {
|
|
796
|
+
const r = _decodeAt(buf, offset);
|
|
797
|
+
arr[i] = r.value;
|
|
798
|
+
offset = r.offset;
|
|
799
|
+
}
|
|
800
|
+
return { value: arr, offset };
|
|
801
|
+
}
|
|
802
|
+
function _decodeMap(buf, offset, count) {
|
|
803
|
+
const obj = {};
|
|
804
|
+
for (let i = 0; i < count; i++) {
|
|
805
|
+
const k = _decodeAt(buf, offset);
|
|
806
|
+
offset = k.offset;
|
|
807
|
+
const v = _decodeAt(buf, offset);
|
|
808
|
+
offset = v.offset;
|
|
809
|
+
obj[String(k.value)] = v.value;
|
|
810
|
+
}
|
|
811
|
+
return { value: obj, offset };
|
|
812
|
+
}
|
|
813
|
+
function parseRoomFrame(buf) {
|
|
814
|
+
if (buf.length < 6) return null;
|
|
815
|
+
let offset = 0;
|
|
816
|
+
const frameType = buf[offset++];
|
|
817
|
+
const compIdLen = buf[offset++];
|
|
818
|
+
if (offset + compIdLen > buf.length) return null;
|
|
819
|
+
const componentId = _decoder.decode(buf.subarray(offset, offset + compIdLen));
|
|
820
|
+
offset += compIdLen;
|
|
821
|
+
const roomIdLen = buf[offset++];
|
|
822
|
+
if (offset + roomIdLen > buf.length) return null;
|
|
823
|
+
const roomId = _decoder.decode(buf.subarray(offset, offset + roomIdLen));
|
|
824
|
+
offset += roomIdLen;
|
|
825
|
+
if (offset + 2 > buf.length) return null;
|
|
826
|
+
const eventLen = buf[offset] << 8 | buf[offset + 1];
|
|
827
|
+
offset += 2;
|
|
828
|
+
if (offset + eventLen > buf.length) return null;
|
|
829
|
+
const event = _decoder.decode(buf.subarray(offset, offset + eventLen));
|
|
830
|
+
offset += eventLen;
|
|
831
|
+
return { frameType, componentId, roomId, event, payload: buf.subarray(offset) };
|
|
832
|
+
}
|
|
833
|
+
var ROOM_RESERVED_KEYS = /* @__PURE__ */ new Set([
|
|
834
|
+
"id",
|
|
835
|
+
"joined",
|
|
836
|
+
"state",
|
|
837
|
+
"join",
|
|
838
|
+
"leave",
|
|
839
|
+
"emit",
|
|
840
|
+
"on",
|
|
841
|
+
"onSystem",
|
|
842
|
+
"setState",
|
|
843
|
+
"call",
|
|
844
|
+
"apply",
|
|
845
|
+
"bind",
|
|
846
|
+
"prototype",
|
|
847
|
+
"length",
|
|
848
|
+
"name",
|
|
849
|
+
"arguments",
|
|
850
|
+
"caller",
|
|
851
|
+
Symbol.toPrimitive,
|
|
852
|
+
Symbol.toStringTag,
|
|
853
|
+
Symbol.hasInstance
|
|
854
|
+
]);
|
|
855
|
+
function wrapWithStateProxy(target, getState, setStateFn) {
|
|
856
|
+
return new Proxy(target, {
|
|
857
|
+
get(obj, prop, receiver) {
|
|
858
|
+
if (ROOM_RESERVED_KEYS.has(prop) || typeof prop === "symbol") {
|
|
859
|
+
return Reflect.get(obj, prop, receiver);
|
|
860
|
+
}
|
|
861
|
+
const desc = Object.getOwnPropertyDescriptor(obj, prop);
|
|
862
|
+
if (desc) return Reflect.get(obj, prop, receiver);
|
|
863
|
+
if (prop in obj) return Reflect.get(obj, prop, receiver);
|
|
864
|
+
const st = getState();
|
|
865
|
+
return st?.[prop];
|
|
866
|
+
},
|
|
867
|
+
set(_obj, prop, value) {
|
|
868
|
+
if (typeof prop === "symbol") return false;
|
|
869
|
+
setStateFn({ [prop]: value });
|
|
870
|
+
return true;
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
}
|
|
592
874
|
var RoomManager = class {
|
|
593
875
|
constructor(options) {
|
|
594
876
|
__publicField(this, "componentId");
|
|
@@ -598,11 +880,28 @@ var FluxstackLive = (() => {
|
|
|
598
880
|
__publicField(this, "sendMessage");
|
|
599
881
|
__publicField(this, "sendMessageAndWait");
|
|
600
882
|
__publicField(this, "globalUnsubscribe", null);
|
|
883
|
+
__publicField(this, "binaryUnsubscribe", null);
|
|
884
|
+
__publicField(this, "onBinaryMessage", null);
|
|
885
|
+
__publicField(this, "onMessageFactory", null);
|
|
601
886
|
this.componentId = options.componentId;
|
|
602
887
|
this.defaultRoom = options.defaultRoom || null;
|
|
603
888
|
this.sendMessage = options.sendMessage;
|
|
604
889
|
this.sendMessageAndWait = options.sendMessageAndWait;
|
|
890
|
+
this.onBinaryMessage = options.onBinaryMessage ?? null;
|
|
891
|
+
this.onMessageFactory = options.onMessage;
|
|
605
892
|
this.globalUnsubscribe = options.onMessage((msg) => this.handleServerMessage(msg));
|
|
893
|
+
if (options.onBinaryMessage) {
|
|
894
|
+
this.binaryUnsubscribe = options.onBinaryMessage((frame) => this.handleBinaryFrame(frame));
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
/** Re-subscribe message and binary handlers (needed after destroy/remount in React Strict Mode) */
|
|
898
|
+
resubscribe() {
|
|
899
|
+
if (!this.globalUnsubscribe && this.onMessageFactory) {
|
|
900
|
+
this.globalUnsubscribe = this.onMessageFactory((msg) => this.handleServerMessage(msg));
|
|
901
|
+
}
|
|
902
|
+
if (!this.binaryUnsubscribe && this.onBinaryMessage) {
|
|
903
|
+
this.binaryUnsubscribe = this.onBinaryMessage((frame) => this.handleBinaryFrame(frame));
|
|
904
|
+
}
|
|
606
905
|
}
|
|
607
906
|
handleServerMessage(msg) {
|
|
608
907
|
if (msg.componentId !== this.componentId) return;
|
|
@@ -624,10 +923,11 @@ var FluxstackLive = (() => {
|
|
|
624
923
|
break;
|
|
625
924
|
}
|
|
626
925
|
case "ROOM_STATE": {
|
|
627
|
-
|
|
926
|
+
const stateChanges = msg.data?.state ?? msg.data;
|
|
927
|
+
room.state = deepMerge2(room.state, stateChanges);
|
|
628
928
|
const stateHandlers = room.handlers.get("$state:change");
|
|
629
929
|
if (stateHandlers) {
|
|
630
|
-
for (const handler of stateHandlers) handler(
|
|
930
|
+
for (const handler of stateHandlers) handler(stateChanges);
|
|
631
931
|
}
|
|
632
932
|
break;
|
|
633
933
|
}
|
|
@@ -640,6 +940,34 @@ var FluxstackLive = (() => {
|
|
|
640
940
|
break;
|
|
641
941
|
}
|
|
642
942
|
}
|
|
943
|
+
/** Handle binary room frames (0x02 ROOM_EVENT, 0x03 ROOM_STATE) */
|
|
944
|
+
handleBinaryFrame(frame) {
|
|
945
|
+
const parsed = parseRoomFrame(frame);
|
|
946
|
+
if (!parsed) return;
|
|
947
|
+
if (parsed.componentId !== this.componentId) return;
|
|
948
|
+
const room = this.rooms.get(parsed.roomId);
|
|
949
|
+
if (!room) return;
|
|
950
|
+
const data = msgpackDecode(parsed.payload);
|
|
951
|
+
if (parsed.frameType === BINARY_ROOM_EVENT) {
|
|
952
|
+
const handlers = room.handlers.get(parsed.event);
|
|
953
|
+
if (handlers) {
|
|
954
|
+
for (const handler of handlers) {
|
|
955
|
+
try {
|
|
956
|
+
handler(data);
|
|
957
|
+
} catch (error) {
|
|
958
|
+
console.error(`[Room:${parsed.roomId}] Handler error for '${parsed.event}':`, error);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
} else if (parsed.frameType === BINARY_ROOM_STATE) {
|
|
963
|
+
const stateChanges = data?.state ?? data;
|
|
964
|
+
room.state = deepMerge2(room.state, stateChanges);
|
|
965
|
+
const stateHandlers = room.handlers.get("$state:change");
|
|
966
|
+
if (stateHandlers) {
|
|
967
|
+
for (const handler of stateHandlers) handler(stateChanges);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
643
971
|
getOrCreateRoom(roomId) {
|
|
644
972
|
if (!this.rooms.has(roomId)) {
|
|
645
973
|
this.rooms.set(roomId, {
|
|
@@ -720,7 +1048,7 @@ var FluxstackLive = (() => {
|
|
|
720
1048
|
},
|
|
721
1049
|
setState: (updates) => {
|
|
722
1050
|
if (!this.componentId) return;
|
|
723
|
-
room.state =
|
|
1051
|
+
room.state = deepMerge2(room.state, updates);
|
|
724
1052
|
this.sendMessage({
|
|
725
1053
|
type: "ROOM_STATE_SET",
|
|
726
1054
|
componentId: this.componentId,
|
|
@@ -730,8 +1058,13 @@ var FluxstackLive = (() => {
|
|
|
730
1058
|
});
|
|
731
1059
|
}
|
|
732
1060
|
};
|
|
733
|
-
|
|
734
|
-
|
|
1061
|
+
const proxied = wrapWithStateProxy(
|
|
1062
|
+
handle,
|
|
1063
|
+
() => room.state,
|
|
1064
|
+
(updates) => handle.setState(updates)
|
|
1065
|
+
);
|
|
1066
|
+
this.handles.set(roomId, proxied);
|
|
1067
|
+
return proxied;
|
|
735
1068
|
}
|
|
736
1069
|
/** Create the $room proxy */
|
|
737
1070
|
createProxy() {
|
|
@@ -781,6 +1114,14 @@ var FluxstackLive = (() => {
|
|
|
781
1114
|
}
|
|
782
1115
|
}
|
|
783
1116
|
});
|
|
1117
|
+
if (this.defaultRoom && defaultHandle) {
|
|
1118
|
+
const room = this.getOrCreateRoom(this.defaultRoom);
|
|
1119
|
+
return wrapWithStateProxy(
|
|
1120
|
+
proxyFn,
|
|
1121
|
+
() => room.state,
|
|
1122
|
+
(updates) => defaultHandle.setState(updates)
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
784
1125
|
return proxyFn;
|
|
785
1126
|
}
|
|
786
1127
|
/** List of rooms currently joined */
|
|
@@ -795,9 +1136,12 @@ var FluxstackLive = (() => {
|
|
|
795
1136
|
setComponentId(id) {
|
|
796
1137
|
this.componentId = id;
|
|
797
1138
|
}
|
|
798
|
-
/** Cleanup */
|
|
1139
|
+
/** Cleanup — unsubscribes handlers but keeps factory refs for resubscribe() */
|
|
799
1140
|
destroy() {
|
|
800
1141
|
this.globalUnsubscribe?.();
|
|
1142
|
+
this.globalUnsubscribe = null;
|
|
1143
|
+
this.binaryUnsubscribe?.();
|
|
1144
|
+
this.binaryUnsubscribe = null;
|
|
801
1145
|
for (const [, room] of this.rooms) {
|
|
802
1146
|
room.handlers.clear();
|
|
803
1147
|
}
|
|
@@ -1170,6 +1514,70 @@ var FluxstackLive = (() => {
|
|
|
1170
1514
|
};
|
|
1171
1515
|
}
|
|
1172
1516
|
};
|
|
1517
|
+
|
|
1518
|
+
// src/index.ts
|
|
1519
|
+
var _sharedConnection = null;
|
|
1520
|
+
var _sharedConnectionUrl = null;
|
|
1521
|
+
var _statusListeners = /* @__PURE__ */ new Set();
|
|
1522
|
+
function getOrCreateConnection(url) {
|
|
1523
|
+
const resolvedUrl = url ?? `ws://${typeof location !== "undefined" ? location.host : "localhost:3000"}/api/live/ws`;
|
|
1524
|
+
if (_sharedConnection && _sharedConnectionUrl === resolvedUrl) {
|
|
1525
|
+
return _sharedConnection;
|
|
1526
|
+
}
|
|
1527
|
+
if (_sharedConnection) {
|
|
1528
|
+
_sharedConnection.destroy();
|
|
1529
|
+
}
|
|
1530
|
+
_sharedConnection = new LiveConnection({ url: resolvedUrl });
|
|
1531
|
+
_sharedConnectionUrl = resolvedUrl;
|
|
1532
|
+
_sharedConnection.onStateChange((state) => {
|
|
1533
|
+
for (const cb of _statusListeners) {
|
|
1534
|
+
cb(state.connected);
|
|
1535
|
+
}
|
|
1536
|
+
});
|
|
1537
|
+
return _sharedConnection;
|
|
1538
|
+
}
|
|
1539
|
+
function useLive(componentName, initialState, options = {}) {
|
|
1540
|
+
const { url, room, userId, autoMount = true, debug = false } = options;
|
|
1541
|
+
const connection = getOrCreateConnection(url);
|
|
1542
|
+
const handle = new LiveComponentHandle(connection, componentName, {
|
|
1543
|
+
initialState,
|
|
1544
|
+
room,
|
|
1545
|
+
userId,
|
|
1546
|
+
autoMount,
|
|
1547
|
+
debug
|
|
1548
|
+
});
|
|
1549
|
+
return {
|
|
1550
|
+
call: (action, payload) => handle.call(action, payload ?? {}),
|
|
1551
|
+
on: (callback) => handle.onStateChange(callback),
|
|
1552
|
+
onError: (callback) => handle.onError(callback),
|
|
1553
|
+
get state() {
|
|
1554
|
+
return handle.state;
|
|
1555
|
+
},
|
|
1556
|
+
get mounted() {
|
|
1557
|
+
return handle.mounted;
|
|
1558
|
+
},
|
|
1559
|
+
get componentId() {
|
|
1560
|
+
return handle.componentId;
|
|
1561
|
+
},
|
|
1562
|
+
get error() {
|
|
1563
|
+
return handle.error;
|
|
1564
|
+
},
|
|
1565
|
+
destroy: () => handle.destroy(),
|
|
1566
|
+
handle
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
function onConnectionChange(callback) {
|
|
1570
|
+
_statusListeners.add(callback);
|
|
1571
|
+
if (_sharedConnection) {
|
|
1572
|
+
callback(_sharedConnection.state.connected);
|
|
1573
|
+
}
|
|
1574
|
+
return () => {
|
|
1575
|
+
_statusListeners.delete(callback);
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
function getConnection(url) {
|
|
1579
|
+
return getOrCreateConnection(url);
|
|
1580
|
+
}
|
|
1173
1581
|
return __toCommonJS(src_exports);
|
|
1174
1582
|
})();
|
|
1175
1583
|
//# sourceMappingURL=live-client.browser.global.js.map
|