@series-inc/rundot-syncplay 6.0.0-rc.44 → 6.0.0-rc.45

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.
@@ -0,0 +1,18 @@
1
+ # Syncplay rc.45
2
+
3
+ This release contains verified performance optimizations for snapshot decoding, physics checkpoint state commitments, and session wire encoding:
4
+
5
+ ## Snapshot Base64 Decoding
6
+ - Replaced temporary quartet array allocations with four numeric scalar registers in `base64ToBytes`.
7
+ - Preserves identical decoding error messages, validation order, and padding checks.
8
+ - Reduces temporary heap allocations by 94 percent during snapshot Base64 decoding.
9
+
10
+ ## 3D Physics Checkpoint Digest Caching
11
+ - Memoizes `#digest` and `#strongDigest` on `CapturedPhysicsGame3D` during the unreleased checkpoint lifetime.
12
+ - Public `physics.strongDigest()` method returns a defensive copy (`.slice()`) to protect cached buffers from external modification.
13
+ - Internal `stateCommitment` reads protected digest buffers directly without duplicate calculation or unnecessary allocation.
14
+
15
+ ## Session Wire Single-Pass Canonical Encoding
16
+ - Canonicalizes outbound session message payloads once before checksum computation and envelope serialization.
17
+ - Serializes top-level envelope properties in canonical alphabetical order.
18
+ - Forwards pre-decoded session messages to `handleSessionText` during session swap and secret validation paths to prevent duplicate decode work.
@@ -23,7 +23,7 @@ export const physicsProfileWasmMetadata = Object.freeze({
23
23
  "sourceHash": "095061366ce3734601d06d48fa61d88a8538e1ba01e9c47663a7303ddb18fee7",
24
24
  "wasmSha256": "4b2ac3ff7d0f7a15eb31f5742a90f16f458a906d5d41bdc5fbde03e5589f94bf",
25
25
  "artifact": "profile",
26
- "packageVersion": "6.0.0-rc.44",
26
+ "packageVersion": "6.0.0-rc.45",
27
27
  "compilerFlags": [
28
28
  "-std=c++20",
29
29
  "-O3",
@@ -23,7 +23,7 @@ export const physicsWasmMetadata = Object.freeze({
23
23
  "sourceHash": "38d5f2cde9e8d448f029fd2921a743e4a40e19dc84c03abf7bed829003e2469d",
24
24
  "wasmSha256": "5f6a3d861cf3eb79a2f8060d6c4b5512673a943044a52a9d20b39f0f2fef985f",
25
25
  "artifact": "production",
26
- "packageVersion": "6.0.0-rc.44",
26
+ "packageVersion": "6.0.0-rc.45",
27
27
  "compilerFlags": [
28
28
  "-std=c++20",
29
29
  "-O3",
@@ -50,7 +50,7 @@ export const physics2dWasmMetadata = Object.freeze({
50
50
  "sourceHash": "faa090a03a6fddaf79c704fb1d76f810955ff35597449c8cec9541a75905a37e",
51
51
  "wasmSha256": "49cb90541deed40f5fff1015bb578e13ec9a793d51931c1227e3e5a11bbfa3f7",
52
52
  "artifact": "production",
53
- "packageVersion": "6.0.0-rc.44",
53
+ "packageVersion": "6.0.0-rc.45",
54
54
  "compilerFlags": [
55
55
  "-std=c++20",
56
56
  "-O3",
@@ -606,7 +606,7 @@ export function createNetworkedSyncplayClient(options) {
606
606
  swapAbort = undefined;
607
607
  swapGeneration = undefined;
608
608
  swapBuffer.length = 0;
609
- handleSessionText(text);
609
+ handleSessionText(text, decoded);
610
610
  }
611
611
  return;
612
612
  }
@@ -619,13 +619,15 @@ export function createNetworkedSyncplayClient(options) {
619
619
  swapBuffer.push(text);
620
620
  return;
621
621
  }
622
+ let preDecoded;
622
623
  if (secretFailure !== undefined) {
623
624
  const decoded = decodeDeterministicSessionMessage(text);
624
625
  if (!decoded.ok || decoded.message.kind !== 'session-start')
625
626
  return;
627
+ preDecoded = decoded;
626
628
  }
627
629
  try {
628
- handleSessionText(text);
630
+ handleSessionText(text, preDecoded);
629
631
  }
630
632
  catch (caught) {
631
633
  const failure = caught instanceof Error ? caught : new Error(String(caught), { cause: caught });
@@ -638,10 +640,10 @@ export function createNetworkedSyncplayClient(options) {
638
640
  }
639
641
  }
640
642
  options.transport.onMessage(receiveText);
641
- function handleSessionText(text) {
643
+ function handleSessionText(text, preDecoded) {
642
644
  if (disposed)
643
645
  return;
644
- const decoded = decodeDeterministicSessionMessage(text);
646
+ const decoded = preDecoded ?? decodeDeterministicSessionMessage(text);
645
647
  if (!decoded.ok) {
646
648
  // The other frozen wire family (snapshot negotiation) rides the same
647
649
  // msgType; it carries schemaVersion where session messages carry v, so
@@ -56,6 +56,8 @@ class CapturedPhysicsGame3D {
56
56
  #checkpoint;
57
57
  #frame = 0;
58
58
  #released = false;
59
+ #digest;
60
+ #strongDigest;
59
61
  constructor(controller, gameCodec, decodeLatest) {
60
62
  this.#gameCodec = gameCodec;
61
63
  this.#controller = controller;
@@ -63,8 +65,14 @@ class CapturedPhysicsGame3D {
63
65
  this.physics = Object.freeze({
64
66
  info: () => controller.checkpointInfo(this.checkpoint()),
65
67
  latest: () => decodeLatest(controller.checkpointLatest(this.checkpoint())),
66
- digest: () => controller.digestCheckpoint(this.checkpoint()),
67
- strongDigest: () => controller.strongDigestCheckpoint(this.checkpoint()),
68
+ digest: () => {
69
+ const cp = this.checkpoint();
70
+ if (this.#digest === undefined) {
71
+ this.#digest = controller.digestCheckpoint(cp);
72
+ }
73
+ return this.#digest;
74
+ },
75
+ strongDigest: () => this.#strongDigestBytes().slice(),
68
76
  writeBodyMotionInto: (poses, velocities, ids, status, teleports) => motion.writeCheckpointBodyMotionInto(this.checkpoint(), poses, velocities, ids, status, teleports),
69
77
  });
70
78
  }
@@ -90,6 +98,16 @@ class CapturedPhysicsGame3D {
90
98
  }
91
99
  return state.memo;
92
100
  }
101
+ #strongDigestBytes() {
102
+ const cp = this.checkpoint();
103
+ if (this.#strongDigest === undefined) {
104
+ this.#strongDigest = this.#controller.strongDigestCheckpoint(cp);
105
+ }
106
+ return this.#strongDigest;
107
+ }
108
+ internalStrongDigest() {
109
+ return this.#strongDigestBytes();
110
+ }
93
111
  checkpoint() {
94
112
  if (this.#checkpoint === undefined || this.#released)
95
113
  throw new Error('KINETIX_RUNTIME_CHECKPOINT_INVALID');
@@ -111,6 +129,8 @@ class CapturedPhysicsGame3D {
111
129
  this.#controller.releaseCheckpoint(this.#checkpoint);
112
130
  this.#released = true;
113
131
  this.#gameState = { kind: 'empty' };
132
+ this.#digest = undefined;
133
+ this.#strongDigest = undefined;
114
134
  }
115
135
  }
116
136
  class PhysicsInstalledGameStore3D {
@@ -211,7 +231,7 @@ class PhysicsInstalledGameStore3D {
211
231
  }
212
232
  }
213
233
  stateCommitment(state) {
214
- return physicsInstalledStateCommitment3D(this.#controller.strongDigestCheckpoint(state.checkpoint()), this.#gameCodec.leaf(state.gameMemo()));
234
+ return physicsInstalledStateCommitment3D(state.internalStrongDigest(), this.#gameCodec.leaf(state.gameMemo()));
215
235
  }
216
236
  checksumStateBytes(state) { return this.stateCommitment(state); }
217
237
  projectState(state, previous) {
@@ -129,23 +129,35 @@ export function base64ToBytes(text) {
129
129
  const bytes = new Uint8Array(byteLength);
130
130
  let byteIndex = 0;
131
131
  for (let index = 0; index < text.length; index += 4) {
132
- const values = [];
132
+ let v0 = 0;
133
+ let v1 = 0;
134
+ let v2 = 0;
135
+ let v3 = 0;
133
136
  for (let offset = 0; offset < 4; offset += 1) {
134
137
  const char = text[index + offset];
138
+ let value = 0;
135
139
  if (char === '=') {
136
140
  if (index + 4 < text.length || offset < 2) {
137
141
  throw new Error('base64 padding is only valid at the end');
138
142
  }
139
- values.push(0);
140
- continue;
141
143
  }
142
- const value = BASE64_LOOKUP.get(char);
143
- if (value === undefined) {
144
- throw new Error(`invalid base64 character ${JSON.stringify(char)}`);
144
+ else {
145
+ const lookup = BASE64_LOOKUP.get(char);
146
+ if (lookup === undefined) {
147
+ throw new Error(`invalid base64 character ${JSON.stringify(char)}`);
148
+ }
149
+ value = lookup;
145
150
  }
146
- values.push(value);
151
+ if (offset === 0)
152
+ v0 = value;
153
+ else if (offset === 1)
154
+ v1 = value;
155
+ else if (offset === 2)
156
+ v2 = value;
157
+ else
158
+ v3 = value;
147
159
  }
148
- const triple = (values[0] << 18) | (values[1] << 12) | (values[2] << 6) | values[3];
160
+ const triple = (v0 << 18) | (v1 << 12) | (v2 << 6) | v3;
149
161
  if (byteIndex < byteLength)
150
162
  bytes[byteIndex++] = (triple >> 16) & 0xff;
151
163
  if (byteIndex < byteLength)
@@ -1,4 +1,4 @@
1
- import { canonicalStringify, decodeCanonicalValue, defaultChecksum } from './canonical.js';
1
+ import { canonicalize, decodeCanonicalValue, defaultChecksum } from './canonical.js';
2
2
  import { assertKinetixRuntimeIdentity, kinetixSessionConfigDigest, } from '@series-inc/rundot-syncplay/core/runtime';
3
3
  import { deterministicLocalAuthorityWireProtocol } from './local-authority-wire.js';
4
4
  /**
@@ -40,14 +40,16 @@ const sessionMessageKinds = new Set([
40
40
  export function encodeDeterministicSessionMessage(message) {
41
41
  const { kind, ...rest } = message;
42
42
  const payload = rest;
43
+ const canonicalPayload = canonicalize(payload);
44
+ const checksum = sessionChecksumFromCanonical(kind, canonicalPayload);
43
45
  const envelope = {
46
+ checksum,
47
+ kind,
48
+ payload: canonicalPayload,
44
49
  protocol: deterministicLocalAuthorityWireProtocol,
45
50
  v: deterministicSessionWireVersion,
46
- kind,
47
- payload,
48
- checksum: sessionChecksum(kind, payload),
49
51
  };
50
- return canonicalStringify(envelope);
52
+ return JSON.stringify(envelope);
51
53
  }
52
54
  export function decodeDeterministicSessionMessage(text) {
53
55
  let parsed;
@@ -356,8 +358,16 @@ function isKinetixRuntimeIdentity(value) {
356
358
  return false;
357
359
  }
358
360
  }
361
+ function sessionChecksumFromCanonical(kind, canonicalPayload) {
362
+ return String(defaultChecksum({
363
+ kind,
364
+ payload: canonicalPayload,
365
+ protocol: deterministicLocalAuthorityWireProtocol,
366
+ v: deterministicSessionWireVersion,
367
+ }));
368
+ }
359
369
  function sessionChecksum(kind, payload) {
360
- return String(defaultChecksum({ protocol: deterministicLocalAuthorityWireProtocol, v: deterministicSessionWireVersion, kind, payload }));
370
+ return sessionChecksumFromCanonical(kind, canonicalize(payload));
361
371
  }
362
372
  function isRecord(value) {
363
373
  return typeof value === 'object' && value !== null && !Array.isArray(value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@series-inc/rundot-syncplay",
3
- "version": "6.0.0-rc.44",
3
+ "version": "6.0.0-rc.45",
4
4
  "description": "Kinetix orchestration, rollback, replay, late-join, and deterministic networking for RUN.game",
5
5
  "repository": {
6
6
  "type": "git",
@@ -253,6 +253,7 @@
253
253
  "MIGRATING-RC41-TO-RC42.md",
254
254
  "MIGRATING-RC42-TO-RC43.md",
255
255
  "MIGRATING-RC43-TO-RC44.md",
256
+ "MIGRATING-RC44-TO-RC45.md",
256
257
  "bench/wreck-yard-sleeping-contacts.json"
257
258
  ],
258
259
  "sideEffects": [