@bluebottle_gg/league-broadcast-client 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +10 -0
- package/dist/index.js +451 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -2975,6 +2975,10 @@ interface LeagueBroadcastClientConfig {
|
|
|
2975
2975
|
cacheRoute?: string;
|
|
2976
2976
|
useHttps?: boolean;
|
|
2977
2977
|
autoConnect?: boolean;
|
|
2978
|
+
overlayHealth?: boolean | {
|
|
2979
|
+
enabled?: boolean;
|
|
2980
|
+
name?: string;
|
|
2981
|
+
};
|
|
2978
2982
|
}
|
|
2979
2983
|
interface IngameEventHandlers {
|
|
2980
2984
|
onPlayerEvent?: (event: playerUpdateEvent) => void;
|
|
@@ -3029,6 +3033,7 @@ declare class LeagueBroadcastClient {
|
|
|
3029
3033
|
private ingameWs;
|
|
3030
3034
|
private preGameWs;
|
|
3031
3035
|
private config;
|
|
3036
|
+
private _overlayHealth;
|
|
3032
3037
|
private gameData;
|
|
3033
3038
|
private gameState;
|
|
3034
3039
|
private _isTestingEnvironment;
|
|
@@ -3082,6 +3087,11 @@ declare class LeagueBroadcastClient {
|
|
|
3082
3087
|
* Disconnect from both WebSocket endpoints.
|
|
3083
3088
|
*/
|
|
3084
3089
|
disconnect(): void;
|
|
3090
|
+
/**
|
|
3091
|
+
* Starts passive frontend-health monitoring once, unless disabled via the
|
|
3092
|
+
* `overlayHealth` config. Guarded so it never throws into the client.
|
|
3093
|
+
*/
|
|
3094
|
+
private startOverlayHealthIfEnabled;
|
|
3085
3095
|
/**
|
|
3086
3096
|
* Whether the in-game WebSocket is connected.
|
|
3087
3097
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1389,10 +1389,436 @@ function createIngameTimerUtils(getGameTime) {
|
|
|
1389
1389
|
};
|
|
1390
1390
|
}
|
|
1391
1391
|
|
|
1392
|
+
// ../shared-overlay-health/src/transport.ts
|
|
1393
|
+
function resolveEndpoint(opts) {
|
|
1394
|
+
if (opts.endpoint) return opts.endpoint;
|
|
1395
|
+
const loc = typeof location !== "undefined" ? location : void 0;
|
|
1396
|
+
const https = opts.useHttps ?? loc?.protocol === "https:";
|
|
1397
|
+
const host = opts.host ?? loc?.hostname ?? "localhost";
|
|
1398
|
+
const port = opts.port ?? loc?.port ?? 58869;
|
|
1399
|
+
const portPart = port ? `:${port}` : "";
|
|
1400
|
+
return `${https ? "https" : "http"}://${host}${portPart}/api/overlay/health`;
|
|
1401
|
+
}
|
|
1402
|
+
function sendReport(endpoint, report) {
|
|
1403
|
+
let body;
|
|
1404
|
+
try {
|
|
1405
|
+
body = JSON.stringify(report);
|
|
1406
|
+
} catch {
|
|
1407
|
+
return false;
|
|
1408
|
+
}
|
|
1409
|
+
try {
|
|
1410
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
1411
|
+
const blob = new Blob([body], { type: "text/plain;charset=UTF-8" });
|
|
1412
|
+
if (navigator.sendBeacon(endpoint, blob)) return true;
|
|
1413
|
+
}
|
|
1414
|
+
} catch {
|
|
1415
|
+
}
|
|
1416
|
+
try {
|
|
1417
|
+
if (typeof fetch === "function") {
|
|
1418
|
+
void fetch(endpoint, {
|
|
1419
|
+
method: "POST",
|
|
1420
|
+
body,
|
|
1421
|
+
headers: { "Content-Type": "text/plain;charset=UTF-8" },
|
|
1422
|
+
keepalive: true,
|
|
1423
|
+
mode: "cors",
|
|
1424
|
+
credentials: "omit"
|
|
1425
|
+
}).catch(() => {
|
|
1426
|
+
});
|
|
1427
|
+
return true;
|
|
1428
|
+
}
|
|
1429
|
+
} catch {
|
|
1430
|
+
}
|
|
1431
|
+
return false;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// ../shared-overlay-health/src/types.ts
|
|
1435
|
+
var DEFAULT_THRESHOLDS = {
|
|
1436
|
+
maxDecodedBytes: 24 * 1024 * 1024,
|
|
1437
|
+
// ~2500×2500 px
|
|
1438
|
+
maxImageDimension: 4096,
|
|
1439
|
+
maxOversampleRatio: 4,
|
|
1440
|
+
oversampleMinArea: 1e6,
|
|
1441
|
+
// ~1 MP
|
|
1442
|
+
maxAssetBytes: 5 * 1024 * 1024,
|
|
1443
|
+
// 5 MB on the wire
|
|
1444
|
+
minFps: 30,
|
|
1445
|
+
maxP95FrameMs: 50,
|
|
1446
|
+
longTaskMs: 200,
|
|
1447
|
+
maxJsHeapMb: 768,
|
|
1448
|
+
maxDomNodes: 5e3
|
|
1449
|
+
};
|
|
1450
|
+
|
|
1451
|
+
// ../shared-overlay-health/src/detector.ts
|
|
1452
|
+
var FRAME_WINDOW_MS = 3e3;
|
|
1453
|
+
var MAX_ISSUES = 50;
|
|
1454
|
+
var TRANSIENT_TTL_MS = 2e4;
|
|
1455
|
+
var TRANSIENT_KINDS = /* @__PURE__ */ new Set([
|
|
1456
|
+
"low-fps",
|
|
1457
|
+
"long-task",
|
|
1458
|
+
"high-memory",
|
|
1459
|
+
"dom-bloat"
|
|
1460
|
+
]);
|
|
1461
|
+
function now() {
|
|
1462
|
+
return Date.now();
|
|
1463
|
+
}
|
|
1464
|
+
function randomId() {
|
|
1465
|
+
return Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
|
|
1466
|
+
}
|
|
1467
|
+
function describeElement(el) {
|
|
1468
|
+
const id = el.id ? `#${el.id}` : "";
|
|
1469
|
+
const cls = typeof el.className === "string" && el.className ? `.${el.className.trim().split(/\s+/).slice(0, 2).join(".")}` : "";
|
|
1470
|
+
return `${el.tagName.toLowerCase()}${id}${cls}`;
|
|
1471
|
+
}
|
|
1472
|
+
var OverlayHealthMonitor = class {
|
|
1473
|
+
constructor(opts) {
|
|
1474
|
+
this.sessionId = randomId();
|
|
1475
|
+
this.issues = /* @__PURE__ */ new Map();
|
|
1476
|
+
/** Recent frame timestamps (epoch ms) within FRAME_WINDOW_MS. */
|
|
1477
|
+
this.frameTimes = [];
|
|
1478
|
+
this.lastFrameAt = 0;
|
|
1479
|
+
this.longTaskTimes = [];
|
|
1480
|
+
this.rafId = null;
|
|
1481
|
+
this.reportTimer = null;
|
|
1482
|
+
this.immediateTimer = null;
|
|
1483
|
+
this.perfObserver = null;
|
|
1484
|
+
this.stopped = false;
|
|
1485
|
+
// ── Detection ──────────────────────────────────────────────────────────
|
|
1486
|
+
this.onError = (e) => {
|
|
1487
|
+
try {
|
|
1488
|
+
const target = e.target;
|
|
1489
|
+
if (target instanceof HTMLImageElement) {
|
|
1490
|
+
const url = target.currentSrc || target.src || target.getAttribute("src") || "(unknown)";
|
|
1491
|
+
this.record({
|
|
1492
|
+
kind: "broken-image",
|
|
1493
|
+
severity: "error",
|
|
1494
|
+
key: url,
|
|
1495
|
+
message: "Image failed to load",
|
|
1496
|
+
url,
|
|
1497
|
+
element: describeElement(target)
|
|
1498
|
+
});
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
const errEvent = e;
|
|
1502
|
+
if (!(target instanceof Element) && errEvent.message) {
|
|
1503
|
+
const where = errEvent.filename ? `${errEvent.filename}:${errEvent.lineno ?? 0}` : "";
|
|
1504
|
+
this.record({
|
|
1505
|
+
kind: "js-error",
|
|
1506
|
+
severity: "error",
|
|
1507
|
+
key: `${errEvent.message}|${where}`,
|
|
1508
|
+
message: errEvent.message,
|
|
1509
|
+
detail: where ? { location: where } : void 0
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
} catch {
|
|
1513
|
+
}
|
|
1514
|
+
};
|
|
1515
|
+
this.onLoad = (e) => {
|
|
1516
|
+
try {
|
|
1517
|
+
const target = e.target;
|
|
1518
|
+
if (target instanceof HTMLImageElement) {
|
|
1519
|
+
this.inspectImage(target);
|
|
1520
|
+
}
|
|
1521
|
+
} catch {
|
|
1522
|
+
}
|
|
1523
|
+
};
|
|
1524
|
+
this.onRejection = (e) => {
|
|
1525
|
+
try {
|
|
1526
|
+
const reason = e.reason instanceof Error ? e.reason.message : typeof e.reason === "string" ? e.reason : "Unhandled promise rejection";
|
|
1527
|
+
this.record({
|
|
1528
|
+
kind: "js-error",
|
|
1529
|
+
severity: "error",
|
|
1530
|
+
key: `rejection|${reason}`,
|
|
1531
|
+
message: `Unhandled rejection: ${reason}`
|
|
1532
|
+
});
|
|
1533
|
+
} catch {
|
|
1534
|
+
}
|
|
1535
|
+
};
|
|
1536
|
+
this.onVisibilityChange = () => {
|
|
1537
|
+
if (document.visibilityState === "hidden") this.flush();
|
|
1538
|
+
};
|
|
1539
|
+
this.onPageHide = () => {
|
|
1540
|
+
this.flush();
|
|
1541
|
+
};
|
|
1542
|
+
this.options = {
|
|
1543
|
+
source: opts.source,
|
|
1544
|
+
enabled: opts.enabled ?? true,
|
|
1545
|
+
reportIntervalMs: opts.reportIntervalMs ?? 5e3,
|
|
1546
|
+
thresholds: { ...DEFAULT_THRESHOLDS, ...opts.thresholds ?? {} }
|
|
1547
|
+
};
|
|
1548
|
+
this.endpoint = resolveEndpoint(opts);
|
|
1549
|
+
}
|
|
1550
|
+
start() {
|
|
1551
|
+
if (this.stopped || !this.options.enabled) return;
|
|
1552
|
+
if (typeof window === "undefined") return;
|
|
1553
|
+
window.addEventListener("error", this.onError, true);
|
|
1554
|
+
window.addEventListener("load", this.onLoad, true);
|
|
1555
|
+
window.addEventListener("unhandledrejection", this.onRejection);
|
|
1556
|
+
document.addEventListener("visibilitychange", this.onVisibilityChange);
|
|
1557
|
+
window.addEventListener("pagehide", this.onPageHide);
|
|
1558
|
+
this.startFrameSampling();
|
|
1559
|
+
this.startPerfObserver();
|
|
1560
|
+
this.reportTimer = setInterval(() => this.flush(), this.options.reportIntervalMs);
|
|
1561
|
+
}
|
|
1562
|
+
flush() {
|
|
1563
|
+
if (this.stopped) return;
|
|
1564
|
+
try {
|
|
1565
|
+
const report = this.buildReport();
|
|
1566
|
+
sendReport(this.endpoint, report);
|
|
1567
|
+
} catch {
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
stop() {
|
|
1571
|
+
if (this.stopped) return;
|
|
1572
|
+
this.stopped = true;
|
|
1573
|
+
window.removeEventListener("error", this.onError, true);
|
|
1574
|
+
window.removeEventListener("load", this.onLoad, true);
|
|
1575
|
+
window.removeEventListener("unhandledrejection", this.onRejection);
|
|
1576
|
+
document.removeEventListener("visibilitychange", this.onVisibilityChange);
|
|
1577
|
+
window.removeEventListener("pagehide", this.onPageHide);
|
|
1578
|
+
if (this.rafId !== null) cancelAnimationFrame(this.rafId);
|
|
1579
|
+
if (this.reportTimer !== null) clearInterval(this.reportTimer);
|
|
1580
|
+
if (this.immediateTimer !== null) clearTimeout(this.immediateTimer);
|
|
1581
|
+
this.perfObserver?.disconnect();
|
|
1582
|
+
}
|
|
1583
|
+
inspectImage(img) {
|
|
1584
|
+
const nW = img.naturalWidth;
|
|
1585
|
+
const nH = img.naturalHeight;
|
|
1586
|
+
if (!nW || !nH) return;
|
|
1587
|
+
const naturalArea = nW * nH;
|
|
1588
|
+
const decodedBytes = naturalArea * 4;
|
|
1589
|
+
const displayedArea = Math.max(1, img.clientWidth * img.clientHeight);
|
|
1590
|
+
const oversampleRatio = naturalArea / displayedArea;
|
|
1591
|
+
const t = this.options.thresholds;
|
|
1592
|
+
const url = img.currentSrc || img.src;
|
|
1593
|
+
const tooBig = decodedBytes > t.maxDecodedBytes || nW > t.maxImageDimension || nH > t.maxImageDimension;
|
|
1594
|
+
const oversampled = naturalArea > t.oversampleMinArea && oversampleRatio > t.maxOversampleRatio;
|
|
1595
|
+
if (tooBig || oversampled) {
|
|
1596
|
+
this.record({
|
|
1597
|
+
kind: "oversized-image",
|
|
1598
|
+
severity: "warning",
|
|
1599
|
+
key: url,
|
|
1600
|
+
message: tooBig ? `Very large image (${nW}\xD7${nH})` : `Image is much larger than displayed (${nW}\xD7${nH})`,
|
|
1601
|
+
url,
|
|
1602
|
+
element: describeElement(img),
|
|
1603
|
+
detail: {
|
|
1604
|
+
naturalWidth: nW,
|
|
1605
|
+
naturalHeight: nH,
|
|
1606
|
+
displayedWidth: img.clientWidth,
|
|
1607
|
+
displayedHeight: img.clientHeight,
|
|
1608
|
+
approxDecodedMb: Math.round(decodedBytes / (1024 * 1024) * 10) / 10,
|
|
1609
|
+
oversampleRatio: Math.round(oversampleRatio * 10) / 10
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
startPerfObserver() {
|
|
1615
|
+
if (typeof PerformanceObserver === "undefined") return;
|
|
1616
|
+
try {
|
|
1617
|
+
this.perfObserver = new PerformanceObserver((list) => {
|
|
1618
|
+
for (const entry of list.getEntries()) {
|
|
1619
|
+
if (entry.entryType === "longtask") {
|
|
1620
|
+
this.longTaskTimes.push(now());
|
|
1621
|
+
if (entry.duration >= this.options.thresholds.longTaskMs) {
|
|
1622
|
+
this.record({
|
|
1623
|
+
kind: "long-task",
|
|
1624
|
+
severity: "warning",
|
|
1625
|
+
key: "long-task",
|
|
1626
|
+
message: "Main thread blocked by a long task",
|
|
1627
|
+
detail: { lastDurationMs: Math.round(entry.duration) }
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
} else if (entry.entryType === "resource") {
|
|
1631
|
+
this.inspectResource(entry);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
});
|
|
1635
|
+
this.tryObserve({ type: "longtask", buffered: true });
|
|
1636
|
+
this.tryObserve({ type: "resource", buffered: true });
|
|
1637
|
+
} catch {
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
tryObserve(init) {
|
|
1641
|
+
try {
|
|
1642
|
+
this.perfObserver?.observe(init);
|
|
1643
|
+
} catch {
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
inspectResource(entry) {
|
|
1647
|
+
if (entry.initiatorType !== "img" && entry.initiatorType !== "css") return;
|
|
1648
|
+
const bytes = entry.encodedBodySize || entry.transferSize || 0;
|
|
1649
|
+
if (bytes > this.options.thresholds.maxAssetBytes) {
|
|
1650
|
+
this.record({
|
|
1651
|
+
kind: "large-asset",
|
|
1652
|
+
severity: "warning",
|
|
1653
|
+
key: entry.name,
|
|
1654
|
+
message: `Large asset download (${Math.round(bytes / (1024 * 1024) * 10) / 10} MB)`,
|
|
1655
|
+
url: entry.name,
|
|
1656
|
+
detail: {
|
|
1657
|
+
encodedBytes: bytes,
|
|
1658
|
+
decodedBytes: entry.decodedBodySize || 0,
|
|
1659
|
+
initiator: entry.initiatorType
|
|
1660
|
+
}
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
startFrameSampling() {
|
|
1665
|
+
if (typeof requestAnimationFrame === "undefined") return;
|
|
1666
|
+
this.lastFrameAt = now();
|
|
1667
|
+
const tick = () => {
|
|
1668
|
+
if (this.stopped) return;
|
|
1669
|
+
const t = now();
|
|
1670
|
+
this.frameTimes.push(t);
|
|
1671
|
+
const cutoff = t - FRAME_WINDOW_MS;
|
|
1672
|
+
while (this.frameTimes.length && this.frameTimes[0] < cutoff) this.frameTimes.shift();
|
|
1673
|
+
this.lastFrameAt = t;
|
|
1674
|
+
this.rafId = requestAnimationFrame(tick);
|
|
1675
|
+
};
|
|
1676
|
+
this.rafId = requestAnimationFrame(tick);
|
|
1677
|
+
}
|
|
1678
|
+
// ── Metrics + reporting ────────────────────────────────────────────────
|
|
1679
|
+
computeMetrics() {
|
|
1680
|
+
const frames = this.frameTimes;
|
|
1681
|
+
const deltas = [];
|
|
1682
|
+
for (let i = 1; i < frames.length; i++) deltas.push(frames[i] - frames[i - 1]);
|
|
1683
|
+
const spanMs = frames.length > 1 ? frames[frames.length - 1] - frames[0] : 0;
|
|
1684
|
+
const fps = spanMs > 0 ? Math.round((frames.length - 1) / spanMs * 1e3) : 0;
|
|
1685
|
+
let p95 = 0;
|
|
1686
|
+
let longest = 0;
|
|
1687
|
+
if (deltas.length) {
|
|
1688
|
+
const sorted = [...deltas].sort((a, b) => a - b);
|
|
1689
|
+
p95 = sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))];
|
|
1690
|
+
longest = sorted[sorted.length - 1];
|
|
1691
|
+
}
|
|
1692
|
+
const cutoff = now() - FRAME_WINDOW_MS;
|
|
1693
|
+
this.longTaskTimes = this.longTaskTimes.filter((tt) => tt >= cutoff);
|
|
1694
|
+
const metrics = {
|
|
1695
|
+
fps,
|
|
1696
|
+
p95FrameMs: Math.round(p95),
|
|
1697
|
+
longestFrameMs: Math.round(longest),
|
|
1698
|
+
longTasksInWindow: this.longTaskTimes.length,
|
|
1699
|
+
domNodes: document.getElementsByTagName("*").length
|
|
1700
|
+
};
|
|
1701
|
+
const mem = performance.memory;
|
|
1702
|
+
if (mem) {
|
|
1703
|
+
metrics.jsHeapUsedMb = Math.round(mem.usedJSHeapSize / (1024 * 1024));
|
|
1704
|
+
metrics.jsHeapLimitMb = Math.round(mem.jsHeapSizeLimit / (1024 * 1024));
|
|
1705
|
+
}
|
|
1706
|
+
return metrics;
|
|
1707
|
+
}
|
|
1708
|
+
/** Evaluate window-derived issues (low fps, high memory, dom bloat). */
|
|
1709
|
+
evaluateMetricIssues(metrics) {
|
|
1710
|
+
const t = this.options.thresholds;
|
|
1711
|
+
if (this.frameTimes.length > 30) {
|
|
1712
|
+
if (metrics.fps < t.minFps || metrics.p95FrameMs > t.maxP95FrameMs) {
|
|
1713
|
+
this.record({
|
|
1714
|
+
kind: "low-fps",
|
|
1715
|
+
severity: "warning",
|
|
1716
|
+
key: "low-fps",
|
|
1717
|
+
message: `Low frame rate (${metrics.fps} fps)`,
|
|
1718
|
+
detail: { fps: metrics.fps, p95FrameMs: metrics.p95FrameMs }
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
if (metrics.jsHeapUsedMb !== void 0 && metrics.jsHeapUsedMb > t.maxJsHeapMb) {
|
|
1723
|
+
this.record({
|
|
1724
|
+
kind: "high-memory",
|
|
1725
|
+
severity: "warning",
|
|
1726
|
+
key: "high-memory",
|
|
1727
|
+
message: `High memory use (${metrics.jsHeapUsedMb} MB)`,
|
|
1728
|
+
detail: { jsHeapUsedMb: metrics.jsHeapUsedMb }
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
if (metrics.domNodes > t.maxDomNodes) {
|
|
1732
|
+
this.record({
|
|
1733
|
+
kind: "dom-bloat",
|
|
1734
|
+
severity: metrics.domNodes > t.maxDomNodes * 2 ? "warning" : "info",
|
|
1735
|
+
key: "dom-bloat",
|
|
1736
|
+
message: `Large DOM (${metrics.domNodes} elements)`,
|
|
1737
|
+
detail: { domNodes: metrics.domNodes }
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
buildReport() {
|
|
1742
|
+
const metrics = this.computeMetrics();
|
|
1743
|
+
this.evaluateMetricIssues(metrics);
|
|
1744
|
+
this.pruneTransientIssues();
|
|
1745
|
+
return {
|
|
1746
|
+
source: this.options.source,
|
|
1747
|
+
sessionId: this.sessionId,
|
|
1748
|
+
url: typeof location !== "undefined" ? location.href : "",
|
|
1749
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "",
|
|
1750
|
+
timestamp: now(),
|
|
1751
|
+
metrics,
|
|
1752
|
+
issues: [...this.issues.values()]
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
record(input) {
|
|
1756
|
+
const mapKey = `${input.kind}:${input.key}`;
|
|
1757
|
+
const ts = now();
|
|
1758
|
+
const existing = this.issues.get(mapKey);
|
|
1759
|
+
if (existing) {
|
|
1760
|
+
existing.count++;
|
|
1761
|
+
existing.lastSeen = ts;
|
|
1762
|
+
existing.message = input.message;
|
|
1763
|
+
if (input.detail) existing.detail = input.detail;
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
if (this.issues.size >= MAX_ISSUES) this.evictOldest();
|
|
1767
|
+
this.issues.set(mapKey, {
|
|
1768
|
+
kind: input.kind,
|
|
1769
|
+
severity: input.severity,
|
|
1770
|
+
key: input.key,
|
|
1771
|
+
message: input.message,
|
|
1772
|
+
url: input.url,
|
|
1773
|
+
element: input.element,
|
|
1774
|
+
detail: input.detail,
|
|
1775
|
+
count: 1,
|
|
1776
|
+
firstSeen: ts,
|
|
1777
|
+
lastSeen: ts
|
|
1778
|
+
});
|
|
1779
|
+
if (input.severity === "error") this.scheduleImmediate();
|
|
1780
|
+
}
|
|
1781
|
+
scheduleImmediate() {
|
|
1782
|
+
if (this.immediateTimer !== null) return;
|
|
1783
|
+
this.immediateTimer = setTimeout(() => {
|
|
1784
|
+
this.immediateTimer = null;
|
|
1785
|
+
this.flush();
|
|
1786
|
+
}, 750);
|
|
1787
|
+
}
|
|
1788
|
+
pruneTransientIssues() {
|
|
1789
|
+
const cutoff = now() - TRANSIENT_TTL_MS;
|
|
1790
|
+
for (const [k, issue] of this.issues) {
|
|
1791
|
+
if (TRANSIENT_KINDS.has(issue.kind) && issue.lastSeen < cutoff) {
|
|
1792
|
+
this.issues.delete(k);
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
evictOldest() {
|
|
1797
|
+
let oldestKey = null;
|
|
1798
|
+
let oldest = Infinity;
|
|
1799
|
+
for (const [k, issue] of this.issues) {
|
|
1800
|
+
if (issue.lastSeen < oldest) {
|
|
1801
|
+
oldest = issue.lastSeen;
|
|
1802
|
+
oldestKey = k;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
if (oldestKey) this.issues.delete(oldestKey);
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
|
|
1809
|
+
// ../shared-overlay-health/src/index.ts
|
|
1810
|
+
function startOverlayHealth(options) {
|
|
1811
|
+
const monitor = new OverlayHealthMonitor(options);
|
|
1812
|
+
monitor.start();
|
|
1813
|
+
return monitor;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1392
1816
|
// src/LeagueBroadcastClient.ts
|
|
1393
1817
|
var _clockInterval = null;
|
|
1394
1818
|
var LeagueBroadcastClient = class {
|
|
1395
1819
|
constructor(config) {
|
|
1820
|
+
// -- Frontend-health monitoring ---------------------------------------------
|
|
1821
|
+
this._overlayHealth = null;
|
|
1396
1822
|
this.gameState = 0 /* OutOfGame */;
|
|
1397
1823
|
this._isTestingEnvironment = false;
|
|
1398
1824
|
this.champSelectWasActive = false;
|
|
@@ -1424,7 +1850,8 @@ var LeagueBroadcastClient = class {
|
|
|
1424
1850
|
apiRoute: config.apiRoute ?? "/api",
|
|
1425
1851
|
cacheRoute: config.cacheRoute ?? "/cache",
|
|
1426
1852
|
useHttps: config.useHttps ?? false,
|
|
1427
|
-
autoConnect: config.autoConnect ?? true
|
|
1853
|
+
autoConnect: config.autoConnect ?? true,
|
|
1854
|
+
overlayHealth: config.overlayHealth ?? true
|
|
1428
1855
|
};
|
|
1429
1856
|
const httpProtocol = this.config.useHttps ? "https" : "http";
|
|
1430
1857
|
const apiBaseUrl = `${httpProtocol}://${this.config.host}:${this.config.port}${this.config.apiRoute}`;
|
|
@@ -1473,6 +1900,7 @@ var LeagueBroadcastClient = class {
|
|
|
1473
1900
|
* promise rejects, but the other connection may still succeed.
|
|
1474
1901
|
*/
|
|
1475
1902
|
async connect() {
|
|
1903
|
+
this.startOverlayHealthIfEnabled();
|
|
1476
1904
|
const protocol = this.config.useHttps ? "wss" : "ws";
|
|
1477
1905
|
const base = `${protocol}://${this.config.host}:${this.config.port}`;
|
|
1478
1906
|
const ingameUrl = `${base}${this.config.ingameWsRoute}`;
|
|
@@ -1499,6 +1927,28 @@ var LeagueBroadcastClient = class {
|
|
|
1499
1927
|
disconnect() {
|
|
1500
1928
|
this.ingameWs.disconnect();
|
|
1501
1929
|
this.preGameWs.disconnect();
|
|
1930
|
+
this._overlayHealth?.stop();
|
|
1931
|
+
this._overlayHealth = null;
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Starts passive frontend-health monitoring once, unless disabled via the
|
|
1935
|
+
* `overlayHealth` config. Guarded so it never throws into the client.
|
|
1936
|
+
*/
|
|
1937
|
+
startOverlayHealthIfEnabled() {
|
|
1938
|
+
if (this._overlayHealth) return;
|
|
1939
|
+
const setting = this.config.overlayHealth;
|
|
1940
|
+
if (setting === false) return;
|
|
1941
|
+
const opts = typeof setting === "object" ? setting : {};
|
|
1942
|
+
if (opts.enabled === false) return;
|
|
1943
|
+
try {
|
|
1944
|
+
this._overlayHealth = startOverlayHealth({
|
|
1945
|
+
source: { kind: "sdk", name: opts.name },
|
|
1946
|
+
host: this.config.host,
|
|
1947
|
+
port: this.config.port,
|
|
1948
|
+
useHttps: this.config.useHttps
|
|
1949
|
+
});
|
|
1950
|
+
} catch {
|
|
1951
|
+
}
|
|
1502
1952
|
}
|
|
1503
1953
|
/**
|
|
1504
1954
|
* Whether the in-game WebSocket is connected.
|
|
@@ -1819,11 +2269,6 @@ var LeagueBroadcastClient = class {
|
|
|
1819
2269
|
(event) => this.ingameEventHandlers.onKillFeedEvent.forEach((h) => h(event))
|
|
1820
2270
|
);
|
|
1821
2271
|
}
|
|
1822
|
-
if (events.announcements) {
|
|
1823
|
-
events.announcements.forEach(
|
|
1824
|
-
(event) => this.ingameEventHandlers.onAnnouncementEvent.forEach((h) => h(event))
|
|
1825
|
-
);
|
|
1826
|
-
}
|
|
1827
2272
|
if (events.smiteReaction !== void 0) {
|
|
1828
2273
|
this.ingameEventHandlers.onSmiteReactionEvent.forEach(
|
|
1829
2274
|
(h) => h(events.smiteReaction)
|