@noya-app/noya-multiplayer-react 0.1.88 → 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 +53 -0
- package/dist/index.bundle.js +34 -30
- package/dist/index.d.mts +187 -18
- package/dist/index.d.ts +187 -18
- package/dist/index.js +496 -47
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +499 -49
- 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/src/useResourceUrl.ts +4 -3
package/dist/index.mjs
CHANGED
|
@@ -4100,49 +4100,67 @@ var gistSchema = Type.Object({
|
|
|
4100
4100
|
|
|
4101
4101
|
// ../noya-schemas/src/jsonSchema.ts
|
|
4102
4102
|
var jsonSchema = Type.Recursive(
|
|
4103
|
-
(This) => Type.Union(
|
|
4104
|
-
|
|
4105
|
-
Type.
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
Type.
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
Type.
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
Type.
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
Type.
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
Type.
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4103
|
+
(This) => Type.Union([
|
|
4104
|
+
Type.Object({
|
|
4105
|
+
type: Type.Literal("string"),
|
|
4106
|
+
default: Type.Optional(Type.String()),
|
|
4107
|
+
_kind: Type.Literal("String")
|
|
4108
|
+
}),
|
|
4109
|
+
Type.Object({
|
|
4110
|
+
type: Type.Literal("number"),
|
|
4111
|
+
default: Type.Optional(Type.Number()),
|
|
4112
|
+
minimum: Type.Optional(Type.Number()),
|
|
4113
|
+
maximum: Type.Optional(Type.Number()),
|
|
4114
|
+
_kind: Type.Literal("Number")
|
|
4115
|
+
}),
|
|
4116
|
+
Type.Object({
|
|
4117
|
+
type: Type.Literal("boolean"),
|
|
4118
|
+
default: Type.Optional(Type.Boolean()),
|
|
4119
|
+
_kind: Type.Literal("Boolean")
|
|
4120
|
+
}),
|
|
4121
|
+
Type.Object({
|
|
4122
|
+
type: Type.Literal("null"),
|
|
4123
|
+
_kind: Type.Literal("Null")
|
|
4124
|
+
}),
|
|
4125
|
+
Type.Object({
|
|
4126
|
+
type: Type.Literal("array"),
|
|
4127
|
+
items: This,
|
|
4128
|
+
// default: Type.Optional(Type.Array(Type.Any())),
|
|
4129
|
+
// minItems: Type.Optional(Type.Number()),
|
|
4130
|
+
// maxItems: Type.Optional(Type.Number()),
|
|
4131
|
+
_kind: Type.Literal("Array")
|
|
4132
|
+
}),
|
|
4133
|
+
Type.Object({
|
|
4134
|
+
type: Type.Literal("object"),
|
|
4135
|
+
properties: Type.Record(Type.String(), This),
|
|
4136
|
+
required: Type.Optional(Type.Array(Type.String())),
|
|
4137
|
+
// default: Type.Optional(Type.Record(Type.String(), Type.Any())),
|
|
4138
|
+
_kind: Type.Literal("Object")
|
|
4139
|
+
}),
|
|
4140
|
+
Type.Object({
|
|
4141
|
+
const: Type.Union([
|
|
4142
|
+
Type.String(),
|
|
4143
|
+
Type.Number(),
|
|
4144
|
+
Type.Boolean(),
|
|
4145
|
+
Type.Null()
|
|
4146
|
+
]),
|
|
4147
|
+
type: Type.Optional(
|
|
4148
|
+
Type.Union([
|
|
4149
|
+
Type.Literal("string"),
|
|
4150
|
+
Type.Literal("number"),
|
|
4151
|
+
Type.Literal("boolean"),
|
|
4152
|
+
Type.Literal("null")
|
|
4153
|
+
])
|
|
4154
|
+
),
|
|
4155
|
+
_kind: Type.Literal("Literal")
|
|
4156
|
+
}),
|
|
4157
|
+
Type.Object({
|
|
4158
|
+
anyOf: Type.Array(This, { minItems: 1 }),
|
|
4159
|
+
discriminator: Type.Optional(Type.String()),
|
|
4160
|
+
default: Type.Optional(Type.Any()),
|
|
4161
|
+
_kind: Type.Literal("Union")
|
|
4162
|
+
})
|
|
4163
|
+
]),
|
|
4146
4164
|
{
|
|
4147
4165
|
$id: "jsonSchema"
|
|
4148
4166
|
}
|
|
@@ -4412,6 +4430,13 @@ function findAllDefs(schema) {
|
|
|
4412
4430
|
}
|
|
4413
4431
|
|
|
4414
4432
|
// ../noya-schemas/src/resourceSchema.ts
|
|
4433
|
+
var targetFileSchema = Type.Object({
|
|
4434
|
+
id: Type.String(),
|
|
4435
|
+
toolId: Type.String(),
|
|
4436
|
+
tool: Type.Object({
|
|
4437
|
+
theme: Type.Optional(Type.Any())
|
|
4438
|
+
})
|
|
4439
|
+
});
|
|
4415
4440
|
var baseSchemaProperties = {
|
|
4416
4441
|
id: Type.String(),
|
|
4417
4442
|
stableId: Type.String(),
|
|
@@ -4427,6 +4452,7 @@ var baseSchemaProperties = {
|
|
|
4427
4452
|
})
|
|
4428
4453
|
),
|
|
4429
4454
|
accessibleByFileVersionId: Nullable(Type.String()),
|
|
4455
|
+
file: Type.Optional(Nullable(targetFileSchema)),
|
|
4430
4456
|
url: Type.Optional(Type.String()),
|
|
4431
4457
|
transformUrl: Type.Optional(Type.String())
|
|
4432
4458
|
};
|
|
@@ -4818,12 +4844,12 @@ var TOKEN_PRIMITIVE = Symbol("primitive");
|
|
|
4818
4844
|
var TOKEN_ARGSEP = Symbol("argsep");
|
|
4819
4845
|
|
|
4820
4846
|
// ../../node_modules/uuid/dist/esm-node/rng.js
|
|
4821
|
-
import
|
|
4847
|
+
import crypto2 from "crypto";
|
|
4822
4848
|
var rnds8Pool = new Uint8Array(256);
|
|
4823
4849
|
var poolPtr = rnds8Pool.length;
|
|
4824
4850
|
function rng() {
|
|
4825
4851
|
if (poolPtr > rnds8Pool.length - 16) {
|
|
4826
|
-
|
|
4852
|
+
crypto2.randomFillSync(rnds8Pool);
|
|
4827
4853
|
poolPtr = 0;
|
|
4828
4854
|
}
|
|
4829
4855
|
return rnds8Pool.slice(poolPtr, poolPtr += 16);
|
|
@@ -7734,6 +7760,16 @@ function parseAppDataParameter(options) {
|
|
|
7734
7760
|
return null;
|
|
7735
7761
|
}
|
|
7736
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
|
+
}
|
|
7737
7773
|
function applyClientIdentityFromAppData(appData) {
|
|
7738
7774
|
if (appData.clientId !== void 0) {
|
|
7739
7775
|
setClientId(appData.clientId);
|
|
@@ -7748,6 +7784,8 @@ function applyClientIdentityFromAppData(appData) {
|
|
|
7748
7784
|
function getAppData(initialState, schema, options) {
|
|
7749
7785
|
const resolvedInitialState = initialState instanceof Function ? initialState() : initialState;
|
|
7750
7786
|
let appData = parseAppDataParameter(options) ?? createDefaultAppData(resolvedInitialState);
|
|
7787
|
+
const inspector = parseInspectorParameter(options);
|
|
7788
|
+
if (inspector !== null) appData.inspector = inspector;
|
|
7751
7789
|
if (options?.overrideExistingState || appData.initialState === null) {
|
|
7752
7790
|
appData.initialState = resolvedInitialState;
|
|
7753
7791
|
}
|
|
@@ -8523,21 +8561,49 @@ var UserPointersOverlay = memoGeneric2(function UserPointers({
|
|
|
8523
8561
|
|
|
8524
8562
|
// src/useResourceUrl.ts
|
|
8525
8563
|
import { getResourceUrl } from "@noya-app/state-manager";
|
|
8526
|
-
function useResourceUrl(idOrStableId) {
|
|
8564
|
+
function useResourceUrl(idOrStableId, options) {
|
|
8527
8565
|
const { noyaManager } = useAnyNoyaStateContext();
|
|
8528
8566
|
const resources = useObservable(
|
|
8529
8567
|
noyaManager.resourceManager.optimisticResources$
|
|
8530
8568
|
);
|
|
8531
8569
|
const assets = useObservable(noyaManager.assetManager.assets$);
|
|
8532
8570
|
if (!idOrStableId) return void 0;
|
|
8533
|
-
return getResourceUrl(idOrStableId, resources, assets);
|
|
8571
|
+
return getResourceUrl(idOrStableId, resources, assets, options);
|
|
8534
8572
|
}
|
|
8535
8573
|
|
|
8536
8574
|
// src/WebSocketConnection.ts
|
|
8537
8575
|
import {
|
|
8576
|
+
MAX_CLIENT_MESSAGE_BATCH_SIZE,
|
|
8577
|
+
MAX_REALTIME_INPUT_BATCHES_IN_FLIGHT,
|
|
8538
8578
|
ReconnectingWebSocket,
|
|
8539
8579
|
UserActivityDetector
|
|
8540
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
|
+
}
|
|
8541
8607
|
var WebSocketConnection = class {
|
|
8542
8608
|
constructor(url, options = {}) {
|
|
8543
8609
|
this.url = url;
|
|
@@ -8552,12 +8618,40 @@ var WebSocketConnection = class {
|
|
|
8552
8618
|
}
|
|
8553
8619
|
ws;
|
|
8554
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;
|
|
8555
8646
|
async connect() {
|
|
8556
8647
|
if (this.closedForever) return;
|
|
8557
8648
|
this.ws.connect(this.url.toString());
|
|
8558
8649
|
}
|
|
8559
8650
|
handleOpen = () => {
|
|
8560
8651
|
if (this.closedForever) return;
|
|
8652
|
+
this.startHeartbeat();
|
|
8653
|
+
this.replayUnconfirmedRealtimeBatches();
|
|
8654
|
+
this.armRealtimeInputWatchdog();
|
|
8561
8655
|
if (this.options.debug) {
|
|
8562
8656
|
console.info("ws connected");
|
|
8563
8657
|
}
|
|
@@ -8568,16 +8662,124 @@ var WebSocketConnection = class {
|
|
|
8568
8662
|
};
|
|
8569
8663
|
handleMessage = (event) => {
|
|
8570
8664
|
if (this.closedForever) return;
|
|
8665
|
+
this.armSilentTimeout();
|
|
8571
8666
|
if (this.options.debug) {
|
|
8572
8667
|
console.info("ws receiving message ", event.data);
|
|
8573
8668
|
}
|
|
8574
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) {
|
|
8575
8719
|
this.options.onConnectionEvent?.({
|
|
8576
8720
|
type: "receive",
|
|
8577
|
-
message
|
|
8721
|
+
message
|
|
8578
8722
|
});
|
|
8579
|
-
}
|
|
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
|
+
}
|
|
8580
8779
|
handleClose = () => {
|
|
8780
|
+
this.stopHeartbeat();
|
|
8781
|
+
this.stopSimulationClockPulses();
|
|
8782
|
+
this.stopRealtimeInputWatchdog();
|
|
8581
8783
|
if (this.closedForever) return;
|
|
8582
8784
|
if (this.options.debug) {
|
|
8583
8785
|
console.info("ws disconnected");
|
|
@@ -8587,8 +8789,140 @@ var WebSocketConnection = class {
|
|
|
8587
8789
|
state: "CLOSED"
|
|
8588
8790
|
});
|
|
8589
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
|
+
}
|
|
8590
8916
|
send(message) {
|
|
8591
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) {
|
|
8592
8926
|
if (this.options.debug) {
|
|
8593
8927
|
console.info("ws sending message", message);
|
|
8594
8928
|
}
|
|
@@ -8598,9 +8932,124 @@ var WebSocketConnection = class {
|
|
|
8598
8932
|
});
|
|
8599
8933
|
this.ws.send(JSON.stringify(message));
|
|
8600
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
|
+
}
|
|
8601
9043
|
close() {
|
|
8602
9044
|
if (this.closedForever) return;
|
|
8603
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();
|
|
8604
9053
|
this.ws.shutdown();
|
|
8605
9054
|
this.ws.listeners.forEach((listener) => {
|
|
8606
9055
|
this.ws.removeListener(listener);
|
|
@@ -8636,6 +9085,7 @@ export {
|
|
|
8636
9085
|
importAll,
|
|
8637
9086
|
initializeNoyaSingleton,
|
|
8638
9087
|
parseAppDataParameter,
|
|
9088
|
+
parseInspectorParameter,
|
|
8639
9089
|
replaceDeep,
|
|
8640
9090
|
resourceToCreateParameters,
|
|
8641
9091
|
resourceToMediaItem,
|