@noya-app/noya-multiplayer-react 0.1.89 → 0.2.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/.turbo/turbo-build.log +11 -11
- package/CHANGELOG.md +44 -0
- package/dist/index.bundle.js +30 -30
- package/dist/index.d.mts +64 -15
- package/dist/index.d.ts +64 -15
- package/dist/index.js +425 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +428 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/WebSocketConnection.ts +556 -20
- package/src/__tests__/WebSocketConnection.test.ts +1180 -0
- package/src/__tests__/noyaApp.test.ts +28 -0
- package/src/noyaApp.ts +15 -0
package/dist/index.mjs
CHANGED
|
@@ -4844,12 +4844,12 @@ var TOKEN_PRIMITIVE = Symbol("primitive");
|
|
|
4844
4844
|
var TOKEN_ARGSEP = Symbol("argsep");
|
|
4845
4845
|
|
|
4846
4846
|
// ../../node_modules/uuid/dist/esm-node/rng.js
|
|
4847
|
-
import
|
|
4847
|
+
import crypto2 from "crypto";
|
|
4848
4848
|
var rnds8Pool = new Uint8Array(256);
|
|
4849
4849
|
var poolPtr = rnds8Pool.length;
|
|
4850
4850
|
function rng() {
|
|
4851
4851
|
if (poolPtr > rnds8Pool.length - 16) {
|
|
4852
|
-
|
|
4852
|
+
crypto2.randomFillSync(rnds8Pool);
|
|
4853
4853
|
poolPtr = 0;
|
|
4854
4854
|
}
|
|
4855
4855
|
return rnds8Pool.slice(poolPtr, poolPtr += 16);
|
|
@@ -7760,6 +7760,16 @@ function parseAppDataParameter(options) {
|
|
|
7760
7760
|
return null;
|
|
7761
7761
|
}
|
|
7762
7762
|
}
|
|
7763
|
+
function parseInspectorParameter(options) {
|
|
7764
|
+
const window2 = options?.window ?? globalThis;
|
|
7765
|
+
const location = window2.location;
|
|
7766
|
+
if (!location) return null;
|
|
7767
|
+
const params = new URLSearchParams((location.hash || "").replace(/^#/, ""));
|
|
7768
|
+
const inspector = params.get("inspector");
|
|
7769
|
+
if (inspector === "true") return true;
|
|
7770
|
+
if (inspector === "false") return false;
|
|
7771
|
+
return null;
|
|
7772
|
+
}
|
|
7763
7773
|
function applyClientIdentityFromAppData(appData) {
|
|
7764
7774
|
if (appData.clientId !== void 0) {
|
|
7765
7775
|
setClientId(appData.clientId);
|
|
@@ -7774,6 +7784,8 @@ function applyClientIdentityFromAppData(appData) {
|
|
|
7774
7784
|
function getAppData(initialState, schema, options) {
|
|
7775
7785
|
const resolvedInitialState = initialState instanceof Function ? initialState() : initialState;
|
|
7776
7786
|
let appData = parseAppDataParameter(options) ?? createDefaultAppData(resolvedInitialState);
|
|
7787
|
+
const inspector = parseInspectorParameter(options);
|
|
7788
|
+
if (inspector !== null) appData.inspector = inspector;
|
|
7777
7789
|
if (options?.overrideExistingState || appData.initialState === null) {
|
|
7778
7790
|
appData.initialState = resolvedInitialState;
|
|
7779
7791
|
}
|
|
@@ -8561,9 +8573,37 @@ function useResourceUrl(idOrStableId, options) {
|
|
|
8561
8573
|
|
|
8562
8574
|
// src/WebSocketConnection.ts
|
|
8563
8575
|
import {
|
|
8576
|
+
MAX_CLIENT_MESSAGE_BATCH_SIZE,
|
|
8577
|
+
MAX_REALTIME_INPUT_BATCHES_IN_FLIGHT,
|
|
8564
8578
|
ReconnectingWebSocket,
|
|
8565
8579
|
UserActivityDetector
|
|
8566
8580
|
} from "@noya-app/state-manager";
|
|
8581
|
+
function simulationSessionIdentity(state) {
|
|
8582
|
+
if (!state || typeof state !== "object" || Array.isArray(state)) return;
|
|
8583
|
+
const game = Reflect.get(state, "$game");
|
|
8584
|
+
if (!game || typeof game !== "object" || Array.isArray(game)) return;
|
|
8585
|
+
const startedAt = Reflect.get(game, "startedAt");
|
|
8586
|
+
if (startedAt === null || typeof startedAt === "string" || typeof startedAt === "number" && Number.isFinite(startedAt)) {
|
|
8587
|
+
return `startedAt:${typeof startedAt}:${String(startedAt)}`;
|
|
8588
|
+
}
|
|
8589
|
+
}
|
|
8590
|
+
function simulationStateSequence(state) {
|
|
8591
|
+
if (!state || typeof state !== "object" || Array.isArray(state)) return;
|
|
8592
|
+
const simulation = Reflect.get(state, "simulation");
|
|
8593
|
+
const simulationTick = simulation && typeof simulation === "object" ? Reflect.get(simulation, "tick") : void 0;
|
|
8594
|
+
if (Number.isSafeInteger(simulationTick) && simulationTick >= 0) {
|
|
8595
|
+
return simulationTick;
|
|
8596
|
+
}
|
|
8597
|
+
const game = Reflect.get(state, "$game");
|
|
8598
|
+
const gameTick = game && typeof game === "object" ? Reflect.get(game, "simulationTick") : void 0;
|
|
8599
|
+
return Number.isSafeInteger(gameTick) && gameTick >= 0 ? gameTick : void 0;
|
|
8600
|
+
}
|
|
8601
|
+
function sameSimulationSession(left, right) {
|
|
8602
|
+
return left.sessionIdentity === void 0 || right.sessionIdentity === void 0 || left.sessionIdentity === right.sessionIdentity;
|
|
8603
|
+
}
|
|
8604
|
+
function createRealtimeInputSessionId() {
|
|
8605
|
+
return typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
|
|
8606
|
+
}
|
|
8567
8607
|
var WebSocketConnection = class {
|
|
8568
8608
|
constructor(url, options = {}) {
|
|
8569
8609
|
this.url = url;
|
|
@@ -8578,12 +8618,40 @@ var WebSocketConnection = class {
|
|
|
8578
8618
|
}
|
|
8579
8619
|
ws;
|
|
8580
8620
|
closedForever = false;
|
|
8621
|
+
heartbeatIntervalHandle;
|
|
8622
|
+
silentTimeoutHandle;
|
|
8623
|
+
simulationClockPulseIntervalHandle;
|
|
8624
|
+
simulationClockPulseSocket;
|
|
8625
|
+
simulationClockDirectFailed = false;
|
|
8626
|
+
heartbeatSequence = 0;
|
|
8627
|
+
pendingSimulationFrames = /* @__PURE__ */ new Map();
|
|
8628
|
+
frameFlushHandle;
|
|
8629
|
+
frameFlushScheduler;
|
|
8630
|
+
droppedSimulationFrames = 0;
|
|
8631
|
+
pendingFrameReceipts = /* @__PURE__ */ new Map();
|
|
8632
|
+
deliveredSimulationFrameCursors = /* @__PURE__ */ new Map();
|
|
8633
|
+
queuedRealtimeInputs = [];
|
|
8634
|
+
/**
|
|
8635
|
+
* A batch stays here until the server explicitly confirms ingestion.
|
|
8636
|
+
* Reconnects resend these exact batches, so a frame that was already in
|
|
8637
|
+
* flight can never create a hole in an input stream.
|
|
8638
|
+
*/
|
|
8639
|
+
unconfirmedRealtimeBatches = [];
|
|
8640
|
+
lastSentRealtimeSequences = /* @__PURE__ */ new Map();
|
|
8641
|
+
realtimeInputSessionId = createRealtimeInputSessionId();
|
|
8642
|
+
nextRealtimeInputBatchSequence = 0;
|
|
8643
|
+
realtimeInputWatchdogHandle;
|
|
8644
|
+
realtimeBatchesSent = 0;
|
|
8645
|
+
realtimeInputsSent = 0;
|
|
8581
8646
|
async connect() {
|
|
8582
8647
|
if (this.closedForever) return;
|
|
8583
8648
|
this.ws.connect(this.url.toString());
|
|
8584
8649
|
}
|
|
8585
8650
|
handleOpen = () => {
|
|
8586
8651
|
if (this.closedForever) return;
|
|
8652
|
+
this.startHeartbeat();
|
|
8653
|
+
this.replayUnconfirmedRealtimeBatches();
|
|
8654
|
+
this.armRealtimeInputWatchdog();
|
|
8587
8655
|
if (this.options.debug) {
|
|
8588
8656
|
console.info("ws connected");
|
|
8589
8657
|
}
|
|
@@ -8594,16 +8662,124 @@ var WebSocketConnection = class {
|
|
|
8594
8662
|
};
|
|
8595
8663
|
handleMessage = (event) => {
|
|
8596
8664
|
if (this.closedForever) return;
|
|
8665
|
+
this.armSilentTimeout();
|
|
8597
8666
|
if (this.options.debug) {
|
|
8598
8667
|
console.info("ws receiving message ", event.data);
|
|
8599
8668
|
}
|
|
8600
8669
|
const parsed = JSON.parse(event.data);
|
|
8670
|
+
if (parsed.type === "simulationClockConfig") {
|
|
8671
|
+
this.applySimulationClockConfig(parsed);
|
|
8672
|
+
return;
|
|
8673
|
+
}
|
|
8674
|
+
if (parsed.type === "simulationFrame") {
|
|
8675
|
+
const cursor = {
|
|
8676
|
+
sequence: parsed.frame.sequence,
|
|
8677
|
+
sessionIdentity: simulationSessionIdentity(parsed.frame.state)
|
|
8678
|
+
};
|
|
8679
|
+
const receipt = {
|
|
8680
|
+
type: "simulationFrameReceipt",
|
|
8681
|
+
scriptId: parsed.frame.scriptId,
|
|
8682
|
+
sequence: parsed.frame.sequence
|
|
8683
|
+
};
|
|
8684
|
+
const pendingReceipt = this.pendingFrameReceipts.get(
|
|
8685
|
+
parsed.frame.scriptId
|
|
8686
|
+
);
|
|
8687
|
+
if (!pendingReceipt || receipt.sequence > pendingReceipt.sequence) {
|
|
8688
|
+
this.pendingFrameReceipts.set(parsed.frame.scriptId, receipt);
|
|
8689
|
+
}
|
|
8690
|
+
const delivered = this.deliveredSimulationFrameCursors.get(
|
|
8691
|
+
parsed.frame.scriptId
|
|
8692
|
+
);
|
|
8693
|
+
const pending = this.pendingSimulationFrames.get(parsed.frame.scriptId);
|
|
8694
|
+
const pendingCursor = pending ? {
|
|
8695
|
+
sequence: pending.frame.sequence,
|
|
8696
|
+
sessionIdentity: simulationSessionIdentity(pending.frame.state)
|
|
8697
|
+
} : void 0;
|
|
8698
|
+
if (delivered && sameSimulationSession(delivered, cursor) && cursor.sequence <= delivered.sequence || pendingCursor && sameSimulationSession(pendingCursor, cursor) && cursor.sequence <= pendingCursor.sequence) {
|
|
8699
|
+
this.droppedSimulationFrames += 1;
|
|
8700
|
+
this.scheduleSimulationFrameFlush();
|
|
8701
|
+
return;
|
|
8702
|
+
}
|
|
8703
|
+
if (pending) this.droppedSimulationFrames += 1;
|
|
8704
|
+
this.pendingSimulationFrames.set(parsed.frame.scriptId, parsed);
|
|
8705
|
+
this.scheduleSimulationFrameFlush();
|
|
8706
|
+
return;
|
|
8707
|
+
}
|
|
8708
|
+
if (parsed.type === "object" && this.shouldSuppressNonMonotonicObject(parsed.object)) {
|
|
8709
|
+
return;
|
|
8710
|
+
}
|
|
8711
|
+
if (parsed.type === "realtimeInputReceipt") {
|
|
8712
|
+
this.confirmRealtimeInputBatches(parsed.sessionId, parsed.sequence);
|
|
8713
|
+
return;
|
|
8714
|
+
}
|
|
8715
|
+
this.flushSimulationFrames();
|
|
8716
|
+
this.emitReceivedMessage(parsed);
|
|
8717
|
+
};
|
|
8718
|
+
emitReceivedMessage(message) {
|
|
8601
8719
|
this.options.onConnectionEvent?.({
|
|
8602
8720
|
type: "receive",
|
|
8603
|
-
message
|
|
8721
|
+
message
|
|
8604
8722
|
});
|
|
8605
|
-
}
|
|
8723
|
+
}
|
|
8724
|
+
scheduleSimulationFrameFlush() {
|
|
8725
|
+
if (this.frameFlushHandle !== void 0) return;
|
|
8726
|
+
const flush = () => {
|
|
8727
|
+
this.frameFlushHandle = void 0;
|
|
8728
|
+
this.frameFlushScheduler = void 0;
|
|
8729
|
+
this.flushSimulationFrames();
|
|
8730
|
+
};
|
|
8731
|
+
if (typeof requestAnimationFrame === "function") {
|
|
8732
|
+
this.frameFlushScheduler = "animationFrame";
|
|
8733
|
+
this.frameFlushHandle = requestAnimationFrame(flush);
|
|
8734
|
+
} else {
|
|
8735
|
+
this.frameFlushScheduler = "timeout";
|
|
8736
|
+
this.frameFlushHandle = setTimeout(flush, 0);
|
|
8737
|
+
}
|
|
8738
|
+
}
|
|
8739
|
+
flushSimulationFrames() {
|
|
8740
|
+
this.cancelSimulationFrameFlush();
|
|
8741
|
+
this.flushPendingFrameReceipts();
|
|
8742
|
+
const frames = Array.from(this.pendingSimulationFrames.values());
|
|
8743
|
+
this.pendingSimulationFrames.clear();
|
|
8744
|
+
for (const frame of frames) {
|
|
8745
|
+
this.deliveredSimulationFrameCursors.set(frame.frame.scriptId, {
|
|
8746
|
+
sequence: frame.frame.sequence,
|
|
8747
|
+
sessionIdentity: simulationSessionIdentity(frame.frame.state)
|
|
8748
|
+
});
|
|
8749
|
+
this.emitReceivedMessage(frame);
|
|
8750
|
+
}
|
|
8751
|
+
}
|
|
8752
|
+
shouldSuppressNonMonotonicObject(state) {
|
|
8753
|
+
const sequence = simulationStateSequence(state);
|
|
8754
|
+
if (sequence === void 0) return false;
|
|
8755
|
+
const cursor = {
|
|
8756
|
+
sequence,
|
|
8757
|
+
sessionIdentity: simulationSessionIdentity(state)
|
|
8758
|
+
};
|
|
8759
|
+
const previous = Array.from(this.deliveredSimulationFrameCursors.values());
|
|
8760
|
+
const comparable = previous.filter(
|
|
8761
|
+
(entry) => sameSimulationSession(entry, cursor)
|
|
8762
|
+
);
|
|
8763
|
+
if (cursor.sessionIdentity !== void 0 && previous.some((entry) => entry.sessionIdentity !== void 0) && comparable.length === 0) {
|
|
8764
|
+
this.deliveredSimulationFrameCursors.clear();
|
|
8765
|
+
return false;
|
|
8766
|
+
}
|
|
8767
|
+
return comparable.some((entry) => entry.sequence > cursor.sequence);
|
|
8768
|
+
}
|
|
8769
|
+
cancelSimulationFrameFlush() {
|
|
8770
|
+
if (this.frameFlushHandle === void 0) return;
|
|
8771
|
+
if (this.frameFlushScheduler === "animationFrame") {
|
|
8772
|
+
cancelAnimationFrame(this.frameFlushHandle);
|
|
8773
|
+
} else {
|
|
8774
|
+
clearTimeout(this.frameFlushHandle);
|
|
8775
|
+
}
|
|
8776
|
+
this.frameFlushHandle = void 0;
|
|
8777
|
+
this.frameFlushScheduler = void 0;
|
|
8778
|
+
}
|
|
8606
8779
|
handleClose = () => {
|
|
8780
|
+
this.stopHeartbeat();
|
|
8781
|
+
this.stopSimulationClockPulses();
|
|
8782
|
+
this.stopRealtimeInputWatchdog();
|
|
8607
8783
|
if (this.closedForever) return;
|
|
8608
8784
|
if (this.options.debug) {
|
|
8609
8785
|
console.info("ws disconnected");
|
|
@@ -8613,8 +8789,140 @@ var WebSocketConnection = class {
|
|
|
8613
8789
|
state: "CLOSED"
|
|
8614
8790
|
});
|
|
8615
8791
|
};
|
|
8792
|
+
startHeartbeat() {
|
|
8793
|
+
this.stopHeartbeat();
|
|
8794
|
+
this.armSilentTimeout();
|
|
8795
|
+
const intervalMs = this.options.heartbeatIntervalMs ?? 15e3;
|
|
8796
|
+
if (intervalMs <= 0) return;
|
|
8797
|
+
this.heartbeatIntervalHandle = setInterval(() => {
|
|
8798
|
+
if (this.closedForever || this.ws.state !== "OPEN") return;
|
|
8799
|
+
this.send({
|
|
8800
|
+
type: "ping",
|
|
8801
|
+
id: `heartbeat-${Date.now()}-${++this.heartbeatSequence}`,
|
|
8802
|
+
clientSentAt: Date.now()
|
|
8803
|
+
});
|
|
8804
|
+
}, intervalMs);
|
|
8805
|
+
}
|
|
8806
|
+
armSilentTimeout() {
|
|
8807
|
+
if (this.silentTimeoutHandle !== void 0) {
|
|
8808
|
+
clearTimeout(this.silentTimeoutHandle);
|
|
8809
|
+
this.silentTimeoutHandle = void 0;
|
|
8810
|
+
}
|
|
8811
|
+
const timeoutMs = this.options.silentTimeoutMs ?? 45e3;
|
|
8812
|
+
if (timeoutMs <= 0 || this.closedForever || this.ws.state !== "OPEN")
|
|
8813
|
+
return;
|
|
8814
|
+
this.silentTimeoutHandle = setTimeout(() => {
|
|
8815
|
+
this.silentTimeoutHandle = void 0;
|
|
8816
|
+
if (this.closedForever || this.ws.state !== "OPEN") return;
|
|
8817
|
+
this.stopSimulationClockPulses();
|
|
8818
|
+
this.stopHeartbeat();
|
|
8819
|
+
this.ws.reconnect();
|
|
8820
|
+
}, timeoutMs);
|
|
8821
|
+
}
|
|
8822
|
+
stopHeartbeat() {
|
|
8823
|
+
if (this.heartbeatIntervalHandle !== void 0) {
|
|
8824
|
+
clearInterval(this.heartbeatIntervalHandle);
|
|
8825
|
+
this.heartbeatIntervalHandle = void 0;
|
|
8826
|
+
}
|
|
8827
|
+
if (this.silentTimeoutHandle !== void 0) {
|
|
8828
|
+
clearTimeout(this.silentTimeoutHandle);
|
|
8829
|
+
this.silentTimeoutHandle = void 0;
|
|
8830
|
+
}
|
|
8831
|
+
this.cancelSimulationFrameFlush();
|
|
8832
|
+
this.pendingSimulationFrames.clear();
|
|
8833
|
+
this.pendingFrameReceipts.clear();
|
|
8834
|
+
}
|
|
8835
|
+
applySimulationClockConfig(config) {
|
|
8836
|
+
this.stopSimulationClockPulses();
|
|
8837
|
+
if (config.enabled !== true || !Number.isFinite(config.intervalMs) || config.intervalMs <= 0 || this.closedForever || this.ws.state !== "OPEN") {
|
|
8838
|
+
return;
|
|
8839
|
+
}
|
|
8840
|
+
const intervalMs = Math.max(1, Math.floor(config.intervalMs));
|
|
8841
|
+
if (config.pulsePath !== void 0) {
|
|
8842
|
+
this.openSimulationClockPulseSocket(config.pulsePath);
|
|
8843
|
+
}
|
|
8844
|
+
this.simulationClockPulseIntervalHandle = setInterval(() => {
|
|
8845
|
+
if (this.closedForever || this.ws.state !== "OPEN") return;
|
|
8846
|
+
const pulse = JSON.stringify({ type: "simulationClockPulse" });
|
|
8847
|
+
if (this.simulationClockPulseSocket?.state === "OPEN") {
|
|
8848
|
+
this.simulationClockPulseSocket.send(pulse);
|
|
8849
|
+
return;
|
|
8850
|
+
}
|
|
8851
|
+
if (this.simulationClockPulseSocket && !this.simulationClockDirectFailed) {
|
|
8852
|
+
return;
|
|
8853
|
+
}
|
|
8854
|
+
this.sendTransportMessage({ type: "simulationClockPulse" });
|
|
8855
|
+
}, intervalMs);
|
|
8856
|
+
}
|
|
8857
|
+
openSimulationClockPulseSocket(pulsePath) {
|
|
8858
|
+
if (!pulsePath.startsWith("/") || pulsePath.includes("?") || pulsePath.includes("#")) {
|
|
8859
|
+
this.simulationClockDirectFailed = true;
|
|
8860
|
+
return;
|
|
8861
|
+
}
|
|
8862
|
+
const pulseUrl = new URL(this.url);
|
|
8863
|
+
pulseUrl.pathname = pulsePath;
|
|
8864
|
+
pulseUrl.hash = "";
|
|
8865
|
+
const socket = new ReconnectingWebSocket({
|
|
8866
|
+
onopen: () => {
|
|
8867
|
+
if (this.simulationClockPulseSocket !== socket) return;
|
|
8868
|
+
this.simulationClockDirectFailed = false;
|
|
8869
|
+
},
|
|
8870
|
+
onclose: () => {
|
|
8871
|
+
if (this.simulationClockPulseSocket !== socket) return;
|
|
8872
|
+
this.simulationClockDirectFailed = true;
|
|
8873
|
+
},
|
|
8874
|
+
onerror: () => {
|
|
8875
|
+
if (this.simulationClockPulseSocket !== socket) return;
|
|
8876
|
+
this.simulationClockDirectFailed = true;
|
|
8877
|
+
}
|
|
8878
|
+
});
|
|
8879
|
+
this.simulationClockPulseSocket = socket;
|
|
8880
|
+
this.simulationClockDirectFailed = false;
|
|
8881
|
+
try {
|
|
8882
|
+
socket.connect(pulseUrl.toString());
|
|
8883
|
+
} catch {
|
|
8884
|
+
this.simulationClockPulseSocket = void 0;
|
|
8885
|
+
this.simulationClockDirectFailed = true;
|
|
8886
|
+
socket.shutdown();
|
|
8887
|
+
}
|
|
8888
|
+
}
|
|
8889
|
+
stopSimulationClockPulses() {
|
|
8890
|
+
if (this.simulationClockPulseIntervalHandle !== void 0) {
|
|
8891
|
+
clearInterval(this.simulationClockPulseIntervalHandle);
|
|
8892
|
+
this.simulationClockPulseIntervalHandle = void 0;
|
|
8893
|
+
}
|
|
8894
|
+
const socket = this.simulationClockPulseSocket;
|
|
8895
|
+
this.simulationClockPulseSocket = void 0;
|
|
8896
|
+
this.simulationClockDirectFailed = false;
|
|
8897
|
+
socket?.shutdown();
|
|
8898
|
+
}
|
|
8899
|
+
getSimulationFrameDiagnostics() {
|
|
8900
|
+
return {
|
|
8901
|
+
pending: this.pendingSimulationFrames.size,
|
|
8902
|
+
dropped: this.droppedSimulationFrames,
|
|
8903
|
+
pendingReceipts: this.pendingFrameReceipts.size,
|
|
8904
|
+
queuedRealtimeInputs: this.queuedRealtimeInputs.length,
|
|
8905
|
+
replayRealtimeInputs: this.unconfirmedRealtimeBatches.reduce(
|
|
8906
|
+
(count, batch) => count + batch.messages.length,
|
|
8907
|
+
0
|
|
8908
|
+
),
|
|
8909
|
+
unconfirmedRealtimeBatches: this.unconfirmedRealtimeBatches.length,
|
|
8910
|
+
realtimeBatchesSent: this.realtimeBatchesSent,
|
|
8911
|
+
realtimeInputsSent: this.realtimeInputsSent,
|
|
8912
|
+
realtimeWatchdogArmed: this.realtimeInputWatchdogHandle !== void 0,
|
|
8913
|
+
simulationClockPulsesActive: this.simulationClockPulseIntervalHandle !== void 0
|
|
8914
|
+
};
|
|
8915
|
+
}
|
|
8616
8916
|
send(message) {
|
|
8617
8917
|
if (this.closedForever) return;
|
|
8918
|
+
if (message.type === "enqueueInput" && message.transport === "realtime" && this.getRealtimeInputSequence(message)) {
|
|
8919
|
+
this.queuedRealtimeInputs.push(message);
|
|
8920
|
+
this.armRealtimeInputWatchdog();
|
|
8921
|
+
return;
|
|
8922
|
+
}
|
|
8923
|
+
this.sendTransportMessage(message);
|
|
8924
|
+
}
|
|
8925
|
+
sendTransportMessage(message) {
|
|
8618
8926
|
if (this.options.debug) {
|
|
8619
8927
|
console.info("ws sending message", message);
|
|
8620
8928
|
}
|
|
@@ -8624,9 +8932,124 @@ var WebSocketConnection = class {
|
|
|
8624
8932
|
});
|
|
8625
8933
|
this.ws.send(JSON.stringify(message));
|
|
8626
8934
|
}
|
|
8935
|
+
getRealtimeInputSequence(message) {
|
|
8936
|
+
const payload = message.payload;
|
|
8937
|
+
if (!payload || typeof payload !== "object") return;
|
|
8938
|
+
const streamId = Reflect.get(payload, "streamId");
|
|
8939
|
+
const sequence = Reflect.get(payload, "sequence");
|
|
8940
|
+
if (typeof streamId !== "string" || streamId.length === 0 || !Number.isInteger(sequence) || sequence < 0) {
|
|
8941
|
+
return;
|
|
8942
|
+
}
|
|
8943
|
+
return {
|
|
8944
|
+
key: `${message.queue}\0${streamId}`,
|
|
8945
|
+
sequence
|
|
8946
|
+
};
|
|
8947
|
+
}
|
|
8948
|
+
getContiguousRealtimeInputCount() {
|
|
8949
|
+
const sequences = new Map(this.lastSentRealtimeSequences);
|
|
8950
|
+
let count = 0;
|
|
8951
|
+
for (const message of this.queuedRealtimeInputs) {
|
|
8952
|
+
if (count >= MAX_CLIENT_MESSAGE_BATCH_SIZE) break;
|
|
8953
|
+
const input = this.getRealtimeInputSequence(message);
|
|
8954
|
+
if (!input) break;
|
|
8955
|
+
const previous = sequences.get(input.key);
|
|
8956
|
+
if (input.sequence !== (previous === void 0 ? 0 : previous + 1)) {
|
|
8957
|
+
break;
|
|
8958
|
+
}
|
|
8959
|
+
sequences.set(input.key, input.sequence);
|
|
8960
|
+
count += 1;
|
|
8961
|
+
}
|
|
8962
|
+
return { count, sequences };
|
|
8963
|
+
}
|
|
8964
|
+
flushRealtimeBatch(receipt) {
|
|
8965
|
+
if (this.closedForever || this.ws.state !== "OPEN") return false;
|
|
8966
|
+
const canSendInputBatch = this.unconfirmedRealtimeBatches.length < MAX_REALTIME_INPUT_BATCHES_IN_FLIGHT;
|
|
8967
|
+
const contiguous = this.getContiguousRealtimeInputCount();
|
|
8968
|
+
const messages = canSendInputBatch ? this.queuedRealtimeInputs.slice(0, contiguous.count) : [];
|
|
8969
|
+
if (!receipt && messages.length === 0) return false;
|
|
8970
|
+
const batch = {
|
|
8971
|
+
type: "clientMessageBatch",
|
|
8972
|
+
...receipt && { receipt },
|
|
8973
|
+
...messages.length > 0 && {
|
|
8974
|
+
inputBatch: {
|
|
8975
|
+
sessionId: this.realtimeInputSessionId,
|
|
8976
|
+
sequence: this.nextRealtimeInputBatchSequence
|
|
8977
|
+
}
|
|
8978
|
+
},
|
|
8979
|
+
messages
|
|
8980
|
+
};
|
|
8981
|
+
this.sendTransportMessage(batch);
|
|
8982
|
+
if (messages.length > 0) {
|
|
8983
|
+
this.queuedRealtimeInputs.splice(0, messages.length);
|
|
8984
|
+
this.lastSentRealtimeSequences = contiguous.sequences;
|
|
8985
|
+
this.unconfirmedRealtimeBatches.push(batch);
|
|
8986
|
+
this.nextRealtimeInputBatchSequence += 1;
|
|
8987
|
+
this.realtimeInputsSent += messages.length;
|
|
8988
|
+
}
|
|
8989
|
+
this.realtimeBatchesSent += 1;
|
|
8990
|
+
return true;
|
|
8991
|
+
}
|
|
8992
|
+
flushPendingFrameReceipts() {
|
|
8993
|
+
for (const [scriptId, receipt] of Array.from(
|
|
8994
|
+
this.pendingFrameReceipts.entries()
|
|
8995
|
+
)) {
|
|
8996
|
+
if (!this.flushRealtimeBatch(receipt)) continue;
|
|
8997
|
+
if (this.pendingFrameReceipts.get(scriptId) === receipt) {
|
|
8998
|
+
this.pendingFrameReceipts.delete(scriptId);
|
|
8999
|
+
}
|
|
9000
|
+
}
|
|
9001
|
+
if (this.queuedRealtimeInputs.length > 0) {
|
|
9002
|
+
this.armRealtimeInputWatchdog();
|
|
9003
|
+
} else {
|
|
9004
|
+
this.stopRealtimeInputWatchdog();
|
|
9005
|
+
}
|
|
9006
|
+
}
|
|
9007
|
+
armRealtimeInputWatchdog() {
|
|
9008
|
+
if (this.realtimeInputWatchdogHandle !== void 0 || this.queuedRealtimeInputs.length === 0 || this.closedForever || this.ws.state !== "OPEN") {
|
|
9009
|
+
return;
|
|
9010
|
+
}
|
|
9011
|
+
const configuredDelay = this.options.realtimeInputWatchdogMs;
|
|
9012
|
+
const delay = configuredDelay !== void 0 && Number.isFinite(configuredDelay) ? Math.max(1, Math.floor(configuredDelay)) : 200;
|
|
9013
|
+
this.realtimeInputWatchdogHandle = setTimeout(() => {
|
|
9014
|
+
this.realtimeInputWatchdogHandle = void 0;
|
|
9015
|
+
this.flushRealtimeBatch();
|
|
9016
|
+
if (this.queuedRealtimeInputs.length > 0) {
|
|
9017
|
+
this.armRealtimeInputWatchdog();
|
|
9018
|
+
}
|
|
9019
|
+
}, delay);
|
|
9020
|
+
}
|
|
9021
|
+
stopRealtimeInputWatchdog() {
|
|
9022
|
+
if (this.realtimeInputWatchdogHandle === void 0) return;
|
|
9023
|
+
clearTimeout(this.realtimeInputWatchdogHandle);
|
|
9024
|
+
this.realtimeInputWatchdogHandle = void 0;
|
|
9025
|
+
}
|
|
9026
|
+
replayUnconfirmedRealtimeBatches() {
|
|
9027
|
+
if (this.closedForever || this.ws.state !== "OPEN") return;
|
|
9028
|
+
for (const batch of this.unconfirmedRealtimeBatches) {
|
|
9029
|
+
this.sendTransportMessage(batch);
|
|
9030
|
+
}
|
|
9031
|
+
}
|
|
9032
|
+
confirmRealtimeInputBatches(sessionId, sequence) {
|
|
9033
|
+
if (sessionId !== this.realtimeInputSessionId || !Number.isInteger(sequence) || sequence < 0) {
|
|
9034
|
+
return;
|
|
9035
|
+
}
|
|
9036
|
+
this.unconfirmedRealtimeBatches = this.unconfirmedRealtimeBatches.filter(
|
|
9037
|
+
(batch) => !batch.inputBatch || batch.inputBatch.sequence > sequence
|
|
9038
|
+
);
|
|
9039
|
+
if (this.queuedRealtimeInputs.length > 0) {
|
|
9040
|
+
this.armRealtimeInputWatchdog();
|
|
9041
|
+
}
|
|
9042
|
+
}
|
|
8627
9043
|
close() {
|
|
8628
9044
|
if (this.closedForever) return;
|
|
8629
9045
|
this.closedForever = true;
|
|
9046
|
+
this.stopHeartbeat();
|
|
9047
|
+
this.stopSimulationClockPulses();
|
|
9048
|
+
this.stopRealtimeInputWatchdog();
|
|
9049
|
+
this.queuedRealtimeInputs = [];
|
|
9050
|
+
this.unconfirmedRealtimeBatches = [];
|
|
9051
|
+
this.lastSentRealtimeSequences.clear();
|
|
9052
|
+
this.deliveredSimulationFrameCursors.clear();
|
|
8630
9053
|
this.ws.shutdown();
|
|
8631
9054
|
this.ws.listeners.forEach((listener) => {
|
|
8632
9055
|
this.ws.removeListener(listener);
|
|
@@ -8662,6 +9085,7 @@ export {
|
|
|
8662
9085
|
importAll,
|
|
8663
9086
|
initializeNoyaSingleton,
|
|
8664
9087
|
parseAppDataParameter,
|
|
9088
|
+
parseInspectorParameter,
|
|
8665
9089
|
replaceDeep,
|
|
8666
9090
|
resourceToCreateParameters,
|
|
8667
9091
|
resourceToMediaItem,
|