@camstack/addon-pipeline 1.2.100 → 1.2.101

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.
@@ -4,6 +4,7 @@ Object.defineProperties(exports, {
4
4
  });
5
5
  const require_dist = require("../dist-dm3t4BOt.js");
6
6
  const require_remote_restream = require("../remote-restream-BYbAsgUf.js");
7
+ const require_retire_root_keys = require("../retire-root-keys-KE6D6Xh_.js");
7
8
  let node_crypto = require("node:crypto");
8
9
  node_crypto = require_dist.__toESM(node_crypto, 1);
9
10
  let node_child_process = require("node:child_process");
@@ -117,8 +118,9 @@ function createClientLogIngest(onEntries) {
117
118
  */
118
119
  /**
119
120
  * Last-observed runtime probe (present only after the stream was pulled once).
120
- * Zod-backed because it is persisted durably (per-broker) so the field stays
121
- * populated across a hub restart — the schema is the single source of truth.
121
+ * Zod-backed because it is persisted durably (ONE ROW per broker in
122
+ * `stream-broker:probe-snapshots`) so the field stays populated across a hub
123
+ * restart — the schema is the single source of truth, applied per row.
122
124
  */
123
125
  var ProbedSnapshotSchema = require_dist.object({
124
126
  /** Detected video codec — preferred over the declared one when present. */
@@ -134,8 +136,7 @@ var ProbedSnapshotSchema = require_dist.object({
134
136
  /** Epoch ms the snapshot was captured. */
135
137
  at: require_dist.number()
136
138
  });
137
- /** Durable map keyed by `brokerIdFor(deviceId, camStreamId)`. */
138
- var ProbedSnapshotMapSchema = require_dist.record(require_dist.string(), ProbedSnapshotSchema);
139
+ require_dist.record(require_dist.string(), ProbedSnapshotSchema);
139
140
  var SEP = " · ";
140
141
  function formatCodec(codec) {
141
142
  const lower = codec.toLowerCase();
@@ -184,6 +185,208 @@ function formatProbedSummary(declared, snapshot, opts) {
184
185
  return parts.join(SEP);
185
186
  }
186
187
  //#endregion
188
+ //#region src/stream-broker/broker-row-stores.ts
189
+ /**
190
+ * The stream-broker's flattened settings maps — one row per entry.
191
+ *
192
+ * ## What this replaced
193
+ *
194
+ * `stream-broker:addon-settings` / `root` was 33 155 bytes on the reference
195
+ * hub, and three keys were 32 735 of it: `probeSnapshots` (17 282 B, 109
196
+ * streams), `rtspTokens` (13 147 B, 243 streams) and `profileMap` (2 306 B, 29
197
+ * cameras — a per-DEVICE map, so it grows with the fleet). Every read of that
198
+ * row parsed all of it, including `BaseAddon.resolveConfig`, and every write
199
+ * rewrote all of it — a probe flush every few seconds while streams are live.
200
+ *
201
+ * Two keys deliberately STAY blob keys on that row, and the reasons are not
202
+ * "they are small":
203
+ *
204
+ * - `rtspEnabled` — brokerId → boolean, EMPTY on the reference hub. Its
205
+ * persister replaces the whole map from the set of registered restreamers,
206
+ * and reproducing that on rows means an authoritative prune. A prune driven
207
+ * by a map that shrinks whenever a broker unregisters is exactly the shape
208
+ * that turns an unbind into a wipe (D130, D49), and the twin key
209
+ * `rtspTokens` carries a comment about precisely that loss. Nothing is
210
+ * bought by moving 0 bytes at that risk.
211
+ * - `deviceOverrides` — 354 B, two cameras, same replace-the-whole-map
212
+ * persister, written only when an operator edits an override.
213
+ *
214
+ * The row survives either way: it is also where `BaseAddon.resolveConfig` reads
215
+ * the operator's config fields from. The problem was a 33 KB row, not a blob.
216
+ */
217
+ /**
218
+ * The addon id — the settings-store namespace whose `root` row the flattened
219
+ * keys are being retired from. Matches `packages/addon-pipeline/package.json`
220
+ * (`camstack.addons[].id`).
221
+ */
222
+ var STREAM_BROKER_ADDON_ID = "stream-broker";
223
+ /**
224
+ * Every one of these collections is keyed by the BROKER ID
225
+ * (`<deviceId>/<camStreamId>`) or by the numeric deviceId. `brokerDeviceId`
226
+ * recovers the numeric device from either, for the projected column that makes
227
+ * a per-camera question an indexed query — and for `tags: { deviceId }` on
228
+ * every line these stores log.
229
+ *
230
+ * Returns `null` rather than guessing: a key whose head is not a number is a
231
+ * key this addon did not write, and inventing a device for it would put a row
232
+ * on a camera at random.
233
+ */
234
+ function brokerDeviceId(key) {
235
+ const head = key.includes("/") ? key.slice(0, key.indexOf("/")) : key;
236
+ const id = Number(head);
237
+ return Number.isInteger(id) && id > 0 ? id : null;
238
+ }
239
+ /**
240
+ * Bound for a whole-map read. `settings-store.query` silently caps an unbounded
241
+ * read at 2 000 rows and answers as if that were everything; at 974 devices ×
242
+ * up to 5 streams the default is HALF the margin, so every read here names its
243
+ * own ceiling and a truncation is reported rather than shrunk.
244
+ */
245
+ var BROKER_ROWS_LIMIT = 2e4;
246
+ /**
247
+ * @durable class=mirror owner=stream-broker
248
+ * write="one row per broker (`<deviceId>/<camStreamId>`), replaced wholesale from the
249
+ * manager's live probe map by the throttled metrics flush and once on shutdown"
250
+ * retention="the flush is authoritative: a broker the manager no longer holds loses its
251
+ * row. Losing the whole collection costs the settings 'Probed' field showing 'not yet
252
+ * probed' until the stream is pulled again — it is re-derived from the next probe."
253
+ */
254
+ var BROKER_PROBE_SNAPSHOTS_COLLECTION = "stream-broker:probe-snapshots";
255
+ var BROKER_PROBE_SNAPSHOTS_SPEC = {
256
+ collection: BROKER_PROBE_SNAPSHOTS_COLLECTION,
257
+ schema: ProbedSnapshotSchema,
258
+ columns: [{
259
+ name: "deviceId",
260
+ type: "INTEGER"
261
+ }, {
262
+ name: "at",
263
+ type: "INTEGER",
264
+ notNull: true
265
+ }],
266
+ indexes: [{
267
+ name: "idx_stream_broker_probe_device",
268
+ columns: ["deviceId"]
269
+ }],
270
+ project: (key, value) => ({
271
+ deviceId: brokerDeviceId(key),
272
+ at: value.at
273
+ }),
274
+ deviceIdColumn: "deviceId",
275
+ loadLimit: BROKER_ROWS_LIMIT
276
+ };
277
+ function probeSnapshotsStore(store, logger) {
278
+ return new require_retire_root_keys.RowMapStore({
279
+ spec: BROKER_PROBE_SNAPSHOTS_SPEC,
280
+ store,
281
+ logger
282
+ });
283
+ }
284
+ /**
285
+ * The token, wrapped in an object rather than stored bare.
286
+ *
287
+ * A bare string in a `JSON` column round-trips only because the engine's
288
+ * decoder JSON-parses a stored string ONLY when it starts with `{` or `[`. That
289
+ * is a heuristic, not a contract; a token is opaque hex today and a future
290
+ * format that starts with a brace would come back as an object. One field costs
291
+ * nothing and makes the row self-describing.
292
+ */
293
+ var RtspTokenRowSchema = require_dist.object({ token: require_dist.string().min(1) });
294
+ /**
295
+ * @durable class=ledger owner=stream-broker
296
+ * write="one row per broker (`<deviceId>/<camStreamId>`), upserted when the restream
297
+ * server mints a token. MERGE only — a token is stable forever per stream unless it
298
+ * is explicitly regenerated"
299
+ * retention="nothing deletes a row. The persister receives only the CURRENTLY registered
300
+ * restreamers, so a replace would erase the token of an unregistered broker and turn
301
+ * an external NVR URL into a dead 404 at the next idle or reboot."
302
+ */
303
+ var BROKER_RTSP_TOKENS_COLLECTION = "stream-broker:rtsp-tokens";
304
+ var BROKER_RTSP_TOKENS_SPEC = {
305
+ collection: BROKER_RTSP_TOKENS_COLLECTION,
306
+ schema: RtspTokenRowSchema,
307
+ columns: [{
308
+ name: "deviceId",
309
+ type: "INTEGER"
310
+ }],
311
+ indexes: [{
312
+ name: "idx_stream_broker_rtsp_tokens_device",
313
+ columns: ["deviceId"]
314
+ }],
315
+ project: (key) => ({ deviceId: brokerDeviceId(key) }),
316
+ deviceIdColumn: "deviceId",
317
+ loadLimit: BROKER_ROWS_LIMIT
318
+ };
319
+ function rtspTokensStore(store, logger) {
320
+ return new require_retire_root_keys.RowMapStore({
321
+ spec: BROKER_RTSP_TOKENS_SPEC,
322
+ store,
323
+ logger
324
+ });
325
+ }
326
+ /** One camera's profile → camStreamId assignment, plus whether it is automatic. */
327
+ var ProfileAssignmentSchema = require_dist.object({
328
+ map: require_dist.record(require_dist.string(), require_dist.string()),
329
+ auto: require_dist.boolean()
330
+ });
331
+ /**
332
+ * @durable class=config owner=stream-broker
333
+ * write="one row per camera, replaced wholesale from the manager's assignment map on
334
+ * every profile mutation — the map is authoritative and its boot pass PRUNES cameras
335
+ * the device registry no longer knows, which is why the write is a replace and not
336
+ * an upsert"
337
+ * retention="the replace is the retention: a camera dropped from the manager's map loses
338
+ * its row. Losing a row costs one automatic re-assignment on the next catalog pull."
339
+ */
340
+ var BROKER_PROFILE_MAP_COLLECTION = "stream-broker:profile-map";
341
+ var BROKER_PROFILE_MAP_SPEC = {
342
+ collection: BROKER_PROFILE_MAP_COLLECTION,
343
+ schema: ProfileAssignmentSchema,
344
+ columns: [{
345
+ name: "deviceId",
346
+ type: "INTEGER",
347
+ notNull: true
348
+ }, {
349
+ name: "auto",
350
+ type: "BOOLEAN",
351
+ notNull: true
352
+ }],
353
+ project: (key, value) => ({
354
+ deviceId: Number(key),
355
+ auto: value.auto
356
+ }),
357
+ deviceIdColumn: "deviceId",
358
+ loadLimit: BROKER_ROWS_LIMIT
359
+ };
360
+ function profileMapStore(store, logger) {
361
+ return new require_retire_root_keys.RowMapStore({
362
+ spec: BROKER_PROFILE_MAP_SPEC,
363
+ store,
364
+ logger
365
+ });
366
+ }
367
+ /**
368
+ * The dead `addon-settings` / `root` keys. A CLAIM that each key is dead,
369
+ * enforced by nothing else — the migration script names the same three, keep
370
+ * the two in lockstep.
371
+ */
372
+ var STREAM_BROKER_RETIRED_ROOT_KEYS = [
373
+ {
374
+ key: "probeSnapshots",
375
+ successor: BROKER_PROBE_SNAPSHOTS_COLLECTION,
376
+ reason: "one row per broker since the flatten; as a blob it was 17 KB of the 33 KB root row, rewritten whole by the throttled metrics flush"
377
+ },
378
+ {
379
+ key: "rtspTokens",
380
+ successor: BROKER_RTSP_TOKENS_COLLECTION,
381
+ reason: "one row per broker since the flatten; as a blob a single minted token rewrote 33 KB"
382
+ },
383
+ {
384
+ key: "profileMap",
385
+ successor: BROKER_PROFILE_MAP_COLLECTION,
386
+ reason: "one row per camera since the flatten; a per-DEVICE map, so as a blob it grew with the fleet — 2.3 KB at 29 cameras, ~77 KB at 974"
387
+ }
388
+ ];
389
+ //#endregion
187
390
  //#region src/stream-broker/battery-wake-on-play.ts
188
391
  /**
189
392
  * Wake-on-play — pressing play on a sleeping battery camera wakes it, and the
@@ -14195,15 +14398,18 @@ function readDeclaredAudioEnabled(v) {
14195
14398
  /** Whole-blob schema for the persisted `deviceOverrides` store key:
14196
14399
  * numeric-string deviceId → DeviceOverride (includes `derivedStreams`). */
14197
14400
  var DeviceOverridesMapSchema = require_dist.record(require_dist.string(), DeviceOverrideSchema);
14198
- /** Persisted store-key schemas for the broker's other durable blobs.
14199
- * `rtspTokens`: camStreamKey token hex. `rtspEnabled`: brokerId →
14200
- * enabled. `profileMap`: numeric-string deviceId {map, auto}. */
14201
- var RtspTokensSchema = require_dist.record(require_dist.string(), require_dist.string());
14401
+ /**
14402
+ * The one persisted store KEY the broker still keeps as a blob: `rtspEnabled`,
14403
+ * brokerIdenabled.
14404
+ *
14405
+ * `rtspTokens` and `profileMap` moved to their own collections in the
14406
+ * 2026-08-19 flatten and their whole-blob schemas are gone with them — the row
14407
+ * shapes live in `broker-row-stores.ts`. `rtspEnabled` stayed, and why is
14408
+ * argued in that file's header: its persister replaces the map from the set of
14409
+ * CURRENTLY registered restreamers, and reproducing that on rows would mean a
14410
+ * prune driven by a map that shrinks on every unregister.
14411
+ */
14202
14412
  var RtspEnabledSchema = require_dist.record(require_dist.string(), require_dist.boolean());
14203
- var ProfileMapSchema = require_dist.record(require_dist.string(), require_dist.object({
14204
- map: require_dist.record(require_dist.string(), require_dist.string()),
14205
- auto: require_dist.boolean()
14206
- }));
14207
14413
  var PUSH_KINDS = new Set(["push-annexb"]);
14208
14414
  /** Walk a profile map and return the slot that points at `camStreamId`, or null. */
14209
14415
  function findProfileForStream(map, camStreamId) {
@@ -34952,14 +35158,16 @@ var EventLoopStallMonitor = class {
34952
35158
  //#endregion
34953
35159
  //#region src/stream-broker/addon.ts
34954
35160
  /**
34955
- * Hydrate the persisted RTSP restream state (`rtspTokens` / `rtspEnabled`)
34956
- * through the durable `state()` handles the sanctioned settings accessor
34957
- * instead of a raw `readAddonStore()`. Same store keys, same persisted
34958
- * shapes; a corrupt blob falls back to empty (durable-state semantics) and
34959
- * absent keys keep first-boot silence (no loader call, no log).
35161
+ * Hydrate the persisted RTSP restream state. Tokens come from their own
35162
+ * collection (one row per broker); `rtspEnabled` is still a key of the settings
35163
+ * row. An unparseable token row is skipped by the store — one dead token, not
35164
+ * every token and an empty set keeps first-boot silence (no loader call, no
35165
+ * log).
34960
35166
  */
34961
35167
  async function hydratePersistedRtspState(deps) {
34962
- const tokens = new Map(Object.entries(await deps.tokensState.get()));
35168
+ const rows = await deps.tokensStore.readAll();
35169
+ const tokens = /* @__PURE__ */ new Map();
35170
+ for (const [brokerId, row] of rows) tokens.set(brokerId, row.token);
34963
35171
  if (tokens.size > 0) {
34964
35172
  deps.loadTokens(tokens);
34965
35173
  deps.logger.info("Loaded persisted RTSP tokens", { meta: { tokenCount: tokens.size } });
@@ -35061,7 +35269,7 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
35061
35269
  /** Durable per-stream probe map (powers the settings "Probed" field across
35062
35270
  * restarts). Loaded on activate, flushed throttled from the metrics loop and
35063
35271
  * once on shutdown. */
35064
- probeSnapshotsState = null;
35272
+ probeSnapshots = null;
35065
35273
  lastProbePersistAt = 0;
35066
35274
  constructor() {
35067
35275
  super({
@@ -35137,13 +35345,15 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
35137
35345
  const localNodeId = localNode ? localNode.includes("/") ? localNode.split("/")[0] : localNode : void 0;
35138
35346
  this.brokerManager.setApiAccess(this.ctx.api, localNodeId, this.ctx.kernel.hwaccel);
35139
35347
  await this.loadFfmpegConfig();
35348
+ await this.declareRowCollections();
35349
+ await this.purgeFlattenedSettingsKeys();
35140
35350
  const rtspProvider = this.brokerManager.getRtspRestreamProvider();
35141
35351
  const brokerManager = this.brokerManager;
35142
- const rtspTokensState = this.state("rtspTokens", RtspTokensSchema, {});
35352
+ const rtspTokens = rtspTokensStore(this.ctx.api.settingsStore, this.ctx.logger);
35143
35353
  const rtspEnabledState = this.state("rtspEnabled", RtspEnabledSchema, {});
35144
35354
  try {
35145
35355
  await hydratePersistedRtspState({
35146
- tokensState: rtspTokensState,
35356
+ tokensStore: rtspTokens,
35147
35357
  enabledState: rtspEnabledState,
35148
35358
  loadTokens: (tokens) => brokerManager.loadPersistedTokens(tokens),
35149
35359
  loadEnabled: (states) => rtspProvider.loadPersistedEnabled(states),
@@ -35152,11 +35362,7 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
35152
35362
  } catch {}
35153
35363
  rtspProvider.setTokenPersister(async (tokens) => {
35154
35364
  try {
35155
- await rtspTokensState.update((prev) => {
35156
- const merged = { ...prev };
35157
- for (const [k, v] of tokens) merged[k] = v;
35158
- return merged;
35159
- });
35365
+ for (const [brokerId, token] of tokens) await rtspTokens.write(brokerId, { token });
35160
35366
  this.ctx.logger.info("Persisted RTSP tokens", { meta: { current: tokens.size } });
35161
35367
  } catch (err) {
35162
35368
  this.ctx.logger.warn("Failed to persist RTSP tokens", { meta: { error: require_dist.errMsg(err) } });
@@ -35189,22 +35395,22 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
35189
35395
  this.ctx.logger.warn("Failed to persist device overrides", { meta: { error: require_dist.errMsg(err) } });
35190
35396
  });
35191
35397
  });
35192
- const profileMapState = this.state("profileMap", ProfileMapSchema, {});
35398
+ const profileMap = profileMapStore(this.ctx.api.settingsStore, this.ctx.logger);
35193
35399
  const profileMapPersister = (assignments) => {
35194
- const obj = {};
35195
- for (const [deviceId, assignment] of assignments) obj[`${deviceId}`] = {
35400
+ const next = /* @__PURE__ */ new Map();
35401
+ for (const [deviceId, assignment] of assignments) next.set(`${deviceId}`, {
35196
35402
  map: { ...assignment.map },
35197
35403
  auto: assignment.auto
35198
- };
35199
- profileMapState.set(obj).catch((err) => {
35404
+ });
35405
+ profileMap.replaceAll(next).catch((err) => {
35200
35406
  this.ctx.logger.warn("Failed to persist profile map", { meta: { error: require_dist.errMsg(err) } });
35201
35407
  });
35202
35408
  };
35203
35409
  this.brokerManager.setProfileMapPersister(profileMapPersister);
35204
35410
  try {
35205
- const obj = await profileMapState.get();
35411
+ const rows = await profileMap.readAll();
35206
35412
  const loaded = /* @__PURE__ */ new Map();
35207
- for (const [rawKey, val] of Object.entries(obj)) {
35413
+ for (const [rawKey, val] of rows) {
35208
35414
  const deviceId = Number(rawKey);
35209
35415
  if (!Number.isInteger(deviceId)) continue;
35210
35416
  const map = {};
@@ -35222,12 +35428,13 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
35222
35428
  } catch (err) {
35223
35429
  this.ctx.logger.warn("Failed to load profile map", { meta: { error: require_dist.errMsg(err) } });
35224
35430
  }
35225
- const probeSnapshotsState = this.state("probeSnapshots", ProbedSnapshotMapSchema, {});
35226
- this.probeSnapshotsState = probeSnapshotsState;
35431
+ const probeSnapshots = probeSnapshotsStore(this.ctx.api.settingsStore, this.ctx.logger);
35432
+ this.probeSnapshots = probeSnapshots;
35227
35433
  try {
35228
- const blob = await probeSnapshotsState.get();
35434
+ const rows = await probeSnapshots.readAll();
35435
+ const blob = Object.fromEntries(rows);
35229
35436
  this.brokerManager.loadProbeSnapshots(blob);
35230
- this.ctx.logger.info("Loaded persisted probe snapshots", { meta: { streamCount: Object.keys(blob).length } });
35437
+ this.ctx.logger.info("Loaded persisted probe snapshots", { meta: { streamCount: rows.size } });
35231
35438
  } catch (err) {
35232
35439
  this.ctx.logger.warn("Failed to load probe snapshots", { meta: { error: require_dist.errMsg(err) } });
35233
35440
  }
@@ -35584,17 +35791,54 @@ var StreamBrokerAddon = class extends require_dist.BaseAddon {
35584
35791
  this.flushProbeSnapshots(timestamp, false);
35585
35792
  }
35586
35793
  /**
35794
+ * Declare the three row collections that replaced the `probeSnapshots`,
35795
+ * `rtspTokens` and `profileMap` blob keys. Idempotent — a re-declaration of
35796
+ * the same shape is a no-op in the engine.
35797
+ *
35798
+ * Not swallowed: a broker whose `stream-broker:rtsp-tokens` is undeclared
35799
+ * mints a new token for every stream and every external NVR URL in the
35800
+ * installation goes dead. Crashing loudly is bounded by `CrashSupervisor`
35801
+ * (D6); silently reissuing tokens is not.
35802
+ */
35803
+ async declareRowCollections() {
35804
+ const store = this.ctx.api.settingsStore;
35805
+ const logger = this.ctx.logger;
35806
+ await probeSnapshotsStore(store, logger).declare();
35807
+ await rtspTokensStore(store, logger).declare();
35808
+ await profileMapStore(store, logger).declare();
35809
+ }
35810
+ /**
35811
+ * Drop the flattened keys from `stream-broker:addon-settings` / `root`, each
35812
+ * gated on its own successor collection being non-empty. `rtspEnabled` and
35813
+ * `deviceOverrides` stay — see `broker-row-stores.ts` for why.
35814
+ *
35815
+ * Best-effort: a purge that throws must not take the broker down. The keys
35816
+ * are dead weight, not a fault.
35817
+ */
35818
+ async purgeFlattenedSettingsKeys() {
35819
+ try {
35820
+ await require_retire_root_keys.retireRootKeys({
35821
+ store: this.ctx.api.settingsStore,
35822
+ addonId: STREAM_BROKER_ADDON_ID,
35823
+ logger: this.ctx.logger,
35824
+ specs: STREAM_BROKER_RETIRED_ROOT_KEYS
35825
+ });
35826
+ } catch (err) {
35827
+ this.ctx.logger.warn("stream-broker: retiring the flattened settings keys failed", { meta: { error: require_dist.errMsg(err) } });
35828
+ }
35829
+ }
35830
+ /**
35587
35831
  * Persist the manager's probe map to the durable handle. Throttled to
35588
35832
  * PROBE_SNAPSHOTS_PERSIST_INTERVAL_MS (or `force` on shutdown) so a busy
35589
35833
  * streaming fleet doesn't drive a write per metrics tick.
35590
35834
  */
35591
35835
  flushProbeSnapshots(now, force) {
35592
- const state = this.probeSnapshotsState;
35836
+ const store = this.probeSnapshots;
35593
35837
  const manager = this.brokerManager;
35594
- if (!state || !manager) return;
35838
+ if (!store || !manager) return;
35595
35839
  if (!force && now - this.lastProbePersistAt < PROBE_SNAPSHOTS_PERSIST_INTERVAL_MS) return;
35596
35840
  this.lastProbePersistAt = now;
35597
- state.set(manager.getProbeSnapshots()).catch((err) => {
35841
+ store.replaceAll(new Map(Object.entries(manager.getProbeSnapshots()))).catch((err) => {
35598
35842
  this.ctx.logger.warn("Failed to persist probe snapshots", { meta: { error: require_dist.errMsg(err) } });
35599
35843
  });
35600
35844
  }