@decentnetwork/lan 0.1.191 → 0.1.192

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,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
- if (bps >= NEAR_DEAD_EXIT_BPS) {
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 >= NEAR_DEAD_EXIT_BPS);
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
- return Math.max(1, selectHlsPrimaryPool(pool).length);
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.191",
3
+ "version": "0.1.192",
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",