@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/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