@bloopjs/web 0.0.44 → 0.0.46
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/App.d.ts +30 -1
- package/dist/App.d.ts.map +1 -1
- package/dist/mod.d.ts +1 -0
- package/dist/mod.d.ts.map +1 -1
- package/dist/mod.js +798 -3
- package/dist/mod.js.map +9 -4
- package/dist/netcode/broker.d.ts +12 -0
- package/dist/netcode/broker.d.ts.map +1 -0
- package/dist/netcode/logs.d.ts +37 -0
- package/dist/netcode/logs.d.ts.map +1 -0
- package/dist/netcode/mod.d.ts +7 -0
- package/dist/netcode/mod.d.ts.map +1 -0
- package/dist/netcode/protocol.d.ts +42 -0
- package/dist/netcode/protocol.d.ts.map +1 -0
- package/dist/netcode/transport.d.ts +21 -0
- package/dist/netcode/transport.d.ts.map +1 -0
- package/package.json +4 -3
- package/src/App.ts +48 -3
- package/src/mod.ts +1 -0
- package/src/netcode/broker.ts +132 -0
- package/src/netcode/logs.ts +81 -0
- package/src/netcode/mod.ts +14 -0
- package/src/netcode/protocol.ts +56 -0
- package/src/netcode/transport.ts +291 -0
package/dist/mod.js
CHANGED
|
@@ -2578,7 +2578,787 @@ var TIME_CTX_OFFSET2 = 0;
|
|
|
2578
2578
|
var INPUT_CTX_OFFSET2 = TIME_CTX_OFFSET2 + 4;
|
|
2579
2579
|
var EVENTS_OFFSET2 = INPUT_CTX_OFFSET2 + 4;
|
|
2580
2580
|
|
|
2581
|
+
// ../../node_modules/partysocket/dist/chunk-V6LO7DXK.mjs
|
|
2582
|
+
if (!globalThis.EventTarget || !globalThis.Event) {
|
|
2583
|
+
console.error(`
|
|
2584
|
+
PartySocket requires a global 'EventTarget' class to be available!
|
|
2585
|
+
You can polyfill this global by adding this to your code before any partysocket imports:
|
|
2586
|
+
|
|
2587
|
+
\`\`\`
|
|
2588
|
+
import 'partysocket/event-target-polyfill';
|
|
2589
|
+
\`\`\`
|
|
2590
|
+
Please file an issue at https://github.com/partykit/partykit if you're still having trouble.
|
|
2591
|
+
`);
|
|
2592
|
+
}
|
|
2593
|
+
var ErrorEvent = class extends Event {
|
|
2594
|
+
message;
|
|
2595
|
+
error;
|
|
2596
|
+
constructor(error, target) {
|
|
2597
|
+
super("error", target);
|
|
2598
|
+
this.message = error.message;
|
|
2599
|
+
this.error = error;
|
|
2600
|
+
}
|
|
2601
|
+
};
|
|
2602
|
+
var CloseEvent = class extends Event {
|
|
2603
|
+
code;
|
|
2604
|
+
reason;
|
|
2605
|
+
wasClean = true;
|
|
2606
|
+
constructor(code = 1000, reason = "", target) {
|
|
2607
|
+
super("close", target);
|
|
2608
|
+
this.code = code;
|
|
2609
|
+
this.reason = reason;
|
|
2610
|
+
}
|
|
2611
|
+
};
|
|
2612
|
+
var Events = {
|
|
2613
|
+
Event,
|
|
2614
|
+
ErrorEvent,
|
|
2615
|
+
CloseEvent
|
|
2616
|
+
};
|
|
2617
|
+
function assert2(condition, msg) {
|
|
2618
|
+
if (!condition) {
|
|
2619
|
+
throw new Error(msg);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
function cloneEventBrowser(e) {
|
|
2623
|
+
return new e.constructor(e.type, e);
|
|
2624
|
+
}
|
|
2625
|
+
function cloneEventNode(e) {
|
|
2626
|
+
if ("data" in e) {
|
|
2627
|
+
const evt2 = new MessageEvent(e.type, e);
|
|
2628
|
+
return evt2;
|
|
2629
|
+
}
|
|
2630
|
+
if ("code" in e || "reason" in e) {
|
|
2631
|
+
const evt2 = new CloseEvent(e.code || 1999, e.reason || "unknown reason", e);
|
|
2632
|
+
return evt2;
|
|
2633
|
+
}
|
|
2634
|
+
if ("error" in e) {
|
|
2635
|
+
const evt2 = new ErrorEvent(e.error, e);
|
|
2636
|
+
return evt2;
|
|
2637
|
+
}
|
|
2638
|
+
const evt = new Event(e.type, e);
|
|
2639
|
+
return evt;
|
|
2640
|
+
}
|
|
2641
|
+
var _a;
|
|
2642
|
+
var isNode = typeof process !== "undefined" && typeof ((_a = process.versions) == null ? undefined : _a.node) !== "undefined" && typeof document === "undefined";
|
|
2643
|
+
var cloneEvent = isNode ? cloneEventNode : cloneEventBrowser;
|
|
2644
|
+
var DEFAULT = {
|
|
2645
|
+
maxReconnectionDelay: 1e4,
|
|
2646
|
+
minReconnectionDelay: 1000 + Math.random() * 4000,
|
|
2647
|
+
minUptime: 5000,
|
|
2648
|
+
reconnectionDelayGrowFactor: 1.3,
|
|
2649
|
+
connectionTimeout: 4000,
|
|
2650
|
+
maxRetries: Number.POSITIVE_INFINITY,
|
|
2651
|
+
maxEnqueuedMessages: Number.POSITIVE_INFINITY,
|
|
2652
|
+
startClosed: false,
|
|
2653
|
+
debug: false
|
|
2654
|
+
};
|
|
2655
|
+
var didWarnAboutMissingWebSocket = false;
|
|
2656
|
+
var ReconnectingWebSocket = class _ReconnectingWebSocket extends EventTarget {
|
|
2657
|
+
_ws;
|
|
2658
|
+
_retryCount = -1;
|
|
2659
|
+
_uptimeTimeout;
|
|
2660
|
+
_connectTimeout;
|
|
2661
|
+
_shouldReconnect = true;
|
|
2662
|
+
_connectLock = false;
|
|
2663
|
+
_binaryType = "blob";
|
|
2664
|
+
_closeCalled = false;
|
|
2665
|
+
_messageQueue = [];
|
|
2666
|
+
_debugLogger = console.log.bind(console);
|
|
2667
|
+
_url;
|
|
2668
|
+
_protocols;
|
|
2669
|
+
_options;
|
|
2670
|
+
constructor(url, protocols, options = {}) {
|
|
2671
|
+
super();
|
|
2672
|
+
this._url = url;
|
|
2673
|
+
this._protocols = protocols;
|
|
2674
|
+
this._options = options;
|
|
2675
|
+
if (this._options.startClosed) {
|
|
2676
|
+
this._shouldReconnect = false;
|
|
2677
|
+
}
|
|
2678
|
+
if (this._options.debugLogger) {
|
|
2679
|
+
this._debugLogger = this._options.debugLogger;
|
|
2680
|
+
}
|
|
2681
|
+
this._connect();
|
|
2682
|
+
}
|
|
2683
|
+
static get CONNECTING() {
|
|
2684
|
+
return 0;
|
|
2685
|
+
}
|
|
2686
|
+
static get OPEN() {
|
|
2687
|
+
return 1;
|
|
2688
|
+
}
|
|
2689
|
+
static get CLOSING() {
|
|
2690
|
+
return 2;
|
|
2691
|
+
}
|
|
2692
|
+
static get CLOSED() {
|
|
2693
|
+
return 3;
|
|
2694
|
+
}
|
|
2695
|
+
get CONNECTING() {
|
|
2696
|
+
return _ReconnectingWebSocket.CONNECTING;
|
|
2697
|
+
}
|
|
2698
|
+
get OPEN() {
|
|
2699
|
+
return _ReconnectingWebSocket.OPEN;
|
|
2700
|
+
}
|
|
2701
|
+
get CLOSING() {
|
|
2702
|
+
return _ReconnectingWebSocket.CLOSING;
|
|
2703
|
+
}
|
|
2704
|
+
get CLOSED() {
|
|
2705
|
+
return _ReconnectingWebSocket.CLOSED;
|
|
2706
|
+
}
|
|
2707
|
+
get binaryType() {
|
|
2708
|
+
return this._ws ? this._ws.binaryType : this._binaryType;
|
|
2709
|
+
}
|
|
2710
|
+
set binaryType(value) {
|
|
2711
|
+
this._binaryType = value;
|
|
2712
|
+
if (this._ws) {
|
|
2713
|
+
this._ws.binaryType = value;
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
get retryCount() {
|
|
2717
|
+
return Math.max(this._retryCount, 0);
|
|
2718
|
+
}
|
|
2719
|
+
get bufferedAmount() {
|
|
2720
|
+
const bytes = this._messageQueue.reduce((acc, message) => {
|
|
2721
|
+
if (typeof message === "string") {
|
|
2722
|
+
acc += message.length;
|
|
2723
|
+
} else if (message instanceof Blob) {
|
|
2724
|
+
acc += message.size;
|
|
2725
|
+
} else {
|
|
2726
|
+
acc += message.byteLength;
|
|
2727
|
+
}
|
|
2728
|
+
return acc;
|
|
2729
|
+
}, 0);
|
|
2730
|
+
return bytes + (this._ws ? this._ws.bufferedAmount : 0);
|
|
2731
|
+
}
|
|
2732
|
+
get extensions() {
|
|
2733
|
+
return this._ws ? this._ws.extensions : "";
|
|
2734
|
+
}
|
|
2735
|
+
get protocol() {
|
|
2736
|
+
return this._ws ? this._ws.protocol : "";
|
|
2737
|
+
}
|
|
2738
|
+
get readyState() {
|
|
2739
|
+
if (this._ws) {
|
|
2740
|
+
return this._ws.readyState;
|
|
2741
|
+
}
|
|
2742
|
+
return this._options.startClosed ? _ReconnectingWebSocket.CLOSED : _ReconnectingWebSocket.CONNECTING;
|
|
2743
|
+
}
|
|
2744
|
+
get url() {
|
|
2745
|
+
return this._ws ? this._ws.url : "";
|
|
2746
|
+
}
|
|
2747
|
+
get shouldReconnect() {
|
|
2748
|
+
return this._shouldReconnect;
|
|
2749
|
+
}
|
|
2750
|
+
onclose = null;
|
|
2751
|
+
onerror = null;
|
|
2752
|
+
onmessage = null;
|
|
2753
|
+
onopen = null;
|
|
2754
|
+
close(code = 1000, reason) {
|
|
2755
|
+
this._closeCalled = true;
|
|
2756
|
+
this._shouldReconnect = false;
|
|
2757
|
+
this._clearTimeouts();
|
|
2758
|
+
if (!this._ws) {
|
|
2759
|
+
this._debug("close enqueued: no ws instance");
|
|
2760
|
+
return;
|
|
2761
|
+
}
|
|
2762
|
+
if (this._ws.readyState === this.CLOSED) {
|
|
2763
|
+
this._debug("close: already closed");
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
this._ws.close(code, reason);
|
|
2767
|
+
}
|
|
2768
|
+
reconnect(code, reason) {
|
|
2769
|
+
this._shouldReconnect = true;
|
|
2770
|
+
this._closeCalled = false;
|
|
2771
|
+
this._retryCount = -1;
|
|
2772
|
+
if (!this._ws || this._ws.readyState === this.CLOSED) {
|
|
2773
|
+
this._connect();
|
|
2774
|
+
} else {
|
|
2775
|
+
this._disconnect(code, reason);
|
|
2776
|
+
this._connect();
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
send(data) {
|
|
2780
|
+
if (this._ws && this._ws.readyState === this.OPEN) {
|
|
2781
|
+
this._debug("send", data);
|
|
2782
|
+
this._ws.send(data);
|
|
2783
|
+
} else {
|
|
2784
|
+
const { maxEnqueuedMessages = DEFAULT.maxEnqueuedMessages } = this._options;
|
|
2785
|
+
if (this._messageQueue.length < maxEnqueuedMessages) {
|
|
2786
|
+
this._debug("enqueue", data);
|
|
2787
|
+
this._messageQueue.push(data);
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
_debug(...args) {
|
|
2792
|
+
if (this._options.debug) {
|
|
2793
|
+
this._debugLogger("RWS>", ...args);
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
_getNextDelay() {
|
|
2797
|
+
const {
|
|
2798
|
+
reconnectionDelayGrowFactor = DEFAULT.reconnectionDelayGrowFactor,
|
|
2799
|
+
minReconnectionDelay = DEFAULT.minReconnectionDelay,
|
|
2800
|
+
maxReconnectionDelay = DEFAULT.maxReconnectionDelay
|
|
2801
|
+
} = this._options;
|
|
2802
|
+
let delay = 0;
|
|
2803
|
+
if (this._retryCount > 0) {
|
|
2804
|
+
delay = minReconnectionDelay * reconnectionDelayGrowFactor ** (this._retryCount - 1);
|
|
2805
|
+
if (delay > maxReconnectionDelay) {
|
|
2806
|
+
delay = maxReconnectionDelay;
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
this._debug("next delay", delay);
|
|
2810
|
+
return delay;
|
|
2811
|
+
}
|
|
2812
|
+
_wait() {
|
|
2813
|
+
return new Promise((resolve) => {
|
|
2814
|
+
setTimeout(resolve, this._getNextDelay());
|
|
2815
|
+
});
|
|
2816
|
+
}
|
|
2817
|
+
_getNextProtocols(protocolsProvider) {
|
|
2818
|
+
if (!protocolsProvider)
|
|
2819
|
+
return Promise.resolve(null);
|
|
2820
|
+
if (typeof protocolsProvider === "string" || Array.isArray(protocolsProvider)) {
|
|
2821
|
+
return Promise.resolve(protocolsProvider);
|
|
2822
|
+
}
|
|
2823
|
+
if (typeof protocolsProvider === "function") {
|
|
2824
|
+
const protocols = protocolsProvider();
|
|
2825
|
+
if (!protocols)
|
|
2826
|
+
return Promise.resolve(null);
|
|
2827
|
+
if (typeof protocols === "string" || Array.isArray(protocols)) {
|
|
2828
|
+
return Promise.resolve(protocols);
|
|
2829
|
+
}
|
|
2830
|
+
if (protocols.then) {
|
|
2831
|
+
return protocols;
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
throw Error("Invalid protocols");
|
|
2835
|
+
}
|
|
2836
|
+
_getNextUrl(urlProvider) {
|
|
2837
|
+
if (typeof urlProvider === "string") {
|
|
2838
|
+
return Promise.resolve(urlProvider);
|
|
2839
|
+
}
|
|
2840
|
+
if (typeof urlProvider === "function") {
|
|
2841
|
+
const url = urlProvider();
|
|
2842
|
+
if (typeof url === "string") {
|
|
2843
|
+
return Promise.resolve(url);
|
|
2844
|
+
}
|
|
2845
|
+
if (url.then) {
|
|
2846
|
+
return url;
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
throw Error("Invalid URL");
|
|
2850
|
+
}
|
|
2851
|
+
_connect() {
|
|
2852
|
+
if (this._connectLock || !this._shouldReconnect) {
|
|
2853
|
+
return;
|
|
2854
|
+
}
|
|
2855
|
+
this._connectLock = true;
|
|
2856
|
+
const {
|
|
2857
|
+
maxRetries = DEFAULT.maxRetries,
|
|
2858
|
+
connectionTimeout = DEFAULT.connectionTimeout
|
|
2859
|
+
} = this._options;
|
|
2860
|
+
if (this._retryCount >= maxRetries) {
|
|
2861
|
+
this._debug("max retries reached", this._retryCount, ">=", maxRetries);
|
|
2862
|
+
return;
|
|
2863
|
+
}
|
|
2864
|
+
this._retryCount++;
|
|
2865
|
+
this._debug("connect", this._retryCount);
|
|
2866
|
+
this._removeListeners();
|
|
2867
|
+
this._wait().then(() => Promise.all([
|
|
2868
|
+
this._getNextUrl(this._url),
|
|
2869
|
+
this._getNextProtocols(this._protocols || null)
|
|
2870
|
+
])).then(([url, protocols]) => {
|
|
2871
|
+
if (this._closeCalled) {
|
|
2872
|
+
this._connectLock = false;
|
|
2873
|
+
return;
|
|
2874
|
+
}
|
|
2875
|
+
if (!this._options.WebSocket && typeof WebSocket === "undefined" && !didWarnAboutMissingWebSocket) {
|
|
2876
|
+
console.error(`‼️ No WebSocket implementation available. You should define options.WebSocket.
|
|
2877
|
+
|
|
2878
|
+
For example, if you're using node.js, run \`npm install ws\`, and then in your code:
|
|
2879
|
+
|
|
2880
|
+
import PartySocket from 'partysocket';
|
|
2881
|
+
import WS from 'ws';
|
|
2882
|
+
|
|
2883
|
+
const partysocket = new PartySocket({
|
|
2884
|
+
host: "127.0.0.1:1999",
|
|
2885
|
+
room: "test-room",
|
|
2886
|
+
WebSocket: WS
|
|
2887
|
+
});
|
|
2888
|
+
|
|
2889
|
+
`);
|
|
2890
|
+
didWarnAboutMissingWebSocket = true;
|
|
2891
|
+
}
|
|
2892
|
+
const WS = this._options.WebSocket || WebSocket;
|
|
2893
|
+
this._debug("connect", { url, protocols });
|
|
2894
|
+
this._ws = protocols ? new WS(url, protocols) : new WS(url);
|
|
2895
|
+
this._ws.binaryType = this._binaryType;
|
|
2896
|
+
this._connectLock = false;
|
|
2897
|
+
this._addListeners();
|
|
2898
|
+
this._connectTimeout = setTimeout(() => this._handleTimeout(), connectionTimeout);
|
|
2899
|
+
}).catch((err) => {
|
|
2900
|
+
this._connectLock = false;
|
|
2901
|
+
this._handleError(new Events.ErrorEvent(Error(err.message), this));
|
|
2902
|
+
});
|
|
2903
|
+
}
|
|
2904
|
+
_handleTimeout() {
|
|
2905
|
+
this._debug("timeout event");
|
|
2906
|
+
this._handleError(new Events.ErrorEvent(Error("TIMEOUT"), this));
|
|
2907
|
+
}
|
|
2908
|
+
_disconnect(code = 1000, reason) {
|
|
2909
|
+
this._clearTimeouts();
|
|
2910
|
+
if (!this._ws) {
|
|
2911
|
+
return;
|
|
2912
|
+
}
|
|
2913
|
+
this._removeListeners();
|
|
2914
|
+
try {
|
|
2915
|
+
if (this._ws.readyState === this.OPEN || this._ws.readyState === this.CONNECTING) {
|
|
2916
|
+
this._ws.close(code, reason);
|
|
2917
|
+
}
|
|
2918
|
+
this._handleClose(new Events.CloseEvent(code, reason, this));
|
|
2919
|
+
} catch (_error) {}
|
|
2920
|
+
}
|
|
2921
|
+
_acceptOpen() {
|
|
2922
|
+
this._debug("accept open");
|
|
2923
|
+
this._retryCount = 0;
|
|
2924
|
+
}
|
|
2925
|
+
_handleOpen = (event) => {
|
|
2926
|
+
this._debug("open event");
|
|
2927
|
+
const { minUptime = DEFAULT.minUptime } = this._options;
|
|
2928
|
+
clearTimeout(this._connectTimeout);
|
|
2929
|
+
this._uptimeTimeout = setTimeout(() => this._acceptOpen(), minUptime);
|
|
2930
|
+
assert2(this._ws, "WebSocket is not defined");
|
|
2931
|
+
this._ws.binaryType = this._binaryType;
|
|
2932
|
+
this._messageQueue.forEach((message) => {
|
|
2933
|
+
var _a2;
|
|
2934
|
+
(_a2 = this._ws) == null || _a2.send(message);
|
|
2935
|
+
});
|
|
2936
|
+
this._messageQueue = [];
|
|
2937
|
+
if (this.onopen) {
|
|
2938
|
+
this.onopen(event);
|
|
2939
|
+
}
|
|
2940
|
+
this.dispatchEvent(cloneEvent(event));
|
|
2941
|
+
};
|
|
2942
|
+
_handleMessage = (event) => {
|
|
2943
|
+
this._debug("message event");
|
|
2944
|
+
if (this.onmessage) {
|
|
2945
|
+
this.onmessage(event);
|
|
2946
|
+
}
|
|
2947
|
+
this.dispatchEvent(cloneEvent(event));
|
|
2948
|
+
};
|
|
2949
|
+
_handleError = (event) => {
|
|
2950
|
+
this._debug("error event", event.message);
|
|
2951
|
+
this._disconnect(undefined, event.message === "TIMEOUT" ? "timeout" : undefined);
|
|
2952
|
+
if (this.onerror) {
|
|
2953
|
+
this.onerror(event);
|
|
2954
|
+
}
|
|
2955
|
+
this._debug("exec error listeners");
|
|
2956
|
+
this.dispatchEvent(cloneEvent(event));
|
|
2957
|
+
this._connect();
|
|
2958
|
+
};
|
|
2959
|
+
_handleClose = (event) => {
|
|
2960
|
+
this._debug("close event");
|
|
2961
|
+
this._clearTimeouts();
|
|
2962
|
+
if (this._shouldReconnect) {
|
|
2963
|
+
this._connect();
|
|
2964
|
+
}
|
|
2965
|
+
if (this.onclose) {
|
|
2966
|
+
this.onclose(event);
|
|
2967
|
+
}
|
|
2968
|
+
this.dispatchEvent(cloneEvent(event));
|
|
2969
|
+
};
|
|
2970
|
+
_removeListeners() {
|
|
2971
|
+
if (!this._ws) {
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
2974
|
+
this._debug("removeListeners");
|
|
2975
|
+
this._ws.removeEventListener("open", this._handleOpen);
|
|
2976
|
+
this._ws.removeEventListener("close", this._handleClose);
|
|
2977
|
+
this._ws.removeEventListener("message", this._handleMessage);
|
|
2978
|
+
this._ws.removeEventListener("error", this._handleError);
|
|
2979
|
+
}
|
|
2980
|
+
_addListeners() {
|
|
2981
|
+
if (!this._ws) {
|
|
2982
|
+
return;
|
|
2983
|
+
}
|
|
2984
|
+
this._debug("addListeners");
|
|
2985
|
+
this._ws.addEventListener("open", this._handleOpen);
|
|
2986
|
+
this._ws.addEventListener("close", this._handleClose);
|
|
2987
|
+
this._ws.addEventListener("message", this._handleMessage);
|
|
2988
|
+
this._ws.addEventListener("error", this._handleError);
|
|
2989
|
+
}
|
|
2990
|
+
_clearTimeouts() {
|
|
2991
|
+
clearTimeout(this._connectTimeout);
|
|
2992
|
+
clearTimeout(this._uptimeTimeout);
|
|
2993
|
+
}
|
|
2994
|
+
};
|
|
2995
|
+
/*!
|
|
2996
|
+
* Reconnecting WebSocket
|
|
2997
|
+
* by Pedro Ladaria <pedro.ladaria@gmail.com>
|
|
2998
|
+
* https://github.com/pladaria/reconnecting-websocket
|
|
2999
|
+
* License MIT
|
|
3000
|
+
*/
|
|
3001
|
+
|
|
3002
|
+
// src/netcode/logs.ts
|
|
3003
|
+
var logger = {
|
|
3004
|
+
onLog: null,
|
|
3005
|
+
matchFrame: -1,
|
|
3006
|
+
frameNumber: -1,
|
|
3007
|
+
log(opts) {
|
|
3008
|
+
this.onLog?.({
|
|
3009
|
+
...opts,
|
|
3010
|
+
frame_number: this.frameNumber,
|
|
3011
|
+
match_frame: this.matchFrame >= 0 ? this.matchFrame : null,
|
|
3012
|
+
timestamp: Date.now(),
|
|
3013
|
+
severity: "log"
|
|
3014
|
+
});
|
|
3015
|
+
},
|
|
3016
|
+
warn(opts) {
|
|
3017
|
+
this.onLog?.({
|
|
3018
|
+
...opts,
|
|
3019
|
+
frame_number: this.frameNumber,
|
|
3020
|
+
match_frame: this.matchFrame >= 0 ? this.matchFrame : null,
|
|
3021
|
+
timestamp: Date.now(),
|
|
3022
|
+
severity: "warn"
|
|
3023
|
+
});
|
|
3024
|
+
},
|
|
3025
|
+
error(opts) {
|
|
3026
|
+
this.onLog?.({
|
|
3027
|
+
...opts,
|
|
3028
|
+
frame_number: this.frameNumber,
|
|
3029
|
+
match_frame: this.matchFrame >= 0 ? this.matchFrame : null,
|
|
3030
|
+
timestamp: Date.now(),
|
|
3031
|
+
severity: "error"
|
|
3032
|
+
});
|
|
3033
|
+
}
|
|
3034
|
+
};
|
|
3035
|
+
|
|
3036
|
+
// src/netcode/transport.ts
|
|
3037
|
+
var cachedIceServers = null;
|
|
3038
|
+
var cacheExpiry = 0;
|
|
3039
|
+
var TURN_CREDENTIALS_URL = "https://webrtc-divine-glade-8064.fly.dev/turn-credentials";
|
|
3040
|
+
var DEFAULT_ICE_TIMEOUT = 60000;
|
|
3041
|
+
async function getIceServers() {
|
|
3042
|
+
const now = Date.now();
|
|
3043
|
+
if (cachedIceServers && now < cacheExpiry) {
|
|
3044
|
+
return cachedIceServers;
|
|
3045
|
+
}
|
|
3046
|
+
const res = await fetch(TURN_CREDENTIALS_URL);
|
|
3047
|
+
if (!res.ok) {
|
|
3048
|
+
throw new Error(`TURN credentials fetch failed: ${res.status}`);
|
|
3049
|
+
}
|
|
3050
|
+
const data = await res.json();
|
|
3051
|
+
if (!data.iceServers) {
|
|
3052
|
+
throw new Error("No iceServers in TURN response");
|
|
3053
|
+
}
|
|
3054
|
+
cachedIceServers = data.iceServers.map((server) => {
|
|
3055
|
+
if (Array.isArray(server.urls)) {
|
|
3056
|
+
return {
|
|
3057
|
+
...server,
|
|
3058
|
+
urls: server.urls.filter((url) => !url.includes(":53"))
|
|
3059
|
+
};
|
|
3060
|
+
}
|
|
3061
|
+
return server;
|
|
3062
|
+
});
|
|
3063
|
+
assert(cachedIceServers && cachedIceServers.length > 0, "No valid iceServers after filtering");
|
|
3064
|
+
cacheExpiry = now + 60 * 60 * 1000;
|
|
3065
|
+
logger.log({
|
|
3066
|
+
source: "webrtc",
|
|
3067
|
+
label: `Got TURN credentials`,
|
|
3068
|
+
json: {
|
|
3069
|
+
iceServers: cachedIceServers
|
|
3070
|
+
}
|
|
3071
|
+
});
|
|
3072
|
+
return cachedIceServers;
|
|
3073
|
+
}
|
|
3074
|
+
async function connect(ws, peerId, timeoutMs = DEFAULT_ICE_TIMEOUT) {
|
|
3075
|
+
const iceServers = await getIceServers();
|
|
3076
|
+
const pc = new RTCPeerConnection({ iceServers });
|
|
3077
|
+
const reliable = pc.createDataChannel("reliable", {});
|
|
3078
|
+
const unreliable = pc.createDataChannel("unreliable", {
|
|
3079
|
+
ordered: false,
|
|
3080
|
+
maxRetransmits: 0
|
|
3081
|
+
});
|
|
3082
|
+
const offer = await pc.createOffer();
|
|
3083
|
+
await pc.setLocalDescription(offer);
|
|
3084
|
+
logger.log({ source: "webrtc", label: "set offer", json: offer });
|
|
3085
|
+
await gatherIce(pc, timeoutMs);
|
|
3086
|
+
logger.log({ source: "webrtc", label: "gathered ICE candidates" });
|
|
3087
|
+
logger.log({
|
|
3088
|
+
source: "ws",
|
|
3089
|
+
label: "sending local description",
|
|
3090
|
+
json: pc.localDescription,
|
|
3091
|
+
to: peerId
|
|
3092
|
+
});
|
|
3093
|
+
send(ws, {
|
|
3094
|
+
type: "offer",
|
|
3095
|
+
payload: btoa(JSON.stringify(pc.localDescription)),
|
|
3096
|
+
target: peerId
|
|
3097
|
+
});
|
|
3098
|
+
await waitForAnswer(ws, pc, timeoutMs);
|
|
3099
|
+
return {
|
|
3100
|
+
peerConnection: pc,
|
|
3101
|
+
reliable,
|
|
3102
|
+
unreliable,
|
|
3103
|
+
peerId
|
|
3104
|
+
};
|
|
3105
|
+
}
|
|
3106
|
+
async function logErrors(dc) {
|
|
3107
|
+
dc.onerror = (event) => {
|
|
3108
|
+
logger.error({
|
|
3109
|
+
source: "webrtc",
|
|
3110
|
+
label: `error on ${dc.label} channel`,
|
|
3111
|
+
json: event.error
|
|
3112
|
+
});
|
|
3113
|
+
};
|
|
3114
|
+
dc.onclosing = (_event) => {
|
|
3115
|
+
logger.log({
|
|
3116
|
+
source: "webrtc",
|
|
3117
|
+
label: `closing ${dc.label} channel`
|
|
3118
|
+
});
|
|
3119
|
+
};
|
|
3120
|
+
dc.onopen = (_event) => {
|
|
3121
|
+
logger.log({
|
|
3122
|
+
source: "webrtc",
|
|
3123
|
+
label: `opened ${dc.label} channel`
|
|
3124
|
+
});
|
|
3125
|
+
};
|
|
3126
|
+
dc.onclose = (_event) => {
|
|
3127
|
+
logger.log({
|
|
3128
|
+
source: "webrtc",
|
|
3129
|
+
label: `closed ${dc.label} channel`
|
|
3130
|
+
});
|
|
3131
|
+
};
|
|
3132
|
+
}
|
|
3133
|
+
async function logPeerConnection(pc, peerId) {
|
|
3134
|
+
pc.onconnectionstatechange = () => {
|
|
3135
|
+
logger.log({
|
|
3136
|
+
source: "webrtc",
|
|
3137
|
+
label: `[${peerId.substring(0, 6)}] connectionState = ${pc.connectionState}`
|
|
3138
|
+
});
|
|
3139
|
+
};
|
|
3140
|
+
pc.onsignalingstatechange = () => {
|
|
3141
|
+
logger.log({
|
|
3142
|
+
source: "webrtc",
|
|
3143
|
+
label: `[${peerId.substring(0, 6)}] signalingState = ${pc.signalingState}`
|
|
3144
|
+
});
|
|
3145
|
+
};
|
|
3146
|
+
}
|
|
3147
|
+
async function gatherIce(pc, timeoutMs) {
|
|
3148
|
+
return new Promise((yes, no) => {
|
|
3149
|
+
setTimeout(() => no(new Error("Ice Gathering Timeout")), timeoutMs);
|
|
3150
|
+
pc.onicegatheringstatechange = () => {
|
|
3151
|
+
logger.log({
|
|
3152
|
+
source: "webrtc",
|
|
3153
|
+
label: `icegatheringstatechange: ${pc.iceGatheringState}`
|
|
3154
|
+
});
|
|
3155
|
+
if (pc.iceGatheringState === "complete") {
|
|
3156
|
+
yes(true);
|
|
3157
|
+
}
|
|
3158
|
+
};
|
|
3159
|
+
});
|
|
3160
|
+
}
|
|
3161
|
+
async function waitForAnswer(ws, pc, timeoutMs) {
|
|
3162
|
+
return new Promise((yes, no) => {
|
|
3163
|
+
const timeoutId = setTimeout(no, timeoutMs);
|
|
3164
|
+
const messageListener = async (event) => {
|
|
3165
|
+
const serverMsg = JSON.parse(event.data);
|
|
3166
|
+
if (serverMsg.type !== "message:json") {
|
|
3167
|
+
return;
|
|
3168
|
+
}
|
|
3169
|
+
const peerMsg = serverMsg.message;
|
|
3170
|
+
if (peerMsg.type !== "answer") {
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
3173
|
+
const answerDesc = JSON.parse(atob(peerMsg.payload));
|
|
3174
|
+
logger.log({
|
|
3175
|
+
source: "webrtc",
|
|
3176
|
+
label: "received answer",
|
|
3177
|
+
json: answerDesc
|
|
3178
|
+
});
|
|
3179
|
+
await pc.setRemoteDescription(new RTCSessionDescription(answerDesc));
|
|
3180
|
+
logger.log({
|
|
3181
|
+
source: "webrtc",
|
|
3182
|
+
label: "set remote description with answer"
|
|
3183
|
+
});
|
|
3184
|
+
clearTimeout(timeoutId);
|
|
3185
|
+
yes();
|
|
3186
|
+
};
|
|
3187
|
+
ws.addEventListener("message", messageListener);
|
|
3188
|
+
});
|
|
3189
|
+
}
|
|
3190
|
+
function listenForOffers(ws, cb) {
|
|
3191
|
+
const messageListener = async (event) => {
|
|
3192
|
+
const envelope = JSON.parse(event.data);
|
|
3193
|
+
if (envelope.type !== "message:json") {
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
const msg = envelope.message;
|
|
3197
|
+
if (msg.type !== "offer") {
|
|
3198
|
+
return;
|
|
3199
|
+
}
|
|
3200
|
+
logger.log({ source: "webrtc", label: "received offer" });
|
|
3201
|
+
const offer = JSON.parse(atob(msg.payload));
|
|
3202
|
+
const iceServers = await getIceServers();
|
|
3203
|
+
const pc = new RTCPeerConnection({ iceServers });
|
|
3204
|
+
await pc.setRemoteDescription(offer);
|
|
3205
|
+
logger.log({
|
|
3206
|
+
source: "webrtc",
|
|
3207
|
+
label: "set remote description",
|
|
3208
|
+
json: { offer, remoteDescription: pc.remoteDescription }
|
|
3209
|
+
});
|
|
3210
|
+
const answer = await pc.createAnswer();
|
|
3211
|
+
await pc.setLocalDescription(answer);
|
|
3212
|
+
logger.log({
|
|
3213
|
+
source: "webrtc",
|
|
3214
|
+
label: "set local description",
|
|
3215
|
+
json: pc.localDescription
|
|
3216
|
+
});
|
|
3217
|
+
await gatherIce(pc, DEFAULT_ICE_TIMEOUT);
|
|
3218
|
+
const channels = {
|
|
3219
|
+
reliable: null,
|
|
3220
|
+
unreliable: null
|
|
3221
|
+
};
|
|
3222
|
+
pc.ondatachannel = (event2) => {
|
|
3223
|
+
logger.log({
|
|
3224
|
+
source: "webrtc",
|
|
3225
|
+
label: `received datachannel ${event2.channel.label}`
|
|
3226
|
+
});
|
|
3227
|
+
switch (event2.channel.label) {
|
|
3228
|
+
case "reliable":
|
|
3229
|
+
channels.reliable = event2.channel;
|
|
3230
|
+
break;
|
|
3231
|
+
case "unreliable":
|
|
3232
|
+
channels.unreliable = event2.channel;
|
|
3233
|
+
break;
|
|
3234
|
+
}
|
|
3235
|
+
if (channels.reliable && channels.unreliable) {
|
|
3236
|
+
pc.ondatachannel = null;
|
|
3237
|
+
cb({
|
|
3238
|
+
peerConnection: pc,
|
|
3239
|
+
reliable: channels.reliable,
|
|
3240
|
+
unreliable: channels.unreliable,
|
|
3241
|
+
peerId: envelope.peerId
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
};
|
|
3245
|
+
logger.log({
|
|
3246
|
+
source: "webrtc",
|
|
3247
|
+
label: "sending answer",
|
|
3248
|
+
json: pc.localDescription
|
|
3249
|
+
});
|
|
3250
|
+
send(ws, {
|
|
3251
|
+
type: "answer",
|
|
3252
|
+
payload: btoa(JSON.stringify(pc.localDescription)),
|
|
3253
|
+
target: envelope.peerId
|
|
3254
|
+
});
|
|
3255
|
+
};
|
|
3256
|
+
ws.addEventListener("message", messageListener);
|
|
3257
|
+
}
|
|
3258
|
+
function send(ws, msg) {
|
|
3259
|
+
ws.send(JSON.stringify(msg));
|
|
3260
|
+
logger.log({ source: "ws", direction: "outbound", json: msg });
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
// src/netcode/broker.ts
|
|
3264
|
+
function joinRoom(brokerUrl, _roomId, cbs) {
|
|
3265
|
+
const broker = new ReconnectingWebSocket(brokerUrl);
|
|
3266
|
+
broker.addEventListener("open", () => {
|
|
3267
|
+
logger.log({
|
|
3268
|
+
source: "ws",
|
|
3269
|
+
label: "Connection opened"
|
|
3270
|
+
});
|
|
3271
|
+
});
|
|
3272
|
+
broker.addEventListener("close", (event) => {
|
|
3273
|
+
logger.warn({
|
|
3274
|
+
source: "ws",
|
|
3275
|
+
label: "Connection closed",
|
|
3276
|
+
json: event
|
|
3277
|
+
});
|
|
3278
|
+
});
|
|
3279
|
+
broker.addEventListener("error", (event) => {
|
|
3280
|
+
logger.error({
|
|
3281
|
+
source: "ws",
|
|
3282
|
+
label: "Connection error",
|
|
3283
|
+
json: event
|
|
3284
|
+
});
|
|
3285
|
+
});
|
|
3286
|
+
const pipes = new Map;
|
|
3287
|
+
let ourId = "";
|
|
3288
|
+
broker.addEventListener("message", async (event) => {
|
|
3289
|
+
try {
|
|
3290
|
+
const envelope = JSON.parse(event.data);
|
|
3291
|
+
logger.log({
|
|
3292
|
+
source: "ws",
|
|
3293
|
+
direction: "inbound",
|
|
3294
|
+
json: envelope
|
|
3295
|
+
});
|
|
3296
|
+
switch (envelope.type) {
|
|
3297
|
+
case "welcome":
|
|
3298
|
+
ourId = envelope.yourId;
|
|
3299
|
+
cbs.onPeerIdAssign(envelope.yourId);
|
|
3300
|
+
for (const peerId of envelope.peerIds) {
|
|
3301
|
+
if (peerId === ourId)
|
|
3302
|
+
continue;
|
|
3303
|
+
cbs.onPeerConnected(peerId);
|
|
3304
|
+
}
|
|
3305
|
+
break;
|
|
3306
|
+
case "message:json":
|
|
3307
|
+
break;
|
|
3308
|
+
case "peer:connect": {
|
|
3309
|
+
const pipe = await connect(broker, envelope.peerId);
|
|
3310
|
+
registerPipe(pipe, cbs);
|
|
3311
|
+
cbs.onPeerConnected(envelope.peerId);
|
|
3312
|
+
break;
|
|
3313
|
+
}
|
|
3314
|
+
case "peer:disconnect":
|
|
3315
|
+
cbs.onPeerDisconnected(envelope.peerId);
|
|
3316
|
+
break;
|
|
3317
|
+
default:
|
|
3318
|
+
logger.warn({
|
|
3319
|
+
source: "ws",
|
|
3320
|
+
label: `Unknown message type: ${envelope.type}`,
|
|
3321
|
+
json: envelope
|
|
3322
|
+
});
|
|
3323
|
+
}
|
|
3324
|
+
} catch (e) {
|
|
3325
|
+
logger.error({
|
|
3326
|
+
source: "ws",
|
|
3327
|
+
label: "Failure in message handler",
|
|
3328
|
+
json: {
|
|
3329
|
+
data: event.data,
|
|
3330
|
+
error: e
|
|
3331
|
+
}
|
|
3332
|
+
});
|
|
3333
|
+
}
|
|
3334
|
+
});
|
|
3335
|
+
listenForOffers(broker, (pipe) => {
|
|
3336
|
+
registerPipe(pipe, cbs);
|
|
3337
|
+
});
|
|
3338
|
+
function registerPipe(pipe, cbs2) {
|
|
3339
|
+
logErrors(pipe.reliable);
|
|
3340
|
+
logErrors(pipe.unreliable);
|
|
3341
|
+
logPeerConnection(pipe.peerConnection, ourId);
|
|
3342
|
+
cbs2.onDataChannelOpen(pipe.peerId, true, pipe.reliable);
|
|
3343
|
+
cbs2.onDataChannelOpen(pipe.peerId, false, pipe.unreliable);
|
|
3344
|
+
pipe.reliable.onmessage = (event) => {
|
|
3345
|
+
cbs2.onMessage(pipe.peerId, event.data, true);
|
|
3346
|
+
};
|
|
3347
|
+
pipe.reliable.onclose = () => {
|
|
3348
|
+
cbs2.onDataChannelClose(pipe.peerId, true);
|
|
3349
|
+
};
|
|
3350
|
+
pipe.unreliable.onmessage = (event) => {
|
|
3351
|
+
cbs2.onMessage(pipe.peerId, event.data, false);
|
|
3352
|
+
};
|
|
3353
|
+
pipe.unreliable.onclose = () => {
|
|
3354
|
+
cbs2.onDataChannelClose(pipe.peerId, false);
|
|
3355
|
+
};
|
|
3356
|
+
pipes.set(pipe.peerId, pipe);
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
|
|
2581
3360
|
// src/App.ts
|
|
3361
|
+
var DEFAULT_BROKER_URL = "wss://webrtc-divine-glade-8064.fly.dev/ws";
|
|
2582
3362
|
async function start(opts) {
|
|
2583
3363
|
if (!opts.sim) {
|
|
2584
3364
|
const { sim } = await mount({
|
|
@@ -2588,20 +3368,24 @@ async function start(opts) {
|
|
|
2588
3368
|
});
|
|
2589
3369
|
opts.sim = sim;
|
|
2590
3370
|
}
|
|
2591
|
-
const app = new App(opts.sim, opts.game);
|
|
3371
|
+
const app = new App(opts.sim, opts.game, opts.brokerUrl ?? DEFAULT_BROKER_URL);
|
|
2592
3372
|
return app;
|
|
2593
3373
|
}
|
|
2594
3374
|
|
|
2595
3375
|
class App {
|
|
2596
3376
|
#sim;
|
|
2597
3377
|
game;
|
|
3378
|
+
brokerUrl;
|
|
2598
3379
|
#rafHandle = null;
|
|
2599
3380
|
#unsubscribe = null;
|
|
2600
3381
|
#now = performance.now();
|
|
2601
|
-
constructor(sim, game) {
|
|
3382
|
+
constructor(sim, game, brokerUrl) {
|
|
2602
3383
|
this.#sim = sim;
|
|
2603
3384
|
this.game = game;
|
|
3385
|
+
this.brokerUrl = brokerUrl;
|
|
2604
3386
|
this.game.hooks.beforeFrame = (frame) => {
|
|
3387
|
+
logger.frameNumber = this.#sim.time.frame;
|
|
3388
|
+
logger.matchFrame = this.#sim.wasm.get_match_frame();
|
|
2605
3389
|
this.beforeFrame.notify(frame);
|
|
2606
3390
|
};
|
|
2607
3391
|
this.subscribe();
|
|
@@ -2612,6 +3396,9 @@ class App {
|
|
|
2612
3396
|
set sim(sim) {
|
|
2613
3397
|
this.#sim = sim;
|
|
2614
3398
|
}
|
|
3399
|
+
joinRoom(roomId, callbacks) {
|
|
3400
|
+
joinRoom(this.brokerUrl, roomId, callbacks);
|
|
3401
|
+
}
|
|
2615
3402
|
beforeFrame = createListener();
|
|
2616
3403
|
afterFrame = createListener();
|
|
2617
3404
|
subscribe() {
|
|
@@ -2748,10 +3535,18 @@ function createListener() {
|
|
|
2748
3535
|
unsubscribeAll: () => listeners.clear()
|
|
2749
3536
|
};
|
|
2750
3537
|
}
|
|
3538
|
+
// src/netcode/protocol.ts
|
|
3539
|
+
var PacketType;
|
|
3540
|
+
((PacketType2) => {
|
|
3541
|
+
PacketType2[PacketType2["None"] = 0] = "None";
|
|
3542
|
+
PacketType2[PacketType2["Inputs"] = 1] = "Inputs";
|
|
3543
|
+
})(PacketType ||= {});
|
|
2751
3544
|
export {
|
|
2752
3545
|
start,
|
|
3546
|
+
logger,
|
|
3547
|
+
PacketType,
|
|
2753
3548
|
App
|
|
2754
3549
|
};
|
|
2755
3550
|
|
|
2756
|
-
//# debugId=
|
|
3551
|
+
//# debugId=9F8E122AB076111B64756E2164756E21
|
|
2757
3552
|
//# sourceMappingURL=mod.js.map
|