@phalanx-engine/client 0.1.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/README.md +1037 -0
- package/dist/DesyncDetector.d.ts +80 -0
- package/dist/DesyncDetector.d.ts.map +1 -0
- package/dist/DesyncDetector.js +93 -0
- package/dist/DesyncDetector.js.map +1 -0
- package/dist/DeterministicRandom.d.ts +78 -0
- package/dist/DeterministicRandom.d.ts.map +1 -0
- package/dist/DeterministicRandom.js +122 -0
- package/dist/DeterministicRandom.js.map +1 -0
- package/dist/EventEmitter.d.ts +65 -0
- package/dist/EventEmitter.d.ts.map +1 -0
- package/dist/EventEmitter.js +102 -0
- package/dist/EventEmitter.js.map +1 -0
- package/dist/FixedMath.d.ts +22 -0
- package/dist/FixedMath.d.ts.map +1 -0
- package/dist/FixedMath.js +26 -0
- package/dist/FixedMath.js.map +1 -0
- package/dist/PhalanxClient.d.ts +335 -0
- package/dist/PhalanxClient.d.ts.map +1 -0
- package/dist/PhalanxClient.js +844 -0
- package/dist/PhalanxClient.js.map +1 -0
- package/dist/RenderLoop.d.ts +95 -0
- package/dist/RenderLoop.d.ts.map +1 -0
- package/dist/RenderLoop.js +192 -0
- package/dist/RenderLoop.js.map +1 -0
- package/dist/SocketManager.d.ts +228 -0
- package/dist/SocketManager.d.ts.map +1 -0
- package/dist/SocketManager.js +584 -0
- package/dist/SocketManager.js.map +1 -0
- package/dist/StateHasher.d.ts +76 -0
- package/dist/StateHasher.d.ts.map +1 -0
- package/dist/StateHasher.js +129 -0
- package/dist/StateHasher.js.map +1 -0
- package/dist/auth/AuthManager.d.ts +188 -0
- package/dist/auth/AuthManager.d.ts.map +1 -0
- package/dist/auth/AuthManager.js +462 -0
- package/dist/auth/AuthManager.js.map +1 -0
- package/dist/auth/adapters/GoogleOAuthAdapter.d.ts +164 -0
- package/dist/auth/adapters/GoogleOAuthAdapter.d.ts.map +1 -0
- package/dist/auth/adapters/GoogleOAuthAdapter.js +521 -0
- package/dist/auth/adapters/GoogleOAuthAdapter.js.map +1 -0
- package/dist/auth/index.d.ts +45 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +54 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/storage.d.ts +56 -0
- package/dist/auth/storage.d.ts.map +1 -0
- package/dist/auth/storage.js +78 -0
- package/dist/auth/storage.js.map +1 -0
- package/dist/auth/types.d.ts +212 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +7 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +83 -0
- package/dist/index.js.map +1 -0
- package/dist/recovery/BrowserLifecycle.d.ts +33 -0
- package/dist/recovery/BrowserLifecycle.d.ts.map +1 -0
- package/dist/recovery/BrowserLifecycle.js +62 -0
- package/dist/recovery/BrowserLifecycle.js.map +1 -0
- package/dist/recovery/GuestPlayerIdStore.d.ts +17 -0
- package/dist/recovery/GuestPlayerIdStore.d.ts.map +1 -0
- package/dist/recovery/GuestPlayerIdStore.js +31 -0
- package/dist/recovery/GuestPlayerIdStore.js.map +1 -0
- package/dist/recovery/KeyValueStorage.d.ts +32 -0
- package/dist/recovery/KeyValueStorage.d.ts.map +1 -0
- package/dist/recovery/KeyValueStorage.js +58 -0
- package/dist/recovery/KeyValueStorage.js.map +1 -0
- package/dist/recovery/MobileTransport.d.ts +12 -0
- package/dist/recovery/MobileTransport.d.ts.map +1 -0
- package/dist/recovery/MobileTransport.js +24 -0
- package/dist/recovery/MobileTransport.js.map +1 -0
- package/dist/recovery/NetworkQuality.d.ts +22 -0
- package/dist/recovery/NetworkQuality.d.ts.map +1 -0
- package/dist/recovery/NetworkQuality.js +35 -0
- package/dist/recovery/NetworkQuality.js.map +1 -0
- package/dist/recovery/RoomPersistence.d.ts +55 -0
- package/dist/recovery/RoomPersistence.d.ts.map +1 -0
- package/dist/recovery/RoomPersistence.js +68 -0
- package/dist/recovery/RoomPersistence.js.map +1 -0
- package/dist/recovery/RoomRecoveryController.d.ts +146 -0
- package/dist/recovery/RoomRecoveryController.d.ts.map +1 -0
- package/dist/recovery/RoomRecoveryController.js +348 -0
- package/dist/recovery/RoomRecoveryController.js.map +1 -0
- package/dist/recovery/index.d.ts +13 -0
- package/dist/recovery/index.d.ts.map +1 -0
- package/dist/recovery/index.js +8 -0
- package/dist/recovery/index.js.map +1 -0
- package/dist/types.d.ts +501 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,844 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phalanx Client
|
|
3
|
+
* Client library for connecting to Phalanx Engine servers
|
|
4
|
+
*/
|
|
5
|
+
import { EventEmitter } from './EventEmitter.js';
|
|
6
|
+
import { RenderLoop } from './RenderLoop.js';
|
|
7
|
+
import { SocketManager } from './SocketManager.js';
|
|
8
|
+
import { DesyncDetector } from './DesyncDetector.js';
|
|
9
|
+
import { AuthManager } from './auth/AuthManager.js';
|
|
10
|
+
import { RoomRecoveryController, } from './recovery/index.js';
|
|
11
|
+
import { pickMobileFriendlyTransports, loadOrCreateGuestPlayerId } from './recovery/index.js';
|
|
12
|
+
/**
|
|
13
|
+
* PhalanxClient - Main client class for connecting to Phalanx Engine servers
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const client = await PhalanxClient.create({
|
|
18
|
+
* serverUrl: 'http://localhost:3000',
|
|
19
|
+
* playerId: 'player-123',
|
|
20
|
+
* username: 'MyPlayer',
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* client.on('matchFound', (data) => console.log('Match found!'));
|
|
24
|
+
* client.on('gameStart', () => console.log('Game started!'));
|
|
25
|
+
*
|
|
26
|
+
* await client.joinQueue();
|
|
27
|
+
*
|
|
28
|
+
* client.onTick((tick, commands) => {
|
|
29
|
+
* // Process commands and run simulation
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* client.onFrame((alpha, dt) => {
|
|
33
|
+
* // Interpolate and render
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export class PhalanxClient extends EventEmitter {
|
|
38
|
+
config;
|
|
39
|
+
socketManager;
|
|
40
|
+
renderLoop;
|
|
41
|
+
desyncDetector;
|
|
42
|
+
authManager = null;
|
|
43
|
+
_roomRecovery = null;
|
|
44
|
+
// State
|
|
45
|
+
clientState = 'idle';
|
|
46
|
+
currentMatchId = null;
|
|
47
|
+
currentTick = 0;
|
|
48
|
+
// Auth state
|
|
49
|
+
authState = {
|
|
50
|
+
isAuthenticated: false,
|
|
51
|
+
isLoading: true,
|
|
52
|
+
user: null,
|
|
53
|
+
};
|
|
54
|
+
// Pending commands queue
|
|
55
|
+
pendingCommands = [];
|
|
56
|
+
// ITickFrameProvider pause/resume handler arrays
|
|
57
|
+
pauseHandlers = [];
|
|
58
|
+
resumeHandlers = [];
|
|
59
|
+
constructor(config) {
|
|
60
|
+
super();
|
|
61
|
+
// Generate default player ID if not provided
|
|
62
|
+
const defaultPlayerId = `player-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
63
|
+
// Persist guest player id (across reloads) if requested. Auth, when
|
|
64
|
+
// enabled, will overwrite this with the real user id once the auth
|
|
65
|
+
// flow resolves; until then (and for guests forever) this stays
|
|
66
|
+
// stable across reloads — required for cold-start room recovery.
|
|
67
|
+
let resolvedPlayerId = config.playerId;
|
|
68
|
+
if (!resolvedPlayerId && config.persistGuestPlayerId) {
|
|
69
|
+
const key = typeof config.persistGuestPlayerId === 'string'
|
|
70
|
+
? config.persistGuestPlayerId
|
|
71
|
+
: 'phalanx:guestPlayerId:v1';
|
|
72
|
+
resolvedPlayerId = loadOrCreateGuestPlayerId(key);
|
|
73
|
+
}
|
|
74
|
+
// Resolve socket transports: explicit `socketTransports` wins; else
|
|
75
|
+
// the opt-in `mobileFriendlyTransports` flag picks polling-on-mobile;
|
|
76
|
+
// else fall back to the historic websocket-only default.
|
|
77
|
+
const resolvedSocketTransports = config.socketTransports ??
|
|
78
|
+
(config.mobileFriendlyTransports
|
|
79
|
+
? pickMobileFriendlyTransports()
|
|
80
|
+
: ['websocket']);
|
|
81
|
+
this.config = {
|
|
82
|
+
autoReconnect: true,
|
|
83
|
+
maxReconnectAttempts: 5,
|
|
84
|
+
reconnectDelayMs: 1000,
|
|
85
|
+
connectionTimeoutMs: 10000,
|
|
86
|
+
recoverRoomTimeoutMs: 10000,
|
|
87
|
+
mobileFriendlyTransports: false,
|
|
88
|
+
persistGuestPlayerId: false,
|
|
89
|
+
tickRate: 20,
|
|
90
|
+
debug: false,
|
|
91
|
+
...config,
|
|
92
|
+
socketTransports: resolvedSocketTransports,
|
|
93
|
+
playerId: resolvedPlayerId || defaultPlayerId,
|
|
94
|
+
username: config.username || `Player-${(resolvedPlayerId || defaultPlayerId).slice(-6)}`,
|
|
95
|
+
};
|
|
96
|
+
// Initialize auth if configured
|
|
97
|
+
if (config.auth) {
|
|
98
|
+
this.initializeAuth(config.auth);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
this.authState.isLoading = false;
|
|
102
|
+
}
|
|
103
|
+
// Initialize SocketManager with callbacks
|
|
104
|
+
this.socketManager = new SocketManager({
|
|
105
|
+
serverUrl: this.config.serverUrl,
|
|
106
|
+
playerId: this.config.playerId,
|
|
107
|
+
username: this.config.username,
|
|
108
|
+
authToken: this.config.authToken,
|
|
109
|
+
connectionTimeoutMs: this.config.connectionTimeoutMs,
|
|
110
|
+
recoverRoomTimeoutMs: this.config.recoverRoomTimeoutMs,
|
|
111
|
+
socketTransports: this.config.socketTransports,
|
|
112
|
+
autoReconnect: this.config.autoReconnect,
|
|
113
|
+
maxReconnectAttempts: this.config.maxReconnectAttempts,
|
|
114
|
+
reconnectDelayMs: this.config.reconnectDelayMs,
|
|
115
|
+
debug: this.config.debug,
|
|
116
|
+
}, {
|
|
117
|
+
// Connection events
|
|
118
|
+
onConnected: () => this.emit('connected'),
|
|
119
|
+
onDisconnected: () => {
|
|
120
|
+
this.clientState = 'idle';
|
|
121
|
+
this.emit('disconnected');
|
|
122
|
+
},
|
|
123
|
+
onReconnecting: (attempt) => this.emit('reconnecting', attempt),
|
|
124
|
+
onReconnectFailed: () => this.emit('reconnectFailed'),
|
|
125
|
+
onError: (error) => this.emit('error', error),
|
|
126
|
+
// Match lifecycle events
|
|
127
|
+
onMatchFound: (data) => {
|
|
128
|
+
this.currentMatchId = data.matchId;
|
|
129
|
+
this.clientState = 'match-found';
|
|
130
|
+
this.emit('matchFound', data);
|
|
131
|
+
},
|
|
132
|
+
onCountdown: (data) => this.emit('countdown', data),
|
|
133
|
+
onGameStart: (data) => {
|
|
134
|
+
this.clientState = 'playing';
|
|
135
|
+
this.currentTick = 0;
|
|
136
|
+
this.emit('gameStart', data);
|
|
137
|
+
},
|
|
138
|
+
onMatchEnd: (data) => {
|
|
139
|
+
this.clientState = 'finished';
|
|
140
|
+
this.emit('matchEnd', data);
|
|
141
|
+
},
|
|
142
|
+
// Tick events
|
|
143
|
+
onTickSync: (data) => {
|
|
144
|
+
this.currentTick = data.tick;
|
|
145
|
+
this.renderLoop.updateTickTime();
|
|
146
|
+
this.emit('tick', data);
|
|
147
|
+
},
|
|
148
|
+
onCommandsBatch: (data) => {
|
|
149
|
+
this.currentTick = data.tick;
|
|
150
|
+
this.renderLoop.processTick(data.tick, data.commands);
|
|
151
|
+
this.emit('commands', data);
|
|
152
|
+
},
|
|
153
|
+
// Player events
|
|
154
|
+
onPlayerDisconnected: (data) => this.emit('playerDisconnected', data),
|
|
155
|
+
onPlayerReconnected: (data) => this.emit('playerReconnected', data),
|
|
156
|
+
onPlayerReady: (data) => this.emit('playerReady', data),
|
|
157
|
+
// Reconnection events
|
|
158
|
+
onReconnectState: (data) => {
|
|
159
|
+
this.currentMatchId = data.matchId;
|
|
160
|
+
this.currentTick = data.currentTick;
|
|
161
|
+
if (data.state === 'paused') {
|
|
162
|
+
this.clientState = 'paused';
|
|
163
|
+
}
|
|
164
|
+
else if (data.state === 'playing') {
|
|
165
|
+
this.clientState = 'playing';
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
this.clientState = 'idle';
|
|
169
|
+
}
|
|
170
|
+
this.emit('reconnectState', data);
|
|
171
|
+
},
|
|
172
|
+
onReconnectStatus: (data) => this.emit('reconnectStatus', data),
|
|
173
|
+
// Pause events
|
|
174
|
+
onGamePaused: (data) => {
|
|
175
|
+
this.clientState = 'paused';
|
|
176
|
+
this.emit('gamePaused', data);
|
|
177
|
+
// Notify ITickFrameProvider subscribers (GameWorld listens here)
|
|
178
|
+
for (const handler of this.pauseHandlers)
|
|
179
|
+
handler();
|
|
180
|
+
},
|
|
181
|
+
onGameResumed: (data) => {
|
|
182
|
+
this.clientState = 'playing';
|
|
183
|
+
this.emit('gameResumed', data);
|
|
184
|
+
// Notify ITickFrameProvider subscribers (GameWorld listens here)
|
|
185
|
+
for (const handler of this.resumeHandlers)
|
|
186
|
+
handler();
|
|
187
|
+
},
|
|
188
|
+
// Desync detection events
|
|
189
|
+
onHashComparison: (data) => {
|
|
190
|
+
if (!this.desyncDetector.isEnabled())
|
|
191
|
+
return;
|
|
192
|
+
const hasDesync = !this.desyncDetector.compareWithRemote(data.tick, data.hashes);
|
|
193
|
+
if (hasDesync) {
|
|
194
|
+
const localHash = this.desyncDetector.getLocalHash(data.tick);
|
|
195
|
+
this.emit('desync', {
|
|
196
|
+
tick: data.tick,
|
|
197
|
+
localHash: localHash ?? 'unknown',
|
|
198
|
+
remoteHashes: data.hashes,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
// State queries
|
|
203
|
+
isPlaying: () => this.clientState === 'playing',
|
|
204
|
+
getCurrentMatchId: () => this.currentMatchId,
|
|
205
|
+
// Private room events
|
|
206
|
+
onRoomError: (data) => this.emit('roomError', data),
|
|
207
|
+
onRoomExpired: (data) => this.emit('roomExpired', data),
|
|
208
|
+
onRoomCancelled: (data) => {
|
|
209
|
+
this.clientState = 'idle';
|
|
210
|
+
this.emit('roomCancelled', data);
|
|
211
|
+
},
|
|
212
|
+
onRoomRecovered: (data) => this.emit('roomRecovered', data),
|
|
213
|
+
});
|
|
214
|
+
// Initialize DesyncDetector
|
|
215
|
+
this.desyncDetector = new DesyncDetector();
|
|
216
|
+
// Initialize RenderLoop
|
|
217
|
+
this.renderLoop = new RenderLoop({
|
|
218
|
+
tickRate: this.config.tickRate,
|
|
219
|
+
debug: this.config.debug,
|
|
220
|
+
});
|
|
221
|
+
// Set up command flushing
|
|
222
|
+
this.renderLoop.setCommandFlushCallback(() => this.flushPendingCommands());
|
|
223
|
+
// Handle OAuth callback if present in URL
|
|
224
|
+
if (config.auth && typeof window !== 'undefined') {
|
|
225
|
+
void this.handleAuthCallback();
|
|
226
|
+
}
|
|
227
|
+
// Optionally construct the room recovery controller. Built lazily
|
|
228
|
+
// (after socketManager exists) because it routes through this very
|
|
229
|
+
// client's `connect`/`disconnect`/`recoverRoom`/`on(...)` surface.
|
|
230
|
+
if (config.roomRecovery?.enabled) {
|
|
231
|
+
this._roomRecovery = this.createRoomRecoveryController(config.roomRecovery);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
createRoomRecoveryController(cfg) {
|
|
235
|
+
const port = {
|
|
236
|
+
connect: () => this.connect(),
|
|
237
|
+
disconnect: () => this.socketManager.disconnect(),
|
|
238
|
+
isConnected: () => this.isConnected(),
|
|
239
|
+
recoverRoom: (code, timeoutMs) => this.recoverRoom(code, timeoutMs),
|
|
240
|
+
getPlayerId: () => this.getPlayerId(),
|
|
241
|
+
on: (event, handler) => this.on(event, handler),
|
|
242
|
+
};
|
|
243
|
+
return new RoomRecoveryController(port, {
|
|
244
|
+
storageKey: cfg.storageKey ?? 'phalanx:activeRoom:v1',
|
|
245
|
+
roomTtlMs: cfg.roomTtlMs ?? 5 * 60 * 1000,
|
|
246
|
+
storage: cfg.storage,
|
|
247
|
+
recoverTimeoutBudget: cfg.recoverTimeoutBudget,
|
|
248
|
+
maxRecoverAttempts: cfg.maxRecoverAttempts,
|
|
249
|
+
preGameStallWatchdog: cfg.preGameStallWatchdog,
|
|
250
|
+
preGameStallMs: cfg.preGameStallMs,
|
|
251
|
+
},
|
|
252
|
+
// Forward controller events through the client emitter so games
|
|
253
|
+
// discover them alongside the existing roomExpired/roomCancelled.
|
|
254
|
+
(event, ...args) => {
|
|
255
|
+
// The controller's event map is a strict subset of
|
|
256
|
+
// PhalanxClientEvents; cast through `unknown` for ts to accept.
|
|
257
|
+
this.emit(event, ...args);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Mobile-friendly room recovery controller. `null` unless
|
|
262
|
+
* `roomRecovery.enabled` was set in the config. See
|
|
263
|
+
* `RoomRecoveryController` for the surface — typically you only
|
|
264
|
+
* need `startTrackingHost(code)` after creating a private room and
|
|
265
|
+
* `loadColdStartCode()` on app startup.
|
|
266
|
+
*/
|
|
267
|
+
get roomRecovery() {
|
|
268
|
+
return this._roomRecovery;
|
|
269
|
+
}
|
|
270
|
+
// ============================================
|
|
271
|
+
// AUTHENTICATION
|
|
272
|
+
// ============================================
|
|
273
|
+
/**
|
|
274
|
+
* Initialize authentication manager
|
|
275
|
+
*/
|
|
276
|
+
initializeAuth(authConfig) {
|
|
277
|
+
if (authConfig.provider !== 'google' || !authConfig.google) {
|
|
278
|
+
console.warn('[PhalanxClient] Invalid auth config - only Google is supported');
|
|
279
|
+
this.authState.isLoading = false;
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
this.authManager = new AuthManager({
|
|
283
|
+
provider: 'google',
|
|
284
|
+
google: {
|
|
285
|
+
clientId: authConfig.google.clientId,
|
|
286
|
+
scopes: authConfig.google.scopes || ['openid', 'profile', 'email'],
|
|
287
|
+
redirectUri: authConfig.google.redirectUri || (typeof window !== 'undefined' ? window.location.origin : undefined),
|
|
288
|
+
tokenExchangeUrl: authConfig.google.tokenExchangeUrl,
|
|
289
|
+
},
|
|
290
|
+
debug: this.config.debug,
|
|
291
|
+
onAuthStateChange: (state) => {
|
|
292
|
+
this.handleAuthStateChange(state);
|
|
293
|
+
},
|
|
294
|
+
onAuthError: (error) => {
|
|
295
|
+
this.emit('authError', { message: error.message });
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
// Check for existing session
|
|
299
|
+
void this.authManager.checkSession().then(() => {
|
|
300
|
+
// Session check complete - state already updated via onAuthStateChange
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Handle auth state changes from AuthManager
|
|
305
|
+
*/
|
|
306
|
+
handleAuthStateChange(state) {
|
|
307
|
+
const user = state.user ? {
|
|
308
|
+
id: state.user.id,
|
|
309
|
+
username: state.user.username,
|
|
310
|
+
email: state.user.email,
|
|
311
|
+
avatarUrl: state.user.avatarUrl,
|
|
312
|
+
provider: state.provider || 'google',
|
|
313
|
+
} : null;
|
|
314
|
+
this.authState = {
|
|
315
|
+
isAuthenticated: state.isAuthenticated,
|
|
316
|
+
isLoading: state.isLoading,
|
|
317
|
+
user,
|
|
318
|
+
};
|
|
319
|
+
// Update config with auth user info
|
|
320
|
+
if (user) {
|
|
321
|
+
this.config.playerId = user.id;
|
|
322
|
+
this.config.username = user.username || user.email || `Player-${user.id.slice(-6)}`;
|
|
323
|
+
this.config.authToken = state.token || undefined;
|
|
324
|
+
// Update socket manager with new credentials
|
|
325
|
+
this.socketManager.updateCredentials(user.id, this.config.username, state.token || undefined);
|
|
326
|
+
}
|
|
327
|
+
this.emit('authStateChanged', this.authState);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Handle OAuth callback from redirect
|
|
331
|
+
*/
|
|
332
|
+
async handleAuthCallback() {
|
|
333
|
+
if (!this.authManager)
|
|
334
|
+
return;
|
|
335
|
+
const params = new URLSearchParams(window.location.search);
|
|
336
|
+
const code = params.get('code');
|
|
337
|
+
const error = params.get('error');
|
|
338
|
+
if (!code && !error)
|
|
339
|
+
return;
|
|
340
|
+
this.log('Handling OAuth callback...');
|
|
341
|
+
try {
|
|
342
|
+
const result = await this.authManager.handleCallback({
|
|
343
|
+
code: code || undefined,
|
|
344
|
+
state: params.get('state') || undefined,
|
|
345
|
+
error: error || undefined,
|
|
346
|
+
errorDescription: params.get('error_description') || undefined,
|
|
347
|
+
url: window.location.href,
|
|
348
|
+
});
|
|
349
|
+
if (!result.valid) {
|
|
350
|
+
this.emit('authError', { message: result.error || 'Authentication failed' });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch (err) {
|
|
354
|
+
this.emit('authError', {
|
|
355
|
+
message: err instanceof Error ? err.message : 'Authentication failed'
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
// Clean up URL
|
|
359
|
+
const cleanUrl = window.location.origin + window.location.pathname;
|
|
360
|
+
window.history.replaceState({}, document.title, cleanUrl);
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Start login flow (redirects to OAuth provider)
|
|
364
|
+
*/
|
|
365
|
+
login() {
|
|
366
|
+
if (!this.authManager) {
|
|
367
|
+
this.emit('authError', { message: 'Authentication not configured' });
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
this.authManager.login();
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Log out the current user
|
|
374
|
+
*/
|
|
375
|
+
async logout() {
|
|
376
|
+
if (!this.authManager)
|
|
377
|
+
return;
|
|
378
|
+
await this.authManager.logout();
|
|
379
|
+
// Reset to anonymous player
|
|
380
|
+
const defaultPlayerId = `player-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
381
|
+
this.config.playerId = defaultPlayerId;
|
|
382
|
+
this.config.username = `Player-${defaultPlayerId.slice(-6)}`;
|
|
383
|
+
this.config.authToken = undefined;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Get current authentication state
|
|
387
|
+
*/
|
|
388
|
+
getAuthState() {
|
|
389
|
+
return { ...this.authState };
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Check if user is authenticated
|
|
393
|
+
*/
|
|
394
|
+
isAuthenticated() {
|
|
395
|
+
return this.authState.isAuthenticated;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Get current user info
|
|
399
|
+
*/
|
|
400
|
+
getUser() {
|
|
401
|
+
return this.authState.user;
|
|
402
|
+
}
|
|
403
|
+
// ============================================
|
|
404
|
+
// STATIC FACTORY
|
|
405
|
+
// ============================================
|
|
406
|
+
/**
|
|
407
|
+
* Create and connect a new PhalanxClient
|
|
408
|
+
* @param config Client configuration
|
|
409
|
+
* @returns Connected PhalanxClient instance
|
|
410
|
+
*/
|
|
411
|
+
static async create(config) {
|
|
412
|
+
const client = new PhalanxClient(config);
|
|
413
|
+
await client.connect();
|
|
414
|
+
return client;
|
|
415
|
+
}
|
|
416
|
+
// ============================================
|
|
417
|
+
// CONNECTION MANAGEMENT
|
|
418
|
+
// ============================================
|
|
419
|
+
/**
|
|
420
|
+
* Connect to the Phalanx server
|
|
421
|
+
* @returns Promise that resolves when connected
|
|
422
|
+
* @throws Error if connection fails or times out
|
|
423
|
+
*/
|
|
424
|
+
async connect() {
|
|
425
|
+
return this.socketManager.connect();
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Disconnect from the server
|
|
429
|
+
*/
|
|
430
|
+
disconnect() {
|
|
431
|
+
this.renderLoop.stop();
|
|
432
|
+
this.socketManager.disconnect();
|
|
433
|
+
this.clientState = 'idle';
|
|
434
|
+
this.currentMatchId = null;
|
|
435
|
+
this.currentTick = 0;
|
|
436
|
+
this.pendingCommands = [];
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Destroy the client and clean up all resources
|
|
440
|
+
*/
|
|
441
|
+
destroy() {
|
|
442
|
+
this._roomRecovery?.stop();
|
|
443
|
+
this.renderLoop.dispose();
|
|
444
|
+
this.disconnect();
|
|
445
|
+
this.removeAllListeners();
|
|
446
|
+
this.pendingCommands = [];
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Check if client is connected to the server
|
|
450
|
+
*/
|
|
451
|
+
isConnected() {
|
|
452
|
+
return this.socketManager.isConnected();
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Get current connection state
|
|
456
|
+
*/
|
|
457
|
+
getConnectionState() {
|
|
458
|
+
return this.socketManager.getConnectionState();
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Get current client state
|
|
462
|
+
*/
|
|
463
|
+
getClientState() {
|
|
464
|
+
return this.clientState;
|
|
465
|
+
}
|
|
466
|
+
// ============================================
|
|
467
|
+
// QUEUE MANAGEMENT
|
|
468
|
+
// ============================================
|
|
469
|
+
/**
|
|
470
|
+
* Join the matchmaking queue
|
|
471
|
+
* @returns Promise that resolves with queue status
|
|
472
|
+
*/
|
|
473
|
+
async joinQueue() {
|
|
474
|
+
const status = await this.socketManager.joinQueue();
|
|
475
|
+
this.clientState = 'in-queue';
|
|
476
|
+
this.emit('queueJoined', status);
|
|
477
|
+
return status;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Leave the matchmaking queue
|
|
481
|
+
*/
|
|
482
|
+
leaveQueue() {
|
|
483
|
+
this.socketManager.leaveQueue();
|
|
484
|
+
this.clientState = 'idle';
|
|
485
|
+
this.emit('queueLeft');
|
|
486
|
+
}
|
|
487
|
+
// ============================================
|
|
488
|
+
// PRIVATE ROOMS
|
|
489
|
+
// ============================================
|
|
490
|
+
/**
|
|
491
|
+
* Create a private room and wait for the room code.
|
|
492
|
+
* After creation, the host should wait for match-found (when another player joins).
|
|
493
|
+
*/
|
|
494
|
+
async createRoom(gameType) {
|
|
495
|
+
this.ensureConnected();
|
|
496
|
+
const event = await this.socketManager.createRoom(gameType);
|
|
497
|
+
this.clientState = 'in-queue'; // waiting for opponent
|
|
498
|
+
this.emit('roomCreated', event);
|
|
499
|
+
return event;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Join a private room by code.
|
|
503
|
+
* The server will create a match and emit match-found to both players.
|
|
504
|
+
*/
|
|
505
|
+
joinRoom(code) {
|
|
506
|
+
this.ensureConnected();
|
|
507
|
+
this.socketManager.joinRoom(code);
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Cancel a previously created private room.
|
|
511
|
+
*/
|
|
512
|
+
cancelRoom() {
|
|
513
|
+
this.socketManager.cancelRoom();
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Reclaim a private room after a transient socket disconnect.
|
|
517
|
+
*
|
|
518
|
+
* Call this after `connect()` has re-established the underlying socket
|
|
519
|
+
* (typically inside a `visibilitychange` listener, or the first thing
|
|
520
|
+
* on `pageshow` from bfcache) to tell the server to re-bind the still-
|
|
521
|
+
* living room / in-flight match to this new socket.
|
|
522
|
+
*
|
|
523
|
+
* On success the server emits `match-found` (if a guest had already
|
|
524
|
+
* joined) followed by `reconnect-state` carrying a countdown snapshot,
|
|
525
|
+
* which `SocketManager`'s global `reconnect-state` handler fans out
|
|
526
|
+
* through the local `countdown` and `game-start` listeners so any
|
|
527
|
+
* `waitForCountdown` / `waitForGameStart` promises pending since the
|
|
528
|
+
* original flow resume naturally.
|
|
529
|
+
*
|
|
530
|
+
* Rejects with the server's `room-error` message (e.g. `'Room expired'`)
|
|
531
|
+
* or `'Recover timeout'` if the server doesn't answer before the configured
|
|
532
|
+
* recovery timeout.
|
|
533
|
+
*/
|
|
534
|
+
async recoverRoom(code, timeoutMs) {
|
|
535
|
+
this.ensureConnected();
|
|
536
|
+
const event = await this.socketManager.recoverRoom(code, timeoutMs);
|
|
537
|
+
// Host is back in the "waiting for opponent / countdown" flow —
|
|
538
|
+
// mirror the state createRoom would have left us in so downstream
|
|
539
|
+
// isPlaying() checks stay consistent. The server may also have just
|
|
540
|
+
// emitted match-found / reconnect-state (pending-recover path), which
|
|
541
|
+
// will have already advanced `clientState` past 'in-queue' on its
|
|
542
|
+
// own — so only bump it if we're still in a pre-match phase.
|
|
543
|
+
if (this.clientState === 'idle') {
|
|
544
|
+
this.clientState = 'in-queue';
|
|
545
|
+
}
|
|
546
|
+
return event;
|
|
547
|
+
}
|
|
548
|
+
// ============================================
|
|
549
|
+
// PAUSE / RESUME
|
|
550
|
+
// ============================================
|
|
551
|
+
/**
|
|
552
|
+
* Request the server to pause the game.
|
|
553
|
+
* The game will not freeze immediately — it freezes only when the server
|
|
554
|
+
* broadcasts the 'game-paused' event back to all clients, ensuring
|
|
555
|
+
* every client pauses at the same deterministic point.
|
|
556
|
+
*/
|
|
557
|
+
pauseGame() {
|
|
558
|
+
this.ensureConnected();
|
|
559
|
+
this.socketManager.sendPauseGame();
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Request the server to resume the game.
|
|
563
|
+
* Same as pause — the actual resume occurs when the server broadcasts
|
|
564
|
+
* 'game-resumed' to all clients.
|
|
565
|
+
*/
|
|
566
|
+
resumeGame() {
|
|
567
|
+
this.ensureConnected();
|
|
568
|
+
this.socketManager.sendResumeGame();
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Notify the server that this client has finished loading and is ready to receive ticks.
|
|
572
|
+
* Must be called after assets are loaded and game systems are initialized.
|
|
573
|
+
* The server will not start the tick loop until all clients report ready.
|
|
574
|
+
*/
|
|
575
|
+
sendReady() {
|
|
576
|
+
this.ensureConnected();
|
|
577
|
+
this.socketManager.sendReady();
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Wait for a match to be found
|
|
581
|
+
* @returns Promise that resolves with match found event
|
|
582
|
+
*/
|
|
583
|
+
async waitForMatch() {
|
|
584
|
+
const data = await this.socketManager.waitForMatch();
|
|
585
|
+
this.currentMatchId = data.matchId;
|
|
586
|
+
this.clientState = 'match-found';
|
|
587
|
+
this.emit('matchFound', data);
|
|
588
|
+
return data;
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Join queue and wait for match in one call
|
|
592
|
+
* @returns Promise that resolves with match found event
|
|
593
|
+
*/
|
|
594
|
+
async joinQueueAndWaitForMatch() {
|
|
595
|
+
await this.joinQueue();
|
|
596
|
+
return this.waitForMatch();
|
|
597
|
+
}
|
|
598
|
+
// ============================================
|
|
599
|
+
// GAME LIFECYCLE
|
|
600
|
+
// ============================================
|
|
601
|
+
/**
|
|
602
|
+
* Wait for countdown to complete (listening to countdown events)
|
|
603
|
+
* @param onCountdown Optional callback for each countdown tick
|
|
604
|
+
* @returns Promise that resolves when countdown reaches 0
|
|
605
|
+
*/
|
|
606
|
+
async waitForCountdown(onCountdown) {
|
|
607
|
+
this.clientState = 'countdown';
|
|
608
|
+
return this.socketManager.waitForCountdown(onCountdown);
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Wait for the game to start
|
|
612
|
+
* @returns Promise that resolves with game start event
|
|
613
|
+
*/
|
|
614
|
+
async waitForGameStart() {
|
|
615
|
+
const data = await this.socketManager.waitForGameStart();
|
|
616
|
+
this.clientState = 'playing';
|
|
617
|
+
this.currentTick = 0;
|
|
618
|
+
this.emit('gameStart', data);
|
|
619
|
+
return data;
|
|
620
|
+
}
|
|
621
|
+
// ============================================
|
|
622
|
+
// COMMANDS
|
|
623
|
+
// ============================================
|
|
624
|
+
/**
|
|
625
|
+
* Submit commands for a specific tick
|
|
626
|
+
* @param tick The tick number these commands are for
|
|
627
|
+
* @param commands Array of commands to submit
|
|
628
|
+
* @returns Promise that resolves with acknowledgment
|
|
629
|
+
*/
|
|
630
|
+
async submitCommands(tick, commands) {
|
|
631
|
+
this.ensurePlaying();
|
|
632
|
+
return this.socketManager.submitCommands(tick, commands);
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Submit commands without waiting for acknowledgment (fire and forget)
|
|
636
|
+
* @param tick The tick number these commands are for
|
|
637
|
+
* @param commands Array of commands to submit
|
|
638
|
+
*/
|
|
639
|
+
submitCommandsAsync(tick, commands) {
|
|
640
|
+
this.ensurePlaying();
|
|
641
|
+
this.socketManager.submitCommandsAsync(tick, commands);
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Send a command to the server
|
|
645
|
+
* Commands are buffered and sent automatically each frame
|
|
646
|
+
*
|
|
647
|
+
* @param type Command type (e.g., 'move', 'attack')
|
|
648
|
+
* @param data Command payload
|
|
649
|
+
*/
|
|
650
|
+
sendCommand(type, data) {
|
|
651
|
+
this.pendingCommands.push({ type, data });
|
|
652
|
+
}
|
|
653
|
+
// ============================================
|
|
654
|
+
// SIMPLIFIED API - TICK & FRAME HANDLERS
|
|
655
|
+
// ============================================
|
|
656
|
+
/**
|
|
657
|
+
* Register a callback for simulation ticks
|
|
658
|
+
* Called when the server sends a tick with commands from all players
|
|
659
|
+
*
|
|
660
|
+
* @param handler Callback receiving tick number and commands grouped by player
|
|
661
|
+
* @returns Unsubscribe function
|
|
662
|
+
*/
|
|
663
|
+
onTick(handler) {
|
|
664
|
+
return this.renderLoop.onTick(handler);
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Register a callback for render frames
|
|
668
|
+
* Called every animation frame (~60fps) with interpolation alpha
|
|
669
|
+
* Automatically starts the render loop when first handler is added
|
|
670
|
+
*
|
|
671
|
+
* @param handler Callback receiving alpha (0-1) and delta time in seconds
|
|
672
|
+
* @returns Unsubscribe function
|
|
673
|
+
*/
|
|
674
|
+
onFrame(handler) {
|
|
675
|
+
return this.renderLoop.onFrame(handler);
|
|
676
|
+
}
|
|
677
|
+
// ============================================
|
|
678
|
+
// ITickFrameProvider PAUSE / RESUME
|
|
679
|
+
// ============================================
|
|
680
|
+
/**
|
|
681
|
+
* Request the server to pause the game (ITickFrameProvider contract).
|
|
682
|
+
* The actual pause is signalled asynchronously via onPause when the
|
|
683
|
+
* server broadcasts 'game-paused' to all clients.
|
|
684
|
+
*/
|
|
685
|
+
requestPause() {
|
|
686
|
+
this.pauseGame();
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Request the server to resume the game (ITickFrameProvider contract).
|
|
690
|
+
* The actual resume is signalled asynchronously via onResume when the
|
|
691
|
+
* server broadcasts 'game-resumed' to all clients.
|
|
692
|
+
*/
|
|
693
|
+
requestResume() {
|
|
694
|
+
this.resumeGame();
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Subscribe to the "paused" signal (ITickFrameProvider contract).
|
|
698
|
+
* Fired when the server confirms the pause and broadcasts it.
|
|
699
|
+
*/
|
|
700
|
+
onPause(handler) {
|
|
701
|
+
this.pauseHandlers.push(handler);
|
|
702
|
+
return () => {
|
|
703
|
+
const idx = this.pauseHandlers.indexOf(handler);
|
|
704
|
+
if (idx !== -1)
|
|
705
|
+
this.pauseHandlers.splice(idx, 1);
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Subscribe to the "resumed" signal (ITickFrameProvider contract).
|
|
710
|
+
* Fired when the server confirms the resume and broadcasts it.
|
|
711
|
+
*/
|
|
712
|
+
onResume(handler) {
|
|
713
|
+
this.resumeHandlers.push(handler);
|
|
714
|
+
return () => {
|
|
715
|
+
const idx = this.resumeHandlers.indexOf(handler);
|
|
716
|
+
if (idx !== -1)
|
|
717
|
+
this.resumeHandlers.splice(idx, 1);
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
// ============================================
|
|
721
|
+
// RECONNECTION
|
|
722
|
+
// ============================================
|
|
723
|
+
/**
|
|
724
|
+
* Attempt to reconnect to a match after disconnection
|
|
725
|
+
* @param matchId The match ID to reconnect to
|
|
726
|
+
* @returns Promise that resolves with reconnection state
|
|
727
|
+
*/
|
|
728
|
+
async reconnectToMatch(matchId) {
|
|
729
|
+
this.clientState = 'reconnecting';
|
|
730
|
+
return this.socketManager.reconnectToMatch(matchId);
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Attempt automatic reconnection with retries
|
|
734
|
+
* @returns Promise that resolves when reconnected, rejects if all attempts fail
|
|
735
|
+
*/
|
|
736
|
+
async attemptReconnection() {
|
|
737
|
+
return this.socketManager.attemptReconnection();
|
|
738
|
+
}
|
|
739
|
+
// ============================================
|
|
740
|
+
// DESYNC DETECTION
|
|
741
|
+
// ============================================
|
|
742
|
+
/**
|
|
743
|
+
* Submit state hash for desync detection
|
|
744
|
+
* Call this after each simulation tick (or every N ticks based on your preference)
|
|
745
|
+
*
|
|
746
|
+
* The game is responsible for computing the hash using StateHasher or
|
|
747
|
+
* a custom implementation. The SDK just handles transport and comparison.
|
|
748
|
+
*
|
|
749
|
+
* @param tick - The tick this hash is for
|
|
750
|
+
* @param hash - Hash computed by game (any string)
|
|
751
|
+
*
|
|
752
|
+
* @example
|
|
753
|
+
* ```typescript
|
|
754
|
+
* client.onTick((tick, commands) => {
|
|
755
|
+
* simulation.processTick(tick, commands);
|
|
756
|
+
*
|
|
757
|
+
* // Submit hash every 20 ticks (once per second at 20 TPS)
|
|
758
|
+
* if (tick % 20 === 0) {
|
|
759
|
+
* const hash = computeGameStateHash(tick);
|
|
760
|
+
* client.submitStateHash(tick, hash);
|
|
761
|
+
* }
|
|
762
|
+
* });
|
|
763
|
+
* ```
|
|
764
|
+
*/
|
|
765
|
+
submitStateHash(tick, hash) {
|
|
766
|
+
this.desyncDetector.recordLocalHash(tick, hash);
|
|
767
|
+
if (this.isConnected()) {
|
|
768
|
+
this.socketManager.sendStateHash(tick, hash);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Configure desync detection
|
|
773
|
+
* @param config - Configuration options for desync detection
|
|
774
|
+
*
|
|
775
|
+
* @example
|
|
776
|
+
* ```typescript
|
|
777
|
+
* // Disable desync detection
|
|
778
|
+
* client.configureDesyncDetection({ enabled: false });
|
|
779
|
+
*
|
|
780
|
+
* // Limit stored hashes
|
|
781
|
+
* client.configureDesyncDetection({ maxStoredHashes: 50 });
|
|
782
|
+
* ```
|
|
783
|
+
*/
|
|
784
|
+
configureDesyncDetection(config) {
|
|
785
|
+
this.desyncDetector.configure(config);
|
|
786
|
+
}
|
|
787
|
+
// ============================================
|
|
788
|
+
// STATE GETTERS
|
|
789
|
+
// ============================================
|
|
790
|
+
/**
|
|
791
|
+
* Get current tick number
|
|
792
|
+
*/
|
|
793
|
+
getCurrentTick() {
|
|
794
|
+
return this.currentTick;
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Get current match ID
|
|
798
|
+
*/
|
|
799
|
+
getMatchId() {
|
|
800
|
+
return this.currentMatchId;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* Get player ID configured for this client
|
|
804
|
+
*/
|
|
805
|
+
getPlayerId() {
|
|
806
|
+
return this.config.playerId || '';
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Get username configured for this client
|
|
810
|
+
*/
|
|
811
|
+
getUsername() {
|
|
812
|
+
return this.config.username || '';
|
|
813
|
+
}
|
|
814
|
+
// ============================================
|
|
815
|
+
// PRIVATE METHODS
|
|
816
|
+
// ============================================
|
|
817
|
+
flushPendingCommands() {
|
|
818
|
+
if (this.pendingCommands.length > 0 &&
|
|
819
|
+
this.isConnected() &&
|
|
820
|
+
this.clientState === 'playing') {
|
|
821
|
+
this.submitCommandsAsync(this.currentTick, this.pendingCommands);
|
|
822
|
+
this.pendingCommands = [];
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
ensureConnected() {
|
|
826
|
+
if (!this.isConnected()) {
|
|
827
|
+
throw new Error('Not connected to server. Call connect() first.');
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
ensurePlaying() {
|
|
831
|
+
this.ensureConnected();
|
|
832
|
+
if (this.clientState !== 'playing' &&
|
|
833
|
+
this.clientState !== 'paused' &&
|
|
834
|
+
this.clientState !== 'reconnecting') {
|
|
835
|
+
throw new Error('Not in a game. Join a match first.');
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
log(...args) {
|
|
839
|
+
if (this.config.debug) {
|
|
840
|
+
console.log('[PhalanxClient]', ...args);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
//# sourceMappingURL=PhalanxClient.js.map
|