@series-inc/rundot-syncplay 5.23.0-beta.7
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 +610 -0
- package/dist/animation.d.ts +119 -0
- package/dist/animation.js +488 -0
- package/dist/asset-cooking.d.ts +82 -0
- package/dist/asset-cooking.js +364 -0
- package/dist/authority-room.d.ts +217 -0
- package/dist/authority-room.js +701 -0
- package/dist/bot-documents.d.ts +115 -0
- package/dist/bot-documents.js +549 -0
- package/dist/bot-primitives.d.ts +152 -0
- package/dist/bot-primitives.js +472 -0
- package/dist/browser.d.ts +57 -0
- package/dist/browser.js +53 -0
- package/dist/canonical.d.ts +12 -0
- package/dist/canonical.js +288 -0
- package/dist/certification.d.ts +455 -0
- package/dist/certification.js +1855 -0
- package/dist/chaos-certification.d.ts +56 -0
- package/dist/chaos-certification.js +1397 -0
- package/dist/cjs/animation.js +495 -0
- package/dist/cjs/asset-cooking.js +372 -0
- package/dist/cjs/authority-room.js +704 -0
- package/dist/cjs/bot-documents.js +556 -0
- package/dist/cjs/bot-primitives.js +482 -0
- package/dist/cjs/browser.js +96 -0
- package/dist/cjs/canonical.js +299 -0
- package/dist/cjs/certification.js +1860 -0
- package/dist/cjs/chaos-certification.js +1402 -0
- package/dist/cjs/collections.js +36 -0
- package/dist/cjs/collision.js +96 -0
- package/dist/cjs/commands.js +987 -0
- package/dist/cjs/config-bundle.js +491 -0
- package/dist/cjs/creator.js +30 -0
- package/dist/cjs/deterministic-primitives.js +134 -0
- package/dist/cjs/deterministic-session.js +1410 -0
- package/dist/cjs/ecs-lite.js +155 -0
- package/dist/cjs/effects-adapter.js +97 -0
- package/dist/cjs/engine-complete.js +2297 -0
- package/dist/cjs/engine-core-proof.js +70 -0
- package/dist/cjs/engine-parity-matrix.js +874 -0
- package/dist/cjs/engine-parity.js +3183 -0
- package/dist/cjs/engine-runtime.js +3141 -0
- package/dist/cjs/errors.js +22 -0
- package/dist/cjs/events.js +247 -0
- package/dist/cjs/examples/counter-duel.js +157 -0
- package/dist/cjs/examples/fixed-arena.js +368 -0
- package/dist/cjs/index.js +229 -0
- package/dist/cjs/input-authority.js +150 -0
- package/dist/cjs/input-button.js +256 -0
- package/dist/cjs/input-timeline.js +418 -0
- package/dist/cjs/instant-replay.js +66 -0
- package/dist/cjs/lag-compensation3d.js +837 -0
- package/dist/cjs/local-authority-server-runner.js +1121 -0
- package/dist/cjs/local-authority-wire.js +1497 -0
- package/dist/cjs/local-authority.js +817 -0
- package/dist/cjs/local-ws-adapter.js +150 -0
- package/dist/cjs/match-log.js +61 -0
- package/dist/cjs/math.js +157 -0
- package/dist/cjs/mock-network-adapter.js +26 -0
- package/dist/cjs/movement.js +703 -0
- package/dist/cjs/movement3d.js +1246 -0
- package/dist/cjs/multiclient-authority.js +308 -0
- package/dist/cjs/multiplayer-service.js +434 -0
- package/dist/cjs/navigation.js +347 -0
- package/dist/cjs/navmesh.js +1437 -0
- package/dist/cjs/networked-client.js +691 -0
- package/dist/cjs/networked-input-authority.js +443 -0
- package/dist/cjs/node.js +47 -0
- package/dist/cjs/number-lab.js +78 -0
- package/dist/cjs/offline-session.js +370 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/pause-resume.js +43 -0
- package/dist/cjs/physics-cert.js +426 -0
- package/dist/cjs/physics2d.js +751 -0
- package/dist/cjs/physics3d.js +1996 -0
- package/dist/cjs/prediction-culling.js +203 -0
- package/dist/cjs/protocol-lab.js +1299 -0
- package/dist/cjs/r3f-render-adapter.js +209 -0
- package/dist/cjs/random.js +59 -0
- package/dist/cjs/replay-bundle.js +1598 -0
- package/dist/cjs/replay-inspect.js +236 -0
- package/dist/cjs/replay-scoring.js +68 -0
- package/dist/cjs/replay.js +356 -0
- package/dist/cjs/reports.js +181 -0
- package/dist/cjs/rollback-history.js +251 -0
- package/dist/cjs/runtime-guards.js +111 -0
- package/dist/cjs/sample-lab.js +145 -0
- package/dist/cjs/sample-scenes.js +752 -0
- package/dist/cjs/schema-artifacts.js +359 -0
- package/dist/cjs/schema-codegen.js +544 -0
- package/dist/cjs/schema-dsl-binary.js +655 -0
- package/dist/cjs/schema.js +319 -0
- package/dist/cjs/sdk-offline.js +99 -0
- package/dist/cjs/session-promotion.js +74 -0
- package/dist/cjs/session-snapshot.js +265 -0
- package/dist/cjs/session-wire.js +216 -0
- package/dist/cjs/signals.js +103 -0
- package/dist/cjs/sparse-set.js +169 -0
- package/dist/cjs/synctest.js +136 -0
- package/dist/cjs/system-lifecycle.js +286 -0
- package/dist/cjs/timers.js +61 -0
- package/dist/cjs/tools/static-checker.js +1468 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/venus-mp-budget.js +125 -0
- package/dist/cjs/ws-frame.js +255 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +11379 -0
- package/dist/collections.d.ts +9 -0
- package/dist/collections.js +30 -0
- package/dist/collision.d.ts +25 -0
- package/dist/collision.js +85 -0
- package/dist/commands.d.ts +137 -0
- package/dist/commands.js +977 -0
- package/dist/config-bundle.d.ts +89 -0
- package/dist/config-bundle.js +483 -0
- package/dist/creator.d.ts +20 -0
- package/dist/creator.js +11 -0
- package/dist/deterministic-primitives.d.ts +59 -0
- package/dist/deterministic-primitives.js +126 -0
- package/dist/deterministic-session.d.ts +197 -0
- package/dist/deterministic-session.js +1401 -0
- package/dist/ecs-lite.d.ts +26 -0
- package/dist/ecs-lite.js +152 -0
- package/dist/effects-adapter.d.ts +21 -0
- package/dist/effects-adapter.js +94 -0
- package/dist/engine-complete.d.ts +45 -0
- package/dist/engine-complete.js +2292 -0
- package/dist/engine-core-proof.d.ts +26 -0
- package/dist/engine-core-proof.js +66 -0
- package/dist/engine-parity-matrix.d.ts +300 -0
- package/dist/engine-parity-matrix.js +869 -0
- package/dist/engine-parity.d.ts +74 -0
- package/dist/engine-parity.js +3178 -0
- package/dist/engine-runtime.d.ts +351 -0
- package/dist/engine-runtime.js +3123 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +17 -0
- package/dist/events.d.ts +45 -0
- package/dist/events.js +242 -0
- package/dist/examples/counter-duel.d.ts +43 -0
- package/dist/examples/counter-duel.js +150 -0
- package/dist/examples/fixed-arena.d.ts +110 -0
- package/dist/examples/fixed-arena.js +356 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +66 -0
- package/dist/input-authority.d.ts +82 -0
- package/dist/input-authority.js +147 -0
- package/dist/input-button.d.ts +36 -0
- package/dist/input-button.js +249 -0
- package/dist/input-timeline.d.ts +65 -0
- package/dist/input-timeline.js +412 -0
- package/dist/instant-replay.d.ts +59 -0
- package/dist/instant-replay.js +62 -0
- package/dist/lag-compensation3d.d.ts +146 -0
- package/dist/lag-compensation3d.js +827 -0
- package/dist/local-authority-server-runner.d.ts +79 -0
- package/dist/local-authority-server-runner.js +1118 -0
- package/dist/local-authority-wire.d.ts +142 -0
- package/dist/local-authority-wire.js +1477 -0
- package/dist/local-authority.d.ts +160 -0
- package/dist/local-authority.js +810 -0
- package/dist/local-ws-adapter.d.ts +14 -0
- package/dist/local-ws-adapter.js +146 -0
- package/dist/match-log.d.ts +74 -0
- package/dist/match-log.js +57 -0
- package/dist/math.d.ts +41 -0
- package/dist/math.js +154 -0
- package/dist/mock-network-adapter.d.ts +13 -0
- package/dist/mock-network-adapter.js +23 -0
- package/dist/movement.d.ts +155 -0
- package/dist/movement.js +694 -0
- package/dist/movement3d.d.ts +195 -0
- package/dist/movement3d.js +1238 -0
- package/dist/multiclient-authority.d.ts +30 -0
- package/dist/multiclient-authority.js +305 -0
- package/dist/multiplayer-service.d.ts +153 -0
- package/dist/multiplayer-service.js +429 -0
- package/dist/navigation.d.ts +91 -0
- package/dist/navigation.js +338 -0
- package/dist/navmesh.d.ts +236 -0
- package/dist/navmesh.js +1423 -0
- package/dist/networked-client.d.ts +124 -0
- package/dist/networked-client.js +688 -0
- package/dist/networked-input-authority.d.ts +51 -0
- package/dist/networked-input-authority.js +439 -0
- package/dist/node.d.ts +22 -0
- package/dist/node.js +19 -0
- package/dist/number-lab.d.ts +15 -0
- package/dist/number-lab.js +75 -0
- package/dist/offline-session.d.ts +2 -0
- package/dist/offline-session.js +367 -0
- package/dist/pause-resume.d.ts +48 -0
- package/dist/pause-resume.js +40 -0
- package/dist/physics-cert.d.ts +40 -0
- package/dist/physics-cert.js +389 -0
- package/dist/physics2d.d.ts +142 -0
- package/dist/physics2d.js +741 -0
- package/dist/physics3d.d.ts +285 -0
- package/dist/physics3d.js +1979 -0
- package/dist/prediction-culling.d.ts +34 -0
- package/dist/prediction-culling.js +200 -0
- package/dist/protocol-lab.d.ts +115 -0
- package/dist/protocol-lab.js +1295 -0
- package/dist/r3f-render-adapter.d.ts +61 -0
- package/dist/r3f-render-adapter.js +203 -0
- package/dist/random.d.ts +13 -0
- package/dist/random.js +55 -0
- package/dist/replay-bundle.d.ts +150 -0
- package/dist/replay-bundle.js +1586 -0
- package/dist/replay-inspect.d.ts +60 -0
- package/dist/replay-inspect.js +228 -0
- package/dist/replay-scoring.d.ts +28 -0
- package/dist/replay-scoring.js +65 -0
- package/dist/replay.d.ts +17 -0
- package/dist/replay.js +351 -0
- package/dist/reports.d.ts +176 -0
- package/dist/reports.js +174 -0
- package/dist/rollback-history.d.ts +46 -0
- package/dist/rollback-history.js +247 -0
- package/dist/runtime-guards.d.ts +1 -0
- package/dist/runtime-guards.js +108 -0
- package/dist/sample-lab.d.ts +32 -0
- package/dist/sample-lab.js +134 -0
- package/dist/sample-scenes.d.ts +86 -0
- package/dist/sample-scenes.js +745 -0
- package/dist/schema-artifacts.d.ts +49 -0
- package/dist/schema-artifacts.js +356 -0
- package/dist/schema-codegen.d.ts +21 -0
- package/dist/schema-codegen.js +538 -0
- package/dist/schema-dsl-binary.d.ts +75 -0
- package/dist/schema-dsl-binary.js +648 -0
- package/dist/schema.d.ts +87 -0
- package/dist/schema.js +315 -0
- package/dist/sdk-offline.d.ts +34 -0
- package/dist/sdk-offline.js +96 -0
- package/dist/session-promotion.d.ts +26 -0
- package/dist/session-promotion.js +70 -0
- package/dist/session-snapshot.d.ts +95 -0
- package/dist/session-snapshot.js +255 -0
- package/dist/session-wire.d.ts +89 -0
- package/dist/session-wire.js +211 -0
- package/dist/signals.d.ts +31 -0
- package/dist/signals.js +99 -0
- package/dist/sparse-set.d.ts +34 -0
- package/dist/sparse-set.js +165 -0
- package/dist/synctest.d.ts +31 -0
- package/dist/synctest.js +133 -0
- package/dist/system-lifecycle.d.ts +72 -0
- package/dist/system-lifecycle.js +281 -0
- package/dist/timers.d.ts +17 -0
- package/dist/timers.js +53 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/static-checker.d.ts +31 -0
- package/dist/tools/static-checker.js +1462 -0
- package/dist/tools/vite-plugin.d.ts +69 -0
- package/dist/tools/vite-plugin.js +203 -0
- package/dist/types.d.ts +148 -0
- package/dist/types.js +1 -0
- package/dist/venus-mp-budget.d.ts +29 -0
- package/dist/venus-mp-budget.js +122 -0
- package/dist/ws-frame.d.ts +49 -0
- package/dist/ws-frame.js +247 -0
- package/package.json +169 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_MAX_SNAPSHOT_BYTES = exports.DEFAULT_SNAPSHOT_CHUNK_SIZE = void 0;
|
|
4
|
+
exports.deterministicRoomDescriptorHash = deterministicRoomDescriptorHash;
|
|
5
|
+
exports.encodeDeterministicStateSnapshot = encodeDeterministicStateSnapshot;
|
|
6
|
+
exports.decodeDeterministicStateSnapshot = decodeDeterministicStateSnapshot;
|
|
7
|
+
exports.bytesToBase64 = bytesToBase64;
|
|
8
|
+
exports.base64ToBytes = base64ToBytes;
|
|
9
|
+
exports.createSnapshotTransferFromBytes = createSnapshotTransferFromBytes;
|
|
10
|
+
exports.createSnapshotTransferCollector = createSnapshotTransferCollector;
|
|
11
|
+
const canonical_js_1 = require("./canonical.js");
|
|
12
|
+
/**
|
|
13
|
+
* Room-level snapshot identity, computable by the game-agnostic authority AND
|
|
14
|
+
* every client from the session-start parameters — the `descriptorHash` on
|
|
15
|
+
* snapshot wire envelopes. Game-build identity (`schemaHash`) is layered on
|
|
16
|
+
* top by clients, which know their schema ids; the authority passes it through
|
|
17
|
+
* opaquely.
|
|
18
|
+
*/
|
|
19
|
+
function deterministicRoomDescriptorHash(params) {
|
|
20
|
+
return (0, canonical_js_1.defaultChecksum)({
|
|
21
|
+
kind: 'rdm-room-descriptor',
|
|
22
|
+
seed: params.seed,
|
|
23
|
+
playerCount: params.playerCount,
|
|
24
|
+
tickRateHz: params.tickRateHz,
|
|
25
|
+
hardToleranceTicks: params.hardToleranceTicks,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function encodeDeterministicStateSnapshot(snapshot) {
|
|
29
|
+
return (0, canonical_js_1.canonicalStringify)(snapshot);
|
|
30
|
+
}
|
|
31
|
+
function decodeDeterministicStateSnapshot(text) {
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = JSON.parse(text);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return { ok: false, reason: 'invalid-json' };
|
|
38
|
+
}
|
|
39
|
+
if (!isRecord(parsed)) {
|
|
40
|
+
return { ok: false, reason: 'not-a-snapshot' };
|
|
41
|
+
}
|
|
42
|
+
if (parsed.v !== 1) {
|
|
43
|
+
return { ok: false, reason: 'unsupported-snapshot-version' };
|
|
44
|
+
}
|
|
45
|
+
if (!isNonNegativeInt(parsed.tick) ||
|
|
46
|
+
!isNonNegativeInt(parsed.sessionFrame) ||
|
|
47
|
+
parsed.sessionFrame !== parsed.tick + 1 ||
|
|
48
|
+
typeof parsed.stateBytes !== 'string' ||
|
|
49
|
+
!Number.isInteger(parsed.randomState) ||
|
|
50
|
+
typeof parsed.stateChecksum !== 'string' ||
|
|
51
|
+
parsed.stateChecksum.length === 0 ||
|
|
52
|
+
!Array.isArray(parsed.confirmedInputs)) {
|
|
53
|
+
return { ok: false, reason: 'malformed-snapshot' };
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
snapshot: {
|
|
58
|
+
v: 1,
|
|
59
|
+
tick: parsed.tick,
|
|
60
|
+
sessionFrame: parsed.sessionFrame,
|
|
61
|
+
stateBytes: parsed.stateBytes,
|
|
62
|
+
randomState: parsed.randomState,
|
|
63
|
+
stateChecksum: parsed.stateChecksum,
|
|
64
|
+
confirmedInputs: (0, canonical_js_1.cloneCanonical)(parsed.confirmedInputs),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// ── base64 (browser-safe: no Buffer, no atob dependency) ──
|
|
69
|
+
const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
70
|
+
const BASE64_LOOKUP = new Map([...BASE64_ALPHABET].map((char, index) => [char, index]));
|
|
71
|
+
function bytesToBase64(bytes) {
|
|
72
|
+
let out = '';
|
|
73
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
74
|
+
const b0 = bytes[index];
|
|
75
|
+
const b1 = index + 1 < bytes.length ? bytes[index + 1] : 0;
|
|
76
|
+
const b2 = index + 2 < bytes.length ? bytes[index + 2] : 0;
|
|
77
|
+
out += BASE64_ALPHABET[b0 >> 2];
|
|
78
|
+
out += BASE64_ALPHABET[((b0 & 0x03) << 4) | (b1 >> 4)];
|
|
79
|
+
out += index + 1 < bytes.length ? BASE64_ALPHABET[((b1 & 0x0f) << 2) | (b2 >> 6)] : '=';
|
|
80
|
+
out += index + 2 < bytes.length ? BASE64_ALPHABET[b2 & 0x3f] : '=';
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
function base64ToBytes(text) {
|
|
85
|
+
if (text.length % 4 !== 0) {
|
|
86
|
+
throw new Error('base64 length must be a multiple of 4');
|
|
87
|
+
}
|
|
88
|
+
const padding = text.endsWith('==') ? 2 : text.endsWith('=') ? 1 : 0;
|
|
89
|
+
const byteLength = (text.length / 4) * 3 - padding;
|
|
90
|
+
const bytes = new Uint8Array(byteLength);
|
|
91
|
+
let byteIndex = 0;
|
|
92
|
+
for (let index = 0; index < text.length; index += 4) {
|
|
93
|
+
const values = [];
|
|
94
|
+
for (let offset = 0; offset < 4; offset += 1) {
|
|
95
|
+
const char = text[index + offset];
|
|
96
|
+
if (char === '=') {
|
|
97
|
+
if (index + 4 < text.length || offset < 2) {
|
|
98
|
+
throw new Error('base64 padding is only valid at the end');
|
|
99
|
+
}
|
|
100
|
+
values.push(0);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const value = BASE64_LOOKUP.get(char);
|
|
104
|
+
if (value === undefined) {
|
|
105
|
+
throw new Error(`invalid base64 character ${JSON.stringify(char)}`);
|
|
106
|
+
}
|
|
107
|
+
values.push(value);
|
|
108
|
+
}
|
|
109
|
+
const triple = (values[0] << 18) | (values[1] << 12) | (values[2] << 6) | values[3];
|
|
110
|
+
if (byteIndex < byteLength)
|
|
111
|
+
bytes[byteIndex++] = (triple >> 16) & 0xff;
|
|
112
|
+
if (byteIndex < byteLength)
|
|
113
|
+
bytes[byteIndex++] = (triple >> 8) & 0xff;
|
|
114
|
+
if (byteIndex < byteLength)
|
|
115
|
+
bytes[byteIndex++] = triple & 0xff;
|
|
116
|
+
}
|
|
117
|
+
return bytes;
|
|
118
|
+
}
|
|
119
|
+
// ── transfer builder (raw bytes → frozen wire transfer shape) ──
|
|
120
|
+
exports.DEFAULT_SNAPSHOT_CHUNK_SIZE = 262_144;
|
|
121
|
+
exports.DEFAULT_MAX_SNAPSHOT_BYTES = 4_194_304;
|
|
122
|
+
function createSnapshotTransferFromBytes(snapshotBytes, options) {
|
|
123
|
+
const chunkSize = options.chunkSize ?? exports.DEFAULT_SNAPSHOT_CHUNK_SIZE;
|
|
124
|
+
if (!Number.isInteger(chunkSize) || chunkSize <= 0) {
|
|
125
|
+
throw new Error('Snapshot chunkSize must be a positive integer');
|
|
126
|
+
}
|
|
127
|
+
if (snapshotBytes.length === 0) {
|
|
128
|
+
throw new Error('Snapshot bytes must be non-empty');
|
|
129
|
+
}
|
|
130
|
+
const chunks = [];
|
|
131
|
+
for (let offset = 0; offset < snapshotBytes.length; offset += chunkSize) {
|
|
132
|
+
chunks.push(snapshotBytes.slice(offset, offset + chunkSize));
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
transferId: options.transferId,
|
|
136
|
+
identity: (0, canonical_js_1.cloneCanonical)(options.identity),
|
|
137
|
+
frame: options.frame,
|
|
138
|
+
snapshotChecksum: (0, canonical_js_1.defaultChecksum)(snapshotBytes),
|
|
139
|
+
snapshotBytes,
|
|
140
|
+
chunkSize,
|
|
141
|
+
chunks,
|
|
142
|
+
chunkChecksums: chunks.map((chunk) => (0, canonical_js_1.defaultChecksum)(chunk)),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function createSnapshotTransferCollector(options = {}) {
|
|
146
|
+
const maxSnapshotBytes = options.maxSnapshotBytes ?? exports.DEFAULT_MAX_SNAPSHOT_BYTES;
|
|
147
|
+
const pending = new Map();
|
|
148
|
+
return {
|
|
149
|
+
offer(payload) {
|
|
150
|
+
const identity = parseIdentity(payload.identity);
|
|
151
|
+
if (typeof payload.transferId !== 'string' ||
|
|
152
|
+
payload.transferId.length === 0 ||
|
|
153
|
+
identity === undefined ||
|
|
154
|
+
!isNonNegativeInt(payload.frame) ||
|
|
155
|
+
typeof payload.snapshotChecksum !== 'string' ||
|
|
156
|
+
!isNonNegativeInt(payload.chunkCount) ||
|
|
157
|
+
payload.chunkCount < 1 ||
|
|
158
|
+
!isNonNegativeInt(payload.totalBytes) ||
|
|
159
|
+
payload.totalBytes < 1 ||
|
|
160
|
+
// Every chunk carries at least one character, so a chunkCount that
|
|
161
|
+
// exceeds totalBytes is self-contradictory (forged offer).
|
|
162
|
+
payload.chunkCount > payload.totalBytes ||
|
|
163
|
+
!isStringArray(payload.chunkChecksums) ||
|
|
164
|
+
payload.chunkChecksums.length !== payload.chunkCount) {
|
|
165
|
+
return { accepted: false, reason: 'malformed-snapshot-offer' };
|
|
166
|
+
}
|
|
167
|
+
if (payload.totalBytes > maxSnapshotBytes) {
|
|
168
|
+
return { accepted: false, reason: 'snapshot-too-large' };
|
|
169
|
+
}
|
|
170
|
+
pending.set(payload.transferId, {
|
|
171
|
+
transferId: payload.transferId,
|
|
172
|
+
identity,
|
|
173
|
+
frame: payload.frame,
|
|
174
|
+
snapshotChecksum: payload.snapshotChecksum,
|
|
175
|
+
chunkCount: payload.chunkCount,
|
|
176
|
+
totalBytes: payload.totalBytes,
|
|
177
|
+
chunkChecksums: [...payload.chunkChecksums],
|
|
178
|
+
chunks: new Map(),
|
|
179
|
+
});
|
|
180
|
+
return { accepted: true };
|
|
181
|
+
},
|
|
182
|
+
chunk(payload) {
|
|
183
|
+
if (typeof payload.transferId !== 'string') {
|
|
184
|
+
return { accepted: false, reason: 'malformed-snapshot-chunk' };
|
|
185
|
+
}
|
|
186
|
+
const transfer = pending.get(payload.transferId);
|
|
187
|
+
if (transfer === undefined) {
|
|
188
|
+
return { accepted: false, reason: 'unknown-snapshot-transfer' };
|
|
189
|
+
}
|
|
190
|
+
if (typeof payload.chunk !== 'string' || typeof payload.chunkChecksum !== 'string') {
|
|
191
|
+
return { accepted: false, reason: 'malformed-snapshot-chunk' };
|
|
192
|
+
}
|
|
193
|
+
const chunkIndex = payload.chunkIndex;
|
|
194
|
+
if (!isNonNegativeInt(chunkIndex) || chunkIndex >= transfer.chunkCount) {
|
|
195
|
+
return { accepted: false, reason: 'snapshot-chunk-index-out-of-range' };
|
|
196
|
+
}
|
|
197
|
+
if (transfer.chunks.has(chunkIndex)) {
|
|
198
|
+
return { accepted: false, reason: 'duplicate-snapshot-chunk' };
|
|
199
|
+
}
|
|
200
|
+
if (transfer.chunkChecksums[chunkIndex] !== payload.chunkChecksum ||
|
|
201
|
+
(0, canonical_js_1.defaultChecksum)(payload.chunk) !== payload.chunkChecksum) {
|
|
202
|
+
return { accepted: false, reason: 'snapshot-chunk-checksum-mismatch' };
|
|
203
|
+
}
|
|
204
|
+
transfer.chunks.set(chunkIndex, payload.chunk);
|
|
205
|
+
return { accepted: true };
|
|
206
|
+
},
|
|
207
|
+
complete(payload) {
|
|
208
|
+
if (typeof payload.transferId !== 'string') {
|
|
209
|
+
return { accepted: false, reason: 'malformed-snapshot-complete' };
|
|
210
|
+
}
|
|
211
|
+
const transfer = pending.get(payload.transferId);
|
|
212
|
+
if (transfer === undefined) {
|
|
213
|
+
return { accepted: false, reason: 'unknown-snapshot-transfer' };
|
|
214
|
+
}
|
|
215
|
+
if (transfer.chunks.size !== transfer.chunkCount) {
|
|
216
|
+
return { accepted: false, reason: 'missing-snapshot-chunk' };
|
|
217
|
+
}
|
|
218
|
+
const snapshotBytes = Array.from({ length: transfer.chunkCount }, (_, index) => transfer.chunks.get(index)).join('');
|
|
219
|
+
// A failed terminal check consumes the transfer either way — the bytes are bad.
|
|
220
|
+
pending.delete(payload.transferId);
|
|
221
|
+
if (snapshotBytes.length !== transfer.totalBytes) {
|
|
222
|
+
return { accepted: false, reason: 'snapshot-byte-length-mismatch' };
|
|
223
|
+
}
|
|
224
|
+
if ((0, canonical_js_1.defaultChecksum)(snapshotBytes) !== transfer.snapshotChecksum) {
|
|
225
|
+
return { accepted: false, reason: 'snapshot-transfer-checksum-mismatch' };
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
accepted: true,
|
|
229
|
+
transfer: {
|
|
230
|
+
transferId: transfer.transferId,
|
|
231
|
+
identity: transfer.identity,
|
|
232
|
+
frame: transfer.frame,
|
|
233
|
+
snapshotChecksum: transfer.snapshotChecksum,
|
|
234
|
+
snapshotBytes,
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
},
|
|
238
|
+
abort(transferId) {
|
|
239
|
+
pending.delete(transferId);
|
|
240
|
+
},
|
|
241
|
+
get pendingTransferIds() {
|
|
242
|
+
return [...pending.keys()];
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function parseIdentity(value) {
|
|
247
|
+
if (!isRecord(value) || typeof value.descriptorHash !== 'string' || value.descriptorHash.length === 0) {
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
descriptorHash: value.descriptorHash,
|
|
252
|
+
...(typeof value.schemaHash === 'string' ? { schemaHash: value.schemaHash } : {}),
|
|
253
|
+
...(typeof value.assetDbHash === 'string' ? { assetDbHash: value.assetDbHash } : {}),
|
|
254
|
+
...(typeof value.configHash === 'string' ? { configHash: value.configHash } : {}),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function isRecord(value) {
|
|
258
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
259
|
+
}
|
|
260
|
+
function isNonNegativeInt(value) {
|
|
261
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0;
|
|
262
|
+
}
|
|
263
|
+
function isStringArray(value) {
|
|
264
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
|
|
265
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deterministicSessionWireVersion = void 0;
|
|
4
|
+
exports.encodeDeterministicSessionMessage = encodeDeterministicSessionMessage;
|
|
5
|
+
exports.decodeDeterministicSessionMessage = decodeDeterministicSessionMessage;
|
|
6
|
+
const canonical_js_1 = require("./canonical.js");
|
|
7
|
+
const local_authority_wire_js_1 = require("./local-authority-wire.js");
|
|
8
|
+
/**
|
|
9
|
+
* Frozen v1 session-control wire protocol for game-agnostic input-authority rooms (§8, M1).
|
|
10
|
+
*
|
|
11
|
+
* Covers the server/client roles for a live match: session-start (slot assignment + the
|
|
12
|
+
* server-owned RNG seed + tick params), clock/time-sync, input, confirmed-input (the verified
|
|
13
|
+
* tick), reconnect, spectator, and close/error. Snapshot request/offer/chunk/complete for
|
|
14
|
+
* late-join reuse the existing envelopes in `local-authority-wire.ts` (wired at M5).
|
|
15
|
+
*
|
|
16
|
+
* Both families share the `rdm-local-authority/v1` version namespace. Decoding is FAIL-CLOSED:
|
|
17
|
+
* unknown protocol, unknown version, unknown kind, malformed payload, or checksum mismatch are
|
|
18
|
+
* rejected with a reason — never silently coerced. This is a public forward-compat boundary; the
|
|
19
|
+
* discriminants below are frozen and must only be extended, never repurposed.
|
|
20
|
+
*/
|
|
21
|
+
exports.deterministicSessionWireVersion = 1;
|
|
22
|
+
const sessionMessageKinds = new Set([
|
|
23
|
+
'session-start',
|
|
24
|
+
'time-sync-ping',
|
|
25
|
+
'time-sync-pong',
|
|
26
|
+
'input',
|
|
27
|
+
'confirmed-input',
|
|
28
|
+
'reconnect',
|
|
29
|
+
'spectator-join',
|
|
30
|
+
'checksum-report',
|
|
31
|
+
'net-stats',
|
|
32
|
+
'close',
|
|
33
|
+
'error',
|
|
34
|
+
]);
|
|
35
|
+
function encodeDeterministicSessionMessage(message) {
|
|
36
|
+
const { kind, ...rest } = message;
|
|
37
|
+
const payload = rest;
|
|
38
|
+
const envelope = {
|
|
39
|
+
protocol: local_authority_wire_js_1.deterministicLocalAuthorityWireProtocol,
|
|
40
|
+
v: exports.deterministicSessionWireVersion,
|
|
41
|
+
kind,
|
|
42
|
+
payload,
|
|
43
|
+
checksum: sessionChecksum(kind, payload),
|
|
44
|
+
};
|
|
45
|
+
return (0, canonical_js_1.canonicalStringify)(envelope);
|
|
46
|
+
}
|
|
47
|
+
function decodeDeterministicSessionMessage(text) {
|
|
48
|
+
let parsed;
|
|
49
|
+
try {
|
|
50
|
+
parsed = JSON.parse(text);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return { ok: false, reason: 'invalid-json' };
|
|
54
|
+
}
|
|
55
|
+
if (!isRecord(parsed)) {
|
|
56
|
+
return { ok: false, reason: 'not-an-envelope' };
|
|
57
|
+
}
|
|
58
|
+
if (parsed.protocol !== local_authority_wire_js_1.deterministicLocalAuthorityWireProtocol) {
|
|
59
|
+
return { ok: false, reason: 'unknown-protocol' };
|
|
60
|
+
}
|
|
61
|
+
if (parsed.v !== exports.deterministicSessionWireVersion) {
|
|
62
|
+
return { ok: false, reason: 'unknown-version' };
|
|
63
|
+
}
|
|
64
|
+
if (typeof parsed.kind !== 'string' || !sessionMessageKinds.has(parsed.kind)) {
|
|
65
|
+
return { ok: false, reason: 'unknown-kind' };
|
|
66
|
+
}
|
|
67
|
+
const kind = parsed.kind;
|
|
68
|
+
if (!isRecord(parsed.payload)) {
|
|
69
|
+
return { ok: false, reason: 'malformed-payload' };
|
|
70
|
+
}
|
|
71
|
+
// Symmetry with encode: the encoder checksummed the LIVE payload value and
|
|
72
|
+
// then canonical-encoded it, so sentinel values (-0, NaN, ±Infinity,
|
|
73
|
+
// undefined, bigint, Map/Set/typed arrays) sit on the wire as literal
|
|
74
|
+
// `{"$rdm":…}` objects. Decode them back to real values BEFORE recomputing
|
|
75
|
+
// the checksum — hashing the raw parse output would trip canonicalize's
|
|
76
|
+
// reserved-key guard and throw out of a decoder whose contract is
|
|
77
|
+
// fail-closed. Any malformed/hostile `$rdm` literal lands in the catch.
|
|
78
|
+
let payload;
|
|
79
|
+
let expectedChecksum;
|
|
80
|
+
try {
|
|
81
|
+
const decoded = (0, canonical_js_1.decodeCanonicalValue)(parsed.payload);
|
|
82
|
+
if (!isRecord(decoded)) {
|
|
83
|
+
return { ok: false, reason: 'malformed-payload' };
|
|
84
|
+
}
|
|
85
|
+
payload = decoded;
|
|
86
|
+
expectedChecksum = sessionChecksum(kind, payload);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return { ok: false, reason: 'malformed-payload' };
|
|
90
|
+
}
|
|
91
|
+
if (parsed.checksum !== expectedChecksum) {
|
|
92
|
+
return { ok: false, reason: 'checksum-mismatch' };
|
|
93
|
+
}
|
|
94
|
+
const message = parseSessionPayload(kind, payload);
|
|
95
|
+
if (message === undefined) {
|
|
96
|
+
return { ok: false, reason: 'malformed-payload' };
|
|
97
|
+
}
|
|
98
|
+
return { ok: true, message };
|
|
99
|
+
}
|
|
100
|
+
function parseSessionPayload(kind, payload) {
|
|
101
|
+
switch (kind) {
|
|
102
|
+
case 'session-start':
|
|
103
|
+
if (isInt(payload.slot) &&
|
|
104
|
+
isInt(payload.playerCount) &&
|
|
105
|
+
isInt(payload.seed) &&
|
|
106
|
+
isFiniteNumber(payload.tickRateHz) &&
|
|
107
|
+
isInt(payload.hardToleranceTicks) &&
|
|
108
|
+
isRole(payload.role)) {
|
|
109
|
+
return {
|
|
110
|
+
kind,
|
|
111
|
+
slot: payload.slot,
|
|
112
|
+
playerCount: payload.playerCount,
|
|
113
|
+
seed: payload.seed,
|
|
114
|
+
tickRateHz: payload.tickRateHz,
|
|
115
|
+
hardToleranceTicks: payload.hardToleranceTicks,
|
|
116
|
+
role: payload.role,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
case 'time-sync-ping':
|
|
121
|
+
if (isInt(payload.clientSendTick) && isInt(payload.nonce)) {
|
|
122
|
+
return { kind, clientSendTick: payload.clientSendTick, nonce: payload.nonce };
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
case 'time-sync-pong':
|
|
126
|
+
if (isInt(payload.clientSendTick) && isInt(payload.nonce) && isInt(payload.authorityTick)) {
|
|
127
|
+
return { kind, clientSendTick: payload.clientSendTick, nonce: payload.nonce, authorityTick: payload.authorityTick };
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
case 'input':
|
|
131
|
+
if (isInt(payload.slot) && isInt(payload.tick) && isInt(payload.attempt) && payload.input !== undefined) {
|
|
132
|
+
return { kind, slot: payload.slot, tick: payload.tick, input: payload.input, attempt: payload.attempt };
|
|
133
|
+
}
|
|
134
|
+
return undefined;
|
|
135
|
+
case 'confirmed-input':
|
|
136
|
+
if (isInt(payload.authorityTick) && isConfirmedFrameArray(payload.frames)) {
|
|
137
|
+
return { kind, authorityTick: payload.authorityTick, frames: payload.frames };
|
|
138
|
+
}
|
|
139
|
+
return undefined;
|
|
140
|
+
case 'reconnect':
|
|
141
|
+
if (isInt(payload.slot) && isInt(payload.resumeFromTick)) {
|
|
142
|
+
return { kind, slot: payload.slot, resumeFromTick: payload.resumeFromTick };
|
|
143
|
+
}
|
|
144
|
+
return undefined;
|
|
145
|
+
case 'spectator-join':
|
|
146
|
+
return { kind };
|
|
147
|
+
case 'checksum-report':
|
|
148
|
+
if (isInt(payload.slot) && isInt(payload.tick) && typeof payload.checksum === 'string') {
|
|
149
|
+
return { kind, slot: payload.slot, tick: payload.tick, checksum: payload.checksum };
|
|
150
|
+
}
|
|
151
|
+
return undefined;
|
|
152
|
+
case 'net-stats':
|
|
153
|
+
if (isInt(payload.slot) &&
|
|
154
|
+
isInt(payload.tick) &&
|
|
155
|
+
isInt(payload.rttMs) &&
|
|
156
|
+
isInt(payload.rollbacks) &&
|
|
157
|
+
isInt(payload.maxRollbackDepth) &&
|
|
158
|
+
isInt(payload.timescalePermille) &&
|
|
159
|
+
isInt(payload.inputDelayTicks)) {
|
|
160
|
+
return {
|
|
161
|
+
kind,
|
|
162
|
+
slot: payload.slot,
|
|
163
|
+
tick: payload.tick,
|
|
164
|
+
rttMs: payload.rttMs,
|
|
165
|
+
rollbacks: payload.rollbacks,
|
|
166
|
+
maxRollbackDepth: payload.maxRollbackDepth,
|
|
167
|
+
timescalePermille: payload.timescalePermille,
|
|
168
|
+
inputDelayTicks: payload.inputDelayTicks,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
case 'close':
|
|
173
|
+
if (typeof payload.code === 'string' && typeof payload.reason === 'string') {
|
|
174
|
+
return { kind, code: payload.code, reason: payload.reason };
|
|
175
|
+
}
|
|
176
|
+
return undefined;
|
|
177
|
+
case 'error':
|
|
178
|
+
if (typeof payload.code === 'string' && typeof payload.message === 'string') {
|
|
179
|
+
return { kind, code: payload.code, message: payload.message };
|
|
180
|
+
}
|
|
181
|
+
return undefined;
|
|
182
|
+
default:
|
|
183
|
+
return assertNever(kind);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function sessionChecksum(kind, payload) {
|
|
187
|
+
return String((0, canonical_js_1.defaultChecksum)({ protocol: local_authority_wire_js_1.deterministicLocalAuthorityWireProtocol, v: exports.deterministicSessionWireVersion, kind, payload }));
|
|
188
|
+
}
|
|
189
|
+
function isRecord(value) {
|
|
190
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
191
|
+
}
|
|
192
|
+
function isInt(value) {
|
|
193
|
+
return typeof value === 'number' && Number.isInteger(value);
|
|
194
|
+
}
|
|
195
|
+
function isFiniteNumber(value) {
|
|
196
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
197
|
+
}
|
|
198
|
+
function isRole(value) {
|
|
199
|
+
return value === 'player' || value === 'spectator';
|
|
200
|
+
}
|
|
201
|
+
function isConfirmedFrameArray(value) {
|
|
202
|
+
// The codec is stateless (it does not know playerCount), so it cannot check inputs.length — the
|
|
203
|
+
// consumer validates that against its own slot count. It CAN reject a frame whose substituted-slot
|
|
204
|
+
// indices are not integers (a string index survives JSON.parse and would corrupt slot handling).
|
|
205
|
+
return (Array.isArray(value) &&
|
|
206
|
+
value.every((frame) => isRecord(frame) &&
|
|
207
|
+
isInt(frame.tick) &&
|
|
208
|
+
Array.isArray(frame.inputs) &&
|
|
209
|
+
Array.isArray(frame.substitutedSlots) &&
|
|
210
|
+
frame.substitutedSlots.every((slot) => isInt(slot)) &&
|
|
211
|
+
typeof frame.checksum === 'string'));
|
|
212
|
+
}
|
|
213
|
+
function assertNever(value) {
|
|
214
|
+
void value;
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createDeterministicSignalBus = createDeterministicSignalBus;
|
|
4
|
+
exports.runDeterministicSignalFixture = runDeterministicSignalFixture;
|
|
5
|
+
const canonical_js_1 = require("./canonical.js");
|
|
6
|
+
function createDeterministicSignalBus(subscriptions) {
|
|
7
|
+
const orderedSubscriptions = [...subscriptions].sort(compareSubscriptions);
|
|
8
|
+
const emitted = [];
|
|
9
|
+
const dispatched = [];
|
|
10
|
+
let sequence = 0;
|
|
11
|
+
return {
|
|
12
|
+
emit(signal) {
|
|
13
|
+
emitted.push({ sequence, signal: (0, canonical_js_1.cloneCanonical)(signal) });
|
|
14
|
+
sequence += 1;
|
|
15
|
+
},
|
|
16
|
+
dispatch(state, frame) {
|
|
17
|
+
let nextState = (0, canonical_js_1.cloneCanonical)(state);
|
|
18
|
+
const due = emitted
|
|
19
|
+
.filter((entry) => entry.signal.frame <= frame)
|
|
20
|
+
.sort(compareQueuedSignals);
|
|
21
|
+
for (const entry of due) {
|
|
22
|
+
for (const subscription of orderedSubscriptions) {
|
|
23
|
+
if (subscription.type === entry.signal.type) {
|
|
24
|
+
nextState = subscription.handle(nextState, entry.signal);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
dispatched.push((0, canonical_js_1.cloneCanonical)(entry.signal));
|
|
28
|
+
}
|
|
29
|
+
const dueSequences = new Set(due.map((entry) => entry.sequence));
|
|
30
|
+
for (let index = emitted.length - 1; index >= 0; index -= 1) {
|
|
31
|
+
if (dueSequences.has(emitted[index].sequence)) {
|
|
32
|
+
emitted.splice(index, 1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return nextState;
|
|
36
|
+
},
|
|
37
|
+
snapshot() {
|
|
38
|
+
const snapshot = {
|
|
39
|
+
emitted: emitted.sort(compareQueuedSignals).map((entry) => entry.signal),
|
|
40
|
+
dispatched,
|
|
41
|
+
sequence,
|
|
42
|
+
};
|
|
43
|
+
return (0, canonical_js_1.cloneCanonical)(snapshot);
|
|
44
|
+
},
|
|
45
|
+
restore(snapshot) {
|
|
46
|
+
emitted.length = 0;
|
|
47
|
+
snapshot.emitted.forEach((signal, index) => emitted.push({ sequence: index, signal: (0, canonical_js_1.cloneCanonical)(signal) }));
|
|
48
|
+
dispatched.length = 0;
|
|
49
|
+
snapshot.dispatched.forEach((signal) => dispatched.push((0, canonical_js_1.cloneCanonical)(signal)));
|
|
50
|
+
sequence = snapshot.sequence;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function runDeterministicSignalFixture() {
|
|
55
|
+
const subscriptions = [
|
|
56
|
+
{
|
|
57
|
+
id: 'score',
|
|
58
|
+
type: 'hit',
|
|
59
|
+
order: 20,
|
|
60
|
+
handle: (state, signal) => ({
|
|
61
|
+
...state,
|
|
62
|
+
score: state.score + Number(signal.payload.points),
|
|
63
|
+
trace: [...state.trace, `score:${signal.payload.points}`],
|
|
64
|
+
}),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'damage',
|
|
68
|
+
type: 'hit',
|
|
69
|
+
order: 10,
|
|
70
|
+
handle: (state, signal) => ({
|
|
71
|
+
...state,
|
|
72
|
+
damage: state.damage + Number(signal.payload.damage),
|
|
73
|
+
trace: [...state.trace, `damage:${signal.payload.damage}`],
|
|
74
|
+
}),
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
const bus = createDeterministicSignalBus(subscriptions);
|
|
78
|
+
const initial = { score: 0, damage: 0, trace: [] };
|
|
79
|
+
bus.emit({ frame: 2, type: 'hit', payload: { points: 3, damage: 1 } });
|
|
80
|
+
bus.emit({ frame: 1, type: 'hit', payload: { points: 5, damage: 2 } });
|
|
81
|
+
const snapshot = bus.snapshot();
|
|
82
|
+
const after = bus.dispatch(bus.dispatch(initial, 1), 2);
|
|
83
|
+
const checksum = (0, canonical_js_1.defaultChecksum)(after);
|
|
84
|
+
const restoredBus = createDeterministicSignalBus(subscriptions);
|
|
85
|
+
restoredBus.restore(snapshot);
|
|
86
|
+
const restored = restoredBus.dispatch(restoredBus.dispatch(initial, 1), 2);
|
|
87
|
+
return {
|
|
88
|
+
checksum,
|
|
89
|
+
restoredChecksum: (0, canonical_js_1.defaultChecksum)(restored),
|
|
90
|
+
dispatchedSignals: bus.snapshot().dispatched.length,
|
|
91
|
+
subscriptionOrderStable: after.trace.join('|') === 'damage:2|score:5|damage:1|score:3',
|
|
92
|
+
rollbackEquivalent: checksum === (0, canonical_js_1.defaultChecksum)(restored),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function compareSubscriptions(left, right) {
|
|
96
|
+
return left.order - right.order || compareStrings(left.id, right.id);
|
|
97
|
+
}
|
|
98
|
+
function compareQueuedSignals(left, right) {
|
|
99
|
+
return left.signal.frame - right.signal.frame || left.sequence - right.sequence;
|
|
100
|
+
}
|
|
101
|
+
function compareStrings(left, right) {
|
|
102
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
103
|
+
}
|