@decentnetwork/lan 0.1.184 → 0.1.185

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
@@ -121,42 +121,57 @@ export class HlsPrefetcher {
121
121
  const whole = () => this.#fetcher(u, { host: u.host }, "prefetch");
122
122
  if (this.#stripes <= 1)
123
123
  return whole();
124
- const guess = this.#stripeChunkBytes;
125
- let first;
126
- try {
127
- first = await this.#fetcher(u, { host: u.host, range: `bytes=0-${guess - 1}` }, "stripe");
128
- }
129
- catch {
130
- return whole();
131
- }
132
- if (first.status === 200)
133
- return first; // origin ignored Range → whole body already
134
- if (first.status !== 206)
124
+ // Tiny probe to learn total size + confirm Range support (hedged so a dead
125
+ // exit doesn't stall the whole segment before parallelism starts).
126
+ const probe = await this.#fetchRange(u, "bytes=0-1", 2000, 3);
127
+ if (!probe || probe.status !== 206)
135
128
  return whole();
136
- const total = contentRangeTotal(first.headers["content-range"]);
137
- const outHeaders = { ...first.headers };
129
+ const total = contentRangeTotal(probe.headers["content-range"]);
130
+ const outHeaders = { ...probe.headers };
138
131
  delete outHeaders["content-range"];
139
- if (!total || total <= first.body.length)
140
- return { status: 200, headers: outHeaders, body: first.body };
141
- const remaining = total - first.body.length;
142
- const n = Math.max(1, Math.min(this.#stripes - 1, Math.ceil(remaining / guess)));
143
- const chunk = Math.ceil(remaining / n);
132
+ if (!total)
133
+ return whole();
134
+ if (total <= this.#stripeChunkBytes) {
135
+ // Small segment one range fetch (hedged) is enough.
136
+ const one = await this.#fetchRange(u, `bytes=0-${total - 1}`, 6000, 3);
137
+ return one && one.body.length === total ? { status: 200, headers: outHeaders, body: one.body } : whole();
138
+ }
139
+ const n = Math.max(2, Math.min(this.#stripes, Math.ceil(total / this.#stripeChunkBytes)));
140
+ const chunk = Math.ceil(total / n);
141
+ // Timeout scales with chunk size: allow ~0.3 Mbps before we give up on an
142
+ // exit and retry the SAME range on another (kills the dead-exit straggler
143
+ // that otherwise holds the whole segment hostage).
144
+ const rangeTimeout = Math.max(3000, Math.round((chunk * 8) / 300)); // ms at 0.3 Mbps
144
145
  const parts = await Promise.all(Array.from({ length: n }, (_, i) => {
145
- const start = first.body.length + i * chunk;
146
+ const start = i * chunk;
146
147
  const end = Math.min(start + chunk - 1, total - 1);
147
148
  if (start > end)
148
149
  return Promise.resolve(Buffer.alloc(0));
149
- return this.#fetcher(u, { host: u.host, range: `bytes=${start}-${end}` }, "stripe")
150
- .then((r) => ((r.status === 206 || r.status === 200) && r.body.length === end - start + 1 ? r.body : null))
151
- .catch(() => null);
150
+ return this.#fetchRange(u, `bytes=${start}-${end}`, rangeTimeout, 3)
151
+ .then((r) => (r && r.body.length === end - start + 1 ? r.body : null));
152
152
  }));
153
153
  if (parts.some((p) => p === null))
154
- return whole(); // a stripe failed → refetch whole for integrity
155
- const body = Buffer.concat([first.body, ...parts]);
154
+ return whole(); // a range never landed → refetch whole for integrity
155
+ const body = Buffer.concat(parts);
156
156
  if (body.length !== total)
157
157
  return whole();
158
158
  return { status: 200, headers: outHeaders, body };
159
159
  }
160
+ /** Fetch one byte-range with a slow-transfer TIMEOUT + retry on another exit.
161
+ * A stripe stuck on a dead exit (0.0 Mbps) times out and is re-issued (the
162
+ * router rotates to a different exit), so one bad exit can't stall a segment. */
163
+ async #fetchRange(u, range, timeoutMs, tries) {
164
+ for (let t = 0; t < tries; t++) {
165
+ const got = await Promise.race([
166
+ this.#fetcher(u, { host: u.host, range }, "stripe").catch(() => null),
167
+ new Promise((res) => setTimeout(() => res("timeout"), timeoutMs)),
168
+ ]);
169
+ if (got && got !== "timeout" && (got.status === 206 || got.status === 200))
170
+ return got;
171
+ // timed out (slow exit) or errored → loop; the next fetch rotates exits.
172
+ }
173
+ return null;
174
+ }
160
175
  get stats() {
161
176
  return { hits: this.#hits, misses: this.#misses, entries: this.#cache.size, bytes: this.#cacheBytes };
162
177
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.184",
3
+ "version": "0.1.185",
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",