Skip to content

Commit 6fffde7

Browse files
author
DavidQ
committed
PLAN_PR_LEVEL_12_1_REAL_NETWORK_FOUNDATION: establish transport/session contracts and handshake model for real-network lane
1 parent ae900a4 commit 6fffde7

10 files changed

Lines changed: 586 additions & 39 deletions

docs/dev/CODEX_COMMANDS.md

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,4 @@
1-
MODEL: GPT-5.4
2-
REASONING: medium
3-
1+
MODEL: GPT-5.3-codex
2+
REASONING: high
43
COMMAND:
5-
Create `BUILD_PR_ROADMAP_ADD_FULL_NETWORK_CAPABILITY_BEFORE_3D`.
6-
7-
Roadmap-only update.
8-
9-
Required work:
10-
1. Update `docs/dev/roadmaps/MASTER_ROADMAP_HIGH_LEVEL.md`.
11-
2. Add a new roadmap lane for full real-network capability prior to 3D execution.
12-
3. Include explicit items for:
13-
- real transport/session layer
14-
- authoritative live server runtime
15-
- replication/client application
16-
- playable real multiplayer validation
17-
- server hosting + Docker containerization
18-
- promotion/readiness gate
19-
- include samples for phase 13
20-
4. Update sequencing/dependency wording so real-network capability is completed before active 3D execution begins.
21-
5. Preserve all already-complete network/debug/container items.
22-
6. Do NOT delete existing roadmap content.
23-
7. Do NOT rewrite unrelated roadmap text.
24-
8. Package the updated roadmap bundle into:
25-
`<project folder>/tmp/BUILD_PR_ROADMAP_ADD_FULL_NETWORK_CAPABILITY_BEFORE_3D.zip`
26-
27-
Hard rules:
28-
- roadmap update only
29-
- additive preferred
30-
- no code changes
31-
- no unrelated edits
4+
Implement transport/session abstraction contracts and minimal handshake simulation per PLAN_PR_LEVEL_12_1_REAL_NETWORK_FOUNDATION. No engine API breakage.

docs/dev/COMMIT_COMMENT.txt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
Add full real-network capability lane before 3D, including hosted Docker server path
2-
BUILD_PR_ROADMAP_ADD_FULL_NETWORK_CAPABILITY_BEFORE_3D
1+
PLAN_PR_LEVEL_12_1_REAL_NETWORK_FOUNDATION: establish transport/session contracts and handshake model for real-network lane
Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
- new real-network capability lane added to roadmap
2-
- hosted Docker server path explicitly included
3-
- 3D sequencing updated to depend on real-network completion
4-
- existing completed network items preserved
5-
- roadmap updated additively
6-
- output ZIP created at:
7-
<project folder>/tmp/BUILD_PR_ROADMAP_ADD_FULL_NETWORK_CAPABILITY_BEFORE_3D.zip
1+
- Transport abstraction defined
2+
- Session lifecycle defined
3+
- Handshake flow defined
4+
- No engine boundary violations
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# LEVEL_12_1_REAL_NETWORK_FOUNDATION_CONTRACTS
2+
3+
## Transport Abstraction Boundary
4+
5+
`src/engine/network/TransportContract.js` defines the minimum runtime boundary for transport implementations:
6+
7+
- Required methods: `connect(remote)`, `disconnect()`, `send(packet)`, `drainInbox()`
8+
- Optional connection introspection: `isConnected()`, `getConnectionState()`, or `getState()`
9+
- Packet ownership split:
10+
- Caller-owned fields: `type`, `sessionId`, `from`, `payload`
11+
- Transport-owned metadata: `createdAt`, `simulatedDelayMs`
12+
13+
The boundary is enforced via `assertTransportContract(...)` and wrapped with `createTransportBoundary(...)`.
14+
15+
## Session Lifecycle Contract
16+
17+
`src/engine/network/SessionLifecycleContract.js` defines explicit session states and valid transitions.
18+
19+
States:
20+
21+
- `idle`
22+
- `connecting`
23+
- `handshaking`
24+
- `active`
25+
- `disconnecting`
26+
- `disconnected`
27+
- `failed`
28+
29+
Transition rules are centralized in `ALLOWED_TRANSITIONS` and exposed through:
30+
31+
- `getSessionLifecycleContract()`
32+
- `createSessionLifecycle(...)`
33+
34+
Each transition is validated (`canTransition`) and recorded in state history.
35+
36+
## Minimal Handshake Flow
37+
38+
`src/engine/network/HandshakeSimulator.js` implements a deterministic, transport-backed handshake simulation:
39+
40+
1. Client sends `session.handshake.hello`
41+
2. Host responds with `session.handshake.accept`
42+
3. Client sends `session.handshake.confirm`
43+
4. Both peers reach `active` session state
44+
45+
Disconnect simulation:
46+
47+
- Host emits `session.handshake.bye`
48+
- Both peers transition through `disconnecting` to `disconnected`
49+
50+
`getHandshakeContract()` documents message names and expected flow order.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# PLAN_PR_LEVEL_12_1_REAL_NETWORK_FOUNDATION
2+
3+
## Purpose
4+
Establish real network transport/session foundation as first step toward full real-network capability lane.
5+
6+
## Scope
7+
- Define transport abstraction boundary
8+
- Define session lifecycle contract
9+
- Define minimal server/client handshake model
10+
11+
## Non-Scope
12+
- No gameplay integration
13+
- No replication logic
14+
- No UI/debug expansion
15+
16+
## Testability
17+
- Can simulate connect/disconnect lifecycle
18+
- Can validate session state transitions
19+
20+
## Acceptance Criteria
21+
- Transport contract documented
22+
- Session lifecycle documented
23+
- Handshake flow documented
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
/*
2+
Toolbox Aid
3+
David Quesenberry
4+
04/15/2026
5+
HandshakeSimulator.js
6+
*/
7+
import LoopbackTransport from './LoopbackTransport.js';
8+
import {
9+
createSessionLifecycle,
10+
getSessionLifecycleContract,
11+
SESSION_STATES,
12+
} from './SessionLifecycleContract.js';
13+
import { createTransportBoundary, getTransportContract } from './TransportContract.js';
14+
15+
function clone(value) {
16+
return JSON.parse(JSON.stringify(value));
17+
}
18+
19+
export const HANDSHAKE_MESSAGE_TYPES = Object.freeze({
20+
HELLO: 'session.handshake.hello',
21+
ACCEPT: 'session.handshake.accept',
22+
CONFIRM: 'session.handshake.confirm',
23+
BYE: 'session.handshake.bye',
24+
});
25+
26+
function createHandshakePacket({
27+
type,
28+
sessionId,
29+
from,
30+
to,
31+
payload = {},
32+
}) {
33+
return {
34+
type,
35+
sessionId,
36+
from,
37+
to,
38+
payload: clone(payload),
39+
createdAt: Date.now(),
40+
};
41+
}
42+
43+
export function getHandshakeContract() {
44+
return {
45+
messages: { ...HANDSHAKE_MESSAGE_TYPES },
46+
flow: [
47+
HANDSHAKE_MESSAGE_TYPES.HELLO,
48+
HANDSHAKE_MESSAGE_TYPES.ACCEPT,
49+
HANDSHAKE_MESSAGE_TYPES.CONFIRM,
50+
],
51+
transport: getTransportContract(),
52+
sessionLifecycle: getSessionLifecycleContract(),
53+
};
54+
}
55+
56+
export default class HandshakeSimulator {
57+
constructor({
58+
sessionId = 'session',
59+
hostId = 'host',
60+
clientId = 'client',
61+
hostTransport = null,
62+
clientTransport = null,
63+
} = {}) {
64+
const transports = hostTransport && clientTransport
65+
? [hostTransport, clientTransport]
66+
: LoopbackTransport.createLinkedPair(hostId, clientId);
67+
68+
this.sessionId = sessionId;
69+
this.hostId = hostId;
70+
this.clientId = clientId;
71+
this.hostTransport = createTransportBoundary(transports[0], { name: 'hostTransport' });
72+
this.clientTransport = createTransportBoundary(transports[1], { name: 'clientTransport' });
73+
this.hostSession = createSessionLifecycle({
74+
sessionId,
75+
peerId: hostId,
76+
role: 'host',
77+
});
78+
this.clientSession = createSessionLifecycle({
79+
sessionId,
80+
peerId: clientId,
81+
role: 'client',
82+
});
83+
this.handshakeToken = null;
84+
this.handshakeComplete = false;
85+
this.eventLog = [];
86+
}
87+
88+
log(event, details = {}) {
89+
this.eventLog.push({
90+
event,
91+
details: clone(details),
92+
step: this.eventLog.length,
93+
});
94+
}
95+
96+
begin({ token = `${this.sessionId}:${this.clientId}` } = {}) {
97+
this.handshakeToken = token;
98+
this.hostSession.transition(SESSION_STATES.CONNECTING, 'host-connect');
99+
this.clientSession.transition(SESSION_STATES.CONNECTING, 'client-connect');
100+
this.hostSession.transition(SESSION_STATES.HANDSHAKING, 'host-ready-for-hello');
101+
this.clientSession.transition(SESSION_STATES.HANDSHAKING, 'client-sending-hello');
102+
103+
const accepted = this.clientTransport.send(createHandshakePacket({
104+
type: HANDSHAKE_MESSAGE_TYPES.HELLO,
105+
sessionId: this.sessionId,
106+
from: this.clientId,
107+
to: this.hostId,
108+
payload: { token: this.handshakeToken },
109+
}));
110+
111+
if (!accepted) {
112+
this.hostSession.transition(SESSION_STATES.FAILED, 'hello-send-failed');
113+
this.clientSession.transition(SESSION_STATES.FAILED, 'hello-send-failed');
114+
this.log('handshake.failed', { reason: 'hello-send-failed' });
115+
return false;
116+
}
117+
118+
this.log('handshake.begin', {
119+
sessionId: this.sessionId,
120+
hostId: this.hostId,
121+
clientId: this.clientId,
122+
token: this.handshakeToken,
123+
});
124+
return true;
125+
}
126+
127+
processHostInbox() {
128+
const packets = this.hostTransport.drainInbox();
129+
packets.forEach((packet) => {
130+
if (packet.type === HANDSHAKE_MESSAGE_TYPES.HELLO) {
131+
this.log('handshake.hello.received', { from: packet.from });
132+
this.hostTransport.send(createHandshakePacket({
133+
type: HANDSHAKE_MESSAGE_TYPES.ACCEPT,
134+
sessionId: this.sessionId,
135+
from: this.hostId,
136+
to: this.clientId,
137+
payload: { acceptedToken: packet.payload?.token ?? null },
138+
}));
139+
} else if (packet.type === HANDSHAKE_MESSAGE_TYPES.CONFIRM) {
140+
this.hostSession.transition(SESSION_STATES.ACTIVE, 'client-confirmed');
141+
this.handshakeComplete = this.clientSession.getState() === SESSION_STATES.ACTIVE
142+
&& this.hostSession.getState() === SESSION_STATES.ACTIVE;
143+
this.log('handshake.confirm.received', { from: packet.from });
144+
} else if (packet.type === HANDSHAKE_MESSAGE_TYPES.BYE) {
145+
this.hostSession.transition(SESSION_STATES.DISCONNECTED, 'client-disconnect');
146+
this.log('session.disconnected.host', { reason: packet.payload?.reason ?? 'remote' });
147+
}
148+
});
149+
return packets.length;
150+
}
151+
152+
processClientInbox() {
153+
const packets = this.clientTransport.drainInbox();
154+
packets.forEach((packet) => {
155+
if (packet.type === HANDSHAKE_MESSAGE_TYPES.ACCEPT) {
156+
this.log('handshake.accept.received', { from: packet.from });
157+
this.clientSession.transition(SESSION_STATES.ACTIVE, 'host-accepted');
158+
this.clientTransport.send(createHandshakePacket({
159+
type: HANDSHAKE_MESSAGE_TYPES.CONFIRM,
160+
sessionId: this.sessionId,
161+
from: this.clientId,
162+
to: this.hostId,
163+
payload: { token: packet.payload?.acceptedToken ?? null },
164+
}));
165+
} else if (packet.type === HANDSHAKE_MESSAGE_TYPES.BYE) {
166+
this.clientSession.transition(SESSION_STATES.DISCONNECTED, 'host-disconnect');
167+
this.log('session.disconnected.client', { reason: packet.payload?.reason ?? 'remote' });
168+
}
169+
});
170+
return packets.length;
171+
}
172+
173+
update({ maxPumpIterations = 8 } = {}) {
174+
let iteration = 0;
175+
while (iteration < maxPumpIterations) {
176+
const hostProcessed = this.processHostInbox();
177+
const clientProcessed = this.processClientInbox();
178+
if (!hostProcessed && !clientProcessed) {
179+
break;
180+
}
181+
iteration += 1;
182+
}
183+
184+
return this.getState();
185+
}
186+
187+
disconnect(reason = 'manual') {
188+
const hostState = this.hostSession.getState();
189+
const clientState = this.clientSession.getState();
190+
if (hostState === SESSION_STATES.ACTIVE) {
191+
this.hostSession.transition(SESSION_STATES.DISCONNECTING, 'host-disconnecting');
192+
this.hostTransport.send(createHandshakePacket({
193+
type: HANDSHAKE_MESSAGE_TYPES.BYE,
194+
sessionId: this.sessionId,
195+
from: this.hostId,
196+
to: this.clientId,
197+
payload: { reason },
198+
}));
199+
this.hostSession.transition(SESSION_STATES.DISCONNECTED, reason);
200+
}
201+
202+
if (clientState === SESSION_STATES.ACTIVE) {
203+
this.clientSession.transition(SESSION_STATES.DISCONNECTING, 'client-disconnecting');
204+
this.clientSession.transition(SESSION_STATES.DISCONNECTED, reason);
205+
}
206+
207+
this.hostTransport.disconnect();
208+
this.clientTransport.disconnect();
209+
this.handshakeComplete = false;
210+
this.log('session.disconnect', { reason });
211+
this.update();
212+
return this.getState();
213+
}
214+
215+
getState() {
216+
return {
217+
sessionId: this.sessionId,
218+
host: this.hostSession.getSnapshot(),
219+
client: this.clientSession.getSnapshot(),
220+
handshakeComplete: this.handshakeComplete,
221+
transportConnected: {
222+
host: this.hostTransport.isConnected(),
223+
client: this.clientTransport.isConnected(),
224+
},
225+
events: this.eventLog.map((entry) => ({ ...entry })),
226+
};
227+
}
228+
}
229+

0 commit comments

Comments
 (0)