@camstack/addon-pipeline 1.2.137 → 1.2.138

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.
Files changed (38) hide show
  1. package/dist/audio-analyzer/index.js +5 -5
  2. package/dist/audio-analyzer/index.mjs +3 -3
  3. package/dist/detection-pipeline/index.js +12 -12
  4. package/dist/detection-pipeline/index.mjs +5 -5
  5. package/dist/{dist-4EMKvxCp.js → dist-BuIP0499.js} +40 -0
  6. package/dist/{dist-g0Or5_pR.mjs → dist-CmEYRK7G.mjs} +35 -1
  7. package/dist/{event-loop-stall-monitor-DDm07Ei6.js → event-loop-stall-monitor-Bef2jXMX.js} +1 -1
  8. package/dist/{event-loop-stall-monitor-CskL9Lgu.mjs → event-loop-stall-monitor-JGTFb0Uv.mjs} +1 -1
  9. package/dist/{lazy-sharp-BlA9PxWk.js → lazy-sharp-DH2eVehm.js} +1 -1
  10. package/dist/motion-wasm/index.js +2 -2
  11. package/dist/motion-wasm/index.mjs +1 -1
  12. package/dist/{node-BOHn8gpk.js → node-ChdX9lv7.js} +1 -1
  13. package/dist/{node-Bo1g8hLO.mjs → node-J07GBWT3.mjs} +1 -1
  14. package/dist/pipeline-runner/index.js +55 -12
  15. package/dist/pipeline-runner/index.mjs +54 -11
  16. package/dist/{process-memory-C3084DOt.js → process-memory-CJoZemF4.js} +1 -1
  17. package/dist/{process-memory-DmrHWybb.mjs → process-memory-DAKxUb9d.mjs} +1 -1
  18. package/dist/recorder/index.js +6 -6
  19. package/dist/recorder/index.mjs +3 -3
  20. package/dist/{segment-demux-js-BrQ_A0oi.js → segment-demux-js-BgXbXZnp.js} +1 -1
  21. package/dist/{segment-demux-js-C-a2pghM.mjs → segment-demux-js-Ck9ly4bX.mjs} +1 -1
  22. package/dist/session-decode/decode-worker-child.js +115 -5
  23. package/dist/session-decode/decode-worker-child.mjs +114 -4
  24. package/dist/stream-broker/_stub.js +1 -1
  25. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-AH6OJ3hJ.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Cad8960O.mjs} +3 -3
  26. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-xdmO8wYp.mjs +26 -0
  27. package/dist/stream-broker/demux-worker-child.js +1 -1
  28. package/dist/stream-broker/demux-worker-child.mjs +1 -1
  29. package/dist/stream-broker/{hostInit-XNyn-FTM.mjs → hostInit-Gd3OcDIa.mjs} +3 -3
  30. package/dist/stream-broker/index.js +160 -23
  31. package/dist/stream-broker/index.mjs +160 -23
  32. package/dist/stream-broker/remoteEntry.js +1 -1
  33. package/dist/{worker-protocol-BLfaD5uC.js → worker-protocol-Bpz62eyB.js} +2 -1
  34. package/dist/{worker-protocol-vc8WWhjx.mjs → worker-protocol-wMS29PG9.mjs} +2 -1
  35. package/package.json +5 -3
  36. package/dist/addon-utils-C6b5Tnia.js +0 -625
  37. package/dist/addon-utils-C8KTuPdE.mjs +0 -583
  38. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CQyiEd0l.mjs +0 -26
@@ -1,583 +0,0 @@
1
- import "node:crypto";
2
- import * as fs from "node:fs";
3
- import { createReadStream, promises } from "node:fs";
4
- import * as path$1 from "node:path";
5
- import path from "node:path";
6
- import { promisify } from "node:util";
7
- import { brotliCompress, constants, gzip } from "node:zlib";
8
- //#region ../system/dist/file-data-plane-BhKdxJgf.mjs
9
- function isNonEmptyFile(filePath) {
10
- return fs.existsSync(filePath) && fs.statSync(filePath).size > 0;
11
- }
12
- /**
13
- * Sibling files of a single-file (non-directory) format — extra files the
14
- * format needs, fetched from the same remote directory as `url` and stored
15
- * flat alongside the main file in `modelsDir`. Catalog-declared via
16
- * `formatEntry.files`, e.g. OpenVINO IR lists its `.bin` weights next to the
17
- * `.xml`. The downloader stays format-agnostic; the convention lives in the
18
- * catalog data (like `MLPACKAGE_FILES` for the directory case).
19
- */
20
- function siblingFilesFor(formatEntry) {
21
- return formatEntry.isDirectory ? [] : formatEntry.files ?? [];
22
- }
23
- /** Resolve a sibling's remote URL relative to the main file's directory. */
24
- function siblingUrl(mainUrl, sibling) {
25
- return mainUrl.replace(/[^/]+$/, sibling);
26
- }
27
- /** Build fetch headers, including HF auth token for huggingface.co URLs */
28
- function buildHeaders(url) {
29
- const headers = { "User-Agent": "CamStack/1.0" };
30
- const hfToken = process.env["HF_TOKEN"] ?? process.env["HUGGING_FACE_HUB_TOKEN"];
31
- if (hfToken && url.includes("huggingface.co")) headers["Authorization"] = `Bearer ${hfToken}`;
32
- return headers;
33
- }
34
- var DEFAULT_MAX_REDIRECTS = 5;
35
- function normalizeDownloadOptions(third) {
36
- if (typeof third === "function") return { onProgress: third };
37
- return third ?? {};
38
- }
39
- function isRedirectStatus(status) {
40
- return status === 301 || status === 302 || status === 303 || status === 307 || status === 308;
41
- }
42
- function resolveRedirectUrl(current, location) {
43
- return new URL(location, current);
44
- }
45
- async function downloadFile(url, destPath, onProgressOrOptions) {
46
- if (fs.existsSync(destPath)) return destPath;
47
- const opts = normalizeDownloadOptions(onProgressOrOptions);
48
- const fetchImpl = opts.fetchImpl ?? fetch;
49
- const maxRedirects = opts.maxRedirects ?? DEFAULT_MAX_REDIRECTS;
50
- fs.mkdirSync(path$1.dirname(destPath), { recursive: true });
51
- const tmpPath = destPath + ".downloading";
52
- try {
53
- let current = url;
54
- const seen = /* @__PURE__ */ new Set();
55
- let response;
56
- const manual = opts.redirectPolicy !== void 0;
57
- for (let hop = 0; hop <= maxRedirects; hop++) {
58
- const parsed = new URL(current);
59
- if (opts.redirectPolicy) opts.redirectPolicy(parsed, hop);
60
- if (seen.has(parsed.href)) throw new Error(`Redirect loop detected at ${parsed.href}`);
61
- seen.add(parsed.href);
62
- const controller = opts.timeoutMs !== void 0 ? new AbortController() : void 0;
63
- const timer = controller && opts.timeoutMs !== void 0 ? setTimeout(() => controller.abort(), opts.timeoutMs) : void 0;
64
- try {
65
- response = await fetchImpl(current, {
66
- redirect: manual ? "manual" : "follow",
67
- headers: buildHeaders(current),
68
- ...controller ? { signal: controller.signal } : {}
69
- });
70
- } finally {
71
- if (timer) clearTimeout(timer);
72
- }
73
- if (manual && isRedirectStatus(response.status)) {
74
- const location = response.headers.get("location");
75
- if (!location) throw new Error(`Redirect ${response.status} missing Location header`);
76
- if (hop >= maxRedirects) throw new Error(`Too many redirects (max ${maxRedirects}) downloading ${url}`);
77
- current = resolveRedirectUrl(current, location).href;
78
- continue;
79
- }
80
- break;
81
- }
82
- if (!response) throw new Error(`No response downloading ${url}`);
83
- if (manual && isRedirectStatus(response.status)) throw new Error(`Too many redirects (max ${maxRedirects}) downloading ${url}`);
84
- if (!response.ok) throw new Error(`HTTP ${response.status} downloading ${url}`);
85
- if (!response.body) throw new Error(`No response body from ${url}`);
86
- const total = parseInt(response.headers.get("content-length") ?? "0", 10);
87
- let downloaded = 0;
88
- const fileStream = fs.createWriteStream(tmpPath);
89
- const reader = response.body.getReader();
90
- try {
91
- for (;;) {
92
- const { done, value } = await reader.read();
93
- if (done || !value) break;
94
- downloaded += value.length;
95
- if (opts.maxBytes !== void 0 && downloaded > opts.maxBytes) throw new Error(`Downloaded ${downloaded} bytes exceeds the ${opts.maxBytes}-byte limit`);
96
- fileStream.write(value);
97
- opts.onProgress?.(downloaded, total);
98
- }
99
- } finally {
100
- fileStream.end();
101
- await new Promise((resolve, reject) => {
102
- fileStream.on("finish", resolve);
103
- fileStream.on("error", reject);
104
- });
105
- }
106
- fs.renameSync(tmpPath, destPath);
107
- return destPath;
108
- } catch (err) {
109
- try {
110
- fs.unlinkSync(tmpPath);
111
- } catch {}
112
- throw err;
113
- }
114
- }
115
- /**
116
- * Download every file in a HuggingFace directory bundle (e.g.,
117
- * `.mlpackage` / OpenVINO IR pair) atomically. `knownFiles` lists the
118
- * relative paths inside the directory; the function fetches each from
119
- * `${url}/${file}` and renames the staging directory only on full
120
- * success. Mirrors `ModelDownloadService.downloadDirectory` but
121
- * exposed as a standalone for catalog-less callers.
122
- */
123
- async function downloadDirectory(url, destDir, knownFiles, onProgress) {
124
- const match = url.match(/huggingface\.co\/([^/]+\/[^/]+)\/resolve\/main\/(.+)/);
125
- if (!match) throw new Error(`Cannot parse HuggingFace URL: ${url}`);
126
- const [, repo, dirPath] = match;
127
- const files = (knownFiles ?? []).map((f) => ({
128
- relativePath: f,
129
- fileUrl: `https://huggingface.co/${repo}/resolve/main/${dirPath}/${f}`
130
- }));
131
- if (files.length === 0) throw new Error(`Directory bundle requires explicit \`files\` list (got none for ${url})`);
132
- const tmpDir = destDir + ".downloading";
133
- fs.rmSync(tmpDir, {
134
- recursive: true,
135
- force: true
136
- });
137
- fs.mkdirSync(tmpDir, { recursive: true });
138
- let totalDownloaded = 0;
139
- try {
140
- for (const file of files) {
141
- const destPath = path$1.join(tmpDir, file.relativePath);
142
- fs.mkdirSync(path$1.dirname(destPath), { recursive: true });
143
- await downloadFile(file.fileUrl, destPath, (downloaded, _total) => {
144
- onProgress?.(totalDownloaded + downloaded, void 0);
145
- });
146
- totalDownloaded += fs.statSync(destPath).size;
147
- }
148
- fs.rmSync(destDir, {
149
- recursive: true,
150
- force: true
151
- });
152
- fs.renameSync(tmpDir, destDir);
153
- } catch (err) {
154
- fs.rmSync(tmpDir, {
155
- recursive: true,
156
- force: true
157
- });
158
- throw err;
159
- }
160
- }
161
- /**
162
- * Resolve a `ModelCatalogEntry` against `modelsDir`: download model file
163
- * (or directory bundle) + extra files (labels JSON, charset dict, …),
164
- * skip if already on disk. Returns the local model path.
165
- */
166
- async function ensureModel(modelsDir, entry, format, onProgress) {
167
- const formatEntry = entry.formats[format];
168
- if (!formatEntry) throw new Error(`Model "${entry.id}" has no ${format} format. Available: ${Object.keys(entry.formats).join(", ")}`);
169
- if (entry.extraFiles) for (const extra of entry.extraFiles) await downloadFile(extra.url, path$1.join(modelsDir, extra.filename));
170
- const filename = formatEntry.url.split("/").pop() ?? `${entry.id}.${format}`;
171
- const modelPath = path$1.join(modelsDir, filename);
172
- const siblings = siblingFilesFor(formatEntry);
173
- if (fs.existsSync(modelPath)) if (formatEntry.isDirectory && !fs.existsSync(path$1.join(modelPath, "Manifest.json"))) fs.rmSync(modelPath, {
174
- recursive: true,
175
- force: true
176
- });
177
- else if (siblings.some((f) => !isNonEmptyFile(path$1.join(modelsDir, f)))) {} else return modelPath;
178
- fs.mkdirSync(modelsDir, { recursive: true });
179
- if (formatEntry.isDirectory) await downloadDirectory(formatEntry.url, modelPath, formatEntry.files, onProgress);
180
- else {
181
- await downloadFile(formatEntry.url, modelPath, (downloaded, total) => onProgress?.(downloaded, total === 0 ? void 0 : total));
182
- for (const sibling of siblings) await downloadFile(siblingUrl(formatEntry.url, sibling), path$1.join(modelsDir, sibling));
183
- }
184
- return modelPath;
185
- }
186
- /** Compute the on-disk path for a given model + format, even when not yet downloaded. */
187
- function getModelFilePath(modelsDir, entry, format) {
188
- const formatEntry = entry.formats[format];
189
- if (!formatEntry) return null;
190
- const filename = formatEntry.url.split("/").pop() ?? `${entry.id}.${format}`;
191
- return path$1.join(modelsDir, filename);
192
- }
193
- /** True iff the model file (or `Manifest.json` for directory bundles) exists and is non-empty. */
194
- function isModelDownloaded(modelsDir, entry, format) {
195
- const formatEntry = entry.formats[format];
196
- if (!formatEntry) return false;
197
- const modelPath = getModelFilePath(modelsDir, entry, format);
198
- if (!modelPath || !fs.existsSync(modelPath)) return false;
199
- if (formatEntry.isDirectory) return fs.existsSync(path$1.join(modelPath, "Manifest.json"));
200
- if (fs.statSync(modelPath).size <= 0) return false;
201
- return siblingFilesFor(formatEntry).every((f) => isNonEmptyFile(path$1.join(modelsDir, f)));
202
- }
203
- /** Remove the on-disk model file/directory. Returns true if something was deleted. */
204
- function deleteModelFromDisk(modelsDir, entry, format) {
205
- const modelPath = getModelFilePath(modelsDir, entry, format);
206
- if (!modelPath || !fs.existsSync(modelPath)) return false;
207
- const formatEntry = entry.formats[format];
208
- if (formatEntry?.isDirectory) fs.rmSync(modelPath, {
209
- recursive: true,
210
- force: true
211
- });
212
- else {
213
- fs.unlinkSync(modelPath);
214
- if (formatEntry) for (const sibling of siblingFilesFor(formatEntry)) {
215
- const sibPath = path$1.join(modelsDir, sibling);
216
- if (fs.existsSync(sibPath)) fs.unlinkSync(sibPath);
217
- }
218
- }
219
- return true;
220
- }
221
- /**
222
- * Map a rel path to one candidate absolute path PER root, keeping only roots the
223
- * path stays within (traversal guard). The handler serves the first candidate
224
- * that exists. Rejects a path that escapes every root.
225
- */
226
- function resolveFilePath(roots, rel) {
227
- if (rel.length === 0) return { error: "forbidden" };
228
- const candidates = [];
229
- for (const rootDir of roots) {
230
- const root = path.resolve(rootDir);
231
- const abs = path.resolve(root, rel);
232
- if (abs === root || abs.startsWith(root + path.sep)) candidates.push(abs);
233
- }
234
- if (candidates.length === 0) return { error: "forbidden" };
235
- return { candidates };
236
- }
237
- var DEFAULT_CONTENT_TYPES = {
238
- ".m3u8": "application/vnd.apple.mpegurl",
239
- ".m4s": "video/mp4",
240
- ".mp4": "video/mp4",
241
- ".idx": "application/octet-stream",
242
- ".html": "text/html; charset=utf-8",
243
- ".js": "application/javascript; charset=utf-8",
244
- ".mjs": "application/javascript; charset=utf-8",
245
- ".css": "text/css; charset=utf-8",
246
- ".json": "application/json; charset=utf-8",
247
- ".map": "application/json; charset=utf-8",
248
- ".svg": "image/svg+xml",
249
- ".png": "image/png",
250
- ".jpg": "image/jpeg",
251
- ".jpeg": "image/jpeg",
252
- ".ico": "image/x-icon",
253
- ".woff2": "font/woff2",
254
- ".woff": "font/woff",
255
- ".ttf": "font/ttf"
256
- };
257
- function contentTypeFor(filePath, overrides) {
258
- const ext = path.extname(filePath).toLowerCase();
259
- return overrides?.[ext] ?? DEFAULT_CONTENT_TYPES[ext] ?? "application/octet-stream";
260
- }
261
- /** Parse an HTTP `Range` header against a known size. Null = serve whole file. */
262
- function parseRangeHeader(header, size) {
263
- if (!header) return null;
264
- const m = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
265
- if (!m) return null;
266
- const [, rawStart, rawEnd] = m;
267
- if (rawStart === "" && rawEnd === "") return null;
268
- let start;
269
- let end;
270
- if (rawStart === "") {
271
- const n = Number(rawEnd);
272
- if (n <= 0) return null;
273
- start = Math.max(0, size - n);
274
- end = size - 1;
275
- } else {
276
- start = Number(rawStart);
277
- end = rawEnd === "" ? size - 1 : Number(rawEnd);
278
- }
279
- if (!Number.isFinite(start) || !Number.isFinite(end) || start > end || start >= size) return null;
280
- return {
281
- start,
282
- end: Math.min(end, size - 1)
283
- };
284
- }
285
- /**
286
- * Content-coding for addon-served static assets: `Accept-Encoding` negotiation,
287
- * an eligibility rule, and a BOUNDED cache of the compressed bytes.
288
- *
289
- * ## Why the ADDON owns the encoding
290
- *
291
- * The hub's `/addon/:addonId/*` bridge is registered `{ compress: false }`. That
292
- * is not an oversight — on 2026-07-16 the global `@fastify/compress` was found
293
- * emitting an EMPTY brotli stream (`content-encoding: br`, `content-length: 0`)
294
- * for any compressible body over its 1 KiB threshold once the body had crossed
295
- * the UDS route bridge of a FORKED addon. It silently broke the notifier SVG
296
- * icons; the deploy-bundle pull route had already dodged the same class with
297
- * `compress: false`. The resolution then, and the contract now, is that an addon
298
- * route owns its content-coding END TO END: it negotiates, it compresses, it
299
- * stamps `content-encoding` + `Vary`, and the hub pipes those bytes through
300
- * untouched (`proxyToUpstream` replays the upstream headers verbatim).
301
- *
302
- * So compression belongs HERE, in the shared file data-plane, and nowhere above
303
- * it. Anything that re-compresses on top of this reintroduces the empty-brotli
304
- * bug rather than doubling the ratio.
305
- *
306
- * ## Why the bytes are cached, and why the cache is bounded
307
- *
308
- * Measured on the stream-broker embed bundle (`index-<hash>.js`, 1,443,142 B):
309
- * gzip-6 → 360,868 B, brotli-5 → 327,419 B in 27 ms, brotli-11 → 293,677 B in
310
- * 2,233 ms. Quality 5 is the request-path choice: 77% off the wire for 27 ms
311
- * paid ONCE. Quality 11 buys another 2.3% for eighty times the CPU, on a path
312
- * where a real client is waiting.
313
- *
314
- * The cache is an LRU bounded by BOTH entry count and total bytes, and it
315
- * refuses assets over {@link COMPRESSION_MAX_ASSET_BYTES} outright. An unbounded
316
- * `Map` keyed by URL is how a long-lived process walks into an OOM, and this
317
- * repo has already spent a session on one.
318
- *
319
- * The key carries `mtimeMs` and `size`, so a rebuild that rewrites an asset in
320
- * place under an UNCHANGED name (an SPA shell, a service worker) can never be
321
- * served from the previous build's bytes.
322
- */
323
- var brotliAsync = promisify(brotliCompress);
324
- var gzipAsync = promisify(gzip);
325
- /** LRU bounds. Both are enforced; whichever binds first evicts. */
326
- var MAX_CACHE_ENTRIES = 64;
327
- var MAX_CACHE_BYTES = 32 * 1024 * 1024;
328
- /** Brotli quality — see the file header for the measured trade. */
329
- var BROTLI_QUALITY = 5;
330
- /**
331
- * Content types worth compressing. An ALLOW-list, not a deny-list: an unknown
332
- * type (`application/octet-stream`) is assumed to be opaque binary. This is what
333
- * keeps jpeg/png/webp/mp4/m4s/woff2 out — running brotli over them burns CPU to
334
- * make the body marginally BIGGER.
335
- */
336
- var COMPRESSIBLE_EXACT = new Set([
337
- "application/javascript",
338
- "application/ecmascript",
339
- "application/json",
340
- "application/manifest+json",
341
- "application/wasm",
342
- "application/xml",
343
- "application/xhtml+xml",
344
- "application/vnd.apple.mpegurl",
345
- "image/svg+xml"
346
- ]);
347
- /** Is a body of this content type worth compressing at all? */
348
- function isCompressibleContentType(contentType) {
349
- const base = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
350
- if (base.length === 0) return false;
351
- if (base.startsWith("text/")) return true;
352
- if (base.endsWith("+json") || base.endsWith("+xml")) return true;
353
- return COMPRESSIBLE_EXACT.has(base);
354
- }
355
- /** Parse `Accept-Encoding` into tokens with their q-values (default 1). */
356
- function parseAcceptEncoding(header) {
357
- if (header === void 0 || header.trim().length === 0) return [];
358
- const out = [];
359
- for (const part of header.split(",")) {
360
- const [rawToken, ...params] = part.split(";");
361
- const token = rawToken?.trim().toLowerCase() ?? "";
362
- if (token.length === 0) continue;
363
- let q = 1;
364
- for (const param of params) {
365
- const [name, value] = param.split("=");
366
- if (name?.trim().toLowerCase() !== "q") continue;
367
- const parsed = Number(value?.trim());
368
- q = Number.isFinite(parsed) ? parsed : 1;
369
- }
370
- out.push({
371
- token,
372
- q
373
- });
374
- }
375
- return out;
376
- }
377
- function accepts(prefs, token) {
378
- const exact = prefs.find((p) => p.token === token);
379
- if (exact) return exact.q > 0;
380
- const wildcard = prefs.find((p) => p.token === "*");
381
- return wildcard !== void 0 && wildcard.q > 0;
382
- }
383
- /**
384
- * Pick a coding for this request: brotli when offered, else gzip, else identity.
385
- *
386
- * Returns `null` for identity — including when the header is ABSENT. A client
387
- * that offers nothing gets the raw bytes; guessing on its behalf is how a
388
- * non-negotiating consumer (a native fetch, a probe, an OTA puller) ends up with
389
- * a body it cannot read.
390
- */
391
- function negotiateContentEncoding(acceptEncoding) {
392
- const prefs = parseAcceptEncoding(acceptEncoding);
393
- if (prefs.length === 0) return null;
394
- if (accepts(prefs, "br")) return "br";
395
- if (accepts(prefs, "gzip")) return "gzip";
396
- return null;
397
- }
398
- var cache = /* @__PURE__ */ new Map();
399
- var inFlight = /* @__PURE__ */ new Map();
400
- var compressions = 0;
401
- var hits = 0;
402
- var cachedBytes = 0;
403
- function cacheKey(key) {
404
- return `${key.encoding}|${String(key.mtimeMs)}|${String(key.size)}|${key.path}`;
405
- }
406
- /** Evict least-recently-used entries until both bounds hold. */
407
- function evictToBounds() {
408
- for (const [k, buf] of cache) {
409
- if (cache.size <= MAX_CACHE_ENTRIES && cachedBytes <= MAX_CACHE_BYTES) return;
410
- cache.delete(k);
411
- cachedBytes -= buf.byteLength;
412
- }
413
- }
414
- async function compressBody(raw, encoding) {
415
- if (encoding === "gzip") return await gzipAsync(raw);
416
- return await brotliAsync(raw, { params: {
417
- [constants.BROTLI_PARAM_QUALITY]: BROTLI_QUALITY,
418
- [constants.BROTLI_PARAM_SIZE_HINT]: raw.byteLength
419
- } });
420
- }
421
- /**
422
- * Compressed bytes for one asset, compressing at most once per key.
423
- *
424
- * `null` means "send the original" — the asset is too large to buffer, the
425
- * source could not be read, or compression did not actually shrink it. Every
426
- * failure degrades to identity, so this can only ever be an optimisation.
427
- *
428
- * Concurrent callers for the same key share ONE compression: a cold cache hit by
429
- * a dozen parallel asset requests must not run zlib a dozen times.
430
- */
431
- async function getCompressedAsset(key, readSource) {
432
- if (key.size > 8388608) return null;
433
- const id = cacheKey(key);
434
- const cached = cache.get(id);
435
- if (cached) {
436
- cache.delete(id);
437
- cache.set(id, cached);
438
- hits += 1;
439
- return cached;
440
- }
441
- const pending = inFlight.get(id) ?? (async () => {
442
- let raw;
443
- try {
444
- raw = await readSource();
445
- } catch {
446
- return null;
447
- }
448
- const out = await compressBody(raw, key.encoding);
449
- compressions += 1;
450
- if (out.byteLength >= raw.byteLength) return null;
451
- cache.set(id, out);
452
- cachedBytes += out.byteLength;
453
- evictToBounds();
454
- return out;
455
- })();
456
- inFlight.set(id, pending);
457
- try {
458
- return await pending;
459
- } catch {
460
- return null;
461
- } finally {
462
- inFlight.delete(id);
463
- }
464
- }
465
- /**
466
- * A ready-made data-plane request handler that serves files from a set of roots
467
- * with HTTP `Range`, for addons whose data-plane is "stream files off disk"
468
- * (recording playback, scrub-frame stores, exported clips).
469
- *
470
- * This is the addon-side handler the addon hands to `ctx.dataPlane.serve({ handler })`.
471
- * It receives the REAL Node `req`/`res`, resolves the request path against the
472
- * addon's roots (traversal-guarded), and streams the file. NO token and NO CORS:
473
- * the hub already authenticated the caller and the data plane is same-origin
474
- * through the hub's port. Reuses the shared Range/content-type/traversal helpers
475
- * so there is one implementation across the standalone file-server and this.
476
- *
477
- * It also owns its own CONTENT-CODING. The hub bridge is `{ compress: false }`
478
- * because the global compressor emits an empty brotli stream over the forked-
479
- * addon route bridge, so nothing above this layer will ever compress an addon
480
- * body — see `asset-compression.ts` for the incident and the contract.
481
- */
482
- /** Build a `(req, res)` handler that serves `getRoots()` files with Range. */
483
- function createFileDataPlaneHandler(opts) {
484
- return async (req, res) => {
485
- if (req.method !== "GET" && req.method !== "HEAD") {
486
- res.writeHead(405).end();
487
- return;
488
- }
489
- const urlPath = (req.url ?? "/").split("?")[0] ?? "/";
490
- let rel;
491
- try {
492
- rel = decodeURIComponent(urlPath.replace(/^\/+/, ""));
493
- } catch {
494
- res.writeHead(400).end();
495
- return;
496
- }
497
- if (rel.length === 0) {
498
- res.writeHead(404).end();
499
- return;
500
- }
501
- const resolved = resolveFilePath(opts.getRoots(), rel);
502
- if ("error" in resolved) {
503
- res.writeHead(403).end();
504
- return;
505
- }
506
- let absPath = null;
507
- let size = 0;
508
- let mtimeMs = 0;
509
- for (const candidate of resolved.candidates) try {
510
- const st = await promises.stat(candidate);
511
- if (st.isFile()) {
512
- absPath = candidate;
513
- size = st.size;
514
- mtimeMs = st.mtimeMs;
515
- break;
516
- }
517
- } catch {}
518
- if (absPath === null) {
519
- res.writeHead(404).end();
520
- return;
521
- }
522
- const contentType = contentTypeFor(absPath, opts.contentTypes);
523
- const compressible = isCompressibleContentType(contentType);
524
- const baseHeaders = {
525
- "content-type": contentType,
526
- "accept-ranges": "bytes",
527
- "cache-control": opts.cacheControl?.(rel) ?? "no-cache",
528
- ...compressible ? { vary: "accept-encoding" } : {}
529
- };
530
- const range = parseRangeHeader(req.headers.range, size);
531
- if (range) {
532
- res.writeHead(206, {
533
- ...baseHeaders,
534
- "content-range": `bytes ${range.start}-${range.end}/${size}`,
535
- "content-length": String(range.end - range.start + 1)
536
- });
537
- if (req.method === "HEAD") {
538
- res.end();
539
- return;
540
- }
541
- createReadStream(absPath, {
542
- start: range.start,
543
- end: range.end
544
- }).pipe(res);
545
- return;
546
- }
547
- const encoding = compressible ? negotiateContentEncoding(req.headers["accept-encoding"]) : null;
548
- if (encoding !== null && size >= 1024) {
549
- const filePath = absPath;
550
- const body = await getCompressedAsset({
551
- path: filePath,
552
- mtimeMs,
553
- size,
554
- encoding
555
- }, () => promises.readFile(filePath));
556
- if (body !== null) {
557
- res.writeHead(200, {
558
- ...baseHeaders,
559
- "content-encoding": encoding,
560
- "accept-ranges": "none",
561
- "content-length": String(body.byteLength)
562
- });
563
- if (req.method === "HEAD") {
564
- res.end();
565
- return;
566
- }
567
- res.end(body);
568
- return;
569
- }
570
- }
571
- res.writeHead(200, {
572
- ...baseHeaders,
573
- "content-length": String(size)
574
- });
575
- if (req.method === "HEAD") {
576
- res.end();
577
- return;
578
- }
579
- createReadStream(absPath).pipe(res);
580
- };
581
- }
582
- //#endregion
583
- export { ensureModel as a, downloadFile as i, createFileDataPlaneHandler as n, isModelDownloaded as o, deleteModelFromDisk as r, parseRangeHeader as s, contentTypeFor as t };