Skip to content

Commit baa97a3

Browse files
author
DavidQ
committed
implement client replication and application layer — BUILD_PR_LEVEL_12_3_REPLICATION_CLIENT_APPLICATION
1 parent c5d1e88 commit baa97a3

11 files changed

Lines changed: 565 additions & 69 deletions

docs/dev/CODEX_COMMANDS.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
MODEL: GPT-5.3-codex
22
REASONING: high
3-
COMMAND:
4-
Prepare implementation plan for client replication and application layer per PLAN_PR_LEVEL_12_3_REPLICATION_CLIENT_APPLICATION. No engine API breakage.
3+
COMMAND: Implement BUILD_PR_LEVEL_12_3_REPLICATION_CLIENT_APPLICATION.

docs/dev/COMMIT_COMMENT.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
define client replication and application contractsPLAN_PR_LEVEL_12_3_REPLICATION_CLIENT_APPLICATION
1+
implement client replication and application layerBUILD_PR_LEVEL_12_3_REPLICATION_CLIENT_APPLICATION
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
- Replication contract defined
2-
- Client application defined
3-
- Reconciliation rules defined
1+
- replication intake
2+
- client apply
3+
- no regressions

docs/dev/roadmaps/MASTER_ROADMAP_HIGH_LEVEL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,3 +668,5 @@
668668
[ ] consolidate PR for easier, one stop, review, no need to look as 6 different docs for one capability (a good example of this is bezel/background), all people care about is what it does.
669669
[ ] some games are actually samples/demo, identify and recomment a phase-xx to move to.
670670
[ ] simulated code (like some of the netword samples) should be coverted to use real networks) understanding, tests may need some moch
671+
[ ] single class per file
672+
[ ] organize/rebuild samples/games so the are as if new construction with proper classes/data/etc in proper folder
Lines changed: 1 addition & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1 @@
1-
# BUILD_PR_LEVEL_12_3_REPLICATION_CLIENT_APPLICATION
2-
3-
## Purpose
4-
Implement the Level 12.3 client replication and authoritative application layer on top of the Level 12.1/12.2 network foundation with no engine API breakage.
5-
6-
## Scope
7-
Primary target files:
8-
- `src/engine/network/ReplicationMessageContract.js`
9-
- `src/engine/network/ClientReplicationApplicationLayer.js`
10-
- `src/engine/network/ClientReconciliationStrategy.js`
11-
- `src/engine/network/index.js`
12-
- `tests/final/MultiplayerNetworkingStack.test.mjs`
13-
- `docs/pr/LEVEL_12_3_REPLICATION_CLIENT_APPLICATION_CONTRACTS.md`
14-
15-
Allowed nearby reads:
16-
- `src/engine/network/StateReplication.js`
17-
- `src/engine/network/AuthoritativeServerRuntime.js`
18-
- `src/engine/network/AuthoritativeInputIngestionContract.js`
19-
- `src/engine/network/HandshakeSimulator.js`
20-
- `docs/pr/LEVEL_12_1_REAL_NETWORK_FOUNDATION_CONTRACTS.md`
21-
22-
## Required implementation
23-
- Define a replication message contract for server-to-client authoritative snapshots.
24-
- Implement a client-side application layer that:
25-
- accepts validated replication envelopes
26-
- applies authoritative snapshots into client-owned replicated state
27-
- rejects stale/out-of-order updates deterministically
28-
- Implement a reconciliation/update strategy for Level 12.3 without prediction/rollback:
29-
- authoritative-first apply
30-
- stale snapshot ignore rules
31-
- deterministic metadata updates (`lastAppliedTick`, `appliedSequence`)
32-
- Export new symbols additively from `src/engine/network/index.js` only.
33-
- Extend `tests/final/MultiplayerNetworkingStack.test.mjs` to validate:
34-
- replication envelope validation
35-
- client authoritative apply behavior
36-
- stale update rejection
37-
- existing handshake and server-runtime coverage still passing
38-
39-
## Acceptance criteria
40-
- Replication contract documented and implemented.
41-
- Client application model defined and implemented.
42-
- Reconciliation rules established and enforced by tests.
43-
- No existing engine network exports are removed or renamed.
44-
- Existing Level 12.1 and 12.2 networking tests remain green.
45-
46-
## Validation
47-
Run only:
48-
- `node --check src/engine/network/ReplicationMessageContract.js`
49-
- `node --check src/engine/network/ClientReplicationApplicationLayer.js`
50-
- `node --check src/engine/network/ClientReconciliationStrategy.js`
51-
- `node --input-type=module -e "import('./tests/final/MultiplayerNetworkingStack.test.mjs').then(async ({ run }) => { await run(); console.log('PASS MultiplayerNetworkingStack'); })"`
52-
- `node --input-type=module -e "import('./tests/production/EnginePublicBarrelImports.test.mjs').then(async ({ run }) => { await run(); console.log('PASS EnginePublicBarrelImports'); })"`
53-
54-
## Non-goals
55-
- no client prediction implementation
56-
- no rollback implementation
57-
- no gameplay-specific coupling
58-
- no UI/debug expansion
59-
- no engine core API redesign
60-
- no repo-wide cleanup or unrelated refactor
61-
62-
## Working tree rule
63-
If the tree is already dirty, ignore unrelated files and modify only the scoped files for this PR purpose.
1+
BUILD PR for client replication layer.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# LEVEL_12_3_REPLICATION_CLIENT_APPLICATION_CONTRACTS
2+
3+
## Replication Message Contract
4+
5+
`src/engine/network/ReplicationMessageContract.js` defines the authoritative replication envelope:
6+
7+
- `sessionId` non-empty string
8+
- `replicationSequence` non-negative integer
9+
- `authoritativeTick` non-negative integer
10+
- `snapshotType` (`full` or `delta`)
11+
- `snapshot.entities` array
12+
- optional `snapshot.despawned` array
13+
- `sentAtMs` finite number
14+
15+
Validation rejects malformed envelopes with deterministic rejection codes and normalizes accepted envelopes to a canonical shape.
16+
17+
## Client Application Layer
18+
19+
`src/engine/network/ClientReplicationApplicationLayer.js` defines the client-side apply boundary:
20+
21+
- `ingestReplicationEnvelope(envelope)`
22+
- `applyPendingReplication()`
23+
- `getReplicatedStateSnapshot()`
24+
- `getReplicationStatus()`
25+
26+
The layer only updates client replicated cache from validated authoritative envelopes.
27+
28+
## Reconciliation Rules (No Prediction/Rollback)
29+
30+
`src/engine/network/ClientReconciliationStrategy.js` establishes deterministic Level 12.3 rules:
31+
32+
- Apply by ascending `authoritativeTick`, then `replicationSequence`
33+
- Ignore stale lower ticks (`stale_tick`)
34+
- For equal tick, ignore lower/equal sequences (`stale_sequence`)
35+
- Invalid envelopes are ignored with explicit reason (`invalid_envelope`)
36+
37+
These rules keep authoritative application deterministic while prediction/rollback remain out of scope for this level.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/15/2026
5+
ClientReconciliationStrategy.js
6+
*/
7+
export const REPLICATION_IGNORE_REASONS = Object.freeze({
8+
STALE_TICK: 'stale_tick',
9+
STALE_SEQUENCE: 'stale_sequence',
10+
INVALID_ENVELOPE: 'invalid_envelope',
11+
});
12+
13+
export function compareReplicationEnvelopeOrder(left, right) {
14+
return (
15+
(left.authoritativeTick - right.authoritativeTick)
16+
|| (left.replicationSequence - right.replicationSequence)
17+
|| (left.receivedOrder - right.receivedOrder)
18+
);
19+
}
20+
21+
export function isReplicationEnvelopeStale(
22+
envelope,
23+
{ lastAppliedTick = -1, lastAppliedSequence = -1 } = {},
24+
) {
25+
if (envelope.authoritativeTick < lastAppliedTick) {
26+
return {
27+
stale: true,
28+
reason: REPLICATION_IGNORE_REASONS.STALE_TICK,
29+
};
30+
}
31+
32+
if (
33+
envelope.authoritativeTick === lastAppliedTick
34+
&& envelope.replicationSequence <= lastAppliedSequence
35+
) {
36+
return {
37+
stale: true,
38+
reason: REPLICATION_IGNORE_REASONS.STALE_SEQUENCE,
39+
};
40+
}
41+
42+
return {
43+
stale: false,
44+
reason: null,
45+
};
46+
}
47+
48+
export default class ClientReconciliationStrategy {
49+
shouldApply(envelope, status) {
50+
const staleCheck = isReplicationEnvelopeStale(envelope, status);
51+
return {
52+
apply: !staleCheck.stale,
53+
reason: staleCheck.reason,
54+
};
55+
}
56+
57+
sort(envelopes) {
58+
return envelopes.slice().sort(compareReplicationEnvelopeOrder);
59+
}
60+
}
61+
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/15/2026
5+
ClientReplicationApplicationLayer.js
6+
*/
7+
import ClientReconciliationStrategy, {
8+
REPLICATION_IGNORE_REASONS,
9+
} from './ClientReconciliationStrategy.js';
10+
import ReplicationMessageContract from './ReplicationMessageContract.js';
11+
import StateReplication from './StateReplication.js';
12+
13+
function clone(value) {
14+
return JSON.parse(JSON.stringify(value));
15+
}
16+
17+
export default class ClientReplicationApplicationLayer {
18+
constructor({
19+
sessionId = 'session',
20+
contract = null,
21+
reconciliationStrategy = null,
22+
stateReplication = null,
23+
} = {}) {
24+
this.sessionId = sessionId;
25+
this.contract = contract || new ReplicationMessageContract({ sessionId });
26+
this.reconciliationStrategy = reconciliationStrategy || new ClientReconciliationStrategy();
27+
this.stateReplication = stateReplication || new StateReplication();
28+
29+
this.pendingEnvelopes = [];
30+
this.ignoredEnvelopes = [];
31+
this.replicatedState = [];
32+
this.lastAppliedTick = -1;
33+
this.lastAppliedSequence = -1;
34+
this.appliedCount = 0;
35+
this.ignoredCount = 0;
36+
this.nextReceivedOrder = 0;
37+
}
38+
39+
ingestReplicationEnvelope(envelope, { receivedAtMs = 0 } = {}) {
40+
const validation = this.contract.validate(envelope, { sessionId: this.sessionId });
41+
if (!validation.ok) {
42+
this.ignoredEnvelopes.push({
43+
reason: REPLICATION_IGNORE_REASONS.INVALID_ENVELOPE,
44+
code: validation.code,
45+
envelope: envelope ? clone(envelope) : null,
46+
});
47+
this.ignoredCount += 1;
48+
return {
49+
ok: false,
50+
code: validation.code,
51+
message: validation.message,
52+
};
53+
}
54+
55+
const normalized = this.contract.normalize(envelope, {
56+
receivedAtMs,
57+
receivedOrder: this.nextReceivedOrder,
58+
});
59+
this.nextReceivedOrder += 1;
60+
this.pendingEnvelopes.push(normalized);
61+
return {
62+
ok: true,
63+
pending: this.pendingEnvelopes.length,
64+
};
65+
}
66+
67+
applyPendingReplication() {
68+
const orderedEnvelopes = this.reconciliationStrategy.sort(this.pendingEnvelopes);
69+
this.pendingEnvelopes.length = 0;
70+
71+
let applied = 0;
72+
let ignored = 0;
73+
orderedEnvelopes.forEach((envelope) => {
74+
const decision = this.reconciliationStrategy.shouldApply(envelope, {
75+
lastAppliedTick: this.lastAppliedTick,
76+
lastAppliedSequence: this.lastAppliedSequence,
77+
});
78+
79+
if (!decision.apply) {
80+
this.ignoredEnvelopes.push({
81+
reason: decision.reason,
82+
envelope: clone(envelope),
83+
});
84+
this.ignoredCount += 1;
85+
ignored += 1;
86+
return;
87+
}
88+
89+
const baseState = envelope.snapshotType === 'full' ? [] : this.replicatedState;
90+
this.replicatedState = this.stateReplication.applySnapshot(envelope.snapshot, baseState);
91+
this.lastAppliedTick = envelope.authoritativeTick;
92+
this.lastAppliedSequence = envelope.replicationSequence;
93+
this.appliedCount += 1;
94+
applied += 1;
95+
});
96+
97+
return {
98+
applied,
99+
ignored,
100+
snapshot: this.getReplicationStatus(),
101+
};
102+
}
103+
104+
getReplicatedStateSnapshot() {
105+
return clone(this.replicatedState);
106+
}
107+
108+
getIgnoredEnvelopes() {
109+
return this.ignoredEnvelopes.map((entry) => clone(entry));
110+
}
111+
112+
getReplicationStatus() {
113+
return {
114+
sessionId: this.sessionId,
115+
lastAppliedTick: this.lastAppliedTick,
116+
lastAppliedSequence: this.lastAppliedSequence,
117+
appliedCount: this.appliedCount,
118+
ignoredCount: this.ignoredCount,
119+
pendingEnvelopes: this.pendingEnvelopes.length,
120+
stateEntities: this.replicatedState.length,
121+
contract: this.contract.getStats(),
122+
};
123+
}
124+
}
125+

0 commit comments

Comments
 (0)