@bluebottle_gg/league-broadcast-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/LICENSE +674 -0
- package/README.md +530 -0
- package/dist/index.d.ts +1052 -0
- package/dist/index.js +670 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
// src/WebSocketManager.ts
|
|
2
|
+
var WebSocketManager = class {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.socket = null;
|
|
5
|
+
this.url = "";
|
|
6
|
+
this.reconnectAttempts = 0;
|
|
7
|
+
this.maxReconnectAttempts = 10;
|
|
8
|
+
this.reconnectDelay = 5e3;
|
|
9
|
+
this.reconnectTimeout = null;
|
|
10
|
+
this.messageHandlers = /* @__PURE__ */ new Set();
|
|
11
|
+
this.connectionHandlers = /* @__PURE__ */ new Set();
|
|
12
|
+
this.disconnectionHandlers = /* @__PURE__ */ new Set();
|
|
13
|
+
this.errorHandlers = /* @__PURE__ */ new Set();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Connect to the WebSocket server
|
|
17
|
+
*/
|
|
18
|
+
connect(url) {
|
|
19
|
+
this.url = url;
|
|
20
|
+
this.cleanup();
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
try {
|
|
23
|
+
this.socket = new WebSocket(url);
|
|
24
|
+
this.socket.onopen = () => {
|
|
25
|
+
console.log("[WebSocketManager] Connected to", url);
|
|
26
|
+
this.reconnectAttempts = 0;
|
|
27
|
+
this.connectionHandlers.forEach((handler) => handler());
|
|
28
|
+
resolve();
|
|
29
|
+
};
|
|
30
|
+
this.socket.onmessage = (event) => {
|
|
31
|
+
if (event.data === "KeepAlive") {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const data = JSON.parse(event.data);
|
|
36
|
+
this.messageHandlers.forEach((handler) => handler(data));
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error(
|
|
39
|
+
"[WebSocketManager] Failed to parse message:",
|
|
40
|
+
event.data
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
this.socket.onclose = () => {
|
|
45
|
+
console.log("[WebSocketManager] Disconnected");
|
|
46
|
+
this.disconnectionHandlers.forEach((handler) => handler());
|
|
47
|
+
this.attemptReconnect();
|
|
48
|
+
};
|
|
49
|
+
this.socket.onerror = (error) => {
|
|
50
|
+
console.error("[WebSocketManager] Error:", error);
|
|
51
|
+
this.errorHandlers.forEach((handler) => handler(error));
|
|
52
|
+
reject(error);
|
|
53
|
+
};
|
|
54
|
+
} catch (error) {
|
|
55
|
+
reject(error);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Disconnect from the WebSocket server
|
|
61
|
+
*/
|
|
62
|
+
disconnect() {
|
|
63
|
+
if (this.reconnectTimeout !== null) {
|
|
64
|
+
clearTimeout(this.reconnectTimeout);
|
|
65
|
+
this.reconnectTimeout = null;
|
|
66
|
+
}
|
|
67
|
+
if (this.socket) {
|
|
68
|
+
this.socket.close();
|
|
69
|
+
this.socket = null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Check if connected to the WebSocket server
|
|
74
|
+
*/
|
|
75
|
+
isConnected() {
|
|
76
|
+
return this.socket !== null && this.socket.readyState === WebSocket.OPEN;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Send a message to the server
|
|
80
|
+
*/
|
|
81
|
+
send(message) {
|
|
82
|
+
if (!this.isConnected()) {
|
|
83
|
+
console.warn("[WebSocketManager] Cannot send message: not connected");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const data = typeof message === "string" ? message : JSON.stringify(message);
|
|
87
|
+
this.socket.send(data);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Register a message handler
|
|
91
|
+
*/
|
|
92
|
+
onMessage(handler) {
|
|
93
|
+
this.messageHandlers.add(handler);
|
|
94
|
+
return () => this.messageHandlers.delete(handler);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Register a connection handler
|
|
98
|
+
*/
|
|
99
|
+
onConnect(handler) {
|
|
100
|
+
this.connectionHandlers.add(handler);
|
|
101
|
+
return () => this.connectionHandlers.delete(handler);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Register a disconnection handler
|
|
105
|
+
*/
|
|
106
|
+
onDisconnect(handler) {
|
|
107
|
+
this.disconnectionHandlers.add(handler);
|
|
108
|
+
return () => this.disconnectionHandlers.delete(handler);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Register an error handler
|
|
112
|
+
*/
|
|
113
|
+
onError(handler) {
|
|
114
|
+
this.errorHandlers.add(handler);
|
|
115
|
+
return () => this.errorHandlers.delete(handler);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Attempt to reconnect to the server
|
|
119
|
+
*/
|
|
120
|
+
attemptReconnect() {
|
|
121
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
122
|
+
console.error("[WebSocketManager] Max reconnect attempts reached");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
this.reconnectAttempts++;
|
|
126
|
+
console.log(
|
|
127
|
+
`[WebSocketManager] Reconnecting in ${this.reconnectDelay}ms (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`
|
|
128
|
+
);
|
|
129
|
+
this.reconnectTimeout = window.setTimeout(() => {
|
|
130
|
+
this.connect(this.url).catch((error) => {
|
|
131
|
+
console.error("[WebSocketManager] Reconnect failed:", error);
|
|
132
|
+
});
|
|
133
|
+
}, this.reconnectDelay);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Clean up resources
|
|
137
|
+
*/
|
|
138
|
+
cleanup() {
|
|
139
|
+
if (this.reconnectTimeout !== null) {
|
|
140
|
+
clearTimeout(this.reconnectTimeout);
|
|
141
|
+
this.reconnectTimeout = null;
|
|
142
|
+
}
|
|
143
|
+
if (this.socket) {
|
|
144
|
+
this.socket.onopen = null;
|
|
145
|
+
this.socket.onmessage = null;
|
|
146
|
+
this.socket.onclose = null;
|
|
147
|
+
this.socket.onerror = null;
|
|
148
|
+
if (this.socket.readyState === WebSocket.OPEN) {
|
|
149
|
+
this.socket.close();
|
|
150
|
+
}
|
|
151
|
+
this.socket = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// src/reactivity/GameStateStore.ts
|
|
157
|
+
var GameStateStore = class {
|
|
158
|
+
constructor(initialState, initialGameState) {
|
|
159
|
+
/** Global listeners — called on every state change regardless of selector. */
|
|
160
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
161
|
+
/** Monotonically increasing version — used for cheap stale checks. */
|
|
162
|
+
this.version = 0;
|
|
163
|
+
this.snapshot = Object.freeze({
|
|
164
|
+
gameData: initialState,
|
|
165
|
+
gameState: initialGameState,
|
|
166
|
+
version: this.version
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Core API
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
/** Return the current full snapshot. */
|
|
173
|
+
getSnapshot() {
|
|
174
|
+
return this.snapshot;
|
|
175
|
+
}
|
|
176
|
+
/** Return the current version number. */
|
|
177
|
+
getVersion() {
|
|
178
|
+
return this.version;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Subscribe to **all** state changes (unfiltered).
|
|
182
|
+
* Returns an unsubscribe function.
|
|
183
|
+
*
|
|
184
|
+
* This satisfies the `subscribe` half of `useSyncExternalStore` when
|
|
185
|
+
* combined with `getSnapshot`.
|
|
186
|
+
*/
|
|
187
|
+
subscribe(listener) {
|
|
188
|
+
this.listeners.add(listener);
|
|
189
|
+
return () => {
|
|
190
|
+
this.listeners.delete(listener);
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Selectors — the main reactivity primitive
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
/**
|
|
197
|
+
* Create a **subscribable slice** that only triggers when the selected
|
|
198
|
+
* value changes according to `equalityFn` (defaults to `===`).
|
|
199
|
+
*
|
|
200
|
+
* The returned object satisfies the `useSyncExternalStore` contract and
|
|
201
|
+
* can also be used with `watch()` for callback-style subscriptions.
|
|
202
|
+
*
|
|
203
|
+
* Selector instances are lightweight — create them freely (e.g. inside
|
|
204
|
+
* render functions). Referential identity of the *selector function* is
|
|
205
|
+
* **not** required to be stable.
|
|
206
|
+
*/
|
|
207
|
+
select(selector, equalityFn = Object.is) {
|
|
208
|
+
let currentValue = selector(this.snapshot);
|
|
209
|
+
let lastVersion = this.version;
|
|
210
|
+
const sliceListeners = /* @__PURE__ */ new Set();
|
|
211
|
+
const getSnapshot = () => {
|
|
212
|
+
if (lastVersion !== this.version) {
|
|
213
|
+
const next = selector(this.snapshot);
|
|
214
|
+
if (!equalityFn(currentValue, next)) {
|
|
215
|
+
currentValue = next;
|
|
216
|
+
}
|
|
217
|
+
lastVersion = this.version;
|
|
218
|
+
}
|
|
219
|
+
return currentValue;
|
|
220
|
+
};
|
|
221
|
+
const subscribe = (listener) => {
|
|
222
|
+
sliceListeners.add(listener);
|
|
223
|
+
const unsub = this.subscribe(() => {
|
|
224
|
+
const prev = currentValue;
|
|
225
|
+
const next = selector(this.snapshot);
|
|
226
|
+
lastVersion = this.version;
|
|
227
|
+
if (!equalityFn(prev, next)) {
|
|
228
|
+
currentValue = next;
|
|
229
|
+
listener();
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
return () => {
|
|
233
|
+
sliceListeners.delete(listener);
|
|
234
|
+
unsub();
|
|
235
|
+
};
|
|
236
|
+
};
|
|
237
|
+
return { subscribe, getSnapshot };
|
|
238
|
+
}
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// Convenience helpers
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
/**
|
|
243
|
+
* Watch a derived value and invoke `callback` whenever it changes.
|
|
244
|
+
* Returns an unsubscribe function.
|
|
245
|
+
*
|
|
246
|
+
* This is the simplest API for imperative / vanilla-JS usage:
|
|
247
|
+
*
|
|
248
|
+
* ```ts
|
|
249
|
+
* store.watch(
|
|
250
|
+
* s => s.gameData.scoreboard?.teams[0]?.kills,
|
|
251
|
+
* kills => console.log('Blue kills:', kills),
|
|
252
|
+
* );
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
watch(selector, callback, equalityFn = Object.is) {
|
|
256
|
+
let prev = selector(this.snapshot);
|
|
257
|
+
return this.subscribe(() => {
|
|
258
|
+
const next = selector(this.snapshot);
|
|
259
|
+
if (!equalityFn(prev, next)) {
|
|
260
|
+
const old = prev;
|
|
261
|
+
prev = next;
|
|
262
|
+
callback(next, old);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Watch a derived value and invoke `callback` whenever it changes,
|
|
268
|
+
* **and** invoke it immediately with the current value.
|
|
269
|
+
*/
|
|
270
|
+
watchImmediate(selector, callback, equalityFn = Object.is) {
|
|
271
|
+
const current = selector(this.snapshot);
|
|
272
|
+
callback(current, void 0);
|
|
273
|
+
return this.watch(selector, callback, equalityFn);
|
|
274
|
+
}
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// Internal — called by LeagueBroadcastClient
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
/** @internal Replace game data snapshot and notify listeners. */
|
|
279
|
+
_setGameData(data) {
|
|
280
|
+
this.version++;
|
|
281
|
+
this.snapshot = Object.freeze({
|
|
282
|
+
...this.snapshot,
|
|
283
|
+
gameData: data,
|
|
284
|
+
version: this.version
|
|
285
|
+
});
|
|
286
|
+
this.emit();
|
|
287
|
+
}
|
|
288
|
+
/** @internal Replace game state and notify listeners. */
|
|
289
|
+
_setGameState(state) {
|
|
290
|
+
if (this.snapshot.gameState === state) return;
|
|
291
|
+
this.version++;
|
|
292
|
+
this.snapshot = Object.freeze({
|
|
293
|
+
...this.snapshot,
|
|
294
|
+
gameState: state,
|
|
295
|
+
version: this.version
|
|
296
|
+
});
|
|
297
|
+
this.emit();
|
|
298
|
+
}
|
|
299
|
+
/** @internal Replace entire snapshot (used on game end / reset). */
|
|
300
|
+
_reset(data, state) {
|
|
301
|
+
this.version++;
|
|
302
|
+
this.snapshot = Object.freeze({
|
|
303
|
+
gameData: data,
|
|
304
|
+
gameState: state,
|
|
305
|
+
version: this.version
|
|
306
|
+
});
|
|
307
|
+
this.emit();
|
|
308
|
+
}
|
|
309
|
+
emit() {
|
|
310
|
+
this.listeners.forEach((l) => l());
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
function shallowEqual(a, b) {
|
|
314
|
+
if (Object.is(a, b)) return true;
|
|
315
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
const keysA = Object.keys(a);
|
|
319
|
+
const keysB = Object.keys(b);
|
|
320
|
+
if (keysA.length !== keysB.length) return false;
|
|
321
|
+
for (const key of keysA) {
|
|
322
|
+
if (!Object.prototype.hasOwnProperty.call(b, key) || !Object.is(a[key], b[key])) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/reactivity/structuralShare.ts
|
|
330
|
+
function structuralShare(prev, next) {
|
|
331
|
+
if (Object.is(prev, next)) {
|
|
332
|
+
return prev;
|
|
333
|
+
}
|
|
334
|
+
if (!isObject(prev) || !isObject(next)) {
|
|
335
|
+
return next;
|
|
336
|
+
}
|
|
337
|
+
if (Array.isArray(prev) || Array.isArray(next)) {
|
|
338
|
+
if (!Array.isArray(prev) || !Array.isArray(next)) {
|
|
339
|
+
return next;
|
|
340
|
+
}
|
|
341
|
+
if (prev.length !== next.length) {
|
|
342
|
+
return next.map(
|
|
343
|
+
(value, index) => structuralShare(prev[index], value)
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
let changed2 = false;
|
|
347
|
+
const shared = next.map((value, index) => {
|
|
348
|
+
const nextValue = structuralShare(prev[index], value);
|
|
349
|
+
if (!Object.is(nextValue, prev[index])) {
|
|
350
|
+
changed2 = true;
|
|
351
|
+
}
|
|
352
|
+
return nextValue;
|
|
353
|
+
});
|
|
354
|
+
return changed2 ? shared : prev;
|
|
355
|
+
}
|
|
356
|
+
const prevObj = prev;
|
|
357
|
+
const nextObj = next;
|
|
358
|
+
const nextKeys = Object.keys(nextObj);
|
|
359
|
+
const prevKeys = Object.keys(prevObj);
|
|
360
|
+
let changed = nextKeys.length !== prevKeys.length;
|
|
361
|
+
const result = Object.create(
|
|
362
|
+
Object.getPrototypeOf(next)
|
|
363
|
+
);
|
|
364
|
+
for (const key of nextKeys) {
|
|
365
|
+
const prevValue = prevObj[key];
|
|
366
|
+
const nextValue = nextObj[key];
|
|
367
|
+
const sharedValue = structuralShare(prevValue, nextValue);
|
|
368
|
+
result[key] = sharedValue;
|
|
369
|
+
if (!Object.is(sharedValue, prevValue)) {
|
|
370
|
+
changed = true;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return changed ? result : prev;
|
|
374
|
+
}
|
|
375
|
+
function isObject(value) {
|
|
376
|
+
return value !== null && typeof value === "object";
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// types/shared/gamestate.ts
|
|
380
|
+
var GameState = /* @__PURE__ */ ((GameState2) => {
|
|
381
|
+
GameState2[GameState2["OutOfGame"] = 0] = "OutOfGame";
|
|
382
|
+
GameState2[GameState2["Loading"] = 1] = "Loading";
|
|
383
|
+
GameState2[GameState2["Running"] = 2] = "Running";
|
|
384
|
+
GameState2[GameState2["Paused"] = 3] = "Paused";
|
|
385
|
+
GameState2[GameState2["Mocking"] = 4] = "Mocking";
|
|
386
|
+
GameState2[GameState2["GameOver"] = 5] = "GameOver";
|
|
387
|
+
GameState2[GameState2["ChampionSelect"] = 6] = "ChampionSelect";
|
|
388
|
+
return GameState2;
|
|
389
|
+
})(GameState || {});
|
|
390
|
+
|
|
391
|
+
// types/ingame/ingamefrontenddata.ts
|
|
392
|
+
var ingameFrontendData = class {
|
|
393
|
+
constructor() {
|
|
394
|
+
this.gameTime = 0;
|
|
395
|
+
this.playbackSpeed = 0;
|
|
396
|
+
this.gameVersion = "";
|
|
397
|
+
this.gameStatus = 0 /* OutOfGame */;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
// src/LeagueBroadcastClient.ts
|
|
402
|
+
var LeagueBroadcastClient = class {
|
|
403
|
+
constructor(config) {
|
|
404
|
+
this.gameState = 0 /* OutOfGame */;
|
|
405
|
+
this.isTestingEnvironment = false;
|
|
406
|
+
// Event handlers
|
|
407
|
+
this.stateUpdateHandlers = /* @__PURE__ */ new Set();
|
|
408
|
+
this.gameStatusHandlers = /* @__PURE__ */ new Set();
|
|
409
|
+
this.eventHandlers = {};
|
|
410
|
+
this.config = {
|
|
411
|
+
host: config.host,
|
|
412
|
+
port: config.port ?? 58869,
|
|
413
|
+
wsRoute: config.wsRoute ?? "/ws/in",
|
|
414
|
+
apiRoute: config.apiRoute ?? "/api",
|
|
415
|
+
cacheRoute: config.cacheRoute ?? "/cache",
|
|
416
|
+
useHttps: config.useHttps ?? false,
|
|
417
|
+
autoConnect: config.autoConnect ?? true
|
|
418
|
+
};
|
|
419
|
+
this.ws = new WebSocketManager();
|
|
420
|
+
this.gameData = new ingameFrontendData();
|
|
421
|
+
this.store = new GameStateStore(this.gameData, this.gameState);
|
|
422
|
+
this.setupMessageHandler();
|
|
423
|
+
if (this.config.autoConnect) {
|
|
424
|
+
this.connect();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Connect to the League Broadcast backend
|
|
429
|
+
*/
|
|
430
|
+
async connect() {
|
|
431
|
+
const protocol = this.config.useHttps ? "wss" : "ws";
|
|
432
|
+
const url = `${protocol}://${this.config.host}:${this.config.port}${this.config.wsRoute}`;
|
|
433
|
+
try {
|
|
434
|
+
await this.ws.connect(url);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
console.error("[LeagueBroadcastClient] Connection failed:", error);
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Disconnect from the League Broadcast backend
|
|
442
|
+
*/
|
|
443
|
+
disconnect() {
|
|
444
|
+
this.ws.disconnect();
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Check if connected to the backend
|
|
448
|
+
*/
|
|
449
|
+
isConnected() {
|
|
450
|
+
return this.ws.isConnected();
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Get the current game data
|
|
454
|
+
*/
|
|
455
|
+
getGameData() {
|
|
456
|
+
return this.gameData;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Get the current game state
|
|
460
|
+
*/
|
|
461
|
+
getGameState() {
|
|
462
|
+
return this.gameState;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Check if in testing environment
|
|
466
|
+
*/
|
|
467
|
+
isInTestingEnvironment() {
|
|
468
|
+
return this.isTestingEnvironment;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Register a handler for state updates
|
|
472
|
+
*/
|
|
473
|
+
onStateUpdate(handler) {
|
|
474
|
+
this.stateUpdateHandlers.add(handler);
|
|
475
|
+
return () => this.stateUpdateHandlers.delete(handler);
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Register a handler for game status changes
|
|
479
|
+
*/
|
|
480
|
+
onGameStatusChange(handler) {
|
|
481
|
+
this.gameStatusHandlers.add(handler);
|
|
482
|
+
return () => this.gameStatusHandlers.delete(handler);
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Register handlers for game events
|
|
486
|
+
*/
|
|
487
|
+
onGameEvents(handlers) {
|
|
488
|
+
this.eventHandlers = { ...this.eventHandlers, ...handlers };
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Register a handler for connection events
|
|
492
|
+
*/
|
|
493
|
+
onConnect(handler) {
|
|
494
|
+
return this.ws.onConnect(handler);
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Register a handler for disconnection events
|
|
498
|
+
*/
|
|
499
|
+
onDisconnect(handler) {
|
|
500
|
+
return this.ws.onDisconnect(handler);
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Register a handler for error events
|
|
504
|
+
*/
|
|
505
|
+
onError(handler) {
|
|
506
|
+
return this.ws.onError(handler);
|
|
507
|
+
}
|
|
508
|
+
// ---------------------------------------------------------------------------
|
|
509
|
+
// Reactive API — convenience wrappers around this.store
|
|
510
|
+
// ---------------------------------------------------------------------------
|
|
511
|
+
/**
|
|
512
|
+
* Create a subscribable slice of game state.
|
|
513
|
+
* The slice only notifies listeners when the selected value actually changes.
|
|
514
|
+
*
|
|
515
|
+
* Works directly with React 18+ `useSyncExternalStore`:
|
|
516
|
+
* ```tsx
|
|
517
|
+
* const kills = client.select(s => s.gameData.scoreboard?.teams[0]?.kills);
|
|
518
|
+
* function Kills() {
|
|
519
|
+
* const value = useSyncExternalStore(kills.subscribe, kills.getSnapshot);
|
|
520
|
+
* return <span>{value}</span>;
|
|
521
|
+
* }
|
|
522
|
+
* ```
|
|
523
|
+
*/
|
|
524
|
+
select(selector, equalityFn) {
|
|
525
|
+
return this.store.select(selector, equalityFn);
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Watch a derived value and invoke `callback` whenever it changes.
|
|
529
|
+
* Returns an unsubscribe function.
|
|
530
|
+
*
|
|
531
|
+
* ```ts
|
|
532
|
+
* client.watch(
|
|
533
|
+
* s => s.gameData.scoreboard?.teams[0]?.kills,
|
|
534
|
+
* (kills, prev) => console.log(`Kills: ${prev} → ${kills}`),
|
|
535
|
+
* );
|
|
536
|
+
* ```
|
|
537
|
+
*/
|
|
538
|
+
watch(selector, callback, equalityFn) {
|
|
539
|
+
return this.store.watch(selector, callback, equalityFn);
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Get the base HTTP URL for API requests
|
|
543
|
+
*/
|
|
544
|
+
getApiUrl() {
|
|
545
|
+
const protocol = this.config.useHttps ? "https" : "http";
|
|
546
|
+
return `${protocol}://${this.config.host}:${this.config.port}${this.config.apiRoute}`;
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Get the base URL for cache requests
|
|
550
|
+
*/
|
|
551
|
+
getCacheUrl(path) {
|
|
552
|
+
const protocol = this.config.useHttps ? "https" : "http";
|
|
553
|
+
const baseUrl = `${protocol}://${this.config.host}:${this.config.port}${this.config.cacheRoute}`;
|
|
554
|
+
if (!path) {
|
|
555
|
+
return baseUrl;
|
|
556
|
+
}
|
|
557
|
+
let cleanPath = path;
|
|
558
|
+
if (cleanPath.startsWith("http")) {
|
|
559
|
+
return cleanPath;
|
|
560
|
+
}
|
|
561
|
+
if (cleanPath.startsWith("/")) {
|
|
562
|
+
cleanPath = cleanPath.slice(1);
|
|
563
|
+
}
|
|
564
|
+
if (cleanPath.startsWith("cache")) {
|
|
565
|
+
cleanPath = cleanPath.slice(5);
|
|
566
|
+
}
|
|
567
|
+
if (cleanPath.startsWith("/")) {
|
|
568
|
+
cleanPath = cleanPath.slice(1);
|
|
569
|
+
}
|
|
570
|
+
return `${baseUrl}/${cleanPath}`;
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Setup message handler for WebSocket messages
|
|
574
|
+
*/
|
|
575
|
+
setupMessageHandler() {
|
|
576
|
+
this.ws.onMessage((messageData) => {
|
|
577
|
+
switch (messageData.type) {
|
|
578
|
+
case "ingame-state-update":
|
|
579
|
+
this.handleStateUpdate(messageData.state, messageData.events);
|
|
580
|
+
break;
|
|
581
|
+
case "gameStatus":
|
|
582
|
+
this.handleGameStatusUpdate(
|
|
583
|
+
messageData.gameState,
|
|
584
|
+
messageData.isTestingEnvironment
|
|
585
|
+
);
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Handle state update from backend
|
|
592
|
+
*/
|
|
593
|
+
handleStateUpdate(state, events) {
|
|
594
|
+
const nextData = Object.assign(new ingameFrontendData(), state);
|
|
595
|
+
this.gameData = structuralShare(this.gameData, nextData);
|
|
596
|
+
if (!state.gameTime || state.gameTime === 0) {
|
|
597
|
+
this.handleGameStatusUpdate(0 /* OutOfGame */, false);
|
|
598
|
+
}
|
|
599
|
+
this.store._setGameData(this.gameData);
|
|
600
|
+
this.stateUpdateHandlers.forEach((handler) => handler(this.gameData));
|
|
601
|
+
this.handleGameEvents(events);
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Handle game status update
|
|
605
|
+
*/
|
|
606
|
+
handleGameStatusUpdate(gameStatus, isTestingEnvironment = false) {
|
|
607
|
+
this.isTestingEnvironment = isTestingEnvironment;
|
|
608
|
+
if (gameStatus === 0 /* OutOfGame */ && this.gameState !== 0 /* OutOfGame */) {
|
|
609
|
+
this.endGame();
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
this.gameState = gameStatus;
|
|
613
|
+
this.store._setGameState(gameStatus);
|
|
614
|
+
this.gameStatusHandlers.forEach(
|
|
615
|
+
(handler) => handler(gameStatus, isTestingEnvironment)
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Handle game events
|
|
620
|
+
*/
|
|
621
|
+
handleGameEvents(events) {
|
|
622
|
+
if (!events) {
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (events.player && this.eventHandlers.onPlayerEvent) {
|
|
626
|
+
events.player.forEach(
|
|
627
|
+
(event) => this.eventHandlers.onPlayerEvent(event)
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
if (events.team && this.eventHandlers.onTeamEvent) {
|
|
631
|
+
events.team.forEach((event) => this.eventHandlers.onTeamEvent(event));
|
|
632
|
+
}
|
|
633
|
+
if (events.objective && this.eventHandlers.onObjectiveEvent) {
|
|
634
|
+
events.objective.forEach(
|
|
635
|
+
(event) => this.eventHandlers.onObjectiveEvent(event)
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
if (events.firstTower !== void 0 && this.eventHandlers.onFirstTowerEvent) {
|
|
639
|
+
this.eventHandlers.onFirstTowerEvent(events.firstTower);
|
|
640
|
+
}
|
|
641
|
+
if (events.announcements && this.eventHandlers.onAnnouncementEvent) {
|
|
642
|
+
events.announcements.forEach(
|
|
643
|
+
(event) => this.eventHandlers.onAnnouncementEvent(event)
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
if (events.killFeed && this.eventHandlers.onKillFeedEvent) {
|
|
647
|
+
events.killFeed.forEach(
|
|
648
|
+
(event) => this.eventHandlers.onKillFeedEvent(event)
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* End the game and reset state
|
|
654
|
+
*/
|
|
655
|
+
endGame() {
|
|
656
|
+
console.log("[LeagueBroadcastClient] Game ended, resetting data");
|
|
657
|
+
this.gameData = new ingameFrontendData();
|
|
658
|
+
this.gameState = 0 /* OutOfGame */;
|
|
659
|
+
this.isTestingEnvironment = false;
|
|
660
|
+
this.store._reset(this.gameData, 0 /* OutOfGame */);
|
|
661
|
+
this.gameStatusHandlers.forEach(
|
|
662
|
+
(handler) => handler(0 /* OutOfGame */, false)
|
|
663
|
+
);
|
|
664
|
+
this.stateUpdateHandlers.forEach((handler) => handler(this.gameData));
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
|
|
668
|
+
export { GameState, GameStateStore, LeagueBroadcastClient, WebSocketManager, shallowEqual };
|
|
669
|
+
//# sourceMappingURL=index.js.map
|
|
670
|
+
//# sourceMappingURL=index.js.map
|