@decentnetwork/lan 0.1.183 → 0.1.184

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
@@ -41,6 +41,13 @@ export interface HlsPrefetcherOptions {
41
41
  concurrency?: number;
42
42
  /** Skip caching bodies larger than this (default 32 MB). */
43
43
  maxEntryBytes?: number;
44
+ /** Split each segment into this many parallel byte-range stripes across the
45
+ * exit pool (default 6). 1 disables striping. Lets ONE segment ride the SUM
46
+ * of the exits' bandwidth instead of a single throttled connection. */
47
+ stripes?: number;
48
+ /** Stripe chunk size — also the first probe chunk (default 192 KB). Segments
49
+ * smaller than this aren't striped. */
50
+ stripeChunkBytes?: number;
44
51
  }
45
52
  export declare class HlsPrefetcher {
46
53
  #private;
@@ -17,6 +17,12 @@
17
17
  // master playlist (#EXT-X-STREAM-INF) lists variants, not segments.
18
18
  //
19
19
  const SEGMENT_EXTS = [".ts", ".m4s", ".m4a", ".m4v", ".aac", ".mp3", ".mp4", ".vtt"];
20
+ /** Parse the total size out of a `Content-Range: bytes 0-N/TOTAL` header. */
21
+ function contentRangeTotal(cr) {
22
+ const s = Array.isArray(cr) ? cr[0] : cr;
23
+ const m = s ? /\/(\d+)\s*$/.exec(s) : null;
24
+ return m ? Number(m[1]) : 0;
25
+ }
20
26
  export function looksLikePlaylist(pathname) {
21
27
  const p = pathname.toLowerCase();
22
28
  return p.endsWith(".m3u8") || p.endsWith(".m3u");
@@ -91,6 +97,8 @@ export class HlsPrefetcher {
91
97
  #ahead;
92
98
  #concurrency;
93
99
  #maxEntryBytes;
100
+ #stripes;
101
+ #stripeChunkBytes;
94
102
  #hits = 0;
95
103
  #misses = 0;
96
104
  constructor(fetcher, opts = {}) {
@@ -100,6 +108,54 @@ export class HlsPrefetcher {
100
108
  this.#ahead = opts.prefetchAhead ?? 4;
101
109
  this.#concurrency = opts.concurrency ?? 4;
102
110
  this.#maxEntryBytes = opts.maxEntryBytes ?? 32 * 1024 * 1024;
111
+ this.#stripes = Math.max(1, opts.stripes ?? 6);
112
+ this.#stripeChunkBytes = opts.stripeChunkBytes ?? 192 * 1024;
113
+ }
114
+ /** Fetch `u` as PARALLEL byte-range stripes. Each range is a separate
115
+ * `#fetcher` call, which the router load-balances onto a DIFFERENT exit — so
116
+ * one segment downloads at the SUM of the pool's bandwidth, beating the GFW
117
+ * per-connection throttle (the reason a single stream stalls even with many
118
+ * exits). Falls back to one whole-body fetch if the origin ignores Range or
119
+ * any stripe fails (correctness over speed). */
120
+ async #stripeFetch(u) {
121
+ const whole = () => this.#fetcher(u, { host: u.host }, "prefetch");
122
+ if (this.#stripes <= 1)
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)
135
+ return whole();
136
+ const total = contentRangeTotal(first.headers["content-range"]);
137
+ const outHeaders = { ...first.headers };
138
+ 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);
144
+ const parts = await Promise.all(Array.from({ length: n }, (_, i) => {
145
+ const start = first.body.length + i * chunk;
146
+ const end = Math.min(start + chunk - 1, total - 1);
147
+ if (start > end)
148
+ 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);
152
+ }));
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]);
156
+ if (body.length !== total)
157
+ return whole();
158
+ return { status: 200, headers: outHeaders, body };
103
159
  }
104
160
  get stats() {
105
161
  return { hits: this.#hits, misses: this.#misses, entries: this.#cache.size, bytes: this.#cacheBytes };
@@ -158,7 +214,7 @@ export class HlsPrefetcher {
158
214
  const p = (async () => {
159
215
  try {
160
216
  const u = new URL(url);
161
- const res = await this.#fetcher(u, { host: u.host }, "prefetch");
217
+ const res = await this.#stripeFetch(u);
162
218
  if (res.status !== 200 || res.body.length === 0 || res.body.length > this.#maxEntryBytes)
163
219
  return null;
164
220
  const ct = res.headers["content-type"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/lan",
3
- "version": "0.1.183",
3
+ "version": "0.1.184",
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",