Skip to content

Commit 5db2763

Browse files
Implement Time Synchronization System (TimeSync.js)
Complete implementation of the time synchronization system for Nine Realities Netcode: - NTP-like algorithm for clock synchronization - Smoothed RTT estimation with jitter calculation - Clock offset tracking with drift compensation - Tick-based timestep conversion utilities - Sync quality metrics and reliability checking - Full usage example with simulated network This fulfills the "Time synchronization system" item from Q1 2025 roadmap.
1 parent 174162f commit 5db2763

1 file changed

Lines changed: 335 additions & 0 deletions

File tree

examples/javascript/TimeSync.js

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
/**
2+
* Time Synchronization System for Nine Realities Netcode
3+
*
4+
* Provides clock synchronization between client and server using:
5+
* - Network Time Protocol (NTP)-like algorithm
6+
* - Smoothed RTT (Round-Trip Time) estimation
7+
* - Clock offset calculation with drift compensation
8+
* - Tick-based simulation timestep conversion
9+
*
10+
* Critical for reconciliation: clients must map local inputs to exact server ticks.
11+
*/
12+
13+
class TimeSync {
14+
constructor(config = {}) {
15+
// Configuration
16+
this.tickRate = config.tickRate || 60; // Server ticks per second
17+
this.tickDuration = 1000 / this.tickRate; // ms per tick
18+
this.syncInterval = config.syncInterval || 1000; // How often to sync (ms)
19+
this.rttSamples = config.rttSamples || 10; // Samples for RTT smoothing
20+
21+
// State
22+
this.serverTick = 0; // Last known server tick
23+
this.clockOffset = 0; // Client clock - Server clock (ms)
24+
this.rttHistory = []; // Recent RTT measurements
25+
this.smoothedRtt = 0; // Exponentially weighted RTT
26+
this.clockDrift = 0; // ms/s clock drift rate
27+
28+
// Timestamps
29+
this.lastSyncTime = 0;
30+
this.lastDriftCheckTime = Date.now();
31+
this.lastDriftOffset = 0;
32+
33+
// Statistics
34+
this.syncCount = 0;
35+
this.jitter = 0; // RTT variance
36+
}
37+
38+
/**
39+
* Client: Send time sync request to server
40+
* @returns {Object} Sync request packet
41+
*/
42+
createSyncRequest() {
43+
const clientTimestamp = this.getLocalTime();
44+
return {
45+
type: 'time_sync_req',
46+
clientSendTime: clientTimestamp,
47+
sequenceId: this.syncCount++
48+
};
49+
}
50+
51+
/**
52+
* Server: Process sync request and create response
53+
* @param {Object} request - Client's sync request
54+
* @param {number} serverTick - Current server tick
55+
* @returns {Object} Sync response packet
56+
*/
57+
handleSyncRequest(request, serverTick) {
58+
const serverTime = this.getLocalTime();
59+
return {
60+
type: 'time_sync_res',
61+
clientSendTime: request.clientSendTime,
62+
serverReceiveTime: serverTime,
63+
serverSendTime: serverTime, // Can add processing delay if needed
64+
serverTick: serverTick,
65+
sequenceId: request.sequenceId
66+
};
67+
}
68+
69+
/**
70+
* Client: Process sync response from server
71+
* @param {Object} response - Server's sync response
72+
*/
73+
processSyncResponse(response) {
74+
const clientReceiveTime = this.getLocalTime();
75+
76+
// Calculate RTT: total round-trip time
77+
const rtt = clientReceiveTime - response.clientSendTime;
78+
79+
// Calculate one-way latency (assume symmetric)
80+
const oneWayLatency = rtt / 2;
81+
82+
// Estimate server time when we received the response
83+
const estimatedServerTime = response.serverSendTime + oneWayLatency;
84+
85+
// Calculate clock offset
86+
const newOffset = clientReceiveTime - estimatedServerTime;
87+
88+
// Update RTT statistics
89+
this.updateRttStats(rtt);
90+
91+
// Update clock offset with smoothing
92+
this.updateClockOffset(newOffset);
93+
94+
// Update server tick
95+
this.serverTick = response.serverTick;
96+
this.lastSyncTime = clientReceiveTime;
97+
98+
// Update drift estimation periodically
99+
this.updateClockDrift(newOffset);
100+
101+
console.log(`🕐 Time sync: RTT=${rtt.toFixed(1)}ms, Offset=${this.clockOffset.toFixed(1)}ms, Drift=${this.clockDrift.toFixed(3)}ms/s`);
102+
}
103+
104+
/**
105+
* Update RTT statistics with exponential smoothing
106+
* @param {number} rtt - New RTT measurement
107+
*/
108+
updateRttStats(rtt) {
109+
// Add to history
110+
this.rttHistory.push(rtt);
111+
if (this.rttHistory.length > this.rttSamples) {
112+
this.rttHistory.shift();
113+
}
114+
115+
// Exponential weighted moving average
116+
const alpha = 0.125; // Smoothing factor (typical for TCP RTT)
117+
if (this.smoothedRtt === 0) {
118+
this.smoothedRtt = rtt;
119+
} else {
120+
this.smoothedRtt = alpha * rtt + (1 - alpha) * this.smoothedRtt;
121+
}
122+
123+
// Calculate jitter (mean deviation)
124+
const meanRtt = this.rttHistory.reduce((a, b) => a + b, 0) / this.rttHistory.length;
125+
const variance = this.rttHistory.reduce((sum, val) => sum + Math.abs(val - meanRtt), 0) / this.rttHistory.length;
126+
this.jitter = variance;
127+
}
128+
129+
/**
130+
* Update clock offset with smoothing to prevent jitter
131+
* @param {number} newOffset - New offset measurement
132+
*/
133+
updateClockOffset(newOffset) {
134+
if (this.clockOffset === 0) {
135+
// First measurement: accept immediately
136+
this.clockOffset = newOffset;
137+
} else {
138+
// Smooth updates to prevent visual jitter
139+
const delta = newOffset - this.clockOffset;
140+
141+
// If offset change is large, accept more quickly
142+
const threshold = 50; // ms
143+
const blendFactor = Math.abs(delta) > threshold ? 0.5 : 0.1;
144+
145+
this.clockOffset += delta * blendFactor;
146+
}
147+
}
148+
149+
/**
150+
* Update clock drift estimation
151+
* @param {number} currentOffset - Current offset measurement
152+
*/
153+
updateClockDrift(currentOffset) {
154+
const now = Date.now();
155+
const elapsed = (now - this.lastDriftCheckTime) / 1000; // seconds
156+
157+
// Only update drift estimate after sufficient time has passed
158+
if (elapsed > 10) {
159+
const offsetDelta = currentOffset - this.lastDriftOffset;
160+
this.clockDrift = offsetDelta / elapsed; // ms per second
161+
162+
this.lastDriftCheckTime = now;
163+
this.lastDriftOffset = currentOffset;
164+
}
165+
}
166+
167+
/**
168+
* Get current local timestamp
169+
* @returns {number} Timestamp in milliseconds
170+
*/
171+
getLocalTime() {
172+
return Date.now();
173+
}
174+
175+
/**
176+
* Get estimated server time
177+
* @returns {number} Server time in milliseconds
178+
*/
179+
getServerTime() {
180+
const localTime = this.getLocalTime();
181+
const timeSinceSync = localTime - this.lastSyncTime;
182+
183+
// Apply clock offset and drift correction
184+
const driftCorrection = this.clockDrift * (timeSinceSync / 1000);
185+
return localTime - this.clockOffset - driftCorrection;
186+
}
187+
188+
/**
189+
* Get estimated current server tick
190+
* @returns {number} Server tick
191+
*/
192+
getServerTick() {
193+
const serverTime = this.getServerTime();
194+
const timeSinceSync = serverTime - (this.lastSyncTime - this.clockOffset);
195+
const ticksSinceSync = Math.floor(timeSinceSync / this.tickDuration);
196+
return this.serverTick + ticksSinceSync;
197+
}
198+
199+
/**
200+
* Convert local timestamp to server tick
201+
* @param {number} localTimestamp - Local timestamp
202+
* @returns {number} Corresponding server tick
203+
*/
204+
localTimeToServerTick(localTimestamp) {
205+
const serverTime = localTimestamp - this.clockOffset;
206+
return Math.floor(serverTime / this.tickDuration);
207+
}
208+
209+
/**
210+
* Convert server tick to estimated server time
211+
* @param {number} tick - Server tick
212+
* @returns {number} Server time in milliseconds
213+
*/
214+
serverTickToTime(tick) {
215+
return tick * this.tickDuration;
216+
}
217+
218+
/**
219+
* Get synchronization quality metrics
220+
* @returns {Object} Sync quality stats
221+
*/
222+
getSyncQuality() {
223+
return {
224+
smoothedRtt: this.smoothedRtt,
225+
jitter: this.jitter,
226+
clockOffset: this.clockOffset,
227+
clockDrift: this.clockDrift,
228+
quality: this.calculateQuality()
229+
};
230+
}
231+
232+
/**
233+
* Calculate overall sync quality score (0-1)
234+
* @returns {number} Quality score
235+
*/
236+
calculateQuality() {
237+
// Perfect: RTT < 50ms, jitter < 10ms
238+
// Good: RTT < 100ms, jitter < 20ms
239+
// Fair: RTT < 200ms, jitter < 50ms
240+
// Poor: RTT > 200ms or jitter > 50ms
241+
242+
let score = 1.0;
243+
244+
// RTT penalty
245+
if (this.smoothedRtt > 200) score *= 0.3;
246+
else if (this.smoothedRtt > 100) score *= 0.6;
247+
else if (this.smoothedRtt > 50) score *= 0.8;
248+
249+
// Jitter penalty
250+
if (this.jitter > 50) score *= 0.3;
251+
else if (this.jitter > 20) score *= 0.6;
252+
else if (this.jitter > 10) score *= 0.8;
253+
254+
return score;
255+
}
256+
257+
/**
258+
* Check if synchronization is reliable
259+
* @returns {boolean} True if sync is good
260+
*/
261+
isSyncReliable() {
262+
return this.calculateQuality() > 0.5 && this.syncCount > 3;
263+
}
264+
}
265+
266+
// ============================================
267+
// USAGE EXAMPLE
268+
// ============================================
269+
270+
function demonstrateTimeSync() {
271+
console.log('=== Time Synchronization Demo ===\n');
272+
273+
// Create client and server instances
274+
const clientSync = new TimeSync({ tickRate: 60 });
275+
const serverSync = new TimeSync({ tickRate: 60 });
276+
277+
// Simulate network with variable latency
278+
function simulateNetwork(packet, callback, latencyMs = 50) {
279+
setTimeout(() => callback(packet), latencyMs + Math.random() * 20);
280+
}
281+
282+
let serverTickCounter = 0;
283+
284+
// Perform sync cycle
285+
function performSync() {
286+
console.log(`\n--- Sync #${clientSync.syncCount + 1} ---`);
287+
288+
// Client: Create sync request
289+
const request = clientSync.createSyncRequest();
290+
console.log('Client → Server: Sync request sent');
291+
292+
// Simulate network delay to server
293+
simulateNetwork(request, (req) => {
294+
// Server: Handle request and create response
295+
serverTickCounter += 3; // Simulate server advancing
296+
const response = serverSync.handleSyncRequest(req, serverTickCounter);
297+
console.log(`Server: Processing sync (tick ${serverTickCounter})`);
298+
299+
// Simulate network delay back to client
300+
simulateNetwork(response, (res) => {
301+
// Client: Process response
302+
clientSync.processSyncResponse(res);
303+
304+
// Show results
305+
const quality = clientSync.getSyncQuality();
306+
console.log(`Client estimated server tick: ${clientSync.getServerTick()}`);
307+
console.log(`Sync quality: ${(quality.quality * 100).toFixed(0)}%`);
308+
}, 50);
309+
}, 50);
310+
}
311+
312+
// Perform multiple syncs
313+
performSync();
314+
setTimeout(() => performSync(), 500);
315+
setTimeout(() => performSync(), 1000);
316+
setTimeout(() => performSync(), 1500);
317+
318+
// Show final stats
319+
setTimeout(() => {
320+
console.log('\n=== Final Statistics ===');
321+
const quality = clientSync.getSyncQuality();
322+
console.log(`Smoothed RTT: ${quality.smoothedRtt.toFixed(1)}ms`);
323+
console.log(`Jitter: ${quality.jitter.toFixed(1)}ms`);
324+
console.log(`Clock offset: ${quality.clockOffset.toFixed(1)}ms`);
325+
console.log(`Sync quality: ${(quality.quality * 100).toFixed(0)}%`);
326+
console.log(`Is reliable: ${clientSync.isSyncReliable()}`);
327+
}, 2000);
328+
}
329+
330+
// Run demo if executed directly
331+
if (require.main === module) {
332+
demonstrateTimeSync();
333+
}
334+
335+
module.exports = TimeSync;

0 commit comments

Comments
 (0)