@decentnetwork/lan 0.1.192 → 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.
|
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
|
}
|
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",
|