@camstack/addon-post-analysis 1.2.144 → 1.2.147

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.
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-C8RL0Hzy.js");
5
+ const require_dist = require("../dist-1xYuXmDS.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_dist.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -40213,9 +40213,6 @@ function readJpegSize(data) {
40213
40213
  * table or the disk it consumes."
40214
40214
  */
40215
40215
  var RETRAIN_FRAMES_COLLECTION = "pipeline-analytics:retrain-frames";
40216
- /** Storage location for retrain copies — the same default media root the event
40217
- * media lives on, under a prefix nothing else enumerates. */
40218
- var RETRAIN_MEDIA_LOCATION = "eventMedia";
40219
40216
  /** Path prefix of every retrain copy. Never `<deviceId>/events/…`: the orphan
40220
40217
  * audit resolves ownership from INDEX rows, but a future filesystem-side tool
40221
40218
  * must be able to tell the two corpora apart by path alone. */
@@ -40308,6 +40305,7 @@ var RetrainFrameStore = class {
40308
40305
  readSource;
40309
40306
  now;
40310
40307
  writeGate;
40308
+ resolveEventMediaLocation;
40311
40309
  constructor(deps) {
40312
40310
  this.store = deps.store;
40313
40311
  this.storage = deps.storage;
@@ -40315,6 +40313,29 @@ var RetrainFrameStore = class {
40315
40313
  this.readSource = deps.readSource;
40316
40314
  this.now = deps.now;
40317
40315
  this.writeGate = deps.writeGate;
40316
+ this.resolveEventMediaLocation = deps.resolveEventMediaLocation;
40317
+ }
40318
+ /**
40319
+ * The location a NEW copy is written to, and the id its row NAMES.
40320
+ *
40321
+ * `null` = "no concrete location was resolved", which persists as the legacy
40322
+ * NULL row. A resolver that throws must not refuse the copy — losing an
40323
+ * operator's selection is worse than an unstamped row the seal can fix — but
40324
+ * it says so, because that branch re-creates the exact population a
40325
+ * non-blocking migration is gated on.
40326
+ */
40327
+ async resolveWriteLocation(deviceId) {
40328
+ if (!this.resolveEventMediaLocation) return null;
40329
+ try {
40330
+ const resolved = await this.resolveEventMediaLocation(deviceId);
40331
+ return resolved === "eventMedia" ? null : resolved;
40332
+ } catch (err) {
40333
+ this.logger.warn("retrain copy could not resolve its eventMedia location — the row is written UNSTAMPED and a storage repoint would re-aim it at a disk that does not hold it", {
40334
+ tags: { deviceId },
40335
+ meta: { error: String(err) }
40336
+ });
40337
+ return null;
40338
+ }
40318
40339
  }
40319
40340
  static async declare(store) {
40320
40341
  await store.declareCollection.mutate({
@@ -40473,6 +40494,7 @@ var RetrainFrameStore = class {
40473
40494
  };
40474
40495
  }
40475
40496
  const path = framePath(input.deviceId, input.trackId, frameId);
40497
+ const locationId = await this.resolveWriteLocation(input.deviceId);
40476
40498
  const frame = {
40477
40499
  frameId,
40478
40500
  deviceId: input.deviceId,
@@ -40483,11 +40505,11 @@ var RetrainFrameStore = class {
40483
40505
  width: size.width,
40484
40506
  height: size.height,
40485
40507
  copiedAt: this.now(),
40486
- locationId: null
40508
+ locationId
40487
40509
  };
40488
40510
  const persist = async () => {
40489
40511
  await this.storage.write({
40490
- location: RETRAIN_MEDIA_LOCATION,
40512
+ location: locationId ?? "eventMedia",
40491
40513
  relativePath: path,
40492
40514
  data: bytes
40493
40515
  });
@@ -40504,7 +40526,7 @@ var RetrainFrameStore = class {
40504
40526
  width: frame.width,
40505
40527
  height: frame.height,
40506
40528
  copiedAt: frame.copiedAt,
40507
- locationId: null
40529
+ locationId: frame.locationId
40508
40530
  }
40509
40531
  });
40510
40532
  };
@@ -43503,6 +43525,13 @@ function pickWritableMediaLocation(locations, currentDefaultId) {
43503
43525
  //#region src/pipeline-analytics/media-relocate-engine.ts
43504
43526
  var PAGE_SIZE = 200;
43505
43527
  var DEFAULT_THROTTLE_MBPS = 40;
43528
+ /** What a pass touches, reported on the job so an ops-log row says which of the
43529
+ * three passes ran without anyone reading this file. */
43530
+ function entitiesFor(mode) {
43531
+ if (mode === "seal") return ["mediaRows", "retrainFrameRows"];
43532
+ if (mode === "gallery") return ["galleryMedia"];
43533
+ return ["media", "retrainFrames"];
43534
+ }
43506
43535
  function snapshot(j) {
43507
43536
  return {
43508
43537
  jobId: j.jobId,
@@ -43510,7 +43539,7 @@ function snapshot(j) {
43510
43539
  fromLocationId: j.fromLocationId,
43511
43540
  toLocationId: j.toLocationId,
43512
43541
  deviceId: j.deviceId,
43513
- entities: ["media", "retrainFrames"],
43542
+ entities: [...entitiesFor(j.mode)],
43514
43543
  filesMoved: j.filesMoved,
43515
43544
  bytesMoved: j.bytesMoved,
43516
43545
  filesTotal: null,
@@ -43519,6 +43548,22 @@ function snapshot(j) {
43519
43548
  error: j.error
43520
43549
  };
43521
43550
  }
43551
+ /**
43552
+ * True when the row names NO location — the population `seal` exists for.
43553
+ *
43554
+ * Deliberately NOT `mediaRowLocation(data) === DEFAULT_MEDIA_LOCATION`: that
43555
+ * helper collapses "unstamped" and "stamped with the bare type ref" into one
43556
+ * answer, and only the first is a row whose meaning changes under a repoint.
43557
+ */
43558
+ function rowIsUnstamped(data) {
43559
+ const id = data["locationId"];
43560
+ return !(typeof id === "string" && id.length > 0);
43561
+ }
43562
+ /** True for the enrolled-gallery rows `move` skips and `gallery` selects. */
43563
+ function rowIsGallery(data) {
43564
+ const ownerKind = String(data["ownerKind"] ?? "");
43565
+ return isMediaOwnerKind(ownerKind) && isRetentionExemptOwnerKind(ownerKind);
43566
+ }
43522
43567
  var MediaRelocateEngine = class {
43523
43568
  deps;
43524
43569
  jobs = /* @__PURE__ */ new Map();
@@ -43534,11 +43579,34 @@ var MediaRelocateEngine = class {
43534
43579
  job.cancelRequested = true;
43535
43580
  return true;
43536
43581
  }
43582
+ /**
43583
+ * How many rows still carry no `locationId`, per collection.
43584
+ *
43585
+ * Read-only, and the ONLY number the migration planner's non-blocking gate
43586
+ * is allowed to trust: it is counted here, against the rows themselves,
43587
+ * rather than inferred from "a seal job reported done" — a seal that failed
43588
+ * some rows still reports a terminal state.
43589
+ */
43590
+ async countUnstamped() {
43591
+ const media = await this.countUnstampedIn(MEDIA_COLLECTION, "timestamp");
43592
+ const retrainFrames = await this.countUnstampedIn(RETRAIN_FRAMES_COLLECTION, "copiedAt");
43593
+ return {
43594
+ media,
43595
+ retrainFrames,
43596
+ total: media + retrainFrames
43597
+ };
43598
+ }
43599
+ async countUnstampedIn(collection, orderField) {
43600
+ let unstamped = 0;
43601
+ for await (const row of this.pageRows(collection, orderField)) if (rowIsUnstamped(row.data)) unstamped++;
43602
+ return unstamped;
43603
+ }
43537
43604
  start(input) {
43538
43605
  for (const j of this.jobs.values()) if (j.state === "running") throw new Error(`a media relocation is already running (${j.jobId})`);
43539
43606
  const job = {
43540
43607
  jobId: this.deps.newId(),
43541
43608
  state: "running",
43609
+ mode: input.mode ?? "move",
43542
43610
  fromLocationId: "*",
43543
43611
  toLocationId: input.toLocationId,
43544
43612
  deviceId: null,
@@ -43553,130 +43621,104 @@ var MediaRelocateEngine = class {
43553
43621
  this.run(job, input.throttleMbps ?? DEFAULT_THROTTLE_MBPS);
43554
43622
  return job.jobId;
43555
43623
  }
43624
+ /**
43625
+ * Page one collection oldest-first, yielding every row exactly once.
43626
+ *
43627
+ * The cursor is `(orderField, offset-within-ties)` rather than a plain
43628
+ * offset: 201 rows sharing one timestamp is a real shape here (a burst of
43629
+ * snapshots for one track), and a plain `offset` walk over a collection being
43630
+ * mutated underneath is how rows get skipped. Rows only ever gain a
43631
+ * `locationId` during a pass — never a new order key — so appending inserts
43632
+ * land after the cursor and are picked up, and nothing is missed.
43633
+ */
43634
+ async *pageRows(collection, orderField) {
43635
+ let cursor = 0;
43636
+ let offsetAtCursor = 0;
43637
+ for (;;) {
43638
+ const rows = await this.deps.store.query.query({
43639
+ collection,
43640
+ filter: {
43641
+ whereBetween: { [orderField]: [cursor, Number.MAX_SAFE_INTEGER] },
43642
+ orderBy: {
43643
+ field: orderField,
43644
+ direction: "asc"
43645
+ },
43646
+ limit: PAGE_SIZE,
43647
+ offset: offsetAtCursor
43648
+ }
43649
+ });
43650
+ if (rows.length === 0) return;
43651
+ for (const row of rows) yield row;
43652
+ const last = rows[rows.length - 1];
43653
+ if (last === void 0) return;
43654
+ const lastKey = Number(last.data[orderField] ?? cursor);
43655
+ if (lastKey === cursor) offsetAtCursor += rows.length;
43656
+ else {
43657
+ cursor = lastKey;
43658
+ offsetAtCursor = rows.filter((row) => Number(row.data[orderField]) === lastKey).length;
43659
+ }
43660
+ if (rows.length < PAGE_SIZE) return;
43661
+ }
43662
+ }
43663
+ /** Does this pass act on this row at all? */
43664
+ selects(job, data, isRetrain) {
43665
+ if (job.mode === "seal") return rowIsUnstamped(data);
43666
+ if (mediaRowLocation(data) === job.toLocationId) return false;
43667
+ if (job.mode === "gallery") return !isRetrain && rowIsGallery(data);
43668
+ return isRetrain || !rowIsGallery(data);
43669
+ }
43556
43670
  async run(job, throttleMbps) {
43557
43671
  const sleep = this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
43558
43672
  const bytesPerMs = throttleMbps * 1024 * 1024 / 1e3;
43559
43673
  try {
43560
43674
  await this.deps.resolveTargetRoot(job.toLocationId);
43561
43675
  let failedRows = 0;
43562
- let cursor = 0;
43563
- let offsetAtCursor = 0;
43564
- for (;;) {
43676
+ /** Selected rows that name no blob path. Counted and reported ONCE per
43677
+ * pass rather than per row: a row with no path names no bytes anywhere,
43678
+ * so there is nothing a repoint can orphan — but a pass that steps over
43679
+ * rows in silence reads as "there were none", which is how a migration
43680
+ * gets believed. */
43681
+ let pathlessRows = 0;
43682
+ const collections = job.mode === "gallery" ? [{
43683
+ name: MEDIA_COLLECTION,
43684
+ order: "timestamp"
43685
+ }] : [{
43686
+ name: MEDIA_COLLECTION,
43687
+ order: "timestamp"
43688
+ }, {
43689
+ name: RETRAIN_FRAMES_COLLECTION,
43690
+ order: "copiedAt"
43691
+ }];
43692
+ for (const collection of collections) {
43565
43693
  if (job.cancelRequested) break;
43566
- const rows = await this.deps.store.query.query({
43567
- collection: MEDIA_COLLECTION,
43568
- filter: {
43569
- whereBetween: { timestamp: [cursor, Number.MAX_SAFE_INTEGER] },
43570
- orderBy: {
43571
- field: "timestamp",
43572
- direction: "asc"
43573
- },
43574
- limit: PAGE_SIZE,
43575
- offset: offsetAtCursor
43576
- }
43577
- });
43578
- if (rows.length === 0) break;
43579
- for (const row of rows) {
43694
+ const isRetrain = collection.name === RETRAIN_FRAMES_COLLECTION;
43695
+ for await (const row of this.pageRows(collection.name, collection.order)) {
43580
43696
  if (job.cancelRequested) break;
43581
43697
  const data = row.data;
43582
- const ownerKind = String(data["ownerKind"] ?? "");
43583
- if (isMediaOwnerKind(ownerKind) && isRetentionExemptOwnerKind(ownerKind)) continue;
43584
- const from = mediaRowLocation(data);
43585
- if (from === job.toLocationId) continue;
43586
- const relativePath = String(data["path"] ?? "");
43587
- if (relativePath.length === 0) continue;
43588
- try {
43589
- if (await this.samePhysicalLocation(from, job.toLocationId)) {
43590
- await this.deps.store.set.mutate({
43591
- collection: MEDIA_COLLECTION,
43698
+ if (!this.selects(job, data, isRetrain)) continue;
43699
+ if (job.mode === "seal") {
43700
+ try {
43701
+ await this.stamp(collection.name, row.id, data, job.toLocationId);
43702
+ job.filesMoved++;
43703
+ } catch (err) {
43704
+ failedRows++;
43705
+ this.deps.logger.debug("event-media seal row failed", { meta: {
43706
+ collection: collection.name,
43592
43707
  key: row.id,
43593
- value: {
43594
- ...data,
43595
- locationId: job.toLocationId
43596
- }
43597
- });
43598
- continue;
43708
+ error: String(err)
43709
+ } });
43599
43710
  }
43600
- const bytes = await this.deps.storage.read({
43601
- location: from,
43602
- relativePath
43603
- });
43604
- await this.deps.storage.write({
43605
- location: job.toLocationId,
43606
- relativePath,
43607
- data: bytes
43608
- });
43609
- if ((await this.deps.storage.read({
43610
- location: job.toLocationId,
43611
- relativePath
43612
- })).length !== bytes.length) throw new Error(`relocated media size mismatch for ${relativePath}`);
43613
- await this.deps.store.set.mutate({
43614
- collection: MEDIA_COLLECTION,
43615
- key: row.id,
43616
- value: {
43617
- ...data,
43618
- locationId: job.toLocationId
43619
- }
43620
- });
43621
- await this.deps.storage.delete({
43622
- location: from,
43623
- relativePath
43624
- });
43625
- job.filesMoved++;
43626
- job.bytesMoved += bytes.length;
43627
- await sleep(bytes.length / bytesPerMs);
43628
- } catch (err) {
43629
- failedRows++;
43630
- this.deps.logger.debug("media relocate row failed", { meta: {
43631
- key: row.id,
43632
- error: String(err)
43633
- } });
43634
- }
43635
- }
43636
- const last = rows[rows.length - 1];
43637
- const lastTs = Number(last.data["timestamp"] ?? cursor);
43638
- if (lastTs === cursor) offsetAtCursor += rows.length;
43639
- else {
43640
- cursor = lastTs;
43641
- offsetAtCursor = rows.filter((row) => Number(row.data["timestamp"]) === lastTs).length;
43642
- }
43643
- if (rows.length < PAGE_SIZE) break;
43644
- }
43645
- let retrainOffset = 0;
43646
- for (;;) {
43647
- if (job.cancelRequested) break;
43648
- const rows = await this.deps.store.query.query({
43649
- collection: RETRAIN_FRAMES_COLLECTION,
43650
- filter: {
43651
- orderBy: {
43652
- field: "copiedAt",
43653
- direction: "asc"
43654
- },
43655
- limit: PAGE_SIZE,
43656
- offset: retrainOffset
43711
+ continue;
43657
43712
  }
43658
- });
43659
- if (rows.length === 0) break;
43660
- for (const row of rows) {
43661
- if (job.cancelRequested) break;
43662
- const data = row.data;
43663
43713
  const from = mediaRowLocation(data);
43664
- if (from === job.toLocationId) continue;
43665
43714
  const relativePath = String(data["path"] ?? "");
43666
43715
  if (relativePath.length === 0) {
43667
- failedRows++;
43716
+ pathlessRows++;
43668
43717
  continue;
43669
43718
  }
43670
43719
  try {
43671
43720
  if (await this.samePhysicalLocation(from, job.toLocationId)) {
43672
- await this.deps.store.set.mutate({
43673
- collection: RETRAIN_FRAMES_COLLECTION,
43674
- key: row.id,
43675
- value: {
43676
- ...data,
43677
- locationId: job.toLocationId
43678
- }
43679
- });
43721
+ await this.stamp(collection.name, row.id, data, job.toLocationId);
43680
43722
  continue;
43681
43723
  }
43682
43724
  const bytes = await this.deps.storage.read({
@@ -43691,15 +43733,8 @@ var MediaRelocateEngine = class {
43691
43733
  if ((await this.deps.storage.read({
43692
43734
  location: job.toLocationId,
43693
43735
  relativePath
43694
- })).length !== bytes.length) throw new Error(`relocated retrain frame size mismatch for ${relativePath}`);
43695
- await this.deps.store.set.mutate({
43696
- collection: RETRAIN_FRAMES_COLLECTION,
43697
- key: row.id,
43698
- value: {
43699
- ...data,
43700
- locationId: job.toLocationId
43701
- }
43702
- });
43736
+ })).length !== bytes.length) throw new Error(`relocated media size mismatch for ${relativePath}`);
43737
+ await this.stamp(collection.name, row.id, data, job.toLocationId);
43703
43738
  await this.deps.storage.delete({
43704
43739
  location: from,
43705
43740
  relativePath
@@ -43709,22 +43744,27 @@ var MediaRelocateEngine = class {
43709
43744
  await sleep(bytes.length / bytesPerMs);
43710
43745
  } catch (err) {
43711
43746
  failedRows++;
43712
- this.deps.logger.debug("retrain frame relocate row failed", { meta: {
43747
+ this.deps.logger.debug("media relocate row failed", { meta: {
43748
+ collection: collection.name,
43713
43749
  key: row.id,
43714
43750
  error: String(err)
43715
43751
  } });
43716
43752
  }
43717
43753
  }
43718
- retrainOffset += rows.length;
43719
- if (rows.length < PAGE_SIZE) break;
43720
43754
  }
43721
- if (failedRows > 0) throw new Error(`event-media relocation left ${failedRows} row(s) unmoved`);
43755
+ if (pathlessRows > 0) this.deps.logger.warn("media relocate stepped over rows that name no blob path", { meta: {
43756
+ jobId: job.jobId,
43757
+ mode: job.mode,
43758
+ rows: pathlessRows
43759
+ } });
43760
+ if (failedRows > 0) throw new Error(`event-media ${job.mode} left ${failedRows} row(s) unmoved`);
43722
43761
  job.state = job.cancelRequested ? "cancelled" : "done";
43723
43762
  } catch (err) {
43724
43763
  job.state = "failed";
43725
43764
  job.error = err instanceof Error ? err.message : String(err);
43726
43765
  this.deps.logger.warn("media relocate job failed", { meta: {
43727
43766
  jobId: job.jobId,
43767
+ mode: job.mode,
43728
43768
  error: job.error
43729
43769
  } });
43730
43770
  } finally {
@@ -43732,6 +43772,16 @@ var MediaRelocateEngine = class {
43732
43772
  this.deps.onFinished?.(snapshot(job));
43733
43773
  }
43734
43774
  }
43775
+ async stamp(collection, key, data, locationId) {
43776
+ await this.deps.store.set.mutate({
43777
+ collection,
43778
+ key,
43779
+ value: {
43780
+ ...data,
43781
+ locationId
43782
+ }
43783
+ });
43784
+ }
43735
43785
  async samePhysicalLocation(from, to) {
43736
43786
  if (from === to) return true;
43737
43787
  try {
@@ -62299,6 +62349,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
62299
62349
  logger: logger.child("RetrainFrames"),
62300
62350
  now: () => Date.now(),
62301
62351
  writeGate: this.mediaWriteGate,
62352
+ resolveEventMediaLocation: (deviceId) => this.resolveEventMediaWriteLocation(api, logger, deviceId),
62302
62353
  readSource: async (mediaKey) => {
62303
62354
  const file = await mediaStore.getByKey(mediaKey);
62304
62355
  return file === null ? null : Buffer.from(file.base64, "base64");
@@ -68975,10 +69026,22 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
68975
69026
  const jobId = this.mediaRelocate.start(input);
68976
69027
  this.ctx.logger.info("pipeline-analytics: media relocate job enqueued", { meta: {
68977
69028
  jobId,
69029
+ mode: input.mode ?? "move",
68978
69030
  to: input.toLocationId
68979
69031
  } });
68980
69032
  return { jobId };
68981
69033
  }
69034
+ /**
69035
+ * How many media / retrain rows still carry no `locationId`.
69036
+ *
69037
+ * Counted against the rows, never inferred from a seal job's terminal state:
69038
+ * the migration planner refuses a non-blocking `eventMedia` cutover on this
69039
+ * number, and a seal that failed some rows still finishes.
69040
+ */
69041
+ async countUnstampedEventMedia() {
69042
+ if (!this.mediaRelocate) throw new Error("media relocation unavailable");
69043
+ return this.mediaRelocate.countUnstamped();
69044
+ }
68982
69045
  /** Every relocate job this addon knows about, newest first (in RAM: the
68983
69046
  * move is resumable, so a lost list costs nothing but the display). */
68984
69047
  async listRelocateMediaJobs() {