@gamerstake/game-core 0.1.0 → 0.2.0
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/CHANGELOG.md +142 -0
- package/CLIENT_SDK_README.md +634 -0
- package/README.md +58 -5
- package/dist/client/index.d.ts +574 -0
- package/dist/client/index.js +970 -0
- package/dist/client/index.js.map +1 -0
- package/dist/index.d.ts +90 -1
- package/dist/index.js +356 -1
- package/dist/index.js.map +1 -1
- package/docs/N1-package-overview.md +303 -0
- package/docs/N2-api-reference.md +500 -0
- package/docs/N3-implementation-guide.md +550 -0
- package/docs/N4-testing-validation.md +390 -0
- package/docs/N5-integration-checklist.md +90 -0
- package/docs/README.md +137 -0
- package/examples/simple-game/README.md +17 -6
- package/examples/simple-game/client.ts +25 -14
- package/examples/simple-game/package.json +5 -1
- package/examples/simple-game/server.ts +21 -13
- package/package.json +17 -13
- package/src/adapters/SocketIOAdapter.ts +496 -0
- package/src/client/GameClient.ts +466 -0
- package/src/client/InputBuffer.ts +148 -0
- package/src/client/Interpolator.ts +242 -0
- package/src/client/Reconciler.ts +233 -0
- package/src/client/TimeSync.ts +182 -0
- package/src/client/index.ts +32 -0
- package/src/client/types.ts +247 -0
- package/src/index.ts +8 -0
- package/tests/client/InputBuffer.test.ts +118 -0
- package/tests/client/TimeSync.test.ts +116 -0
- package/tsup.config.ts +4 -1
- package/TESTING_OVERVIEW.md +0 -378
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client SDK Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for the game-core client SDK.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Socket } from 'socket.io-client';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Game client configuration
|
|
11
|
+
*/
|
|
12
|
+
export interface GameClientConfig {
|
|
13
|
+
/** Server URL to connect to */
|
|
14
|
+
serverUrl: string;
|
|
15
|
+
|
|
16
|
+
/** Enable client-side prediction */
|
|
17
|
+
enablePrediction?: boolean;
|
|
18
|
+
|
|
19
|
+
/** Enable server reconciliation */
|
|
20
|
+
enableReconciliation?: boolean;
|
|
21
|
+
|
|
22
|
+
/** Enable entity interpolation */
|
|
23
|
+
enableInterpolation?: boolean;
|
|
24
|
+
|
|
25
|
+
/** Interpolation delay in milliseconds */
|
|
26
|
+
interpolationDelay?: number;
|
|
27
|
+
|
|
28
|
+
/** Maximum interpolation delay in milliseconds */
|
|
29
|
+
maxInterpolationDelay?: number;
|
|
30
|
+
|
|
31
|
+
/** Minimum interpolation delay in milliseconds */
|
|
32
|
+
minInterpolationDelay?: number;
|
|
33
|
+
|
|
34
|
+
/** Number of reconnection attempts */
|
|
35
|
+
reconnectAttempts?: number;
|
|
36
|
+
|
|
37
|
+
/** Delay between reconnection attempts in milliseconds */
|
|
38
|
+
reconnectDelay?: number;
|
|
39
|
+
|
|
40
|
+
/** Heartbeat interval in milliseconds */
|
|
41
|
+
heartbeatInterval?: number;
|
|
42
|
+
|
|
43
|
+
/** Heartbeat timeout in milliseconds */
|
|
44
|
+
heartbeatTimeout?: number;
|
|
45
|
+
|
|
46
|
+
/** Socket.io connection options */
|
|
47
|
+
socketOptions?: Partial<{
|
|
48
|
+
autoConnect: boolean;
|
|
49
|
+
reconnection: boolean;
|
|
50
|
+
reconnectionAttempts: number;
|
|
51
|
+
reconnectionDelay: number;
|
|
52
|
+
transports: string[];
|
|
53
|
+
}>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Command sent from client to server
|
|
58
|
+
*/
|
|
59
|
+
export interface Command {
|
|
60
|
+
/** Command type */
|
|
61
|
+
type: string;
|
|
62
|
+
|
|
63
|
+
/** Action sequence number */
|
|
64
|
+
seq?: number;
|
|
65
|
+
|
|
66
|
+
/** Timestamp when command was created */
|
|
67
|
+
timestamp?: number;
|
|
68
|
+
|
|
69
|
+
/** Additional command data */
|
|
70
|
+
[key: string]: unknown;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Move command
|
|
75
|
+
*/
|
|
76
|
+
export interface MoveCommand extends Command {
|
|
77
|
+
type: 'move';
|
|
78
|
+
targetX: number;
|
|
79
|
+
targetZ: number;
|
|
80
|
+
actionId?: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Stop command
|
|
85
|
+
*/
|
|
86
|
+
export interface StopCommand extends Command {
|
|
87
|
+
type: 'stop';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Network statistics
|
|
92
|
+
*/
|
|
93
|
+
export interface NetworkStats {
|
|
94
|
+
/** Round-trip time in milliseconds */
|
|
95
|
+
rtt: number;
|
|
96
|
+
|
|
97
|
+
/** Network jitter in milliseconds */
|
|
98
|
+
jitter: number;
|
|
99
|
+
|
|
100
|
+
/** Estimated packet loss percentage */
|
|
101
|
+
packetLoss: number;
|
|
102
|
+
|
|
103
|
+
/** Total messages sent */
|
|
104
|
+
messagesSent: number;
|
|
105
|
+
|
|
106
|
+
/** Total messages received */
|
|
107
|
+
messagesReceived: number;
|
|
108
|
+
|
|
109
|
+
/** Whether connected to server */
|
|
110
|
+
connected: boolean;
|
|
111
|
+
|
|
112
|
+
/** Whether currently reconnecting */
|
|
113
|
+
reconnecting: boolean;
|
|
114
|
+
|
|
115
|
+
/** Number of unacknowledged inputs */
|
|
116
|
+
inputBufferSize: number;
|
|
117
|
+
|
|
118
|
+
/** Last state update time */
|
|
119
|
+
lastStateUpdateTime: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Time sync statistics
|
|
124
|
+
*/
|
|
125
|
+
export interface TimeSyncStats {
|
|
126
|
+
/** Current clock offset */
|
|
127
|
+
offset: number;
|
|
128
|
+
|
|
129
|
+
/** Round-trip time */
|
|
130
|
+
rtt: number;
|
|
131
|
+
|
|
132
|
+
/** Network jitter */
|
|
133
|
+
jitter: number;
|
|
134
|
+
|
|
135
|
+
/** Number of samples */
|
|
136
|
+
samples: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Event callbacks
|
|
141
|
+
*/
|
|
142
|
+
export interface GameClientEvents {
|
|
143
|
+
/** Called when connected to server */
|
|
144
|
+
onConnected?: (data?: unknown) => void;
|
|
145
|
+
|
|
146
|
+
/** Called when disconnected from server */
|
|
147
|
+
onDisconnected?: (data: unknown) => void;
|
|
148
|
+
|
|
149
|
+
/** Called when connection is lost */
|
|
150
|
+
onConnectionLost?: (data: unknown) => void;
|
|
151
|
+
|
|
152
|
+
/** Called when reconnected after disconnect */
|
|
153
|
+
onReconnect?: (data?: unknown) => void;
|
|
154
|
+
|
|
155
|
+
/** Called on connection error */
|
|
156
|
+
onError?: (data: unknown) => void;
|
|
157
|
+
|
|
158
|
+
/** Called when state update received */
|
|
159
|
+
onStateUpdate?: (data: unknown) => void;
|
|
160
|
+
|
|
161
|
+
/** Called when move acknowledgment received */
|
|
162
|
+
onMoveAck?: (data: unknown) => void;
|
|
163
|
+
|
|
164
|
+
/** Custom event handlers */
|
|
165
|
+
[key: string]: ((data?: unknown) => void) | undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Input buffer entry
|
|
170
|
+
*/
|
|
171
|
+
export interface BufferedInput {
|
|
172
|
+
/** Sequence number */
|
|
173
|
+
seq: number;
|
|
174
|
+
|
|
175
|
+
/** Command data */
|
|
176
|
+
command: Command;
|
|
177
|
+
|
|
178
|
+
/** Timestamp */
|
|
179
|
+
timestamp: number;
|
|
180
|
+
|
|
181
|
+
/** Whether acknowledged by server */
|
|
182
|
+
acknowledged: boolean;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* State snapshot for interpolation
|
|
187
|
+
*/
|
|
188
|
+
export interface StateSnapshot {
|
|
189
|
+
/** Timestamp */
|
|
190
|
+
timestamp: number;
|
|
191
|
+
|
|
192
|
+
/** Server tick number */
|
|
193
|
+
tick?: number;
|
|
194
|
+
|
|
195
|
+
/** Entity states */
|
|
196
|
+
entities?: any[];
|
|
197
|
+
|
|
198
|
+
/** Custom state data */
|
|
199
|
+
[key: string]: unknown;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Transport interface (abstraction for future protocols)
|
|
204
|
+
*/
|
|
205
|
+
export interface Transport {
|
|
206
|
+
connect(): Promise<void>;
|
|
207
|
+
disconnect(): void;
|
|
208
|
+
send(event: string, data: unknown): void;
|
|
209
|
+
on(event: string, handler: (data: unknown) => void): void;
|
|
210
|
+
off(event: string, handler: (data: unknown) => void): void;
|
|
211
|
+
once(event: string, handler: (data: unknown) => void): void;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Socket.io transport implementation
|
|
216
|
+
*/
|
|
217
|
+
export class SocketIOTransport implements Transport {
|
|
218
|
+
constructor(private socket: Socket) {}
|
|
219
|
+
|
|
220
|
+
async connect(): Promise<void> {
|
|
221
|
+
return new Promise((resolve, reject) => {
|
|
222
|
+
this.socket.once('connect', () => resolve());
|
|
223
|
+
this.socket.once('connect_error', (error) => reject(error));
|
|
224
|
+
this.socket.connect();
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
disconnect(): void {
|
|
229
|
+
this.socket.disconnect();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
send(event: string, data: unknown): void {
|
|
233
|
+
this.socket.emit(event, data);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
on(event: string, handler: (data: unknown) => void): void {
|
|
237
|
+
this.socket.on(event, handler);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
off(event: string, handler: (data: unknown) => void): void {
|
|
241
|
+
this.socket.off(event, handler);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
once(event: string, handler: (data: unknown) => void): void {
|
|
245
|
+
this.socket.once(event, handler);
|
|
246
|
+
}
|
|
247
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -37,6 +37,14 @@ export type {
|
|
|
37
37
|
export { Network } from './network/Network.js';
|
|
38
38
|
export { Snapshot } from './network/Snapshot.js';
|
|
39
39
|
|
|
40
|
+
// Adapters
|
|
41
|
+
export { SocketIOAdapter } from './adapters/SocketIOAdapter.js';
|
|
42
|
+
export type {
|
|
43
|
+
SocketIOAdapterConfig,
|
|
44
|
+
AdapterContext,
|
|
45
|
+
RoomMetadata,
|
|
46
|
+
} from './adapters/SocketIOAdapter.js';
|
|
47
|
+
|
|
40
48
|
// Utils
|
|
41
49
|
export { RingBuffer } from './utils/RingBuffer.js';
|
|
42
50
|
export {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InputBuffer Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { InputBuffer } from '../../src/client/InputBuffer';
|
|
6
|
+
|
|
7
|
+
describe('InputBuffer', () => {
|
|
8
|
+
let buffer: InputBuffer;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
buffer = new InputBuffer(10); // Small buffer for testing
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe('addInput', () => {
|
|
15
|
+
it('should add input and return sequence number', () => {
|
|
16
|
+
const seq = buffer.addInput({ type: 'move', targetX: 100, targetZ: 50 });
|
|
17
|
+
expect(seq).toBe(1);
|
|
18
|
+
expect(buffer.size()).toBe(1);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should increment sequence numbers', () => {
|
|
22
|
+
const seq1 = buffer.addInput({ type: 'move', targetX: 100, targetZ: 50 });
|
|
23
|
+
const seq2 = buffer.addInput({
|
|
24
|
+
type: 'move',
|
|
25
|
+
targetX: 200,
|
|
26
|
+
targetZ: 100,
|
|
27
|
+
});
|
|
28
|
+
expect(seq2).toBe(seq1 + 1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should prevent buffer overflow', () => {
|
|
32
|
+
// Add more than maxSize
|
|
33
|
+
for (let i = 0; i < 15; i++) {
|
|
34
|
+
buffer.addInput({ type: 'move', targetX: i, targetZ: i });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
expect(buffer.size()).toBeLessThanOrEqual(10);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('acknowledge', () => {
|
|
42
|
+
it('should acknowledge input', () => {
|
|
43
|
+
const seq = buffer.addInput({ type: 'move', targetX: 100, targetZ: 50 });
|
|
44
|
+
buffer.acknowledge(seq);
|
|
45
|
+
|
|
46
|
+
expect(buffer.getUnackedCount()).toBe(0);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should remove acknowledged inputs', () => {
|
|
50
|
+
buffer.addInput({ type: 'move', targetX: 100, targetZ: 50 });
|
|
51
|
+
const seq2 = buffer.addInput({
|
|
52
|
+
type: 'move',
|
|
53
|
+
targetX: 200,
|
|
54
|
+
targetZ: 100,
|
|
55
|
+
});
|
|
56
|
+
buffer.addInput({ type: 'move', targetX: 300, targetZ: 150 });
|
|
57
|
+
|
|
58
|
+
buffer.acknowledge(seq2);
|
|
59
|
+
|
|
60
|
+
// Should remove seq1 and seq2
|
|
61
|
+
expect(buffer.getUnackedCount()).toBe(1);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('getUnacknowledgedInputs', () => {
|
|
66
|
+
it('should return unacknowledged inputs in order', () => {
|
|
67
|
+
buffer.addInput({ type: 'move', targetX: 100, targetZ: 50 });
|
|
68
|
+
buffer.addInput({ type: 'move', targetX: 200, targetZ: 100 });
|
|
69
|
+
buffer.addInput({ type: 'move', targetX: 300, targetZ: 150 });
|
|
70
|
+
|
|
71
|
+
const unacked = buffer.getUnacknowledgedInputs();
|
|
72
|
+
expect(unacked.length).toBe(3);
|
|
73
|
+
expect(unacked[0].seq).toBe(1);
|
|
74
|
+
expect(unacked[1].seq).toBe(2);
|
|
75
|
+
expect(unacked[2].seq).toBe(3);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should not return acknowledged inputs', () => {
|
|
79
|
+
const seq1 = buffer.addInput({ type: 'move', targetX: 100, targetZ: 50 });
|
|
80
|
+
buffer.addInput({ type: 'move', targetX: 200, targetZ: 100 });
|
|
81
|
+
|
|
82
|
+
buffer.acknowledge(seq1);
|
|
83
|
+
|
|
84
|
+
const unacked = buffer.getUnacknowledgedInputs();
|
|
85
|
+
expect(unacked.length).toBe(1);
|
|
86
|
+
expect(unacked[0].seq).toBe(2);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('clear', () => {
|
|
91
|
+
it('should clear all inputs', () => {
|
|
92
|
+
buffer.addInput({ type: 'move', targetX: 100, targetZ: 50 });
|
|
93
|
+
buffer.addInput({ type: 'move', targetX: 200, targetZ: 100 });
|
|
94
|
+
|
|
95
|
+
buffer.clear();
|
|
96
|
+
|
|
97
|
+
expect(buffer.size()).toBe(0);
|
|
98
|
+
expect(buffer.isEmpty()).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('removeOlderThan', () => {
|
|
103
|
+
it('should remove old inputs', () => {
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
|
|
106
|
+
buffer.addInput({ type: 'move', targetX: 100, targetZ: 50 });
|
|
107
|
+
|
|
108
|
+
// Wait a bit
|
|
109
|
+
setTimeout(() => {
|
|
110
|
+
buffer.addInput({ type: 'move', targetX: 200, targetZ: 100 });
|
|
111
|
+
|
|
112
|
+
buffer.removeOlderThan(now + 50);
|
|
113
|
+
|
|
114
|
+
expect(buffer.size()).toBe(1);
|
|
115
|
+
}, 100);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TimeSync Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { TimeSync } from '../../src/client/TimeSync';
|
|
6
|
+
|
|
7
|
+
describe('TimeSync', () => {
|
|
8
|
+
let mockSocket: { emit: jest.Mock; on: jest.Mock };
|
|
9
|
+
let timeSync: TimeSync;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockSocket = {
|
|
13
|
+
emit: jest.fn(),
|
|
14
|
+
on: jest.fn(),
|
|
15
|
+
};
|
|
16
|
+
timeSync = new TimeSync(mockSocket as any);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('start/stop', () => {
|
|
20
|
+
it('should start sending pings', () => {
|
|
21
|
+
timeSync.start();
|
|
22
|
+
expect(mockSocket.emit).toHaveBeenCalledWith('timeSyncPing', {
|
|
23
|
+
clientTime: expect.any(Number),
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should stop sending pings', (done) => {
|
|
28
|
+
timeSync.start();
|
|
29
|
+
const initialCount = mockSocket.emit.mock.calls.length;
|
|
30
|
+
timeSync.stop();
|
|
31
|
+
|
|
32
|
+
// Wait a bit and verify no more pings sent
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
const finalCount = mockSocket.emit.mock.calls.length;
|
|
35
|
+
expect(finalCount).toBe(initialCount);
|
|
36
|
+
done();
|
|
37
|
+
}, 100);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('handlePong', () => {
|
|
42
|
+
it('should calculate offset correctly', () => {
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
timeSync.handlePong({
|
|
45
|
+
clientTime: now - 50,
|
|
46
|
+
serverTime: now + 100,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const offset = timeSync.getOffset();
|
|
50
|
+
expect(offset).toBeGreaterThan(0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should calculate RTT correctly', () => {
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
timeSync.handlePong({
|
|
56
|
+
clientTime: now - 100,
|
|
57
|
+
serverTime: now,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const rtt = timeSync.getRTT();
|
|
61
|
+
expect(rtt).toBeGreaterThanOrEqual(100);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should use median for offset (robust to outliers)', () => {
|
|
65
|
+
// Add multiple samples with one outlier
|
|
66
|
+
const baseTime = Date.now();
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < 5; i++) {
|
|
69
|
+
timeSync.handlePong({
|
|
70
|
+
clientTime: baseTime - 50,
|
|
71
|
+
serverTime: baseTime + 100, // ~125ms offset
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Add outlier
|
|
76
|
+
timeSync.handlePong({
|
|
77
|
+
clientTime: baseTime - 50,
|
|
78
|
+
serverTime: baseTime + 1000, // ~1000ms offset (outlier)
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const offset = timeSync.getOffset();
|
|
82
|
+
// Should be close to 125, not affected by outlier
|
|
83
|
+
expect(offset).toBeLessThan(500);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('getServerTime', () => {
|
|
88
|
+
it('should return server time estimate', () => {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
timeSync.handlePong({
|
|
91
|
+
clientTime: now - 100,
|
|
92
|
+
serverTime: now + 50,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const serverTime = timeSync.getServerTime();
|
|
96
|
+
expect(serverTime).toBeGreaterThan(now);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe('reset', () => {
|
|
101
|
+
it('should clear all statistics', () => {
|
|
102
|
+
timeSync.handlePong({
|
|
103
|
+
clientTime: Date.now() - 100,
|
|
104
|
+
serverTime: Date.now(),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(timeSync.getRTT()).toBeGreaterThan(0);
|
|
108
|
+
|
|
109
|
+
timeSync.reset();
|
|
110
|
+
|
|
111
|
+
expect(timeSync.getRTT()).toBe(0);
|
|
112
|
+
expect(timeSync.getOffset()).toBe(0);
|
|
113
|
+
expect(timeSync.getJitter()).toBe(0);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
package/tsup.config.ts
CHANGED