@decentnetwork/lan 0.1.189 → 0.1.191
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.
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -49,6 +49,18 @@ export interface MultiExitRouterOptions {
|
|
|
49
49
|
log?: (msg: string) => void;
|
|
50
50
|
}
|
|
51
51
|
export declare const BUILTIN_REGION_DOMAINS: Record<string, string[]>;
|
|
52
|
+
interface HlsExitScore {
|
|
53
|
+
speedEwma: number | null;
|
|
54
|
+
flooredAtMs: number | null;
|
|
55
|
+
}
|
|
56
|
+
/** Choose exits safe for a foreground HLS fetch. Once any proven-fast exit is
|
|
57
|
+
* available, unproven/redeeming exits cannot borrow its score and displace it.
|
|
58
|
+
* With no proven exit, unproven exits remain the startup/failover path; if every
|
|
59
|
+
* exit is degraded, keep the original pool as a last resort. Exported for the
|
|
60
|
+
* policy regression tests. */
|
|
61
|
+
export declare function selectHlsPrimaryPool<T extends HlsExitScore>(pool: T[]): T[];
|
|
62
|
+
/** Dynamic prefetch concurrency follows only the foreground-safe pool. */
|
|
63
|
+
export declare function countHlsDeliveryExits<T extends HlsExitScore>(pool: T[]): number;
|
|
52
64
|
/**
|
|
53
65
|
* Start the multi-exit router. Returns a stop() that closes the listener.
|
|
54
66
|
* Runs until stopped (the CLI command keeps the process alive).
|
|
@@ -56,3 +68,4 @@ export declare const BUILTIN_REGION_DOMAINS: Record<string, string[]>;
|
|
|
56
68
|
export declare function startMultiExitRouter(opts: MultiExitRouterOptions): {
|
|
57
69
|
stop: () => void;
|
|
58
70
|
};
|
|
71
|
+
export {};
|
|
@@ -206,6 +206,10 @@ class RateMeter {
|
|
|
206
206
|
* segments — but KEEP it in the pool (last-resort failover, small requests). */
|
|
207
207
|
const FLOOR_BPS = 40_000;
|
|
208
208
|
const REDEMPTION_MS = 90_000;
|
|
209
|
+
// Redemption probes are intentionally serialized and spaced out. A degraded
|
|
210
|
+
// exit is tested with a duplicate background segment while the player's normal
|
|
211
|
+
// fetch remains on proven exits, so recovery discovery cannot stall playback.
|
|
212
|
+
const REDEMPTION_PROBE_GAP_MS = 15_000;
|
|
209
213
|
function floorExit(exit) {
|
|
210
214
|
exit.speedEwma = exit.speedEwma === null ? FLOOR_BPS : Math.min(exit.speedEwma, FLOOR_BPS);
|
|
211
215
|
exit.flooredAtMs = Date.now();
|
|
@@ -220,8 +224,38 @@ function sampleSpeed(exit, meter, durMs) {
|
|
|
220
224
|
const bps = meter.peakBps(durMs);
|
|
221
225
|
if (bps <= 0)
|
|
222
226
|
return;
|
|
227
|
+
if (exit.flooredAtMs !== null) {
|
|
228
|
+
// FLOOR_BPS is a penalty, not a real historical sample. A qualifying probe
|
|
229
|
+
// replaces it with the fresh measurement instead of blending the floor into
|
|
230
|
+
// the result. Only a useful-speed transfer proves recovery; a completed but
|
|
231
|
+
// still-too-slow probe remains floored for another redemption interval.
|
|
232
|
+
if (bps >= NEAR_DEAD_EXIT_BPS) {
|
|
233
|
+
exit.speedEwma = bps;
|
|
234
|
+
exit.flooredAtMs = null;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
exit.speedEwma = FLOOR_BPS;
|
|
238
|
+
exit.flooredAtMs = Date.now();
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
223
242
|
exit.speedEwma = exit.speedEwma === null ? bps : SPEED_EWMA_KEEP * exit.speedEwma + (1 - SPEED_EWMA_KEEP) * bps;
|
|
224
|
-
|
|
243
|
+
}
|
|
244
|
+
/** Choose exits safe for a foreground HLS fetch. Once any proven-fast exit is
|
|
245
|
+
* available, unproven/redeeming exits cannot borrow its score and displace it.
|
|
246
|
+
* With no proven exit, unproven exits remain the startup/failover path; if every
|
|
247
|
+
* exit is degraded, keep the original pool as a last resort. Exported for the
|
|
248
|
+
* policy regression tests. */
|
|
249
|
+
export function selectHlsPrimaryPool(pool) {
|
|
250
|
+
const proven = pool.filter((e) => e.flooredAtMs === null && e.speedEwma !== null && e.speedEwma >= NEAR_DEAD_EXIT_BPS);
|
|
251
|
+
if (proven.length > 0)
|
|
252
|
+
return proven;
|
|
253
|
+
const unproven = pool.filter((e) => e.flooredAtMs === null && e.speedEwma === null);
|
|
254
|
+
return unproven.length > 0 ? unproven : pool;
|
|
255
|
+
}
|
|
256
|
+
/** Dynamic prefetch concurrency follows only the foreground-safe pool. */
|
|
257
|
+
export function countHlsDeliveryExits(pool) {
|
|
258
|
+
return Math.max(1, selectHlsPrimaryPool(pool).length);
|
|
225
259
|
}
|
|
226
260
|
function fmtMbps(bps) {
|
|
227
261
|
return bps === null ? "?" : `${((bps * 8) / 1e6).toFixed(1)}Mbps`;
|
|
@@ -1042,7 +1076,41 @@ export function startMultiExitRouter(opts) {
|
|
|
1042
1076
|
}
|
|
1043
1077
|
// ---- HLS segment prefetch (plain-HTTP streams only) ------------------------
|
|
1044
1078
|
// Fetch one URL through the region's exit pool, buffered, with failover.
|
|
1045
|
-
|
|
1079
|
+
let redemptionProbeInFlight = false;
|
|
1080
|
+
let lastRedemptionProbeMs = 0;
|
|
1081
|
+
function maybeStartRedemptionProbe(u, extraHeaders, region) {
|
|
1082
|
+
const now = Date.now();
|
|
1083
|
+
if (redemptionProbeInFlight || now - lastRedemptionProbeMs < REDEMPTION_PROBE_GAP_MS)
|
|
1084
|
+
return;
|
|
1085
|
+
const candidate = exits
|
|
1086
|
+
.filter((e) => e.region === region && e.flooredAtMs !== null &&
|
|
1087
|
+
now - e.flooredAtMs >= REDEMPTION_MS && e.trippedUntil <= now &&
|
|
1088
|
+
(e.healthy || (e.lastSuccessMs !== null && now - e.lastSuccessMs < RECENT_SUCCESS_MS)))
|
|
1089
|
+
.sort((a, b) => (a.flooredAtMs ?? now) - (b.flooredAtMs ?? now))[0];
|
|
1090
|
+
if (!candidate)
|
|
1091
|
+
return;
|
|
1092
|
+
redemptionProbeInFlight = true;
|
|
1093
|
+
lastRedemptionProbeMs = now;
|
|
1094
|
+
// Re-arm before dispatch so a failed/short probe cannot be started again by
|
|
1095
|
+
// every concurrent playlist refresh.
|
|
1096
|
+
candidate.flooredAtMs = now;
|
|
1097
|
+
log(` redemption-probe ${candidate.name}: isolated background segment test`);
|
|
1098
|
+
void fetchViaRegionBuffered(u, extraHeaders, "redemption-probe", undefined, undefined, candidate)
|
|
1099
|
+
.then(() => {
|
|
1100
|
+
if (candidate.flooredAtMs === null) {
|
|
1101
|
+
log(` redemption-probe ${candidate.name}: RECOVERED at ${fmtMbps(candidate.speedEwma)}`);
|
|
1102
|
+
}
|
|
1103
|
+
else {
|
|
1104
|
+
log(` redemption-probe ${candidate.name}: still degraded — keeping it off foreground HLS`);
|
|
1105
|
+
}
|
|
1106
|
+
})
|
|
1107
|
+
.catch((e) => {
|
|
1108
|
+
floorExit(candidate);
|
|
1109
|
+
log(` redemption-probe ${candidate.name}: failed (${e.message}) — keeping it off foreground HLS`);
|
|
1110
|
+
})
|
|
1111
|
+
.finally(() => { redemptionProbeInFlight = false; });
|
|
1112
|
+
}
|
|
1113
|
+
function fetchViaRegionBuffered(u, extraHeaders, label, signal, exitHint, forcedExit) {
|
|
1046
1114
|
return new Promise((resolvep, rejectp) => {
|
|
1047
1115
|
let settled = false;
|
|
1048
1116
|
let curReq = null;
|
|
@@ -1077,7 +1145,9 @@ export function startMultiExitRouter(opts) {
|
|
|
1077
1145
|
reject(new Error("no region for host"));
|
|
1078
1146
|
return;
|
|
1079
1147
|
}
|
|
1080
|
-
|
|
1148
|
+
if (!forcedExit && label === "prefetch")
|
|
1149
|
+
maybeStartRedemptionProbe(u, extraHeaders, region);
|
|
1150
|
+
const base0 = forcedExit ? [forcedExit] : pickOrder(region);
|
|
1081
1151
|
if (base0.length === 0) {
|
|
1082
1152
|
reject(new Error("no healthy exit"));
|
|
1083
1153
|
return;
|
|
@@ -1092,7 +1162,10 @@ export function startMultiExitRouter(opts) {
|
|
|
1092
1162
|
// moderately-slow ones (0.4–0.6 Mbps) that we WANT for aggregation. A
|
|
1093
1163
|
// single (non-striped) fetch instead skips near-dead exits and rotates.
|
|
1094
1164
|
let order;
|
|
1095
|
-
if (
|
|
1165
|
+
if (forcedExit) {
|
|
1166
|
+
order = base0;
|
|
1167
|
+
}
|
|
1168
|
+
else if (exitHint !== undefined) {
|
|
1096
1169
|
// Striped range: use pickOrder's live LOAD-AWARE order directly (base0 is
|
|
1097
1170
|
// sorted by speed/(active+1), least-loaded-fastest first). Because each
|
|
1098
1171
|
// buffered fetch now increments exit.active at connect time, concurrent
|
|
@@ -1110,8 +1183,7 @@ export function startMultiExitRouter(opts) {
|
|
|
1110
1183
|
// splitting one exit's bandwidth N ways (observed: 3 segments all on
|
|
1111
1184
|
// 1.15 at ~1Mbps each while 1.16 sat idle). A blind RR rotation kept
|
|
1112
1185
|
// colliding because it ignored who was actually busy.
|
|
1113
|
-
|
|
1114
|
-
order = fast.length > 0 ? fast : base0;
|
|
1186
|
+
order = selectHlsPrimaryPool(base0);
|
|
1115
1187
|
}
|
|
1116
1188
|
const path = (u.pathname || "/") + (u.search || "");
|
|
1117
1189
|
const tail = (u.pathname || "/").split("/").pop() || path;
|
|
@@ -1123,7 +1195,8 @@ export function startMultiExitRouter(opts) {
|
|
|
1123
1195
|
// bad luck; a fresh retry usually succeeds. Loop the whole exit order a
|
|
1124
1196
|
// few rounds before giving up, so the player almost never sees a hard
|
|
1125
1197
|
// failure (which trips its error watchdog → error.html → black screen).
|
|
1126
|
-
|
|
1198
|
+
const maxRounds = forcedExit ? 1 : MAX_FETCH_ROUNDS;
|
|
1199
|
+
if (round + 1 < maxRounds) {
|
|
1127
1200
|
const t = setTimeout(() => tryIdx(0, round + 1), 150);
|
|
1128
1201
|
t.unref?.();
|
|
1129
1202
|
return;
|
|
@@ -1268,10 +1341,9 @@ export function startMultiExitRouter(opts) {
|
|
|
1268
1341
|
if (!fallbackRegion)
|
|
1269
1342
|
return 1;
|
|
1270
1343
|
const now = Date.now();
|
|
1271
|
-
const
|
|
1272
|
-
(e.healthy || (e.lastSuccessMs !== null && now - e.lastSuccessMs < RECENT_SUCCESS_MS))
|
|
1273
|
-
|
|
1274
|
-
return Math.max(1, n);
|
|
1344
|
+
const eligible = exits.filter((e) => e.region === fallbackRegion && e.trippedUntil <= now &&
|
|
1345
|
+
(e.healthy || (e.lastSuccessMs !== null && now - e.lastSuccessMs < RECENT_SUCCESS_MS)));
|
|
1346
|
+
return countHlsDeliveryExits(eligible);
|
|
1275
1347
|
};
|
|
1276
1348
|
const usableChinaExits = () => Math.min(AGG, deliveringChinaExits());
|
|
1277
1349
|
// Prefetch parallelism = the number of exits ACTUALLY delivering right now
|
|
@@ -1565,17 +1637,6 @@ export function startMultiExitRouter(opts) {
|
|
|
1565
1637
|
});
|
|
1566
1638
|
if (mode === "loadbalance") {
|
|
1567
1639
|
const reporter = setInterval(() => {
|
|
1568
|
-
// Redemption: lift an expired speed-floor back to "unproven" so the exit
|
|
1569
|
-
// gets a fresh segment chance (only segments can re-prove it; without
|
|
1570
|
-
// this a floor is a permanent exclusion, which we never want).
|
|
1571
|
-
const rnow = Date.now();
|
|
1572
|
-
for (const e of exits) {
|
|
1573
|
-
if (e.flooredAtMs !== null && rnow - e.flooredAtMs > REDEMPTION_MS) {
|
|
1574
|
-
e.flooredAtMs = null;
|
|
1575
|
-
e.speedEwma = null;
|
|
1576
|
-
log(`${e.name}: floor lifted after ${Math.round(REDEMPTION_MS / 1000)}s — giving it another chance`);
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
1640
|
const parts = exits.filter((e) => e.healthy).map((e) => `${e.name}[${e.region}]: ${e.active} active / ${e.served} total / ${fmtMbps(e.speedEwma)}`);
|
|
1580
1641
|
parts.push(`direct: ${directActive} active / ${directTotal} total`);
|
|
1581
1642
|
log(`load — ${parts.join(" | ")}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.191",
|
|
4
4
|
"description": "Private virtual LAN for self-hosted services and AI agents, built on Elastos Carrier. NAT-traversal, name service, ACL, all over a peer-to-peer mesh — no public IP required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
81
|
"@decentnetwork/dora": "^0.1.13",
|
|
82
|
-
"@decentnetwork/peer": "^0.1.
|
|
82
|
+
"@decentnetwork/peer": "^0.1.94",
|
|
83
83
|
"@decentnetwork/peer-webrtc": "^0.2.10",
|
|
84
84
|
"ink": "^5.2.1",
|
|
85
85
|
"js-yaml": "^4.1.0",
|