@trops/dash-core 0.1.161 → 0.1.163
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/electron/index.js +606 -76
- package/dist/electron/index.js.map +1 -1
- package/dist/index.esm.js +23 -0
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
package/dist/electron/index.js
CHANGED
|
@@ -29,11 +29,12 @@ var require$$2$4 = require('os');
|
|
|
29
29
|
var require$$4$1 = require('url');
|
|
30
30
|
var require$$2$3 = require('vm');
|
|
31
31
|
var require$$1$5 = require('croner');
|
|
32
|
+
var require$$1$6 = require('node-vibrant/node');
|
|
33
|
+
var require$$3$4 = require('http');
|
|
32
34
|
var require$$0$c = require('events');
|
|
33
|
-
var require$$
|
|
34
|
-
var require$$3$4 = require('net');
|
|
35
|
+
var require$$3$5 = require('net');
|
|
35
36
|
var require$$4$2 = require('tls');
|
|
36
|
-
var require$$1$
|
|
37
|
+
var require$$1$7 = require('crypto');
|
|
37
38
|
var require$$0$a = require('zlib');
|
|
38
39
|
var require$$0$b = require('buffer');
|
|
39
40
|
|
|
@@ -3748,7 +3749,7 @@ const { getFileContents: getFileContents$5, writeToFile: writeToFile$2 } = file;
|
|
|
3748
3749
|
const ObjectsToCsv = require$$5;
|
|
3749
3750
|
const Transform = transform;
|
|
3750
3751
|
const { extractColorsFromImageURL: extractColorsFromImageURL$1 } = color;
|
|
3751
|
-
const https$
|
|
3752
|
+
const https$2 = require$$8;
|
|
3752
3753
|
const appName$5 = "Dashboard";
|
|
3753
3754
|
|
|
3754
3755
|
const dataController$1 = {
|
|
@@ -3901,7 +3902,7 @@ const dataController$1 = {
|
|
|
3901
3902
|
|
|
3902
3903
|
const writeStream = fs$8.createWriteStream(resolvedFilepath);
|
|
3903
3904
|
|
|
3904
|
-
https$
|
|
3905
|
+
https$2
|
|
3905
3906
|
.get(url, (resp) => {
|
|
3906
3907
|
resp.on("data", (chunk) => {
|
|
3907
3908
|
writeStream.write(chunk);
|
|
@@ -38774,10 +38775,14 @@ css$1.stringify = stringify;
|
|
|
38774
38775
|
*
|
|
38775
38776
|
* Color extraction pipeline for generating themes from a website URL.
|
|
38776
38777
|
* Extracts brand colors from HTML meta tags, CSS custom properties,
|
|
38777
|
-
*
|
|
38778
|
+
* computed styles, and favicon/logo images (via node-vibrant).
|
|
38778
38779
|
*/
|
|
38779
38780
|
|
|
38780
38781
|
const css = css$1;
|
|
38782
|
+
const { Vibrant } = require$$1$6;
|
|
38783
|
+
const https$1 = require$$8;
|
|
38784
|
+
const http$2 = require$$3$4;
|
|
38785
|
+
const { URL: URL$2 } = require$$4$1;
|
|
38781
38786
|
|
|
38782
38787
|
// ─── Color conversion helpers ───────────────────────────────────────────────
|
|
38783
38788
|
|
|
@@ -39072,6 +39077,178 @@ function extractComputedColors(computedStyles) {
|
|
|
39072
39077
|
return results;
|
|
39073
39078
|
}
|
|
39074
39079
|
|
|
39080
|
+
// ─── Favicon extraction ──────────────────────────────────────────────────────
|
|
39081
|
+
|
|
39082
|
+
/**
|
|
39083
|
+
* Parse HTML to find favicon and apple-touch-icon URLs.
|
|
39084
|
+
* Prefers apple-touch-icon (higher resolution) and largest available sizes.
|
|
39085
|
+
* @param {string} htmlContent - Raw HTML string
|
|
39086
|
+
* @returns {Array<{url: string, priority: number}>} Sorted by priority (highest first)
|
|
39087
|
+
*/
|
|
39088
|
+
function extractFaviconUrls(htmlContent) {
|
|
39089
|
+
if (!htmlContent) return [];
|
|
39090
|
+
const icons = [];
|
|
39091
|
+
|
|
39092
|
+
// apple-touch-icon (higher resolution, best for extraction)
|
|
39093
|
+
const appleTouchPattern =
|
|
39094
|
+
/<link[^>]*rel\s*=\s*["']apple-touch-icon(?:-precomposed)?["'][^>]*>/gi;
|
|
39095
|
+
let match;
|
|
39096
|
+
while ((match = appleTouchPattern.exec(htmlContent)) !== null) {
|
|
39097
|
+
const hrefMatch = match[0].match(/href\s*=\s*["']([^"']+)["']/i);
|
|
39098
|
+
if (hrefMatch) {
|
|
39099
|
+
const sizesMatch = match[0].match(/sizes\s*=\s*["'](\d+)x(\d+)["']/i);
|
|
39100
|
+
const size = sizesMatch ? parseInt(sizesMatch[1], 10) : 180; // apple-touch-icon defaults to 180
|
|
39101
|
+
icons.push({ url: hrefMatch[1], priority: 100 + size });
|
|
39102
|
+
}
|
|
39103
|
+
}
|
|
39104
|
+
|
|
39105
|
+
// Standard favicon link tags (icon, shortcut icon)
|
|
39106
|
+
const iconPattern =
|
|
39107
|
+
/<link[^>]*rel\s*=\s*["'](?:shortcut\s+)?icon["'][^>]*>/gi;
|
|
39108
|
+
while ((match = iconPattern.exec(htmlContent)) !== null) {
|
|
39109
|
+
const hrefMatch = match[0].match(/href\s*=\s*["']([^"']+)["']/i);
|
|
39110
|
+
if (hrefMatch) {
|
|
39111
|
+
const sizesMatch = match[0].match(/sizes\s*=\s*["'](\d+)x(\d+)["']/i);
|
|
39112
|
+
const size = sizesMatch ? parseInt(sizesMatch[1], 10) : 16;
|
|
39113
|
+
icons.push({ url: hrefMatch[1], priority: size });
|
|
39114
|
+
}
|
|
39115
|
+
}
|
|
39116
|
+
|
|
39117
|
+
// Sort by priority descending (prefer largest / apple-touch-icon)
|
|
39118
|
+
icons.sort((a, b) => b.priority - a.priority);
|
|
39119
|
+
return icons;
|
|
39120
|
+
}
|
|
39121
|
+
|
|
39122
|
+
/**
|
|
39123
|
+
* Resolve a potentially relative URL against a base URL.
|
|
39124
|
+
* @param {string} href - The href from the HTML (may be relative)
|
|
39125
|
+
* @param {string} baseUrl - The page URL to resolve against
|
|
39126
|
+
* @returns {string|null} Absolute URL or null if invalid
|
|
39127
|
+
*/
|
|
39128
|
+
function resolveUrl(href, baseUrl) {
|
|
39129
|
+
try {
|
|
39130
|
+
return new URL$2(href, baseUrl).href;
|
|
39131
|
+
} catch {
|
|
39132
|
+
return null;
|
|
39133
|
+
}
|
|
39134
|
+
}
|
|
39135
|
+
|
|
39136
|
+
/**
|
|
39137
|
+
* Fetch a URL and return the response as a Buffer.
|
|
39138
|
+
* Follows redirects (up to 5). Times out after 10 seconds.
|
|
39139
|
+
* @param {string} url - Absolute URL to fetch
|
|
39140
|
+
* @returns {Promise<Buffer>}
|
|
39141
|
+
*/
|
|
39142
|
+
function fetchBuffer(url) {
|
|
39143
|
+
return new Promise((resolve, reject) => {
|
|
39144
|
+
const parsedUrl = new URL$2(url);
|
|
39145
|
+
const client = parsedUrl.protocol === "https:" ? https$1 : http$2;
|
|
39146
|
+
const request = client.get(
|
|
39147
|
+
url,
|
|
39148
|
+
{ timeout: 10000, headers: { "User-Agent": "Dash/1.0" } },
|
|
39149
|
+
(res) => {
|
|
39150
|
+
// Follow redirects
|
|
39151
|
+
if (
|
|
39152
|
+
res.statusCode >= 300 &&
|
|
39153
|
+
res.statusCode < 400 &&
|
|
39154
|
+
res.headers.location
|
|
39155
|
+
) {
|
|
39156
|
+
const redirectUrl = resolveUrl(res.headers.location, url);
|
|
39157
|
+
if (redirectUrl) {
|
|
39158
|
+
fetchBuffer(redirectUrl).then(resolve).catch(reject);
|
|
39159
|
+
return;
|
|
39160
|
+
}
|
|
39161
|
+
}
|
|
39162
|
+
if (res.statusCode !== 200) {
|
|
39163
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
39164
|
+
return;
|
|
39165
|
+
}
|
|
39166
|
+
const chunks = [];
|
|
39167
|
+
res.on("data", (chunk) => chunks.push(chunk));
|
|
39168
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
39169
|
+
res.on("error", reject);
|
|
39170
|
+
},
|
|
39171
|
+
);
|
|
39172
|
+
request.on("error", reject);
|
|
39173
|
+
request.on("timeout", () => {
|
|
39174
|
+
request.destroy();
|
|
39175
|
+
reject(new Error("Timeout"));
|
|
39176
|
+
});
|
|
39177
|
+
});
|
|
39178
|
+
}
|
|
39179
|
+
|
|
39180
|
+
/**
|
|
39181
|
+
* Extract colors from favicon/logo images using node-vibrant.
|
|
39182
|
+
* Tries icons in priority order (apple-touch-icon first, largest first).
|
|
39183
|
+
* Returns on the first successful extraction.
|
|
39184
|
+
*
|
|
39185
|
+
* @param {string} htmlContent - Raw HTML to parse for icon URLs
|
|
39186
|
+
* @param {string} baseUrl - Page URL for resolving relative icon paths
|
|
39187
|
+
* @returns {Promise<Array<{hex: string, source: string, confidence: number}>>}
|
|
39188
|
+
*/
|
|
39189
|
+
async function extractFaviconColors(htmlContent, baseUrl) {
|
|
39190
|
+
const iconEntries = extractFaviconUrls(htmlContent);
|
|
39191
|
+
if (iconEntries.length === 0) {
|
|
39192
|
+
// Fallback: try /favicon.ico at the domain root
|
|
39193
|
+
try {
|
|
39194
|
+
const rootFavicon = new URL$2("/favicon.ico", baseUrl).href;
|
|
39195
|
+
iconEntries.push({ url: rootFavicon, priority: 1 });
|
|
39196
|
+
} catch {
|
|
39197
|
+
return [];
|
|
39198
|
+
}
|
|
39199
|
+
}
|
|
39200
|
+
|
|
39201
|
+
for (const entry of iconEntries) {
|
|
39202
|
+
const absoluteUrl = resolveUrl(entry.url, baseUrl);
|
|
39203
|
+
if (!absoluteUrl) continue;
|
|
39204
|
+
|
|
39205
|
+
try {
|
|
39206
|
+
const buffer = await fetchBuffer(absoluteUrl);
|
|
39207
|
+
const palette = await Vibrant.from(buffer).getPalette();
|
|
39208
|
+
|
|
39209
|
+
const results = [];
|
|
39210
|
+
const swatchNames = [
|
|
39211
|
+
"Vibrant",
|
|
39212
|
+
"DarkVibrant",
|
|
39213
|
+
"LightVibrant",
|
|
39214
|
+
"Muted",
|
|
39215
|
+
"DarkMuted",
|
|
39216
|
+
"LightMuted",
|
|
39217
|
+
];
|
|
39218
|
+
|
|
39219
|
+
for (const name of swatchNames) {
|
|
39220
|
+
const swatch = palette[name];
|
|
39221
|
+
if (!swatch) continue;
|
|
39222
|
+
const [r, g, b] = swatch.rgb;
|
|
39223
|
+
const hex = rgbToHex({
|
|
39224
|
+
r: Math.round(r),
|
|
39225
|
+
g: Math.round(g),
|
|
39226
|
+
b: Math.round(b),
|
|
39227
|
+
});
|
|
39228
|
+
results.push({
|
|
39229
|
+
hex,
|
|
39230
|
+
source: "favicon",
|
|
39231
|
+
confidence: 0.7,
|
|
39232
|
+
});
|
|
39233
|
+
}
|
|
39234
|
+
|
|
39235
|
+
if (results.length > 0) {
|
|
39236
|
+
console.log(
|
|
39237
|
+
`[themeFromUrlController] Favicon vibrant: ${results.length} swatches from ${absoluteUrl}`,
|
|
39238
|
+
);
|
|
39239
|
+
return results;
|
|
39240
|
+
}
|
|
39241
|
+
} catch (err) {
|
|
39242
|
+
console.warn(
|
|
39243
|
+
`[themeFromUrlController] Favicon extraction failed for ${absoluteUrl}: ${err.message}`,
|
|
39244
|
+
);
|
|
39245
|
+
// Try next icon
|
|
39246
|
+
}
|
|
39247
|
+
}
|
|
39248
|
+
|
|
39249
|
+
return [];
|
|
39250
|
+
}
|
|
39251
|
+
|
|
39075
39252
|
// ─── Merge & rank ────────────────────────────────────────────────────────────
|
|
39076
39253
|
|
|
39077
39254
|
/**
|
|
@@ -39198,13 +39375,23 @@ function mergeAndRank(allColors, maxColors = 6) {
|
|
|
39198
39375
|
/**
|
|
39199
39376
|
* Extract a ranked color palette from website content.
|
|
39200
39377
|
*
|
|
39378
|
+
* When `baseUrl` is provided, also extracts colors from favicon/logo images
|
|
39379
|
+
* via node-vibrant (async). Without `baseUrl`, runs synchronously using only
|
|
39380
|
+
* meta tags, CSS vars, and computed styles.
|
|
39381
|
+
*
|
|
39201
39382
|
* @param {Object} params
|
|
39202
39383
|
* @param {string} params.htmlContent - Raw HTML of the page
|
|
39203
39384
|
* @param {string} params.cssContent - Concatenated CSS content
|
|
39204
39385
|
* @param {Object} params.computedStyles - Map of selector → { color, backgroundColor, borderColor }
|
|
39205
|
-
* @
|
|
39206
|
-
|
|
39207
|
-
|
|
39386
|
+
* @param {string} [params.baseUrl] - Page URL for resolving favicon paths (enables image extraction)
|
|
39387
|
+
* @returns {Promise<{ palette: Array, rawCount: number }>}
|
|
39388
|
+
*/
|
|
39389
|
+
async function extractColorsFromUrl({
|
|
39390
|
+
htmlContent,
|
|
39391
|
+
cssContent,
|
|
39392
|
+
computedStyles,
|
|
39393
|
+
baseUrl,
|
|
39394
|
+
}) {
|
|
39208
39395
|
console.log("[themeFromUrlController] Starting color extraction pipeline");
|
|
39209
39396
|
|
|
39210
39397
|
const metaColors = extractMetaColors(htmlContent);
|
|
@@ -39222,7 +39409,27 @@ function extractColorsFromUrl({ htmlContent, cssContent, computedStyles }) {
|
|
|
39222
39409
|
`[themeFromUrlController] Computed styles: ${computedColors.length} colors`,
|
|
39223
39410
|
);
|
|
39224
39411
|
|
|
39225
|
-
|
|
39412
|
+
// Favicon extraction (async, requires baseUrl)
|
|
39413
|
+
let faviconColors = [];
|
|
39414
|
+
if (baseUrl) {
|
|
39415
|
+
try {
|
|
39416
|
+
faviconColors = await extractFaviconColors(htmlContent, baseUrl);
|
|
39417
|
+
console.log(
|
|
39418
|
+
`[themeFromUrlController] Favicon/logo: ${faviconColors.length} colors`,
|
|
39419
|
+
);
|
|
39420
|
+
} catch (err) {
|
|
39421
|
+
console.warn(
|
|
39422
|
+
`[themeFromUrlController] Favicon extraction failed: ${err.message}`,
|
|
39423
|
+
);
|
|
39424
|
+
}
|
|
39425
|
+
}
|
|
39426
|
+
|
|
39427
|
+
const allColors = [
|
|
39428
|
+
...metaColors,
|
|
39429
|
+
...cssVarColors,
|
|
39430
|
+
...computedColors,
|
|
39431
|
+
...faviconColors,
|
|
39432
|
+
];
|
|
39226
39433
|
console.log(`[themeFromUrlController] Total raw colors: ${allColors.length}`);
|
|
39227
39434
|
|
|
39228
39435
|
const palette = mergeAndRank(allColors);
|
|
@@ -39238,6 +39445,8 @@ function extractColorsFromUrl({ htmlContent, cssContent, computedStyles }) {
|
|
|
39238
39445
|
|
|
39239
39446
|
const themeFromUrlController$1 = {
|
|
39240
39447
|
extractColorsFromUrl,
|
|
39448
|
+
extractFaviconUrls,
|
|
39449
|
+
extractFaviconColors,
|
|
39241
39450
|
};
|
|
39242
39451
|
|
|
39243
39452
|
var themeFromUrlController_1 = themeFromUrlController$1;
|
|
@@ -41465,7 +41674,7 @@ let Receiver$1 = class Receiver extends Writable {
|
|
|
41465
41674
|
var receiver = Receiver$1;
|
|
41466
41675
|
|
|
41467
41676
|
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex" }] */
|
|
41468
|
-
const { randomFillSync } = require$$1$
|
|
41677
|
+
const { randomFillSync } = require$$1$7;
|
|
41469
41678
|
|
|
41470
41679
|
const PerMessageDeflate$2 = permessageDeflate;
|
|
41471
41680
|
const { EMPTY_BUFFER: EMPTY_BUFFER$1, kWebSocket: kWebSocket$2, NOOP: NOOP$1 } = constants;
|
|
@@ -42560,10 +42769,10 @@ var extension$1 = { format: format$1, parse: parse$2 };
|
|
|
42560
42769
|
|
|
42561
42770
|
const EventEmitter$1 = require$$0$c;
|
|
42562
42771
|
const https = require$$8;
|
|
42563
|
-
const http$1 = require$$
|
|
42564
|
-
const net = require$$3$
|
|
42772
|
+
const http$1 = require$$3$4;
|
|
42773
|
+
const net = require$$3$5;
|
|
42565
42774
|
const tls = require$$4$2;
|
|
42566
|
-
const { randomBytes, createHash: createHash$1 } = require$$1$
|
|
42775
|
+
const { randomBytes, createHash: createHash$1 } = require$$1$7;
|
|
42567
42776
|
const { URL: URL$1 } = require$$4$1;
|
|
42568
42777
|
|
|
42569
42778
|
const PerMessageDeflate$1 = permessageDeflate;
|
|
@@ -44170,8 +44379,8 @@ var subprotocol$1 = { parse };
|
|
|
44170
44379
|
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Duplex$", "caughtErrors": "none" }] */
|
|
44171
44380
|
|
|
44172
44381
|
const EventEmitter = require$$0$c;
|
|
44173
|
-
const http = require$$
|
|
44174
|
-
const { createHash } = require$$1$
|
|
44382
|
+
const http = require$$3$4;
|
|
44383
|
+
const { createHash } = require$$1$7;
|
|
44175
44384
|
|
|
44176
44385
|
const extension = extension$1;
|
|
44177
44386
|
const PerMessageDeflate = permessageDeflate;
|
|
@@ -44740,6 +44949,10 @@ var ws = WebSocket$1;
|
|
|
44740
44949
|
* - pendingConnects Map for in-flight deduplication
|
|
44741
44950
|
* - Status constants matching MCP pattern
|
|
44742
44951
|
*
|
|
44952
|
+
* Features:
|
|
44953
|
+
* - Auto-reconnect with exponential backoff (1s → 2s → 4s → 8s → 16s, max 30s, 5 retries)
|
|
44954
|
+
* - Heartbeat ping/pong keepalive (30s interval, 10s pong timeout)
|
|
44955
|
+
*
|
|
44743
44956
|
* Uses the `ws` package (installed in dash-electron) for WebSocket clients.
|
|
44744
44957
|
* Multiple widgets referencing the same provider share a single socket.
|
|
44745
44958
|
*/
|
|
@@ -44750,7 +44963,10 @@ const WebSocket = ws;
|
|
|
44750
44963
|
* Active WebSocket connections
|
|
44751
44964
|
* Map<string, { socket: WebSocket, status: string, config: object,
|
|
44752
44965
|
* consumers: Set<number>, messageCount: number,
|
|
44753
|
-
* connectedAt: number|null, lastMessageAt: number|null
|
|
44966
|
+
* connectedAt: number|null, lastMessageAt: number|null,
|
|
44967
|
+
* retryCount: number, retryTimer: NodeJS.Timeout|null,
|
|
44968
|
+
* heartbeatTimer: NodeJS.Timeout|null, pongTimer: NodeJS.Timeout|null,
|
|
44969
|
+
* intentionalClose: boolean }>
|
|
44754
44970
|
*/
|
|
44755
44971
|
const activeConnections = new Map();
|
|
44756
44972
|
|
|
@@ -44772,6 +44988,24 @@ const STATUS = {
|
|
|
44772
44988
|
ERROR: "error",
|
|
44773
44989
|
};
|
|
44774
44990
|
|
|
44991
|
+
/**
|
|
44992
|
+
* Reconnect configuration
|
|
44993
|
+
*/
|
|
44994
|
+
const RECONNECT = {
|
|
44995
|
+
BASE_DELAY: 1000, // 1 second
|
|
44996
|
+
MULTIPLIER: 2,
|
|
44997
|
+
MAX_DELAY: 30000, // 30 seconds
|
|
44998
|
+
MAX_RETRIES: 5,
|
|
44999
|
+
};
|
|
45000
|
+
|
|
45001
|
+
/**
|
|
45002
|
+
* Heartbeat configuration
|
|
45003
|
+
*/
|
|
45004
|
+
const HEARTBEAT = {
|
|
45005
|
+
PING_INTERVAL: 30000, // 30 seconds
|
|
45006
|
+
PONG_TIMEOUT: 10000, // 10 seconds
|
|
45007
|
+
};
|
|
45008
|
+
|
|
44775
45009
|
/**
|
|
44776
45010
|
* Interpolate {{fieldName}} placeholders in a string with credential values.
|
|
44777
45011
|
* Reuses the same pattern as mcpController for URL and header templates.
|
|
@@ -44792,7 +45026,7 @@ function interpolate(template, credentials) {
|
|
|
44792
45026
|
*
|
|
44793
45027
|
* @param {string} providerName - The provider whose status changed
|
|
44794
45028
|
* @param {string} status - New status value
|
|
44795
|
-
* @param {object} extra - Additional fields (error, etc.)
|
|
45029
|
+
* @param {object} extra - Additional fields (error, retryCount, retryIn, etc.)
|
|
44796
45030
|
*/
|
|
44797
45031
|
function broadcastStatusChange(providerName, status, extra = {}) {
|
|
44798
45032
|
const { WS_STATUS_CHANGE } = webSocketEvents$1;
|
|
@@ -44828,6 +45062,320 @@ function broadcastMessage(providerName, data) {
|
|
|
44828
45062
|
}
|
|
44829
45063
|
}
|
|
44830
45064
|
|
|
45065
|
+
/**
|
|
45066
|
+
* Calculate the backoff delay for a given retry attempt.
|
|
45067
|
+
*
|
|
45068
|
+
* @param {number} retryCount - Current retry attempt (0-based)
|
|
45069
|
+
* @returns {number} Delay in milliseconds
|
|
45070
|
+
*/
|
|
45071
|
+
function getBackoffDelay(retryCount) {
|
|
45072
|
+
const delay =
|
|
45073
|
+
RECONNECT.BASE_DELAY * Math.pow(RECONNECT.MULTIPLIER, retryCount);
|
|
45074
|
+
return Math.min(delay, RECONNECT.MAX_DELAY);
|
|
45075
|
+
}
|
|
45076
|
+
|
|
45077
|
+
/**
|
|
45078
|
+
* Clear heartbeat and pong timers for a connection.
|
|
45079
|
+
*
|
|
45080
|
+
* @param {object} conn - The connection object from activeConnections
|
|
45081
|
+
*/
|
|
45082
|
+
function clearHeartbeatTimers(conn) {
|
|
45083
|
+
if (conn.heartbeatTimer) {
|
|
45084
|
+
clearInterval(conn.heartbeatTimer);
|
|
45085
|
+
conn.heartbeatTimer = null;
|
|
45086
|
+
}
|
|
45087
|
+
if (conn.pongTimer) {
|
|
45088
|
+
clearTimeout(conn.pongTimer);
|
|
45089
|
+
conn.pongTimer = null;
|
|
45090
|
+
}
|
|
45091
|
+
}
|
|
45092
|
+
|
|
45093
|
+
/**
|
|
45094
|
+
* Clear the reconnect timer for a connection.
|
|
45095
|
+
*
|
|
45096
|
+
* @param {object} conn - The connection object from activeConnections
|
|
45097
|
+
*/
|
|
45098
|
+
function clearReconnectTimer(conn) {
|
|
45099
|
+
if (conn.retryTimer) {
|
|
45100
|
+
clearTimeout(conn.retryTimer);
|
|
45101
|
+
conn.retryTimer = null;
|
|
45102
|
+
}
|
|
45103
|
+
}
|
|
45104
|
+
|
|
45105
|
+
/**
|
|
45106
|
+
* Start heartbeat ping/pong for an active connection.
|
|
45107
|
+
* Sends a WebSocket ping frame every HEARTBEAT.PING_INTERVAL ms.
|
|
45108
|
+
* If no pong is received within HEARTBEAT.PONG_TIMEOUT ms, the connection
|
|
45109
|
+
* is considered stale and a reconnect is triggered.
|
|
45110
|
+
*
|
|
45111
|
+
* @param {string} providerName - The provider name
|
|
45112
|
+
*/
|
|
45113
|
+
function startHeartbeat(providerName) {
|
|
45114
|
+
const conn = activeConnections.get(providerName);
|
|
45115
|
+
if (!conn || !conn.socket) return;
|
|
45116
|
+
|
|
45117
|
+
// Clear any existing heartbeat timers
|
|
45118
|
+
clearHeartbeatTimers(conn);
|
|
45119
|
+
|
|
45120
|
+
conn.heartbeatTimer = setInterval(() => {
|
|
45121
|
+
const current = activeConnections.get(providerName);
|
|
45122
|
+
if (
|
|
45123
|
+
!current ||
|
|
45124
|
+
!current.socket ||
|
|
45125
|
+
current.socket.readyState !== WebSocket.OPEN
|
|
45126
|
+
) {
|
|
45127
|
+
clearHeartbeatTimers(current || conn);
|
|
45128
|
+
return;
|
|
45129
|
+
}
|
|
45130
|
+
|
|
45131
|
+
// Send ping
|
|
45132
|
+
try {
|
|
45133
|
+
current.socket.ping();
|
|
45134
|
+
} catch {
|
|
45135
|
+
// Socket errored during ping — will be caught by error/close handlers
|
|
45136
|
+
return;
|
|
45137
|
+
}
|
|
45138
|
+
|
|
45139
|
+
// Start pong timeout
|
|
45140
|
+
current.pongTimer = setTimeout(() => {
|
|
45141
|
+
const staleConn = activeConnections.get(providerName);
|
|
45142
|
+
if (!staleConn || staleConn.intentionalClose) return;
|
|
45143
|
+
|
|
45144
|
+
console.log(
|
|
45145
|
+
`[webSocketController] Heartbeat timeout (no pong): ${providerName}`,
|
|
45146
|
+
);
|
|
45147
|
+
|
|
45148
|
+
// Clear heartbeat before triggering reconnect
|
|
45149
|
+
clearHeartbeatTimers(staleConn);
|
|
45150
|
+
|
|
45151
|
+
// Close the stale socket to trigger the close handler → reconnect
|
|
45152
|
+
if (staleConn.socket) {
|
|
45153
|
+
try {
|
|
45154
|
+
staleConn.socket.terminate();
|
|
45155
|
+
} catch {
|
|
45156
|
+
/* already closing */
|
|
45157
|
+
}
|
|
45158
|
+
}
|
|
45159
|
+
}, HEARTBEAT.PONG_TIMEOUT);
|
|
45160
|
+
}, HEARTBEAT.PING_INTERVAL);
|
|
45161
|
+
}
|
|
45162
|
+
|
|
45163
|
+
/**
|
|
45164
|
+
* Attempt to reconnect a provider with exponential backoff.
|
|
45165
|
+
* Called from the socket close handler when the close was unexpected.
|
|
45166
|
+
*
|
|
45167
|
+
* @param {string} providerName - The provider to reconnect
|
|
45168
|
+
*/
|
|
45169
|
+
function scheduleReconnect(providerName) {
|
|
45170
|
+
const conn = activeConnections.get(providerName);
|
|
45171
|
+
if (!conn) return;
|
|
45172
|
+
|
|
45173
|
+
if (conn.retryCount >= RECONNECT.MAX_RETRIES) {
|
|
45174
|
+
console.log(
|
|
45175
|
+
`[webSocketController] Max retries (${RECONNECT.MAX_RETRIES}) reached for ${providerName}`,
|
|
45176
|
+
);
|
|
45177
|
+
activeConnections.set(providerName, {
|
|
45178
|
+
...conn,
|
|
45179
|
+
socket: null,
|
|
45180
|
+
status: STATUS.ERROR,
|
|
45181
|
+
error: `Reconnect failed after ${RECONNECT.MAX_RETRIES} attempts`,
|
|
45182
|
+
});
|
|
45183
|
+
broadcastStatusChange(providerName, STATUS.ERROR, {
|
|
45184
|
+
error: `Reconnect failed after ${RECONNECT.MAX_RETRIES} attempts`,
|
|
45185
|
+
});
|
|
45186
|
+
return;
|
|
45187
|
+
}
|
|
45188
|
+
|
|
45189
|
+
const delay = getBackoffDelay(conn.retryCount);
|
|
45190
|
+
console.log(
|
|
45191
|
+
`[webSocketController] Reconnecting ${providerName} in ${delay}ms (attempt ${conn.retryCount + 1}/${RECONNECT.MAX_RETRIES})`,
|
|
45192
|
+
);
|
|
45193
|
+
|
|
45194
|
+
// Broadcast connecting status with retry info
|
|
45195
|
+
broadcastStatusChange(providerName, STATUS.CONNECTING, {
|
|
45196
|
+
retryCount: conn.retryCount + 1,
|
|
45197
|
+
retryIn: delay,
|
|
45198
|
+
});
|
|
45199
|
+
|
|
45200
|
+
conn.retryTimer = setTimeout(async () => {
|
|
45201
|
+
const current = activeConnections.get(providerName);
|
|
45202
|
+
if (!current || current.intentionalClose) return;
|
|
45203
|
+
|
|
45204
|
+
// Update retry count before attempting
|
|
45205
|
+
current.retryCount++;
|
|
45206
|
+
|
|
45207
|
+
try {
|
|
45208
|
+
// Use the stored config to reconnect
|
|
45209
|
+
const config = current.config;
|
|
45210
|
+
const url = config.credentials
|
|
45211
|
+
? interpolate(config.url, config.credentials)
|
|
45212
|
+
: config.url;
|
|
45213
|
+
|
|
45214
|
+
const wsOptions = {};
|
|
45215
|
+
if (config.headers) {
|
|
45216
|
+
const headers = {};
|
|
45217
|
+
if (config.credentials) {
|
|
45218
|
+
Object.entries(config.headers).forEach(([headerName, template]) => {
|
|
45219
|
+
headers[headerName] = interpolate(template, config.credentials);
|
|
45220
|
+
});
|
|
45221
|
+
} else {
|
|
45222
|
+
Object.assign(headers, config.headers);
|
|
45223
|
+
}
|
|
45224
|
+
wsOptions.headers = headers;
|
|
45225
|
+
}
|
|
45226
|
+
|
|
45227
|
+
console.log(
|
|
45228
|
+
`[webSocketController] Reconnect attempt ${current.retryCount}/${RECONNECT.MAX_RETRIES}: ${providerName}`,
|
|
45229
|
+
);
|
|
45230
|
+
|
|
45231
|
+
const socket = new WebSocket(url, config.subprotocols || [], wsOptions);
|
|
45232
|
+
|
|
45233
|
+
// Wait for open or error
|
|
45234
|
+
await new Promise((resolve, reject) => {
|
|
45235
|
+
const onOpen = () => {
|
|
45236
|
+
socket.removeListener("error", onError);
|
|
45237
|
+
resolve();
|
|
45238
|
+
};
|
|
45239
|
+
const onError = (err) => {
|
|
45240
|
+
socket.removeListener("open", onOpen);
|
|
45241
|
+
reject(err);
|
|
45242
|
+
};
|
|
45243
|
+
socket.once("open", onOpen);
|
|
45244
|
+
socket.once("error", onError);
|
|
45245
|
+
});
|
|
45246
|
+
|
|
45247
|
+
// Reconnect succeeded — reset retry count, update connection
|
|
45248
|
+
const reconnected = activeConnections.get(providerName);
|
|
45249
|
+
if (!reconnected || reconnected.intentionalClose) {
|
|
45250
|
+
// Was disconnected intentionally during reconnect
|
|
45251
|
+
socket.close(1000, "Client disconnect");
|
|
45252
|
+
return;
|
|
45253
|
+
}
|
|
45254
|
+
|
|
45255
|
+
activeConnections.set(providerName, {
|
|
45256
|
+
...reconnected,
|
|
45257
|
+
socket,
|
|
45258
|
+
status: STATUS.CONNECTED,
|
|
45259
|
+
connectedAt: Date.now(),
|
|
45260
|
+
retryCount: 0,
|
|
45261
|
+
retryTimer: null,
|
|
45262
|
+
error: undefined,
|
|
45263
|
+
});
|
|
45264
|
+
|
|
45265
|
+
// Wire up handlers on the new socket
|
|
45266
|
+
wireSocketHandlers(providerName, socket);
|
|
45267
|
+
|
|
45268
|
+
// Start heartbeat on the new socket
|
|
45269
|
+
startHeartbeat(providerName);
|
|
45270
|
+
|
|
45271
|
+
broadcastStatusChange(providerName, STATUS.CONNECTED);
|
|
45272
|
+
|
|
45273
|
+
console.log(
|
|
45274
|
+
`[webSocketController] Reconnected: ${providerName} (after ${current.retryCount} attempt(s))`,
|
|
45275
|
+
);
|
|
45276
|
+
} catch (err) {
|
|
45277
|
+
console.error(
|
|
45278
|
+
`[webSocketController] Reconnect attempt ${current.retryCount} failed for ${providerName}:`,
|
|
45279
|
+
err.message,
|
|
45280
|
+
);
|
|
45281
|
+
|
|
45282
|
+
// Schedule next retry
|
|
45283
|
+
scheduleReconnect(providerName);
|
|
45284
|
+
}
|
|
45285
|
+
}, delay);
|
|
45286
|
+
}
|
|
45287
|
+
|
|
45288
|
+
/**
|
|
45289
|
+
* Wire up message, close, error, and pong handlers on a WebSocket instance.
|
|
45290
|
+
* Extracted so both initial connect and reconnect use the same handlers.
|
|
45291
|
+
*
|
|
45292
|
+
* @param {string} providerName - The provider name
|
|
45293
|
+
* @param {WebSocket} socket - The WebSocket instance
|
|
45294
|
+
*/
|
|
45295
|
+
function wireSocketHandlers(providerName, socket) {
|
|
45296
|
+
// Message handler
|
|
45297
|
+
socket.on("message", (data) => {
|
|
45298
|
+
const conn = activeConnections.get(providerName);
|
|
45299
|
+
if (conn) {
|
|
45300
|
+
conn.messageCount++;
|
|
45301
|
+
conn.lastMessageAt = Date.now();
|
|
45302
|
+
}
|
|
45303
|
+
|
|
45304
|
+
let parsed;
|
|
45305
|
+
try {
|
|
45306
|
+
parsed = JSON.parse(data.toString());
|
|
45307
|
+
} catch {
|
|
45308
|
+
parsed = data.toString();
|
|
45309
|
+
}
|
|
45310
|
+
|
|
45311
|
+
broadcastMessage(providerName, parsed);
|
|
45312
|
+
});
|
|
45313
|
+
|
|
45314
|
+
// Close handler — triggers auto-reconnect for unexpected closes
|
|
45315
|
+
socket.on("close", (code, reason) => {
|
|
45316
|
+
console.log(
|
|
45317
|
+
`[webSocketController] Connection closed: ${providerName} (code: ${code})`,
|
|
45318
|
+
);
|
|
45319
|
+
const conn = activeConnections.get(providerName);
|
|
45320
|
+
if (!conn || conn.socket !== socket) return;
|
|
45321
|
+
|
|
45322
|
+
// Clean up heartbeat timers
|
|
45323
|
+
clearHeartbeatTimers(conn);
|
|
45324
|
+
|
|
45325
|
+
// Update status
|
|
45326
|
+
activeConnections.set(providerName, {
|
|
45327
|
+
...conn,
|
|
45328
|
+
socket: null,
|
|
45329
|
+
status: STATUS.DISCONNECTED,
|
|
45330
|
+
});
|
|
45331
|
+
|
|
45332
|
+
// Normal close (code 1000) or intentional disconnect — don't reconnect
|
|
45333
|
+
if (conn.intentionalClose || code === 1000) {
|
|
45334
|
+
broadcastStatusChange(providerName, STATUS.DISCONNECTED, {
|
|
45335
|
+
code,
|
|
45336
|
+
reason: reason?.toString(),
|
|
45337
|
+
});
|
|
45338
|
+
return;
|
|
45339
|
+
}
|
|
45340
|
+
|
|
45341
|
+
// Unexpected close — attempt auto-reconnect
|
|
45342
|
+
broadcastStatusChange(providerName, STATUS.DISCONNECTED, {
|
|
45343
|
+
code,
|
|
45344
|
+
reason: reason?.toString(),
|
|
45345
|
+
reconnecting: true,
|
|
45346
|
+
});
|
|
45347
|
+
scheduleReconnect(providerName);
|
|
45348
|
+
});
|
|
45349
|
+
|
|
45350
|
+
// Error handler
|
|
45351
|
+
socket.on("error", (err) => {
|
|
45352
|
+
console.error(
|
|
45353
|
+
`[webSocketController] Socket error for ${providerName}:`,
|
|
45354
|
+
err.message,
|
|
45355
|
+
);
|
|
45356
|
+
const conn = activeConnections.get(providerName);
|
|
45357
|
+
if (conn && conn.socket === socket) {
|
|
45358
|
+
activeConnections.set(providerName, {
|
|
45359
|
+
...conn,
|
|
45360
|
+
status: STATUS.ERROR,
|
|
45361
|
+
error: err.message,
|
|
45362
|
+
});
|
|
45363
|
+
broadcastStatusChange(providerName, STATUS.ERROR, {
|
|
45364
|
+
error: err.message,
|
|
45365
|
+
});
|
|
45366
|
+
}
|
|
45367
|
+
});
|
|
45368
|
+
|
|
45369
|
+
// Pong handler — clear the pong timeout when we receive a pong
|
|
45370
|
+
socket.on("pong", () => {
|
|
45371
|
+
const conn = activeConnections.get(providerName);
|
|
45372
|
+
if (conn && conn.pongTimer) {
|
|
45373
|
+
clearTimeout(conn.pongTimer);
|
|
45374
|
+
conn.pongTimer = null;
|
|
45375
|
+
}
|
|
45376
|
+
});
|
|
45377
|
+
}
|
|
45378
|
+
|
|
44831
45379
|
const webSocketController$1 = {
|
|
44832
45380
|
/**
|
|
44833
45381
|
* connect
|
|
@@ -44867,8 +45415,11 @@ const webSocketController$1 = {
|
|
|
44867
45415
|
// 3. Fresh connect — wrap in a promise and track it
|
|
44868
45416
|
const connectPromise = (async () => {
|
|
44869
45417
|
try {
|
|
44870
|
-
// Clean up stale/error state
|
|
45418
|
+
// Clean up stale/error state (including any pending reconnect timers)
|
|
44871
45419
|
if (activeConnections.has(providerName)) {
|
|
45420
|
+
const stale = activeConnections.get(providerName);
|
|
45421
|
+
clearReconnectTimer(stale);
|
|
45422
|
+
clearHeartbeatTimers(stale);
|
|
44872
45423
|
await webSocketController$1.disconnect(win, providerName);
|
|
44873
45424
|
}
|
|
44874
45425
|
|
|
@@ -44911,6 +45462,11 @@ const webSocketController$1 = {
|
|
|
44911
45462
|
messageCount: 0,
|
|
44912
45463
|
connectedAt: null,
|
|
44913
45464
|
lastMessageAt: null,
|
|
45465
|
+
retryCount: 0,
|
|
45466
|
+
retryTimer: null,
|
|
45467
|
+
heartbeatTimer: null,
|
|
45468
|
+
pongTimer: null,
|
|
45469
|
+
intentionalClose: false,
|
|
44914
45470
|
});
|
|
44915
45471
|
broadcastStatusChange(providerName, STATUS.CONNECTING);
|
|
44916
45472
|
|
|
@@ -44943,64 +45499,18 @@ const webSocketController$1 = {
|
|
|
44943
45499
|
messageCount: 0,
|
|
44944
45500
|
connectedAt: Date.now(),
|
|
44945
45501
|
lastMessageAt: null,
|
|
45502
|
+
retryCount: 0,
|
|
45503
|
+
retryTimer: null,
|
|
45504
|
+
heartbeatTimer: null,
|
|
45505
|
+
pongTimer: null,
|
|
45506
|
+
intentionalClose: false,
|
|
44946
45507
|
});
|
|
44947
45508
|
|
|
44948
|
-
// Wire up
|
|
44949
|
-
|
|
44950
|
-
const conn = activeConnections.get(providerName);
|
|
44951
|
-
if (conn) {
|
|
44952
|
-
conn.messageCount++;
|
|
44953
|
-
conn.lastMessageAt = Date.now();
|
|
44954
|
-
}
|
|
44955
|
-
|
|
44956
|
-
// Parse if JSON, otherwise pass as string
|
|
44957
|
-
let parsed;
|
|
44958
|
-
try {
|
|
44959
|
-
parsed = JSON.parse(data.toString());
|
|
44960
|
-
} catch {
|
|
44961
|
-
parsed = data.toString();
|
|
44962
|
-
}
|
|
45509
|
+
// Wire up socket event handlers
|
|
45510
|
+
wireSocketHandlers(providerName, socket);
|
|
44963
45511
|
|
|
44964
|
-
|
|
44965
|
-
|
|
44966
|
-
|
|
44967
|
-
// Wire up close handler
|
|
44968
|
-
socket.on("close", (code, reason) => {
|
|
44969
|
-
console.log(
|
|
44970
|
-
`[webSocketController] Connection closed: ${providerName} (code: ${code})`,
|
|
44971
|
-
);
|
|
44972
|
-
const conn = activeConnections.get(providerName);
|
|
44973
|
-
if (conn && conn.socket === socket) {
|
|
44974
|
-
activeConnections.set(providerName, {
|
|
44975
|
-
...conn,
|
|
44976
|
-
socket: null,
|
|
44977
|
-
status: STATUS.DISCONNECTED,
|
|
44978
|
-
});
|
|
44979
|
-
broadcastStatusChange(providerName, STATUS.DISCONNECTED, {
|
|
44980
|
-
code,
|
|
44981
|
-
reason: reason?.toString(),
|
|
44982
|
-
});
|
|
44983
|
-
}
|
|
44984
|
-
});
|
|
44985
|
-
|
|
44986
|
-
// Wire up error handler
|
|
44987
|
-
socket.on("error", (err) => {
|
|
44988
|
-
console.error(
|
|
44989
|
-
`[webSocketController] Socket error for ${providerName}:`,
|
|
44990
|
-
err.message,
|
|
44991
|
-
);
|
|
44992
|
-
const conn = activeConnections.get(providerName);
|
|
44993
|
-
if (conn && conn.socket === socket) {
|
|
44994
|
-
activeConnections.set(providerName, {
|
|
44995
|
-
...conn,
|
|
44996
|
-
status: STATUS.ERROR,
|
|
44997
|
-
error: err.message,
|
|
44998
|
-
});
|
|
44999
|
-
broadcastStatusChange(providerName, STATUS.ERROR, {
|
|
45000
|
-
error: err.message,
|
|
45001
|
-
});
|
|
45002
|
-
}
|
|
45003
|
-
});
|
|
45512
|
+
// Start heartbeat
|
|
45513
|
+
startHeartbeat(providerName);
|
|
45004
45514
|
|
|
45005
45515
|
broadcastStatusChange(providerName, STATUS.CONNECTED);
|
|
45006
45516
|
|
|
@@ -45026,6 +45536,11 @@ const webSocketController$1 = {
|
|
|
45026
45536
|
messageCount: 0,
|
|
45027
45537
|
connectedAt: null,
|
|
45028
45538
|
lastMessageAt: null,
|
|
45539
|
+
retryCount: 0,
|
|
45540
|
+
retryTimer: null,
|
|
45541
|
+
heartbeatTimer: null,
|
|
45542
|
+
pongTimer: null,
|
|
45543
|
+
intentionalClose: false,
|
|
45029
45544
|
error: error.message,
|
|
45030
45545
|
});
|
|
45031
45546
|
broadcastStatusChange(providerName, STATUS.ERROR, {
|
|
@@ -45050,6 +45565,7 @@ const webSocketController$1 = {
|
|
|
45050
45565
|
/**
|
|
45051
45566
|
* disconnect
|
|
45052
45567
|
* Close a WebSocket connection and clean up.
|
|
45568
|
+
* Marks the close as intentional to suppress auto-reconnect.
|
|
45053
45569
|
*
|
|
45054
45570
|
* @param {BrowserWindow} win - the requesting window
|
|
45055
45571
|
* @param {string} providerName - the provider to disconnect
|
|
@@ -45077,6 +45593,13 @@ const webSocketController$1 = {
|
|
|
45077
45593
|
|
|
45078
45594
|
console.log(`[webSocketController] Disconnecting: ${providerName}`);
|
|
45079
45595
|
|
|
45596
|
+
// Mark as intentional so the close handler doesn't auto-reconnect
|
|
45597
|
+
conn.intentionalClose = true;
|
|
45598
|
+
|
|
45599
|
+
// Clear all timers
|
|
45600
|
+
clearHeartbeatTimers(conn);
|
|
45601
|
+
clearReconnectTimer(conn);
|
|
45602
|
+
|
|
45080
45603
|
// Close the socket
|
|
45081
45604
|
if (conn.socket) {
|
|
45082
45605
|
try {
|
|
@@ -45104,6 +45627,11 @@ const webSocketController$1 = {
|
|
|
45104
45627
|
error,
|
|
45105
45628
|
);
|
|
45106
45629
|
// Clean up anyway
|
|
45630
|
+
const conn = activeConnections.get(providerName);
|
|
45631
|
+
if (conn) {
|
|
45632
|
+
clearHeartbeatTimers(conn);
|
|
45633
|
+
clearReconnectTimer(conn);
|
|
45634
|
+
}
|
|
45107
45635
|
activeConnections.delete(providerName);
|
|
45108
45636
|
return {
|
|
45109
45637
|
error: true,
|
|
@@ -45158,7 +45686,7 @@ const webSocketController$1 = {
|
|
|
45158
45686
|
*
|
|
45159
45687
|
* @param {BrowserWindow} win - the requesting window
|
|
45160
45688
|
* @param {string} providerName - the provider name
|
|
45161
|
-
* @returns {{ providerName, status, messageCount, connectedAt, lastMessageAt, error }}
|
|
45689
|
+
* @returns {{ providerName, status, messageCount, connectedAt, lastMessageAt, error, retryCount }}
|
|
45162
45690
|
*/
|
|
45163
45691
|
getStatus: (win, providerName) => {
|
|
45164
45692
|
const conn = activeConnections.get(providerName);
|
|
@@ -45179,6 +45707,7 @@ const webSocketController$1 = {
|
|
|
45179
45707
|
connectedAt: conn.connectedAt || null,
|
|
45180
45708
|
lastMessageAt: conn.lastMessageAt || null,
|
|
45181
45709
|
error: conn.error || null,
|
|
45710
|
+
retryCount: conn.retryCount || 0,
|
|
45182
45711
|
};
|
|
45183
45712
|
},
|
|
45184
45713
|
|
|
@@ -45187,7 +45716,7 @@ const webSocketController$1 = {
|
|
|
45187
45716
|
* Returns all active connections with their status.
|
|
45188
45717
|
*
|
|
45189
45718
|
* @param {BrowserWindow} win - the requesting window
|
|
45190
|
-
* @returns {{ connections: Array<{ providerName, status, messageCount, connectedAt, lastMessageAt }> }}
|
|
45719
|
+
* @returns {{ connections: Array<{ providerName, status, messageCount, connectedAt, lastMessageAt, retryCount }> }}
|
|
45191
45720
|
*/
|
|
45192
45721
|
getAll: (win) => {
|
|
45193
45722
|
const connections = [];
|
|
@@ -45199,6 +45728,7 @@ const webSocketController$1 = {
|
|
|
45199
45728
|
connectedAt: conn.connectedAt || null,
|
|
45200
45729
|
lastMessageAt: conn.lastMessageAt || null,
|
|
45201
45730
|
error: conn.error || null,
|
|
45731
|
+
retryCount: conn.retryCount || 0,
|
|
45202
45732
|
});
|
|
45203
45733
|
}
|
|
45204
45734
|
return { success: true, connections };
|