@decentnetwork/lan 0.1.191 → 0.1.193
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/bin/tun-helper-darwin-amd64 +0 -0
- package/bin/tun-helper-darwin-arm64 +0 -0
- package/bin/tun-helper-linux-amd64 +0 -0
- package/bin/tun-helper-linux-arm64 +0 -0
- package/dist/proxy/hls-prefetch.d.ts +3 -0
- package/dist/proxy/hls-prefetch.js +49 -27
- package/dist/proxy/multi-exit-router.d.ts +1 -0
- package/dist/proxy/multi-exit-router.js +23 -5
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -59,6 +59,9 @@ export interface HlsPrefetcherOptions {
|
|
|
59
59
|
* exit just serializes and stalls); as exits recover it rises and aggregation
|
|
60
60
|
* resumes. Overrides `stripes` when provided. */
|
|
61
61
|
stripesFn?: () => number;
|
|
62
|
+
/** Delay before duplicating a slow whole-segment fetch. Primarily exposed for
|
|
63
|
+
* deterministic tests; default 3.5s is just below CCTV's 4s live segment. */
|
|
64
|
+
wholeHedgeMs?: number;
|
|
62
65
|
/** Stripe chunk size — also the first probe chunk (default 256 KB). Segments
|
|
63
66
|
* smaller than this aren't striped. */
|
|
64
67
|
stripeChunkBytes?: number;
|
|
@@ -27,7 +27,7 @@ const STRIPE_TRIES = 3;
|
|
|
27
27
|
* landed in this long is late (a healthy exit delivers in 1-3.5s), so a
|
|
28
28
|
* parallel duplicate on another exit is launched. Rarely fires in steady
|
|
29
29
|
* state; mainly absorbs redemption probes trapped by a still-bad exit. */
|
|
30
|
-
const WHOLE_HEDGE_MS =
|
|
30
|
+
const WHOLE_HEDGE_MS = 3500;
|
|
31
31
|
/** Total size from a `Content-Range: bytes 0-255/900000` header, or null. */
|
|
32
32
|
function contentRangeTotal(h) {
|
|
33
33
|
const v = Array.isArray(h) ? h[0] : h;
|
|
@@ -114,6 +114,7 @@ export class HlsPrefetcher {
|
|
|
114
114
|
#stripes;
|
|
115
115
|
#stripesFn;
|
|
116
116
|
#stripeChunkBytes;
|
|
117
|
+
#wholeHedgeMs;
|
|
117
118
|
#hits = 0;
|
|
118
119
|
#misses = 0;
|
|
119
120
|
constructor(fetcher, opts = {}) {
|
|
@@ -127,6 +128,7 @@ export class HlsPrefetcher {
|
|
|
127
128
|
this.#stripes = Math.max(1, opts.stripes ?? 4);
|
|
128
129
|
this.#stripesFn = opts.stripesFn;
|
|
129
130
|
this.#stripeChunkBytes = opts.stripeChunkBytes ?? 256 * 1024;
|
|
131
|
+
this.#wholeHedgeMs = opts.wholeHedgeMs ?? WHOLE_HEDGE_MS;
|
|
130
132
|
}
|
|
131
133
|
/** Effective stripe count right now (dynamic if a stripesFn was provided). */
|
|
132
134
|
#stripeCount() {
|
|
@@ -179,41 +181,61 @@ export class HlsPrefetcher {
|
|
|
179
181
|
* the origin ignores Range or a stripe can't be completed. No deadline: if
|
|
180
182
|
* every exit is slow, a slow whole body still beats a corrupt/empty one.
|
|
181
183
|
*
|
|
182
|
-
* HEDGED when
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* aborted probe never gets floored and would be re-probed forever). */
|
|
184
|
+
* HEDGED even when only one exit is currently qualified: if the body hasn't
|
|
185
|
+
* landed within 3.5s, launch a second connection and take whichever finishes
|
|
186
|
+
* first. A single proven exit still showed occasional 7-10s per-connection
|
|
187
|
+
* tails; waiting for it alone drained CCTV's four-second live buffer. The
|
|
188
|
+
* losing request is aborted immediately so the rescue does not keep splitting
|
|
189
|
+
* bandwidth after a winner is available. Redemption probes are separate,
|
|
190
|
+
* forced-exit requests and are never aborted by this path. */
|
|
190
191
|
#whole(u, extra) {
|
|
191
|
-
const one = () => this.#fetcher(u, { ...extra, host: u.host },
|
|
192
|
-
const canHedge = (this.#concurrencyFn ? this.#concurrencyFn() : this.#concurrency) >= 2;
|
|
193
|
-
if (!canHedge)
|
|
194
|
-
return one(); // single exit: a duplicate would just split its bandwidth
|
|
192
|
+
const one = (signal, label) => this.#fetcher(u, { ...extra, host: u.host }, label, signal);
|
|
195
193
|
return new Promise((resolve, reject) => {
|
|
196
194
|
let settled = false;
|
|
197
|
-
let
|
|
198
|
-
let
|
|
195
|
+
let pending = 1;
|
|
196
|
+
let hedgeStarted = false;
|
|
199
197
|
let timer = null;
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
198
|
+
const first = new AbortController();
|
|
199
|
+
let hedge = null;
|
|
200
|
+
const win = (winner) => (r) => {
|
|
201
|
+
if (settled)
|
|
202
|
+
return;
|
|
204
203
|
settled = true;
|
|
205
204
|
if (timer)
|
|
206
205
|
clearTimeout(timer);
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
206
|
+
(winner === first ? hedge : first)?.abort();
|
|
207
|
+
// Keep the winner alive through resolution; aborting a fully consumed
|
|
208
|
+
// response would only discard its reusable keep-alive socket.
|
|
209
|
+
void winner;
|
|
210
|
+
resolve(r);
|
|
211
|
+
};
|
|
212
|
+
const lose = (e) => {
|
|
212
213
|
if (settled)
|
|
213
214
|
return;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
pending--;
|
|
216
|
+
if (!hedgeStarted) {
|
|
217
|
+
if (timer)
|
|
218
|
+
clearTimeout(timer);
|
|
219
|
+
startHedge();
|
|
220
|
+
}
|
|
221
|
+
else if (pending === 0) {
|
|
222
|
+
settled = true;
|
|
223
|
+
reject(e);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
const startHedge = () => {
|
|
227
|
+
if (settled || hedgeStarted)
|
|
228
|
+
return;
|
|
229
|
+
hedgeStarted = true;
|
|
230
|
+
hedge = new AbortController();
|
|
231
|
+
pending++;
|
|
232
|
+
one(hedge.signal, "prefetch-hedge").then(win(hedge), lose);
|
|
233
|
+
};
|
|
234
|
+
one(first.signal, "prefetch").then(win(first), lose);
|
|
235
|
+
timer = setTimeout(() => {
|
|
236
|
+
timer = null;
|
|
237
|
+
startHedge();
|
|
238
|
+
}, this.#wholeHedgeMs);
|
|
217
239
|
timer.unref?.();
|
|
218
240
|
});
|
|
219
241
|
}
|
|
@@ -49,6 +49,7 @@ export interface MultiExitRouterOptions {
|
|
|
49
49
|
log?: (msg: string) => void;
|
|
50
50
|
}
|
|
51
51
|
export declare const BUILTIN_REGION_DOMAINS: Record<string, string[]>;
|
|
52
|
+
export declare function isUsefulRecoverySample(peakBps: number, averageBps: number, minBps?: number): boolean;
|
|
52
53
|
interface HlsExitScore {
|
|
53
54
|
speedEwma: number | null;
|
|
54
55
|
flooredAtMs: number | null;
|
|
@@ -149,6 +149,16 @@ const NEVER_LEARN = /(^|\.)(google|gstatic|googleapis|googleusercontent|ggpht|gv
|
|
|
149
149
|
// (≈0.5 Mbit/s) keeps every usable exit in and sits safely ABOVE the 40 KB/s
|
|
150
150
|
// deprioritize floor so a floored exit still drops out.
|
|
151
151
|
const NEAR_DEAD_EXIT_BPS = 60_000;
|
|
152
|
+
// Foreground HLS needs a materially stronger exit than ordinary browsing.
|
|
153
|
+
// CCTV's 720p variant is 1.8 Mbit/s; an exit below ~1.2 Mbit/s cannot deliver
|
|
154
|
+
// one four-second segment quickly enough to be useful even with a small live
|
|
155
|
+
// buffer. Keep such exits available for last-resort failover, but do not count
|
|
156
|
+
// them as HLS delivery capacity or let a slow redemption probe re-enable them.
|
|
157
|
+
// This is bytes/sec (150 KB/s = 1.2 Mbit/s).
|
|
158
|
+
const HLS_DELIVERY_BPS = 150_000;
|
|
159
|
+
export function isUsefulRecoverySample(peakBps, averageBps, minBps = HLS_DELIVERY_BPS) {
|
|
160
|
+
return peakBps >= minBps && averageBps >= minBps;
|
|
161
|
+
}
|
|
152
162
|
// A buffered segment fetch is given a deadline sized to deliver its body at
|
|
153
163
|
// roughly this bitrate; an exit that can't keep up is abandoned and the next is
|
|
154
164
|
// tried. Live HLS needs each ~4s segment in a few seconds, so this is set near
|
|
@@ -217,7 +227,7 @@ function floorExit(exit) {
|
|
|
217
227
|
/** Feed one tunnel's rate meter into the exit's speed EWMA (peak-window based).
|
|
218
228
|
* Only transfers past SPEED_MIN_BYTES count — smaller ones never reveal path
|
|
219
229
|
* capacity on a high-RTT link. */
|
|
220
|
-
function sampleSpeed(exit, meter, durMs) {
|
|
230
|
+
function sampleSpeed(exit, meter, durMs, recoveryMinBps = NEAR_DEAD_EXIT_BPS) {
|
|
221
231
|
exit.bytesDown += meter.total;
|
|
222
232
|
if (meter.total < SPEED_MIN_BYTES)
|
|
223
233
|
return;
|
|
@@ -229,7 +239,12 @@ function sampleSpeed(exit, meter, durMs) {
|
|
|
229
239
|
// replaces it with the fresh measurement instead of blending the floor into
|
|
230
240
|
// the result. Only a useful-speed transfer proves recovery; a completed but
|
|
231
241
|
// still-too-slow probe remains floored for another redemption interval.
|
|
232
|
-
|
|
242
|
+
// Peak-window speed is useful for ranking paths, but HLS redemption must
|
|
243
|
+
// also prove whole-segment average throughput. A probe observed at a 0.9
|
|
244
|
+
// Mbit/s peak but only 0.6 Mbit/s average caused six-way prefetch/hedge
|
|
245
|
+
// congestion when it rejoined a 1.8 Mbit/s stream.
|
|
246
|
+
const averageBps = durMs > 0 ? (meter.total / durMs) * 1000 : 0;
|
|
247
|
+
if (isUsefulRecoverySample(bps, averageBps, recoveryMinBps)) {
|
|
233
248
|
exit.speedEwma = bps;
|
|
234
249
|
exit.flooredAtMs = null;
|
|
235
250
|
}
|
|
@@ -247,7 +262,7 @@ function sampleSpeed(exit, meter, durMs) {
|
|
|
247
262
|
* exit is degraded, keep the original pool as a last resort. Exported for the
|
|
248
263
|
* policy regression tests. */
|
|
249
264
|
export function selectHlsPrimaryPool(pool) {
|
|
250
|
-
const proven = pool.filter((e) => e.flooredAtMs === null && e.speedEwma !== null && e.speedEwma >=
|
|
265
|
+
const proven = pool.filter((e) => e.flooredAtMs === null && e.speedEwma !== null && e.speedEwma >= HLS_DELIVERY_BPS);
|
|
251
266
|
if (proven.length > 0)
|
|
252
267
|
return proven;
|
|
253
268
|
const unproven = pool.filter((e) => e.flooredAtMs === null && e.speedEwma === null);
|
|
@@ -255,7 +270,10 @@ export function selectHlsPrimaryPool(pool) {
|
|
|
255
270
|
}
|
|
256
271
|
/** Dynamic prefetch concurrency follows only the foreground-safe pool. */
|
|
257
272
|
export function countHlsDeliveryExits(pool) {
|
|
258
|
-
|
|
273
|
+
// Two adjacent live segments in flight keep the buffer moving without the
|
|
274
|
+
// runaway 6-8 request fan-out seen when every marginal exit increased both
|
|
275
|
+
// prefetch concurrency and whole-segment hedging.
|
|
276
|
+
return Math.min(2, Math.max(1, selectHlsPrimaryPool(pool).length));
|
|
259
277
|
}
|
|
260
278
|
function fmtMbps(bps) {
|
|
261
279
|
return bps === null ? "?" : `${((bps * 8) / 1e6).toFixed(1)}Mbps`;
|
|
@@ -1274,7 +1292,7 @@ export function startMultiExitRouter(opts) {
|
|
|
1274
1292
|
nextExit(`empty ${status}`);
|
|
1275
1293
|
return;
|
|
1276
1294
|
}
|
|
1277
|
-
sampleSpeed(exit, meter, durMs);
|
|
1295
|
+
sampleSpeed(exit, meter, durMs, HLS_DELIVERY_BPS);
|
|
1278
1296
|
if (label) {
|
|
1279
1297
|
const kb = (body.length / 1024).toFixed(0);
|
|
1280
1298
|
const mbps = durMs > 0 ? ((body.length * 8) / durMs / 1000).toFixed(1) : "?";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/lan",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.193",
|
|
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",
|