@camstack/addon-pipeline 1.2.47 → 1.2.49

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 (37) hide show
  1. package/dist/{model-download-service-D8B-4ktF-7-YxE9Wx.js → addon-utils-DveoBR6G.js} +219 -3
  2. package/dist/{model-download-service-D8B-4ktF-BB7oZL3y.mjs → addon-utils-QuxtOdK1.mjs} +219 -3
  3. package/dist/audio-analyzer/index.js +4 -4
  4. package/dist/audio-analyzer/index.mjs +2 -2
  5. package/dist/detection-pipeline/index.js +9 -9
  6. package/dist/detection-pipeline/index.mjs +3 -3
  7. package/dist/{dist-CfxC7XoI.mjs → dist-CLJKoYEP.mjs} +1301 -1284
  8. package/dist/{dist-C9Q1t0nT.js → dist-J2DeRZAN.js} +1301 -1284
  9. package/dist/{event-loop-stall-monitor-B_XUPTwp.mjs → event-loop-stall-monitor-B6dqQjiX.mjs} +1 -1
  10. package/dist/{event-loop-stall-monitor-Luq9AX4t.js → event-loop-stall-monitor-Bkbiw20z.js} +1 -1
  11. package/dist/motion-wasm/index.js +1 -1
  12. package/dist/motion-wasm/index.mjs +1 -1
  13. package/dist/pipeline-runner/index.js +3 -3
  14. package/dist/pipeline-runner/index.mjs +3 -3
  15. package/dist/recorder/index.js +5 -5
  16. package/dist/recorder/index.mjs +2 -2
  17. package/dist/session-decode/decode-worker-child.js +1 -1
  18. package/dist/session-decode/decode-worker-child.mjs +1 -1
  19. package/dist/stream-broker/_stub.js +2 -2
  20. package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-SKj0Zdz2.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DZikMEgD.mjs} +2 -2
  21. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Cj2xMupA.mjs +26 -0
  22. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Czrsozsg.mjs +26 -0
  23. package/dist/stream-broker/{hostInit-D8gyxL0j.mjs → hostInit-i24FVz-3.mjs} +2 -2
  24. package/dist/stream-broker/index.js +198 -132
  25. package/dist/stream-broker/index.mjs +197 -131
  26. package/dist/stream-broker/remoteEntry.js +1 -1
  27. package/dist/{worker-protocol-rqbyKlm9.mjs → worker-protocol-BxPNiino.mjs} +1 -1
  28. package/dist/{worker-protocol-BUJpm7l3.js → worker-protocol-CNi5srgV.js} +1 -1
  29. package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-DwXIyaHw.js → MaskShapeCanvas-DI4BY7W2-m7YpaY98.js} +1 -1
  30. package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-Cf8EtWEG.js → MotionZonesSettings-NcxxQN8r-619YTOjy.js} +1 -1
  31. package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-BvsVXya9.js → PrivacyMaskSettings-APgPLF7p-B-k8v0O5.js} +1 -1
  32. package/embed-dist/assets/index-CYO4OY8_.js +112 -0
  33. package/embed-dist/index.html +1 -1
  34. package/package.json +1 -1
  35. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-KpixNEma.mjs +0 -26
  36. package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-rd7UaqfV.mjs +0 -26
  37. package/embed-dist/assets/index-BuAeIGv8.js +0 -112
@@ -4,7 +4,9 @@ let node_fs = require("node:fs");
4
4
  node_fs = require_chunk.__toESM(node_fs, 1);
5
5
  let node_path = require("node:path");
6
6
  node_path = require_chunk.__toESM(node_path, 1);
7
- //#region ../system/dist/model-download-service-D8B-4ktF.mjs
7
+ let node_util = require("node:util");
8
+ let node_zlib = require("node:zlib");
9
+ //#region ../system/dist/model-download-service-CTBHzORJ.mjs
8
10
  /**
9
11
  * Map a rel path to one candidate absolute path PER root, keeping only roots the
10
12
  * path stays within (traversal guard). The handler serves the first candidate
@@ -70,6 +72,186 @@ function parseRangeHeader(header, size) {
70
72
  };
71
73
  }
72
74
  /**
75
+ * Content-coding for addon-served static assets: `Accept-Encoding` negotiation,
76
+ * an eligibility rule, and a BOUNDED cache of the compressed bytes.
77
+ *
78
+ * ## Why the ADDON owns the encoding
79
+ *
80
+ * The hub's `/addon/:addonId/*` bridge is registered `{ compress: false }`. That
81
+ * is not an oversight — on 2026-07-16 the global `@fastify/compress` was found
82
+ * emitting an EMPTY brotli stream (`content-encoding: br`, `content-length: 0`)
83
+ * for any compressible body over its 1 KiB threshold once the body had crossed
84
+ * the UDS route bridge of a FORKED addon. It silently broke the notifier SVG
85
+ * icons; the deploy-bundle pull route had already dodged the same class with
86
+ * `compress: false`. The resolution then, and the contract now, is that an addon
87
+ * route owns its content-coding END TO END: it negotiates, it compresses, it
88
+ * stamps `content-encoding` + `Vary`, and the hub pipes those bytes through
89
+ * untouched (`proxyToUpstream` replays the upstream headers verbatim).
90
+ *
91
+ * So compression belongs HERE, in the shared file data-plane, and nowhere above
92
+ * it. Anything that re-compresses on top of this reintroduces the empty-brotli
93
+ * bug rather than doubling the ratio.
94
+ *
95
+ * ## Why the bytes are cached, and why the cache is bounded
96
+ *
97
+ * Measured on the stream-broker embed bundle (`index-<hash>.js`, 1,443,142 B):
98
+ * gzip-6 → 360,868 B, brotli-5 → 327,419 B in 27 ms, brotli-11 → 293,677 B in
99
+ * 2,233 ms. Quality 5 is the request-path choice: 77% off the wire for 27 ms
100
+ * paid ONCE. Quality 11 buys another 2.3% for eighty times the CPU, on a path
101
+ * where a real client is waiting.
102
+ *
103
+ * The cache is an LRU bounded by BOTH entry count and total bytes, and it
104
+ * refuses assets over {@link COMPRESSION_MAX_ASSET_BYTES} outright. An unbounded
105
+ * `Map` keyed by URL is how a long-lived process walks into an OOM, and this
106
+ * repo has already spent a session on one.
107
+ *
108
+ * The key carries `mtimeMs` and `size`, so a rebuild that rewrites an asset in
109
+ * place under an UNCHANGED name (an SPA shell, a service worker) can never be
110
+ * served from the previous build's bytes.
111
+ */
112
+ var brotliAsync = (0, node_util.promisify)(node_zlib.brotliCompress);
113
+ var gzipAsync = (0, node_util.promisify)(node_zlib.gzip);
114
+ /** LRU bounds. Both are enforced; whichever binds first evicts. */
115
+ var MAX_CACHE_ENTRIES = 64;
116
+ var MAX_CACHE_BYTES = 32 * 1024 * 1024;
117
+ /** Brotli quality — see the file header for the measured trade. */
118
+ var BROTLI_QUALITY = 5;
119
+ /**
120
+ * Content types worth compressing. An ALLOW-list, not a deny-list: an unknown
121
+ * type (`application/octet-stream`) is assumed to be opaque binary. This is what
122
+ * keeps jpeg/png/webp/mp4/m4s/woff2 out — running brotli over them burns CPU to
123
+ * make the body marginally BIGGER.
124
+ */
125
+ var COMPRESSIBLE_EXACT = new Set([
126
+ "application/javascript",
127
+ "application/ecmascript",
128
+ "application/json",
129
+ "application/manifest+json",
130
+ "application/wasm",
131
+ "application/xml",
132
+ "application/xhtml+xml",
133
+ "application/vnd.apple.mpegurl",
134
+ "image/svg+xml"
135
+ ]);
136
+ /** Is a body of this content type worth compressing at all? */
137
+ function isCompressibleContentType(contentType) {
138
+ const base = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
139
+ if (base.length === 0) return false;
140
+ if (base.startsWith("text/")) return true;
141
+ if (base.endsWith("+json") || base.endsWith("+xml")) return true;
142
+ return COMPRESSIBLE_EXACT.has(base);
143
+ }
144
+ /** Parse `Accept-Encoding` into tokens with their q-values (default 1). */
145
+ function parseAcceptEncoding(header) {
146
+ if (header === void 0 || header.trim().length === 0) return [];
147
+ const out = [];
148
+ for (const part of header.split(",")) {
149
+ const [rawToken, ...params] = part.split(";");
150
+ const token = rawToken?.trim().toLowerCase() ?? "";
151
+ if (token.length === 0) continue;
152
+ let q = 1;
153
+ for (const param of params) {
154
+ const [name, value] = param.split("=");
155
+ if (name?.trim().toLowerCase() !== "q") continue;
156
+ const parsed = Number(value?.trim());
157
+ q = Number.isFinite(parsed) ? parsed : 1;
158
+ }
159
+ out.push({
160
+ token,
161
+ q
162
+ });
163
+ }
164
+ return out;
165
+ }
166
+ function accepts(prefs, token) {
167
+ const exact = prefs.find((p) => p.token === token);
168
+ if (exact) return exact.q > 0;
169
+ const wildcard = prefs.find((p) => p.token === "*");
170
+ return wildcard !== void 0 && wildcard.q > 0;
171
+ }
172
+ /**
173
+ * Pick a coding for this request: brotli when offered, else gzip, else identity.
174
+ *
175
+ * Returns `null` for identity — including when the header is ABSENT. A client
176
+ * that offers nothing gets the raw bytes; guessing on its behalf is how a
177
+ * non-negotiating consumer (a native fetch, a probe, an OTA puller) ends up with
178
+ * a body it cannot read.
179
+ */
180
+ function negotiateContentEncoding(acceptEncoding) {
181
+ const prefs = parseAcceptEncoding(acceptEncoding);
182
+ if (prefs.length === 0) return null;
183
+ if (accepts(prefs, "br")) return "br";
184
+ if (accepts(prefs, "gzip")) return "gzip";
185
+ return null;
186
+ }
187
+ var cache = /* @__PURE__ */ new Map();
188
+ var inFlight = /* @__PURE__ */ new Map();
189
+ var compressions = 0;
190
+ var hits = 0;
191
+ var cachedBytes = 0;
192
+ function cacheKey(key) {
193
+ return `${key.encoding}|${String(key.mtimeMs)}|${String(key.size)}|${key.path}`;
194
+ }
195
+ /** Evict least-recently-used entries until both bounds hold. */
196
+ function evictToBounds() {
197
+ for (const [k, buf] of cache) {
198
+ if (cache.size <= MAX_CACHE_ENTRIES && cachedBytes <= MAX_CACHE_BYTES) return;
199
+ cache.delete(k);
200
+ cachedBytes -= buf.byteLength;
201
+ }
202
+ }
203
+ async function compressBody(raw, encoding) {
204
+ if (encoding === "gzip") return await gzipAsync(raw);
205
+ return await brotliAsync(raw, { params: {
206
+ [node_zlib.constants.BROTLI_PARAM_QUALITY]: BROTLI_QUALITY,
207
+ [node_zlib.constants.BROTLI_PARAM_SIZE_HINT]: raw.byteLength
208
+ } });
209
+ }
210
+ /**
211
+ * Compressed bytes for one asset, compressing at most once per key.
212
+ *
213
+ * `null` means "send the original" — the asset is too large to buffer, the
214
+ * source could not be read, or compression did not actually shrink it. Every
215
+ * failure degrades to identity, so this can only ever be an optimisation.
216
+ *
217
+ * Concurrent callers for the same key share ONE compression: a cold cache hit by
218
+ * a dozen parallel asset requests must not run zlib a dozen times.
219
+ */
220
+ async function getCompressedAsset(key, readSource) {
221
+ if (key.size > 8388608) return null;
222
+ const id = cacheKey(key);
223
+ const cached = cache.get(id);
224
+ if (cached) {
225
+ cache.delete(id);
226
+ cache.set(id, cached);
227
+ hits += 1;
228
+ return cached;
229
+ }
230
+ const pending = inFlight.get(id) ?? (async () => {
231
+ let raw;
232
+ try {
233
+ raw = await readSource();
234
+ } catch {
235
+ return null;
236
+ }
237
+ const out = await compressBody(raw, key.encoding);
238
+ compressions += 1;
239
+ if (out.byteLength >= raw.byteLength) return null;
240
+ cache.set(id, out);
241
+ cachedBytes += out.byteLength;
242
+ evictToBounds();
243
+ return out;
244
+ })();
245
+ inFlight.set(id, pending);
246
+ try {
247
+ return await pending;
248
+ } catch {
249
+ return null;
250
+ } finally {
251
+ inFlight.delete(id);
252
+ }
253
+ }
254
+ /**
73
255
  * A ready-made data-plane request handler that serves files from a set of roots
74
256
  * with HTTP `Range`, for addons whose data-plane is "stream files off disk"
75
257
  * (recording playback, scrub-frame stores, exported clips).
@@ -80,6 +262,11 @@ function parseRangeHeader(header, size) {
80
262
  * the hub already authenticated the caller and the data plane is same-origin
81
263
  * through the hub's port. Reuses the shared Range/content-type/traversal helpers
82
264
  * so there is one implementation across the standalone file-server and this.
265
+ *
266
+ * It also owns its own CONTENT-CODING. The hub bridge is `{ compress: false }`
267
+ * because the global compressor emits an empty brotli stream over the forked-
268
+ * addon route bridge, so nothing above this layer will ever compress an addon
269
+ * body — see `asset-compression.ts` for the incident and the contract.
83
270
  */
84
271
  /** Build a `(req, res)` handler that serves `getRoots()` files with Range. */
85
272
  function createFileDataPlaneHandler(opts) {
@@ -107,11 +294,13 @@ function createFileDataPlaneHandler(opts) {
107
294
  }
108
295
  let absPath = null;
109
296
  let size = 0;
297
+ let mtimeMs = 0;
110
298
  for (const candidate of resolved.candidates) try {
111
299
  const st = await node_fs.promises.stat(candidate);
112
300
  if (st.isFile()) {
113
301
  absPath = candidate;
114
302
  size = st.size;
303
+ mtimeMs = st.mtimeMs;
115
304
  break;
116
305
  }
117
306
  } catch {}
@@ -119,10 +308,13 @@ function createFileDataPlaneHandler(opts) {
119
308
  res.writeHead(404).end();
120
309
  return;
121
310
  }
311
+ const contentType = contentTypeFor(absPath, opts.contentTypes);
312
+ const compressible = isCompressibleContentType(contentType);
122
313
  const baseHeaders = {
123
- "content-type": contentTypeFor(absPath, opts.contentTypes),
314
+ "content-type": contentType,
124
315
  "accept-ranges": "bytes",
125
- "cache-control": opts.cacheControl?.(rel) ?? "no-cache"
316
+ "cache-control": opts.cacheControl?.(rel) ?? "no-cache",
317
+ ...compressible ? { vary: "accept-encoding" } : {}
126
318
  };
127
319
  const range = parseRangeHeader(req.headers.range, size);
128
320
  if (range) {
@@ -141,6 +333,30 @@ function createFileDataPlaneHandler(opts) {
141
333
  }).pipe(res);
142
334
  return;
143
335
  }
336
+ const encoding = compressible ? negotiateContentEncoding(req.headers["accept-encoding"]) : null;
337
+ if (encoding !== null && size >= 1024) {
338
+ const filePath = absPath;
339
+ const body = await getCompressedAsset({
340
+ path: filePath,
341
+ mtimeMs,
342
+ size,
343
+ encoding
344
+ }, () => node_fs.promises.readFile(filePath));
345
+ if (body !== null) {
346
+ res.writeHead(200, {
347
+ ...baseHeaders,
348
+ "content-encoding": encoding,
349
+ "accept-ranges": "none",
350
+ "content-length": String(body.byteLength)
351
+ });
352
+ if (req.method === "HEAD") {
353
+ res.end();
354
+ return;
355
+ }
356
+ res.end(body);
357
+ return;
358
+ }
359
+ }
144
360
  res.writeHead(200, {
145
361
  ...baseHeaders,
146
362
  "content-length": String(size)
@@ -3,7 +3,9 @@ import * as fs from "node:fs";
3
3
  import { createReadStream, promises } from "node:fs";
4
4
  import * as path$1 from "node:path";
5
5
  import path from "node:path";
6
- //#region ../system/dist/model-download-service-D8B-4ktF.mjs
6
+ import { promisify } from "node:util";
7
+ import { brotliCompress, constants, gzip } from "node:zlib";
8
+ //#region ../system/dist/model-download-service-CTBHzORJ.mjs
7
9
  /**
8
10
  * Map a rel path to one candidate absolute path PER root, keeping only roots the
9
11
  * path stays within (traversal guard). The handler serves the first candidate
@@ -69,6 +71,186 @@ function parseRangeHeader(header, size) {
69
71
  };
70
72
  }
71
73
  /**
74
+ * Content-coding for addon-served static assets: `Accept-Encoding` negotiation,
75
+ * an eligibility rule, and a BOUNDED cache of the compressed bytes.
76
+ *
77
+ * ## Why the ADDON owns the encoding
78
+ *
79
+ * The hub's `/addon/:addonId/*` bridge is registered `{ compress: false }`. That
80
+ * is not an oversight — on 2026-07-16 the global `@fastify/compress` was found
81
+ * emitting an EMPTY brotli stream (`content-encoding: br`, `content-length: 0`)
82
+ * for any compressible body over its 1 KiB threshold once the body had crossed
83
+ * the UDS route bridge of a FORKED addon. It silently broke the notifier SVG
84
+ * icons; the deploy-bundle pull route had already dodged the same class with
85
+ * `compress: false`. The resolution then, and the contract now, is that an addon
86
+ * route owns its content-coding END TO END: it negotiates, it compresses, it
87
+ * stamps `content-encoding` + `Vary`, and the hub pipes those bytes through
88
+ * untouched (`proxyToUpstream` replays the upstream headers verbatim).
89
+ *
90
+ * So compression belongs HERE, in the shared file data-plane, and nowhere above
91
+ * it. Anything that re-compresses on top of this reintroduces the empty-brotli
92
+ * bug rather than doubling the ratio.
93
+ *
94
+ * ## Why the bytes are cached, and why the cache is bounded
95
+ *
96
+ * Measured on the stream-broker embed bundle (`index-<hash>.js`, 1,443,142 B):
97
+ * gzip-6 → 360,868 B, brotli-5 → 327,419 B in 27 ms, brotli-11 → 293,677 B in
98
+ * 2,233 ms. Quality 5 is the request-path choice: 77% off the wire for 27 ms
99
+ * paid ONCE. Quality 11 buys another 2.3% for eighty times the CPU, on a path
100
+ * where a real client is waiting.
101
+ *
102
+ * The cache is an LRU bounded by BOTH entry count and total bytes, and it
103
+ * refuses assets over {@link COMPRESSION_MAX_ASSET_BYTES} outright. An unbounded
104
+ * `Map` keyed by URL is how a long-lived process walks into an OOM, and this
105
+ * repo has already spent a session on one.
106
+ *
107
+ * The key carries `mtimeMs` and `size`, so a rebuild that rewrites an asset in
108
+ * place under an UNCHANGED name (an SPA shell, a service worker) can never be
109
+ * served from the previous build's bytes.
110
+ */
111
+ var brotliAsync = promisify(brotliCompress);
112
+ var gzipAsync = promisify(gzip);
113
+ /** LRU bounds. Both are enforced; whichever binds first evicts. */
114
+ var MAX_CACHE_ENTRIES = 64;
115
+ var MAX_CACHE_BYTES = 32 * 1024 * 1024;
116
+ /** Brotli quality — see the file header for the measured trade. */
117
+ var BROTLI_QUALITY = 5;
118
+ /**
119
+ * Content types worth compressing. An ALLOW-list, not a deny-list: an unknown
120
+ * type (`application/octet-stream`) is assumed to be opaque binary. This is what
121
+ * keeps jpeg/png/webp/mp4/m4s/woff2 out — running brotli over them burns CPU to
122
+ * make the body marginally BIGGER.
123
+ */
124
+ var COMPRESSIBLE_EXACT = new Set([
125
+ "application/javascript",
126
+ "application/ecmascript",
127
+ "application/json",
128
+ "application/manifest+json",
129
+ "application/wasm",
130
+ "application/xml",
131
+ "application/xhtml+xml",
132
+ "application/vnd.apple.mpegurl",
133
+ "image/svg+xml"
134
+ ]);
135
+ /** Is a body of this content type worth compressing at all? */
136
+ function isCompressibleContentType(contentType) {
137
+ const base = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
138
+ if (base.length === 0) return false;
139
+ if (base.startsWith("text/")) return true;
140
+ if (base.endsWith("+json") || base.endsWith("+xml")) return true;
141
+ return COMPRESSIBLE_EXACT.has(base);
142
+ }
143
+ /** Parse `Accept-Encoding` into tokens with their q-values (default 1). */
144
+ function parseAcceptEncoding(header) {
145
+ if (header === void 0 || header.trim().length === 0) return [];
146
+ const out = [];
147
+ for (const part of header.split(",")) {
148
+ const [rawToken, ...params] = part.split(";");
149
+ const token = rawToken?.trim().toLowerCase() ?? "";
150
+ if (token.length === 0) continue;
151
+ let q = 1;
152
+ for (const param of params) {
153
+ const [name, value] = param.split("=");
154
+ if (name?.trim().toLowerCase() !== "q") continue;
155
+ const parsed = Number(value?.trim());
156
+ q = Number.isFinite(parsed) ? parsed : 1;
157
+ }
158
+ out.push({
159
+ token,
160
+ q
161
+ });
162
+ }
163
+ return out;
164
+ }
165
+ function accepts(prefs, token) {
166
+ const exact = prefs.find((p) => p.token === token);
167
+ if (exact) return exact.q > 0;
168
+ const wildcard = prefs.find((p) => p.token === "*");
169
+ return wildcard !== void 0 && wildcard.q > 0;
170
+ }
171
+ /**
172
+ * Pick a coding for this request: brotli when offered, else gzip, else identity.
173
+ *
174
+ * Returns `null` for identity — including when the header is ABSENT. A client
175
+ * that offers nothing gets the raw bytes; guessing on its behalf is how a
176
+ * non-negotiating consumer (a native fetch, a probe, an OTA puller) ends up with
177
+ * a body it cannot read.
178
+ */
179
+ function negotiateContentEncoding(acceptEncoding) {
180
+ const prefs = parseAcceptEncoding(acceptEncoding);
181
+ if (prefs.length === 0) return null;
182
+ if (accepts(prefs, "br")) return "br";
183
+ if (accepts(prefs, "gzip")) return "gzip";
184
+ return null;
185
+ }
186
+ var cache = /* @__PURE__ */ new Map();
187
+ var inFlight = /* @__PURE__ */ new Map();
188
+ var compressions = 0;
189
+ var hits = 0;
190
+ var cachedBytes = 0;
191
+ function cacheKey(key) {
192
+ return `${key.encoding}|${String(key.mtimeMs)}|${String(key.size)}|${key.path}`;
193
+ }
194
+ /** Evict least-recently-used entries until both bounds hold. */
195
+ function evictToBounds() {
196
+ for (const [k, buf] of cache) {
197
+ if (cache.size <= MAX_CACHE_ENTRIES && cachedBytes <= MAX_CACHE_BYTES) return;
198
+ cache.delete(k);
199
+ cachedBytes -= buf.byteLength;
200
+ }
201
+ }
202
+ async function compressBody(raw, encoding) {
203
+ if (encoding === "gzip") return await gzipAsync(raw);
204
+ return await brotliAsync(raw, { params: {
205
+ [constants.BROTLI_PARAM_QUALITY]: BROTLI_QUALITY,
206
+ [constants.BROTLI_PARAM_SIZE_HINT]: raw.byteLength
207
+ } });
208
+ }
209
+ /**
210
+ * Compressed bytes for one asset, compressing at most once per key.
211
+ *
212
+ * `null` means "send the original" — the asset is too large to buffer, the
213
+ * source could not be read, or compression did not actually shrink it. Every
214
+ * failure degrades to identity, so this can only ever be an optimisation.
215
+ *
216
+ * Concurrent callers for the same key share ONE compression: a cold cache hit by
217
+ * a dozen parallel asset requests must not run zlib a dozen times.
218
+ */
219
+ async function getCompressedAsset(key, readSource) {
220
+ if (key.size > 8388608) return null;
221
+ const id = cacheKey(key);
222
+ const cached = cache.get(id);
223
+ if (cached) {
224
+ cache.delete(id);
225
+ cache.set(id, cached);
226
+ hits += 1;
227
+ return cached;
228
+ }
229
+ const pending = inFlight.get(id) ?? (async () => {
230
+ let raw;
231
+ try {
232
+ raw = await readSource();
233
+ } catch {
234
+ return null;
235
+ }
236
+ const out = await compressBody(raw, key.encoding);
237
+ compressions += 1;
238
+ if (out.byteLength >= raw.byteLength) return null;
239
+ cache.set(id, out);
240
+ cachedBytes += out.byteLength;
241
+ evictToBounds();
242
+ return out;
243
+ })();
244
+ inFlight.set(id, pending);
245
+ try {
246
+ return await pending;
247
+ } catch {
248
+ return null;
249
+ } finally {
250
+ inFlight.delete(id);
251
+ }
252
+ }
253
+ /**
72
254
  * A ready-made data-plane request handler that serves files from a set of roots
73
255
  * with HTTP `Range`, for addons whose data-plane is "stream files off disk"
74
256
  * (recording playback, scrub-frame stores, exported clips).
@@ -79,6 +261,11 @@ function parseRangeHeader(header, size) {
79
261
  * the hub already authenticated the caller and the data plane is same-origin
80
262
  * through the hub's port. Reuses the shared Range/content-type/traversal helpers
81
263
  * so there is one implementation across the standalone file-server and this.
264
+ *
265
+ * It also owns its own CONTENT-CODING. The hub bridge is `{ compress: false }`
266
+ * because the global compressor emits an empty brotli stream over the forked-
267
+ * addon route bridge, so nothing above this layer will ever compress an addon
268
+ * body — see `asset-compression.ts` for the incident and the contract.
82
269
  */
83
270
  /** Build a `(req, res)` handler that serves `getRoots()` files with Range. */
84
271
  function createFileDataPlaneHandler(opts) {
@@ -106,11 +293,13 @@ function createFileDataPlaneHandler(opts) {
106
293
  }
107
294
  let absPath = null;
108
295
  let size = 0;
296
+ let mtimeMs = 0;
109
297
  for (const candidate of resolved.candidates) try {
110
298
  const st = await promises.stat(candidate);
111
299
  if (st.isFile()) {
112
300
  absPath = candidate;
113
301
  size = st.size;
302
+ mtimeMs = st.mtimeMs;
114
303
  break;
115
304
  }
116
305
  } catch {}
@@ -118,10 +307,13 @@ function createFileDataPlaneHandler(opts) {
118
307
  res.writeHead(404).end();
119
308
  return;
120
309
  }
310
+ const contentType = contentTypeFor(absPath, opts.contentTypes);
311
+ const compressible = isCompressibleContentType(contentType);
121
312
  const baseHeaders = {
122
- "content-type": contentTypeFor(absPath, opts.contentTypes),
313
+ "content-type": contentType,
123
314
  "accept-ranges": "bytes",
124
- "cache-control": opts.cacheControl?.(rel) ?? "no-cache"
315
+ "cache-control": opts.cacheControl?.(rel) ?? "no-cache",
316
+ ...compressible ? { vary: "accept-encoding" } : {}
125
317
  };
126
318
  const range = parseRangeHeader(req.headers.range, size);
127
319
  if (range) {
@@ -140,6 +332,30 @@ function createFileDataPlaneHandler(opts) {
140
332
  }).pipe(res);
141
333
  return;
142
334
  }
335
+ const encoding = compressible ? negotiateContentEncoding(req.headers["accept-encoding"]) : null;
336
+ if (encoding !== null && size >= 1024) {
337
+ const filePath = absPath;
338
+ const body = await getCompressedAsset({
339
+ path: filePath,
340
+ mtimeMs,
341
+ size,
342
+ encoding
343
+ }, () => promises.readFile(filePath));
344
+ if (body !== null) {
345
+ res.writeHead(200, {
346
+ ...baseHeaders,
347
+ "content-encoding": encoding,
348
+ "accept-ranges": "none",
349
+ "content-length": String(body.byteLength)
350
+ });
351
+ if (req.method === "HEAD") {
352
+ res.end();
353
+ return;
354
+ }
355
+ res.end(body);
356
+ return;
357
+ }
358
+ }
143
359
  res.writeHead(200, {
144
360
  ...baseHeaders,
145
361
  "content-length": String(size)
@@ -3,9 +3,9 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-C9Q1t0nT.js");
6
+ const require_dist = require("../dist-J2DeRZAN.js");
7
7
  const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
8
- const require_model_download_service_D8B_4ktF = require("../model-download-service-D8B-4ktF-7-YxE9Wx.js");
8
+ const require_addon_utils = require("../addon-utils-DveoBR6G.js");
9
9
  let node_fs = require("node:fs");
10
10
  node_fs = require_chunk.__toESM(node_fs);
11
11
  let node_path = require("node:path");
@@ -84,7 +84,7 @@ var YamnetPythonPipeline = class {
84
84
  url: YAMNET_MODEL_URL,
85
85
  dest: modelPath
86
86
  } });
87
- await require_model_download_service_D8B_4ktF.downloadFile(YAMNET_MODEL_URL, modelPath);
87
+ await require_addon_utils.downloadFile(YAMNET_MODEL_URL, modelPath);
88
88
  this.log.info("YAMNet ONNX model downloaded", { meta: { sizeBytes: node_fs.statSync(modelPath).size } });
89
89
  }
90
90
  if (!node_fs.existsSync(labelsPath)) {
@@ -92,7 +92,7 @@ var YamnetPythonPipeline = class {
92
92
  url: YAMNET_LABELS_URL,
93
93
  dest: labelsPath
94
94
  } });
95
- await require_model_download_service_D8B_4ktF.downloadFile(YAMNET_LABELS_URL, labelsPath);
95
+ await require_addon_utils.downloadFile(YAMNET_LABELS_URL, labelsPath);
96
96
  }
97
97
  const pythonDir = resolveAudioPythonDir();
98
98
  if (this.installPythonRequirements) await this.installPythonRequirements(node_path.join(pythonDir, "requirements-audio.txt"));
@@ -1,7 +1,7 @@
1
1
  import { n as __require } from "../chunk-DnnnRqeS.mjs";
2
- import { $ as errMsg, C as audioAnalysisCapability, R as mapAudioLabelToMacro, at as BaseAddon, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, p as HF_BASE_URL, ut as hydrateSchema, w as audioAnalyzerCapability } from "../dist-CfxC7XoI.mjs";
2
+ import { $ as errMsg, C as audioAnalysisCapability, R as mapAudioLabelToMacro, at as BaseAddon, n as AUDIO_BACKEND_CHOICES, o as DEFAULT_AUDIO_ANALYZER_CONFIG, p as HF_BASE_URL, ut as hydrateSchema, w as audioAnalyzerCapability } from "../dist-CLJKoYEP.mjs";
3
3
  import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
4
- import { i as downloadFile } from "../model-download-service-D8B-4ktF-BB7oZL3y.mjs";
4
+ import { i as downloadFile } from "../addon-utils-QuxtOdK1.mjs";
5
5
  import * as fs from "node:fs";
6
6
  import * as path$1 from "node:path";
7
7
  //#region src/audio-analyzer/audio-pipeline.ts
@@ -3,11 +3,11 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../chunk-emK7D4bc.js");
6
- const require_dist = require("../dist-C9Q1t0nT.js");
7
- const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-Luq9AX4t.js");
6
+ const require_dist = require("../dist-J2DeRZAN.js");
7
+ const require_event_loop_stall_monitor = require("../event-loop-stall-monitor-Bkbiw20z.js");
8
8
  const require_lazy_sharp = require("../lazy-sharp-CcYuXtsa.js");
9
9
  const require_node_topology_platform = require("../node-topology-platform-CFZ7F4xW.js");
10
- const require_model_download_service_D8B_4ktF = require("../model-download-service-D8B-4ktF-7-YxE9Wx.js");
10
+ const require_addon_utils = require("../addon-utils-DveoBR6G.js");
11
11
  let sharp = require("sharp");
12
12
  sharp = require_chunk.__toESM(sharp);
13
13
  let node_child_process = require("node:child_process");
@@ -5141,7 +5141,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5141
5141
  if (!step.enabled) continue;
5142
5142
  const modelEntry = await this.resolveModelEntry(step.addonId, step.modelId);
5143
5143
  if (!modelEntry) continue;
5144
- if (require_model_download_service_D8B_4ktF.isModelDownloaded(this.modelsDir, modelEntry, format)) continue;
5144
+ if (require_addon_utils.isModelDownloaded(this.modelsDir, modelEntry, format)) continue;
5145
5145
  await this.downloadWithRetry(modelEntry, format, 3);
5146
5146
  }
5147
5147
  }
@@ -5518,7 +5518,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5518
5518
  const formats = {};
5519
5519
  for (const [formatKey, entry] of Object.entries(m.formats)) {
5520
5520
  if (!entry) continue;
5521
- const downloaded = require_model_download_service_D8B_4ktF.isModelDownloaded(this.modelsDir, m, formatKey);
5521
+ const downloaded = require_addon_utils.isModelDownloaded(this.modelsDir, m, formatKey);
5522
5522
  formats[formatKey] = {
5523
5523
  url: entry.url,
5524
5524
  sizeMB: entry.sizeMB,
@@ -5641,7 +5641,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
5641
5641
  const { modelId, format, addonId } = input;
5642
5642
  const modelEntry = await this.resolveModelEntry(addonId, modelId);
5643
5643
  if (!modelEntry) throw new Error(`Model "${modelId}" not found in step "${addonId}" catalog`);
5644
- if (!require_model_download_service_D8B_4ktF.deleteModelFromDisk(this.modelsDir, modelEntry, format)) throw new Error(`Model "${modelId}" (${format}) is not downloaded — nothing to delete`);
5644
+ if (!require_addon_utils.deleteModelFromDisk(this.modelsDir, modelEntry, format)) throw new Error(`Model "${modelId}" (${format}) is not downloaded — nothing to delete`);
5645
5645
  this.log.info("Model deleted from disk", { meta: {
5646
5646
  modelId,
5647
5647
  format
@@ -6372,7 +6372,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6372
6372
  const work = (async () => {
6373
6373
  for (const step of needed) {
6374
6374
  const modelEntry = await this.resolveModelEntry(step.addonId, step.modelId);
6375
- if (modelEntry && !require_model_download_service_D8B_4ktF.isModelDownloaded(this.modelsDir, modelEntry, format)) {
6375
+ if (modelEntry && !require_addon_utils.isModelDownloaded(this.modelsDir, modelEntry, format)) {
6376
6376
  if (modelEntry.formats[format] === void 0) throw new Error(`Model "${modelEntry.id}" has no ${format} format build (available: ${Object.keys(modelEntry.formats).join(", ") || "none"}) — not retrying a permanent format mismatch`);
6377
6377
  this.log.info("Downloading model for step", { meta: {
6378
6378
  modelId: step.modelId,
@@ -6398,7 +6398,7 @@ var DetectionPipelineProvider = class DetectionPipelineProvider {
6398
6398
  /** Download a model with retry + exponential backoff */
6399
6399
  async downloadWithRetry(entry, format, maxRetries, onProgress) {
6400
6400
  for (let attempt = 1; attempt <= maxRetries; attempt++) try {
6401
- await require_model_download_service_D8B_4ktF.ensureModel(this.modelsDir, entry, format, onProgress);
6401
+ await require_addon_utils.ensureModel(this.modelsDir, entry, format, onProgress);
6402
6402
  this.log.info("Model downloaded successfully", { meta: { modelId: entry.id } });
6403
6403
  return;
6404
6404
  } catch (err) {
@@ -7240,7 +7240,7 @@ function buildSchemaSlots(formats, modelsDir, customByStep) {
7240
7240
  id: m.id,
7241
7241
  name: m.name,
7242
7242
  formats: Object.fromEntries(Object.entries(m.formats).map(([f, entry]) => [f, {
7243
- downloaded: require_model_download_service_D8B_4ktF.isModelDownloaded(modelsDir, m, f),
7243
+ downloaded: require_addon_utils.isModelDownloaded(modelsDir, m, f),
7244
7244
  sizeMB: entry.sizeMB
7245
7245
  }])),
7246
7246
  ...m.group ? { group: m.group } : {},
@@ -1,9 +1,9 @@
1
1
  import { n as __require } from "../chunk-DnnnRqeS.mjs";
2
- import { $ as errMsg, At as EventCategory, Et as object, F as enumerateInferenceDevices, I as evaluateZoneRules, J as runtimeDevices$1, M as detectionPipelineCapability, O as defaultDeviceFor$1, Ot as string, U as pipelineExecutorCapability, Z as supportedRuntimes$1, at as BaseAddon, bt as array, ht as parseJsonUnknown, kt as union, l as DEVICE_BACKEND_TO_FORMAT, lt as createEvent, mt as nodePin, t as APPLE_SA_TO_MACRO, ut as hydrateSchema, vt as sleep, x as YAMNET_TO_MACRO } from "../dist-CfxC7XoI.mjs";
3
- import { a as getDefaultModelForFormat, c as getStepDefinition, i as ALL_STEPS, l as resolveModelForFormat, n as startEventLoopStallMonitor, o as getDefaultModelForFormatFromDef, r as ALL_PIPELINE_STEPS, s as getStep } from "../event-loop-stall-monitor-B_XUPTwp.mjs";
2
+ import { $ as errMsg, At as EventCategory, Et as object, F as enumerateInferenceDevices, I as evaluateZoneRules, J as runtimeDevices$1, M as detectionPipelineCapability, O as defaultDeviceFor$1, Ot as string, U as pipelineExecutorCapability, Z as supportedRuntimes$1, at as BaseAddon, bt as array, ht as parseJsonUnknown, kt as union, l as DEVICE_BACKEND_TO_FORMAT, lt as createEvent, mt as nodePin, t as APPLE_SA_TO_MACRO, ut as hydrateSchema, vt as sleep, x as YAMNET_TO_MACRO } from "../dist-CLJKoYEP.mjs";
3
+ import { a as getDefaultModelForFormat, c as getStepDefinition, i as ALL_STEPS, l as resolveModelForFormat, n as startEventLoopStallMonitor, o as getDefaultModelForFormatFromDef, r as ALL_PIPELINE_STEPS, s as getStep } from "../event-loop-stall-monitor-B6dqQjiX.mjs";
4
4
  import { t as getSharp } from "../lazy-sharp-B-mb8uWx.mjs";
5
5
  import { t as pickNodePlatformArch } from "../node-topology-platform-BkR_k6WT.mjs";
6
- import { a as ensureModel, o as isModelDownloaded, r as deleteModelFromDisk } from "../model-download-service-D8B-4ktF-BB7oZL3y.mjs";
6
+ import { a as ensureModel, o as isModelDownloaded, r as deleteModelFromDisk } from "../addon-utils-QuxtOdK1.mjs";
7
7
  import sharp from "sharp";
8
8
  import { spawn } from "node:child_process";
9
9
  import * as os from "node:os";