@swimmingliu/autovpn 1.6.8 → 1.7.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 +5 -0
- package/dist/backend/node-backend.js +7 -22
- package/dist/jobs/commands.js +38 -7
- package/dist/jobs/read.js +9 -22
- package/dist/pipeline/availability.js +38 -36
- package/dist/pipeline/network-retry.js +54 -0
- package/dist/pipeline/orchestrator.js +989 -730
- package/dist/pipeline/proxy-runtime.js +130 -28
- package/dist/pipeline/run-store.js +602 -0
- package/dist/pipeline/speedtest.js +72 -75
- package/dist/pipeline/streaming-coordinator.js +154 -0
- package/dist/web/renderer/app.js +45 -6
- package/dist/web/renderer/i18n.js +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import net from 'node:net';
|
|
2
2
|
import tls from 'node:tls';
|
|
3
3
|
import { openMihomoRuntime as defaultOpenMihomoRuntime, probeMihomoProxyDelay as defaultProbeMihomoProxyDelay } from './proxy-runtime.js';
|
|
4
|
+
import { retryTransientNetwork } from './network-retry.js';
|
|
4
5
|
const PROBE_MAX_ATTEMPTS = 2;
|
|
5
6
|
export function aggregateSpeedMeasurements(values) {
|
|
6
7
|
if (values.length === 0) {
|
|
@@ -66,7 +67,11 @@ async function mapWithConcurrency(items, concurrency, worker, onComplete) {
|
|
|
66
67
|
onComplete?.(result, index, completed);
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
|
-
await Promise.
|
|
70
|
+
const workers = await Promise.allSettled(Array.from({ length: Math.min(limit, items.length) }, () => runWorker()));
|
|
71
|
+
const failed = workers.find((result) => result.status === 'rejected');
|
|
72
|
+
if (failed) {
|
|
73
|
+
throw failed.reason;
|
|
74
|
+
}
|
|
70
75
|
return results;
|
|
71
76
|
}
|
|
72
77
|
function defaultNow() {
|
|
@@ -156,35 +161,8 @@ function requireOkResponse(response, allowedStatuses) {
|
|
|
156
161
|
throw new Error(`unexpected status ${status}`);
|
|
157
162
|
}
|
|
158
163
|
}
|
|
159
|
-
function isTransientProbeError(error) {
|
|
160
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
161
|
-
const code = typeof error === 'object' && error !== null && 'code' in error
|
|
162
|
-
? String(error.code ?? '')
|
|
163
|
-
: '';
|
|
164
|
-
const statusMatch = /(?:unexpected status|failed with status)\s+(\d{3})\b/i.exec(message);
|
|
165
|
-
if (statusMatch) {
|
|
166
|
-
const status = Number(statusMatch[1]);
|
|
167
|
-
return status >= 500 && status <= 599;
|
|
168
|
-
}
|
|
169
|
-
if (['AUTOVPN_INTERNAL_TIMEOUT', 'ECONNRESET', 'ETIMEDOUT', 'EPIPE', 'ECONNREFUSED', 'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT'].includes(code.toUpperCase())) {
|
|
170
|
-
return true;
|
|
171
|
-
}
|
|
172
|
-
return error instanceof TypeError && message.trim().toLowerCase() === 'fetch failed';
|
|
173
|
-
}
|
|
174
164
|
async function retryTransientProbe(operation) {
|
|
175
|
-
|
|
176
|
-
for (let attempt = 1; attempt <= PROBE_MAX_ATTEMPTS; attempt += 1) {
|
|
177
|
-
try {
|
|
178
|
-
return await operation();
|
|
179
|
-
}
|
|
180
|
-
catch (error) {
|
|
181
|
-
lastError = error;
|
|
182
|
-
if (attempt >= PROBE_MAX_ATTEMPTS || !isTransientProbeError(error)) {
|
|
183
|
-
throw error;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
throw lastError;
|
|
165
|
+
return retryTransientNetwork(operation, { maxAttempts: PROBE_MAX_ATTEMPTS, delayMs: 0 });
|
|
188
166
|
}
|
|
189
167
|
function socketConnect(host, port, timeoutMs) {
|
|
190
168
|
return new Promise((resolve, reject) => {
|
|
@@ -355,8 +333,14 @@ export async function downloadUrlViaHttpProxy(url, proxyUrl, maxBytes, timeoutSe
|
|
|
355
333
|
rejectUnauthorized: false
|
|
356
334
|
});
|
|
357
335
|
await new Promise((resolve, reject) => {
|
|
358
|
-
|
|
359
|
-
|
|
336
|
+
const timer = setTimeout(() => {
|
|
337
|
+
secureSocket.destroy();
|
|
338
|
+
const error = new Error(`TLS handshake timed out after ${timeoutMs}ms`);
|
|
339
|
+
error.code = 'AUTOVPN_INTERNAL_TIMEOUT';
|
|
340
|
+
reject(error);
|
|
341
|
+
}, timeoutMs);
|
|
342
|
+
secureSocket.once('secureConnect', () => { clearTimeout(timer); resolve(); });
|
|
343
|
+
secureSocket.once('error', (error) => { clearTimeout(timer); reject(error); });
|
|
360
344
|
});
|
|
361
345
|
secureSocket.write([
|
|
362
346
|
`GET ${requestPath} HTTP/1.1`,
|
|
@@ -367,7 +351,7 @@ export async function downloadUrlViaHttpProxy(url, proxyUrl, maxBytes, timeoutSe
|
|
|
367
351
|
].join('\r\n'));
|
|
368
352
|
return await readHttpBodyBytes(secureSocket, Math.max(1, maxBytes), timeoutMs);
|
|
369
353
|
}
|
|
370
|
-
async function probeLinksDirect(links, config, options) {
|
|
354
|
+
async function probeLinksDirect(links, config, options, onComplete) {
|
|
371
355
|
const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
372
356
|
if (!fetchImpl) {
|
|
373
357
|
throw new Error('Node speedtest backend requires fetch support');
|
|
@@ -387,9 +371,9 @@ async function probeLinksDirect(links, config, options) {
|
|
|
387
371
|
catch (error) {
|
|
388
372
|
return { link, reachable: false, latency_ms: 0, error: error instanceof Error ? error.message : String(error) };
|
|
389
373
|
}
|
|
390
|
-
});
|
|
374
|
+
}, (result, _index, completed) => onComplete?.(result, completed));
|
|
391
375
|
}
|
|
392
|
-
async function probeLinksMihomo(links, config, runtimePath, options) {
|
|
376
|
+
async function probeLinksMihomo(links, config, runtimePath, options, onComplete) {
|
|
393
377
|
const openRuntime = options.openMihomoRuntime ?? defaultOpenMihomoRuntime;
|
|
394
378
|
const probeDelay = options.probeMihomoProxyDelay ?? defaultProbeMihomoProxyDelay;
|
|
395
379
|
return mapWithConcurrency(links, config.concurrency, async (link) => {
|
|
@@ -413,7 +397,7 @@ async function probeLinksMihomo(links, config, runtimePath, options) {
|
|
|
413
397
|
catch (error) {
|
|
414
398
|
return { link, reachable: false, latency_ms: 0, error: error instanceof Error ? error.message : String(error) };
|
|
415
399
|
}
|
|
416
|
-
});
|
|
400
|
+
}, (result, _index, completed) => onComplete?.(result, completed));
|
|
417
401
|
}
|
|
418
402
|
async function testLinkDirect(link, config, options) {
|
|
419
403
|
const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
|
|
@@ -457,43 +441,44 @@ async function testLinkDirect(link, config, options) {
|
|
|
457
441
|
async function testLinkMihomo(link, config, runtimePath, options) {
|
|
458
442
|
const openRuntime = options.openMihomoRuntime ?? defaultOpenMihomoRuntime;
|
|
459
443
|
const downloadViaProxy = options.downloadUrlViaHttpProxy ?? downloadUrlViaHttpProxy;
|
|
460
|
-
let runtime;
|
|
461
444
|
try {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
startupWaitSeconds: config.startup_wait_seconds,
|
|
465
|
-
env: options.env
|
|
466
|
-
});
|
|
467
|
-
const now = options.now ?? defaultNow;
|
|
468
|
-
const speedValues = [];
|
|
469
|
-
const failures = [];
|
|
470
|
-
for (const url of config.urls) {
|
|
471
|
-
const started = now();
|
|
445
|
+
return await retryTransientNetwork(async () => {
|
|
446
|
+
let runtime;
|
|
472
447
|
try {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
448
|
+
runtime = await openRuntime(link, {
|
|
449
|
+
runtimePath,
|
|
450
|
+
startupWaitSeconds: config.startup_wait_seconds,
|
|
451
|
+
env: options.env
|
|
452
|
+
});
|
|
453
|
+
const now = options.now ?? defaultNow;
|
|
454
|
+
const speedValues = [];
|
|
455
|
+
const failures = [];
|
|
456
|
+
let lastFailure;
|
|
457
|
+
for (const url of config.urls) {
|
|
458
|
+
try {
|
|
459
|
+
const measurement = await retryTransientNetwork(async () => {
|
|
460
|
+
const started = now();
|
|
461
|
+
const total = await downloadViaProxy(url, runtime.proxies.http, Math.max(1, Number(config.max_download_bytes)), config.timeout_seconds);
|
|
462
|
+
return { total, elapsedSeconds: Math.max((now() - started) / 1000, 0.001) };
|
|
463
|
+
});
|
|
464
|
+
speedValues.push(measurement.total / measurement.elapsedSeconds / 1024 / 1024);
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
lastFailure = error;
|
|
468
|
+
failures.push(`${url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (speedValues.length === 0) {
|
|
472
|
+
if (lastFailure)
|
|
473
|
+
throw lastFailure;
|
|
474
|
+
return { link, reachable: false, average_download_mb_s: 0, latency_ms: 0, error: failures.join('; ') || 'all speed test urls failed' };
|
|
475
|
+
}
|
|
476
|
+
return { link, reachable: true, average_download_mb_s: aggregateSpeedMeasurements(speedValues), latency_ms: 0, error: failures.join('; ') };
|
|
476
477
|
}
|
|
477
|
-
|
|
478
|
-
|
|
478
|
+
finally {
|
|
479
|
+
await runtime?.close();
|
|
479
480
|
}
|
|
480
|
-
}
|
|
481
|
-
if (speedValues.length === 0) {
|
|
482
|
-
return {
|
|
483
|
-
link,
|
|
484
|
-
reachable: false,
|
|
485
|
-
average_download_mb_s: 0,
|
|
486
|
-
latency_ms: 0,
|
|
487
|
-
error: failures.join('; ') || 'all speed test urls failed'
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
return {
|
|
491
|
-
link,
|
|
492
|
-
reachable: true,
|
|
493
|
-
average_download_mb_s: aggregateSpeedMeasurements(speedValues),
|
|
494
|
-
latency_ms: 0,
|
|
495
|
-
error: failures.join('; ')
|
|
496
|
-
};
|
|
481
|
+
});
|
|
497
482
|
}
|
|
498
483
|
catch (error) {
|
|
499
484
|
return {
|
|
@@ -504,9 +489,6 @@ async function testLinkMihomo(link, config, runtimePath, options) {
|
|
|
504
489
|
error: error instanceof Error ? error.message : String(error)
|
|
505
490
|
};
|
|
506
491
|
}
|
|
507
|
-
finally {
|
|
508
|
-
await runtime?.close();
|
|
509
|
-
}
|
|
510
492
|
}
|
|
511
493
|
async function speedtestInNode(input, options) {
|
|
512
494
|
if (input.links.length === 0) {
|
|
@@ -587,10 +569,25 @@ export async function probeSpeedtestLinksInNode(input, options = {}) {
|
|
|
587
569
|
const runtimePath = input.runtime_path ?? '';
|
|
588
570
|
const requestedRuntime = String((options.env ?? process.env).AUTOVPN_SPEEDTEST_RUNTIME ?? '').trim().toLowerCase();
|
|
589
571
|
const useMihomoRuntime = requestedRuntime !== 'direct';
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
572
|
+
const emitProbe = (result, completed) => {
|
|
573
|
+
options.progressCallback?.(`[speedtest:probe] ${completed}/${input.links.length} reachable=${result.reachable} latency=${result.latency_ms}ms`);
|
|
574
|
+
emitEvent(options.eventCallback, 'speedtest_probe_result', {
|
|
575
|
+
completed,
|
|
576
|
+
total: input.links.length,
|
|
577
|
+
link: result.link,
|
|
578
|
+
reachable: result.reachable,
|
|
579
|
+
latency_ms: result.latency_ms,
|
|
580
|
+
error: result.error ?? ''
|
|
581
|
+
});
|
|
582
|
+
};
|
|
583
|
+
if (options.probeLinks) {
|
|
584
|
+
const results = await options.probeLinks(input.links, config, { runtime_path: runtimePath });
|
|
585
|
+
results.forEach((result, index) => emitProbe(result, index + 1));
|
|
586
|
+
return results;
|
|
587
|
+
}
|
|
588
|
+
return useMihomoRuntime
|
|
589
|
+
? probeLinksMihomo(input.links, config, runtimePath, options, emitProbe)
|
|
590
|
+
: probeLinksDirect(input.links, config, options, emitProbe);
|
|
594
591
|
}
|
|
595
592
|
export async function testSpeedtestLinkInNode(input, options = {}) {
|
|
596
593
|
const config = normalizeSpeedTestConfig(input.config);
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
export class AsyncPermitPool {
|
|
2
|
+
available;
|
|
3
|
+
waiters = [];
|
|
4
|
+
constructor(limit) {
|
|
5
|
+
if (!Number.isInteger(limit) || limit <= 0)
|
|
6
|
+
throw new RangeError('permit limit must be a positive integer');
|
|
7
|
+
this.available = limit;
|
|
8
|
+
}
|
|
9
|
+
async run(operation) {
|
|
10
|
+
await this.acquire();
|
|
11
|
+
try {
|
|
12
|
+
return await operation();
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
this.release();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async acquire() {
|
|
19
|
+
if (this.available > 0) {
|
|
20
|
+
this.available -= 1;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
await new Promise((resolve) => this.waiters.push(resolve));
|
|
24
|
+
}
|
|
25
|
+
release() {
|
|
26
|
+
const waiter = this.waiters.shift();
|
|
27
|
+
if (waiter)
|
|
28
|
+
waiter();
|
|
29
|
+
else
|
|
30
|
+
this.available += 1;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class BoundedWorkerPool {
|
|
34
|
+
concurrency;
|
|
35
|
+
limit;
|
|
36
|
+
worker;
|
|
37
|
+
queue = [];
|
|
38
|
+
admissionWaiters = [];
|
|
39
|
+
drainWaiters = [];
|
|
40
|
+
active = 0;
|
|
41
|
+
closed = false;
|
|
42
|
+
failure;
|
|
43
|
+
failed = false;
|
|
44
|
+
constructor(options) {
|
|
45
|
+
if (!Number.isInteger(options.concurrency) || options.concurrency <= 0) {
|
|
46
|
+
throw new RangeError('concurrency must be a positive integer');
|
|
47
|
+
}
|
|
48
|
+
if (!Number.isInteger(options.capacity) || options.capacity < 0) {
|
|
49
|
+
throw new RangeError('capacity must be a non-negative integer');
|
|
50
|
+
}
|
|
51
|
+
this.concurrency = options.concurrency;
|
|
52
|
+
this.limit = options.concurrency + options.capacity;
|
|
53
|
+
this.worker = options.worker;
|
|
54
|
+
}
|
|
55
|
+
submit(item) {
|
|
56
|
+
if (this.failed) {
|
|
57
|
+
return Promise.reject(this.failure);
|
|
58
|
+
}
|
|
59
|
+
if (this.closed) {
|
|
60
|
+
return Promise.reject(new Error('worker pool is closed'));
|
|
61
|
+
}
|
|
62
|
+
if (this.active + this.queue.length < this.limit) {
|
|
63
|
+
this.accept(item);
|
|
64
|
+
return Promise.resolve();
|
|
65
|
+
}
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
this.admissionWaiters.push({ item, resolve, reject });
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
close() {
|
|
71
|
+
this.closed = true;
|
|
72
|
+
this.settleDrainWaitersIfIdle();
|
|
73
|
+
}
|
|
74
|
+
drain() {
|
|
75
|
+
if (this.isIdle()) {
|
|
76
|
+
return this.failed ? Promise.reject(this.failure) : Promise.resolve();
|
|
77
|
+
}
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
this.drainWaiters.push({ resolve, reject });
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
abort(error) {
|
|
83
|
+
if (!this.failed) {
|
|
84
|
+
this.failed = true;
|
|
85
|
+
this.failure = error;
|
|
86
|
+
}
|
|
87
|
+
this.closed = true;
|
|
88
|
+
this.queue.length = 0;
|
|
89
|
+
this.rejectAdmissionWaiters(this.failure);
|
|
90
|
+
this.settleDrainWaitersIfIdle();
|
|
91
|
+
}
|
|
92
|
+
accept(item) {
|
|
93
|
+
if (this.active < this.concurrency) {
|
|
94
|
+
this.start(item);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
this.queue.push(item);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
start(item) {
|
|
101
|
+
this.active += 1;
|
|
102
|
+
Promise.resolve()
|
|
103
|
+
.then(() => this.worker(item))
|
|
104
|
+
.catch((error) => this.recordFailure(error))
|
|
105
|
+
.finally(() => {
|
|
106
|
+
this.active -= 1;
|
|
107
|
+
if (!this.failed) {
|
|
108
|
+
this.fillAvailableSlots();
|
|
109
|
+
}
|
|
110
|
+
this.settleDrainWaitersIfIdle();
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
fillAvailableSlots() {
|
|
114
|
+
while (this.active < this.concurrency && this.queue.length > 0) {
|
|
115
|
+
this.start(this.queue.shift());
|
|
116
|
+
}
|
|
117
|
+
while (this.active + this.queue.length < this.limit && this.admissionWaiters.length > 0) {
|
|
118
|
+
const admission = this.admissionWaiters.shift();
|
|
119
|
+
this.accept(admission.item);
|
|
120
|
+
admission.resolve();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
recordFailure(error) {
|
|
124
|
+
if (this.failed) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.failed = true;
|
|
128
|
+
this.failure = error;
|
|
129
|
+
this.closed = true;
|
|
130
|
+
this.queue.length = 0;
|
|
131
|
+
this.rejectAdmissionWaiters(error);
|
|
132
|
+
}
|
|
133
|
+
rejectAdmissionWaiters(error) {
|
|
134
|
+
for (const admission of this.admissionWaiters.splice(0)) {
|
|
135
|
+
admission.reject(error);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
isIdle() {
|
|
139
|
+
return this.active === 0 && this.queue.length === 0;
|
|
140
|
+
}
|
|
141
|
+
settleDrainWaitersIfIdle() {
|
|
142
|
+
if (!this.isIdle()) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
for (const waiter of this.drainWaiters.splice(0)) {
|
|
146
|
+
if (this.failed) {
|
|
147
|
+
waiter.reject(this.failure);
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
waiter.resolve();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
package/dist/web/renderer/app.js
CHANGED
|
@@ -1187,7 +1187,7 @@ function handlePipelineEvent(event, options = {}) {
|
|
|
1187
1187
|
}
|
|
1188
1188
|
|
|
1189
1189
|
if (event.type === 'stage') {
|
|
1190
|
-
state.stageStatus[event.stage] = event.status;
|
|
1190
|
+
state.stageStatus[event.stage] = normalizeStageStatus(event.status);
|
|
1191
1191
|
appendLog(`[stage] ${event.stage} ${event.status}`, {
|
|
1192
1192
|
kind: 'stage',
|
|
1193
1193
|
stage: event.stage,
|
|
@@ -1199,7 +1199,7 @@ function handlePipelineEvent(event, options = {}) {
|
|
|
1199
1199
|
}
|
|
1200
1200
|
|
|
1201
1201
|
if (event.type === 'summary') {
|
|
1202
|
-
state.stageStatus = event.stage_status
|
|
1202
|
+
state.stageStatus = normalizeStageStatuses(event.stage_status);
|
|
1203
1203
|
state.counts = normalizeCounts(event.counts ?? {});
|
|
1204
1204
|
state.sourceCounts = normalizeSourceCounts(event.source_counts ?? state.sourceCounts);
|
|
1205
1205
|
state.retryContext = event.retry_context ?? state.retryContext ?? {};
|
|
@@ -1332,12 +1332,38 @@ function handlePipelineEvent(event, options = {}) {
|
|
|
1332
1332
|
}
|
|
1333
1333
|
|
|
1334
1334
|
if (event.type === 'run_started') {
|
|
1335
|
-
|
|
1335
|
+
const nextArtifactDir = String(event.artifact_dir ?? '');
|
|
1336
|
+
if (nextArtifactDir !== state.artifactDir) {
|
|
1337
|
+
resetRunScopedProgress();
|
|
1338
|
+
state.runState = 'running';
|
|
1339
|
+
state.runResult = 'running';
|
|
1340
|
+
state.runStartedAt = Date.now();
|
|
1341
|
+
}
|
|
1342
|
+
state.artifactDir = nextArtifactDir;
|
|
1336
1343
|
touchUpdate();
|
|
1337
1344
|
renderAll();
|
|
1338
1345
|
}
|
|
1339
1346
|
}
|
|
1340
1347
|
|
|
1348
|
+
function normalizeStageStatus(status) {
|
|
1349
|
+
return status === 'skipped' ? '已跳过' : status;
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function normalizeStageStatuses(stageStatuses = {}) {
|
|
1353
|
+
return Object.fromEntries(
|
|
1354
|
+
Object.entries(stageStatuses ?? {}).map(([stage, status]) => [stage, normalizeStageStatus(status)])
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
function resetRunScopedProgress() {
|
|
1359
|
+
state.stageStatus = {};
|
|
1360
|
+
state.counts = {};
|
|
1361
|
+
state.sourceCounts = {};
|
|
1362
|
+
state.extractDedupedFingerprints = new Set();
|
|
1363
|
+
state.outputFiles = [];
|
|
1364
|
+
state.nodeRows = [];
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1341
1367
|
function normalizeCounts(counts) {
|
|
1342
1368
|
return {
|
|
1343
1369
|
...counts,
|
|
@@ -1368,8 +1394,16 @@ function updateExtractMetrics(event) {
|
|
|
1368
1394
|
}
|
|
1369
1395
|
|
|
1370
1396
|
const previous = state.sourceCounts[sourceName] ?? {};
|
|
1371
|
-
const
|
|
1372
|
-
const
|
|
1397
|
+
const previousRawLinks = validNonNegativeNumber(previous.raw_links) ?? 0;
|
|
1398
|
+
const previousDedupedLinks = validNonNegativeNumber(previous.deduped_links) ?? 0;
|
|
1399
|
+
const rawLinks = Math.max(previousRawLinks, validNonNegativeNumber(event.total_links) ?? previousRawLinks);
|
|
1400
|
+
const eventDedupedLinks = event.deduped_links == null
|
|
1401
|
+
? rawLinks
|
|
1402
|
+
: validNonNegativeNumber(event.deduped_links) ?? previousDedupedLinks;
|
|
1403
|
+
const sourceDedupedLinks = Math.max(
|
|
1404
|
+
previousDedupedLinks,
|
|
1405
|
+
eventDedupedLinks
|
|
1406
|
+
);
|
|
1373
1407
|
if (Array.isArray(event.new_item_fingerprints)) {
|
|
1374
1408
|
for (const fingerprint of event.new_item_fingerprints) {
|
|
1375
1409
|
if (fingerprint) {
|
|
@@ -1395,6 +1429,11 @@ function updateExtractMetrics(event) {
|
|
|
1395
1429
|
}
|
|
1396
1430
|
}
|
|
1397
1431
|
|
|
1432
|
+
function validNonNegativeNumber(value) {
|
|
1433
|
+
const number = Number(value);
|
|
1434
|
+
return Number.isFinite(number) && number >= 0 ? number : null;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1398
1437
|
async function hydrateArtifactPreview() {
|
|
1399
1438
|
if (!state.artifactDir || !window.vpnAutomation?.previewArtifact) {
|
|
1400
1439
|
state.outputFiles = [];
|
|
@@ -1430,7 +1469,7 @@ function hydrateArtifactState(result) {
|
|
|
1430
1469
|
state.runResult = 'failed';
|
|
1431
1470
|
}
|
|
1432
1471
|
if (result.stage_status) {
|
|
1433
|
-
state.stageStatus = result.stage_status;
|
|
1472
|
+
state.stageStatus = normalizeStageStatuses(result.stage_status);
|
|
1434
1473
|
}
|
|
1435
1474
|
}
|
|
1436
1475
|
|