@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/README.md ADDED
@@ -0,0 +1,530 @@
1
+ # League Broadcast Client
2
+
3
+ A TypeScript/JavaScript client library for connecting to the League Broadcast backend and receiving real-time game data.
4
+
5
+ ## Features
6
+
7
+ - 🔌 Automatic WebSocket connection management with reconnection
8
+ - 📊 Real-time game state and data updates
9
+ - 🎮 Event-driven architecture for game events (kills, objectives, etc.)
10
+ - ⚡ **Framework-agnostic reactive store** with fine-grained subscriptions
11
+ - 🔄 Works seamlessly with React, Vue, Svelte, Solid, Angular, and vanilla JS
12
+ - 📦 TypeScript support with full type definitions
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @bluebottle_gg/league-broadcast-client
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```typescript
23
+ import { LeagueBroadcastClient, GameState } from "@bluebottle_gg/league-broadcast-client";
24
+
25
+ // Create client instance
26
+ const client = new LeagueBroadcastClient({
27
+ host: "localhost",
28
+ port: 58869,
29
+ autoConnect: true,
30
+ });
31
+
32
+ // Listen for state updates
33
+ client.onStateUpdate((gameData) => {
34
+ console.log("Game time:", gameData.gameTime);
35
+ console.log("Scoreboard:", gameData.scoreboard);
36
+ });
37
+
38
+ // Listen for game status changes
39
+ client.onGameStatusChange((status, isTestingEnv) => {
40
+ if (status === GameState.InGame) {
41
+ console.log("Game started!");
42
+ } else if (status === GameState.OutOfGame) {
43
+ console.log("Game ended!");
44
+ }
45
+ });
46
+
47
+ // Listen for game events
48
+ client.onGameEvents({
49
+ onKillFeedEvent: (event) => {
50
+ console.log("Kill event:", event);
51
+ },
52
+ onObjectiveEvent: (event) => {
53
+ console.log("Objective taken:", event);
54
+ },
55
+ onPlayerEvent: (event) => {
56
+ console.log("Player event:", event);
57
+ },
58
+ });
59
+ ```
60
+
61
+ ## API Reference
62
+
63
+ ### LeagueBroadcastClient
64
+
65
+ #### Constructor
66
+
67
+ ```typescript
68
+ new LeagueBroadcastClient(config: LeagueBroadcastClientConfig)
69
+ ```
70
+
71
+ **Config Options:**
72
+
73
+ | Option | Type | Default | Description |
74
+ | ------------- | --------- | ------------ | -------------------------------------- |
75
+ | `host` | `string` | **required** | Backend server hostname or IP |
76
+ | `port` | `number` | `58869` | Backend server port |
77
+ | `wsRoute` | `string` | `/ws/in` | WebSocket endpoint route |
78
+ | `apiRoute` | `string` | `/api` | API endpoint route |
79
+ | `cacheRoute` | `string` | `/cache` | Cache endpoint route |
80
+ | `useHttps` | `boolean` | `false` | Use WSS/HTTPS instead of WS/HTTP |
81
+ | `autoConnect` | `boolean` | `true` | Automatically connect on instantiation |
82
+
83
+ #### Methods
84
+
85
+ ##### Connection Management
86
+
87
+ ```typescript
88
+ // Connect to backend
89
+ await client.connect(): Promise<void>
90
+
91
+ // Disconnect from backend
92
+ client.disconnect(): void
93
+
94
+ // Check connection status
95
+ client.isConnected(): boolean
96
+ ```
97
+
98
+ ##### Data Access
99
+
100
+ ```typescript
101
+ // Get current game data
102
+ client.getGameData(): ingameFrontendData
103
+
104
+ // Get current game state
105
+ client.getGameState(): GameState
106
+
107
+ // Check if in testing environment
108
+ client.isInTestingEnvironment(): boolean
109
+ ```
110
+
111
+ ##### URLs
112
+
113
+ ```typescript
114
+ // Get API base URL
115
+ client.getApiUrl(): string
116
+
117
+ // Get cache URL (optionally with path)
118
+ client.getCacheUrl(path?: string): string
119
+ ```
120
+
121
+ ##### Event Handlers
122
+
123
+ ```typescript
124
+ // State updates - called when game data changes
125
+ client.onStateUpdate(handler: (state: ingameFrontendData) => void): () => void
126
+
127
+ // Game status changes - called when game starts/ends
128
+ client.onGameStatusChange(handler: (status: GameState, isTestingEnv: boolean) => void): () => void
129
+
130
+ // Game events - register handlers for various game events
131
+ client.onGameEvents(handlers: GameDataEventHandlers): void
132
+
133
+ // Connection events
134
+ client.onConnect(handler: () => void): () => void
135
+ client.onDisconnect(handler: () => void): () => void
136
+ client.onError(handler: (error: Event) => void): () => void
137
+ ```
138
+
139
+ All event handler registration methods return an unsubscribe function.
140
+
141
+ ### GameDataEventHandlers
142
+
143
+ ```typescript
144
+ interface GameDataEventHandlers {
145
+ onPlayerEvent?: (event: playerEvent) => void;
146
+ onTeamEvent?: (event: teamEvent) => void;
147
+ onObjectiveEvent?: (event: objectiveEvent) => void;
148
+ onFirstTowerEvent?: (event: firstTowerEvent) => void;
149
+ onAnnouncementEvent?: (event: announcerEvent) => void;
150
+ onKillFeedEvent?: (event: killFeedEvent) => void;
151
+ }
152
+ ```
153
+
154
+ ## Reactive API
155
+
156
+ The client includes a **framework-agnostic reactive store** that enables fine-grained subscriptions to specific parts of the game state. The store is compatible with React, Vue, Svelte, Solid, Angular, and vanilla JavaScript.
157
+
158
+ ### Core Concepts
159
+
160
+ The reactive API is built around **selectors** — functions that extract a slice of state. The backend sends a full snapshot each update, and `null` values explicitly mean “not available / don’t show”. The client uses **structural sharing** to preserve nested object references when values are unchanged, so selectors only re-fire when their content actually changes (with configurable equality comparison for derived values).
161
+
162
+ ### Quick Examples
163
+
164
+ #### Vanilla JavaScript
165
+
166
+ ```typescript
167
+ // Watch a specific value
168
+ const unsubscribe = client.watch(
169
+ (s) => s.gameData.scoreboard?.teams[0]?.kills,
170
+ (kills, prevKills) => {
171
+ console.log(`Blue team kills: ${prevKills} → ${kills}`);
172
+ },
173
+ );
174
+
175
+ // Clean up when done
176
+ unsubscribe();
177
+ ```
178
+
179
+ #### React 18+
180
+
181
+ ```typescript
182
+ import { useSyncExternalStore } from 'react';
183
+
184
+ function BlueTeamKills() {
185
+ const slice = client.select(s => s.gameData.scoreboard?.teams[0]?.kills);
186
+ const kills = useSyncExternalStore(slice.subscribe, slice.getSnapshot);
187
+
188
+ return <div>Blue Kills: {kills ?? 0}</div>;
189
+ }
190
+ ```
191
+
192
+ #### Vue 3
193
+
194
+ ```typescript
195
+ import { ref, onUnmounted } from "vue";
196
+
197
+ export default {
198
+ setup() {
199
+ const kills = ref(0);
200
+
201
+ const unsubscribe = client.watch(
202
+ (s) => s.gameData.scoreboard?.teams[0]?.kills,
203
+ (value) => {
204
+ kills.value = value ?? 0;
205
+ },
206
+ );
207
+
208
+ onUnmounted(() => unsubscribe());
209
+
210
+ return { kills };
211
+ },
212
+ };
213
+ ```
214
+
215
+ #### Svelte
216
+
217
+ The `Subscribable` returned by `select()` matches Svelte's store contract:
218
+
219
+ ```svelte
220
+ <script lang="ts">
221
+ const kills = client.select(s => s.gameData.scoreboard?.teams[0]?.kills);
222
+ </script>
223
+
224
+ <div>Blue Kills: {$kills ?? 0}</div>
225
+ ```
226
+
227
+ ### Reactive Methods
228
+
229
+ #### `client.select(selector, equalityFn?)`
230
+
231
+ Create a subscribable slice that only notifies listeners when the selected value changes.
232
+
233
+ ```typescript
234
+ const slice = client.select(
235
+ (s) => s.gameData.gameTime,
236
+ Object.is, // Default equality function (can use shallowEqual for objects)
237
+ );
238
+
239
+ // Get current value
240
+ const currentTime = slice.getSnapshot();
241
+
242
+ // Subscribe to changes
243
+ const unsubscribe = slice.subscribe(() => {
244
+ console.log("Game time changed:", slice.getSnapshot());
245
+ });
246
+ ```
247
+
248
+ **Parameters:**
249
+
250
+ - `selector: (snapshot: GameStateSnapshot) => T` — Function to extract value from state
251
+ - `equalityFn?: (a: T, b: T) => boolean` — Optional equality comparison (defaults to `Object.is`)
252
+
253
+ **Returns:** `Subscribable<T>` with:
254
+
255
+ - `subscribe(listener: () => void): () => void` — Subscribe to changes
256
+ - `getSnapshot(): T` — Get current value
257
+
258
+ #### `client.watch(selector, callback, equalityFn?)`
259
+
260
+ Imperative API for watching a derived value. Callback is invoked whenever the selected value changes.
261
+
262
+ ```typescript
263
+ const unsubscribe = client.watch(
264
+ (s) => s.gameData.scoreboard?.teams[0]?.kills,
265
+ (kills, prevKills) => {
266
+ console.log(`Kills changed: ${prevKills} → ${kills}`);
267
+ },
268
+ );
269
+ ```
270
+
271
+ **Parameters:**
272
+
273
+ - `selector: (snapshot: GameStateSnapshot) => T` — Function to extract value
274
+ - `callback: (value: T, prev: T) => void` — Called when value changes
275
+ - `equalityFn?: (a: T, b: T) => boolean` — Optional equality comparison
276
+
277
+ **Returns:** `() => void` — Unsubscribe function
278
+
279
+ #### `client.store`
280
+
281
+ Direct access to the `GameStateStore` instance for advanced use cases:
282
+
283
+ ```typescript
284
+ // Get full snapshot
285
+ const snapshot = client.store.getSnapshot();
286
+ console.log("Game time:", snapshot.gameData.gameTime);
287
+ console.log("Game state:", snapshot.gameState);
288
+ console.log("Version:", snapshot.version);
289
+
290
+ // Subscribe to all changes (unfiltered)
291
+ const unsubscribe = client.store.subscribe(() => {
292
+ console.log("Store updated");
293
+ });
294
+
295
+ // Watch with immediate callback
296
+ client.store.watchImmediate(
297
+ (s) => s.gameData.gameTime,
298
+ (time, prevTime) => {
299
+ console.log(`Time: ${prevTime} → ${time}`);
300
+ },
301
+ );
302
+ ```
303
+
304
+ ### Equality Functions
305
+
306
+ #### `Object.is` (default)
307
+
308
+ Uses strict equality (`===`). Best for primitives and object references:
309
+
310
+ ```typescript
311
+ client.select((s) => s.gameData.gameTime); // Uses Object.is by default
312
+ ```
313
+
314
+ #### `shallowEqual`
315
+
316
+ Compares objects by their enumerable properties. Use when selecting objects whose identity changes but contents may not:
317
+
318
+ ```typescript
319
+ import { shallowEqual } from "@bluebottle_gg/league-broadcast-client";
320
+
321
+ const stats = client.select(
322
+ (s) => ({
323
+ kills: s.gameData.scoreboard?.teams[0]?.kills ?? 0,
324
+ deaths: s.gameData.scoreboardBottom?.teams[0]?.players[0]?.deaths ?? 0,
325
+ }),
326
+ shallowEqual, // Only trigger when values change, not object identity
327
+ );
328
+ ```
329
+
330
+ ### GameStateSnapshot
331
+
332
+ The snapshot passed to selectors contains:
333
+
334
+ ```typescript
335
+ interface GameStateSnapshot {
336
+ gameData: ingameFrontendData; // Full game data
337
+ gameState: GameState; // Current game state enum
338
+ version: number; // Monotonic version counter
339
+ }
340
+ ```
341
+
342
+ ### Performance Tips
343
+
344
+ **✅ DO:** Use selectors to watch specific values
345
+
346
+ ```typescript
347
+ client.watch((s) => s.gameData.scoreboard?.teams[0]?.kills, handleKills);
348
+ ```
349
+
350
+ **❌ DON'T:** Subscribe to entire store and manually filter
351
+
352
+ ```typescript
353
+ // This triggers on EVERY change, even unrelated ones
354
+ client.store.subscribe(() => {
355
+ const kills = client.store.getSnapshot().gameData.scoreboard?.teams[0]?.kills;
356
+ handleKills(kills);
357
+ });
358
+ ```
359
+
360
+ **✅ DO:** Use `shallowEqual` for derived objects
361
+
362
+ ```typescript
363
+ client.watch(
364
+ (s) => ({ kills: s.gameData.scoreboard?.teams[0]?.kills ?? 0 }),
365
+ handleStats,
366
+ shallowEqual,
367
+ );
368
+ ```
369
+
370
+ **✅ DO:** Select nested objects by reference (structural sharing keeps stable references when unchanged)
371
+
372
+ ```typescript
373
+ // Only triggers when team object reference changes
374
+ client.watch((s) => s.gameData.scoreboard?.teams[0], handleTeam);
375
+ ```
376
+
377
+ **✅ DO:** Handle `null` as “not available”
378
+
379
+ ```typescript
380
+ client.watch((s) => s.gameData.scoreboard, (scoreboard) => {
381
+ if (!scoreboard) {
382
+ hideScoreboard();
383
+ return;
384
+ }
385
+ renderScoreboard(scoreboard);
386
+ });
387
+ ```
388
+
389
+ ### Framework Integration Examples
390
+
391
+ See [examples/reactivity.ts](examples/reactivity.ts) for complete examples with:
392
+
393
+ - React (with `useSyncExternalStore` and custom hooks)
394
+ - Vue 3 (Composition API)
395
+ - Svelte (native store contract)
396
+ - Solid.js
397
+ - Angular (with Signals)
398
+ - Vanilla JavaScript
399
+
400
+ ## Examples
401
+
402
+ ### Basic Usage
403
+
404
+ ```typescript
405
+ import { LeagueBroadcastClient } from "@bluebottle_gg/league-broadcast-client";
406
+
407
+ const client = new LeagueBroadcastClient({
408
+ host: "192.168.1.100",
409
+ port: 58869,
410
+ });
411
+
412
+ // Game data is automatically updated in real-time
413
+ setInterval(() => {
414
+ const data = client.getGameData();
415
+ console.log("Current game time:", data.gameTime);
416
+ }, 1000);
417
+ ```
418
+
419
+ ### React Integration
420
+
421
+ ```typescript
422
+ import { useEffect, useState } from 'react';
423
+ import { LeagueBroadcastClient, ingameFrontendData, GameState } from '@bluebottle_gg/league-broadcast-client';
424
+
425
+ function useLeagueBroadcast(host: string, port: number) {
426
+ const [client] = useState(() => new LeagueBroadcastClient({ host, port }));
427
+ const [gameData, setGameData] = useState<ingameFrontendData | null>(null);
428
+ const [gameState, setGameState] = useState<GameState>(GameState.OutOfGame);
429
+
430
+ useEffect(() => {
431
+ const unsubscribeState = client.onStateUpdate((data) => {
432
+ setGameData(data);
433
+ });
434
+
435
+ const unsubscribeStatus = client.onGameStatusChange((status) => {
436
+ setGameState(status);
437
+ });
438
+
439
+ return () => {
440
+ unsubscribeState();
441
+ unsubscribeStatus();
442
+ client.disconnect();
443
+ };
444
+ }, [client]);
445
+
446
+ return { gameData, gameState, client };
447
+ }
448
+
449
+ // Usage in component
450
+ function GameDisplay() {
451
+ const { gameData, gameState } = useLeagueBroadcast('localhost', 58869);
452
+
453
+ if (gameState === GameState.OutOfGame) {
454
+ return <div>No game in progress</div>;
455
+ }
456
+
457
+ return (
458
+ <div>
459
+ <h1>Game Time: {gameData?.gameTime}</h1>
460
+ <div>Blue Team Score: {gameData?.scoreboard?.teams[0]?.totalKills}</div>
461
+ <div>Red Team Score: {gameData?.scoreboard?.teams[1]?.totalKills}</div>
462
+ </div>
463
+ );
464
+ }
465
+ ```
466
+
467
+ ### Event Handling
468
+
469
+ ```typescript
470
+ import { LeagueBroadcastClient } from "@bluebottle_gg/league-broadcast-client";
471
+
472
+ const client = new LeagueBroadcastClient({
473
+ host: "localhost",
474
+ port: 58869,
475
+ });
476
+
477
+ // Handle all game events
478
+ client.onGameEvents({
479
+ onKillFeedEvent: (event) => {
480
+ console.log(`${event.killerName} killed ${event.victimName}`);
481
+ },
482
+ onObjectiveEvent: (event) => {
483
+ console.log(`Objective secured: ${event.objectiveType}`);
484
+ },
485
+ onFirstTowerEvent: (event) => {
486
+ console.log(`First tower taken by team ${event.teamId}`);
487
+ },
488
+ onAnnouncementEvent: (event) => {
489
+ console.log(`Announcement: ${event.announcementType}`);
490
+ },
491
+ onPlayerEvent: (event) => {
492
+ console.log(`Player event: ${event.eventType}`);
493
+ },
494
+ onTeamEvent: (event) => {
495
+ console.log(`Team event: ${event.eventType}`);
496
+ },
497
+ });
498
+ ```
499
+
500
+ ### Manual Connection Control
501
+
502
+ ```typescript
503
+ import { LeagueBroadcastClient } from "@bluebottle_gg/league-broadcast-client";
504
+
505
+ const client = new LeagueBroadcastClient({
506
+ host: "localhost",
507
+ port: 58869,
508
+ autoConnect: false, // Don't connect automatically
509
+ });
510
+
511
+ // Connect manually when ready
512
+ async function connectToGame() {
513
+ try {
514
+ await client.connect();
515
+ console.log("Connected successfully!");
516
+ } catch (error) {
517
+ console.error("Connection failed:", error);
518
+ }
519
+ }
520
+
521
+ // Disconnect when done
522
+ function disconnectFromGame() {
523
+ client.disconnect();
524
+ console.log("Disconnected");
525
+ }
526
+ ```
527
+
528
+ ## License
529
+
530
+ GPLv3