@camstack/addon-post-analysis 1.2.126 → 1.2.128
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.
- package/dist/{dist-BDy0xpfI.mjs → dist-CRDS5ykk.mjs} +217 -51
- package/dist/{dist-BarcMD8R.js → dist-D6O14xf_.js} +217 -51
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Dj-k47_c.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BZm5TUIO.mjs} +3 -3
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Kj0oid5Z.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-UGfPXz4L.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-XLZIUPmq.mjs → hostInit-CQz_Ev1M.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +418 -28
- package/dist/pipeline-analytics/index.mjs +418 -28
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -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-
|
|
5
|
+
const require_dist = require("../dist-D6O14xf_.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");
|
|
@@ -31823,11 +31823,13 @@ var MediaStore = class {
|
|
|
31823
31823
|
store;
|
|
31824
31824
|
logger;
|
|
31825
31825
|
writeGate;
|
|
31826
|
+
resolveEventMediaLocation;
|
|
31826
31827
|
constructor(deps) {
|
|
31827
31828
|
this.storage = deps.storage;
|
|
31828
31829
|
this.store = deps.store;
|
|
31829
31830
|
this.logger = deps.logger;
|
|
31830
31831
|
this.writeGate = deps.writeGate;
|
|
31832
|
+
this.resolveEventMediaLocation = deps.resolveEventMediaLocation;
|
|
31831
31833
|
}
|
|
31832
31834
|
gated(mutation) {
|
|
31833
31835
|
return this.writeGate ? this.writeGate.run(mutation) : mutation();
|
|
@@ -31848,7 +31850,8 @@ var MediaStore = class {
|
|
|
31848
31850
|
async putUnchecked(params) {
|
|
31849
31851
|
const key = buildKey(params);
|
|
31850
31852
|
const path = buildPath(params);
|
|
31851
|
-
const
|
|
31853
|
+
const staticLocation = locationForOwnerKind(params.ownerKind);
|
|
31854
|
+
const location = staticLocation === "eventMedia" && this.resolveEventMediaLocation ? await this.resolveEventMediaLocation(params.deviceId) : staticLocation;
|
|
31852
31855
|
const record = {
|
|
31853
31856
|
deviceId: params.deviceId,
|
|
31854
31857
|
ownerKind: params.ownerKind,
|
|
@@ -41541,6 +41544,21 @@ var MediaCaptureLogAggregator = class {
|
|
|
41541
41544
|
}
|
|
41542
41545
|
};
|
|
41543
41546
|
//#endregion
|
|
41547
|
+
//#region src/pipeline-analytics/media-locations.ts
|
|
41548
|
+
/**
|
|
41549
|
+
* Picks the location a new `eventMedia` blob should be written to, or `null`
|
|
41550
|
+
* when none is writable — the caller MUST refuse the write rather than fall
|
|
41551
|
+
* back to a read-only location, which would fail far downstream as an opaque
|
|
41552
|
+
* filesystem error instead of a named storage decision.
|
|
41553
|
+
*/
|
|
41554
|
+
function pickWritableMediaLocation(locations, currentDefaultId) {
|
|
41555
|
+
const writable = locations.filter((l) => l.enabled && !l.readOnly);
|
|
41556
|
+
if (writable.length === 0) return null;
|
|
41557
|
+
const keep = writable.find((l) => l.id === currentDefaultId);
|
|
41558
|
+
if (keep) return keep.id;
|
|
41559
|
+
return writable.toSorted((a, b) => b.headroomBytes - a.headroomBytes)[0]?.id ?? null;
|
|
41560
|
+
}
|
|
41561
|
+
//#endregion
|
|
41544
41562
|
//#region src/pipeline-analytics/media-relocate-engine.ts
|
|
41545
41563
|
var PAGE_SIZE = 200;
|
|
41546
41564
|
var DEFAULT_THROTTLE_MBPS = 40;
|
|
@@ -46374,6 +46392,50 @@ var AUDIO_COLUMNS = [
|
|
|
46374
46392
|
type: "JSON"
|
|
46375
46393
|
}
|
|
46376
46394
|
];
|
|
46395
|
+
/**
|
|
46396
|
+
* The columns `slimMotion` reads. `id` is the primary key — always returned,
|
|
46397
|
+
* never part of a projection.
|
|
46398
|
+
*
|
|
46399
|
+
* What it leaves behind is the point: `regions` is a JSON array with one entry
|
|
46400
|
+
* per motion rectangle, and it is the only reason a motion row is not tiny.
|
|
46401
|
+
* `frameWidth` / `frameHeight` go with it — they exist to interpret `regions`
|
|
46402
|
+
* and mean nothing without it.
|
|
46403
|
+
*/
|
|
46404
|
+
var SLIM_MOTION_COLUMNS = [
|
|
46405
|
+
"deviceId",
|
|
46406
|
+
"timestamp",
|
|
46407
|
+
"regionCount"
|
|
46408
|
+
];
|
|
46409
|
+
/**
|
|
46410
|
+
* The columns `slimObject` reads — and ONLY those.
|
|
46411
|
+
*
|
|
46412
|
+
* Four JSON columns are dropped (`bbox`, `zones`, `labelMeta`, `subLabelMeta`)
|
|
46413
|
+
* plus the numbers that only exist to argue about a value after the fact
|
|
46414
|
+
* (`confidence`, `labelScore`, `subLabelScore`) and the frame geometry that,
|
|
46415
|
+
* like motion's, is meaningless without the box it describes.
|
|
46416
|
+
*
|
|
46417
|
+
* `label` / `subLabel` stay because the timeline renders `subLabel ?? label`,
|
|
46418
|
+
* which cannot be evaluated from half a row. `frameId` / `trackId` /
|
|
46419
|
+
* `importance` stay because they are the grouping and sort keys the events page
|
|
46420
|
+
* needs — all three are light.
|
|
46421
|
+
*
|
|
46422
|
+
* ⚠ `slimObject` calls `readTieredLabelColumns`, which reads all SIX tiered
|
|
46423
|
+
* columns and returns the four unprojected ones as `undefined`. That is
|
|
46424
|
+
* harmless here only because `slimObject` uses exactly `label` and `subLabel`
|
|
46425
|
+
* from what it gets back. If it ever starts reading a score, this list is the
|
|
46426
|
+
* thing that has to learn about it — the failure would otherwise be a score
|
|
46427
|
+
* that reads as absent on every slim row.
|
|
46428
|
+
*/
|
|
46429
|
+
var SLIM_OBJECT_COLUMNS = [
|
|
46430
|
+
"deviceId",
|
|
46431
|
+
"timestamp",
|
|
46432
|
+
"className",
|
|
46433
|
+
"frameId",
|
|
46434
|
+
"trackId",
|
|
46435
|
+
"importance",
|
|
46436
|
+
"label",
|
|
46437
|
+
"subLabel"
|
|
46438
|
+
];
|
|
46377
46439
|
var COMMON_INDEXES = (prefix) => [{
|
|
46378
46440
|
name: `idx_${prefix}_device_ts`,
|
|
46379
46441
|
columns: ["deviceId", "timestamp"]
|
|
@@ -46557,6 +46619,19 @@ var EventStore = class {
|
|
|
46557
46619
|
filter
|
|
46558
46620
|
})]);
|
|
46559
46621
|
}
|
|
46622
|
+
/**
|
|
46623
|
+
* Spreadable `columns` for a slim read — omitted entirely for `full`.
|
|
46624
|
+
*
|
|
46625
|
+
* The projection is decided in ONE place per collection ({@link
|
|
46626
|
+
* SLIM_MOTION_COLUMNS} / {@link SLIM_OBJECT_COLUMNS}) because a projection is
|
|
46627
|
+
* a promise between the SELECT and the mapper: the day they disagree nothing
|
|
46628
|
+
* fails. SQLite returns the row without the column, `slimObject` reads
|
|
46629
|
+
* `data['className']`, gets `undefined`, and hands back an event whose class
|
|
46630
|
+
* is missing — indistinguishable from a row that never had one.
|
|
46631
|
+
*/
|
|
46632
|
+
slimColumns(projection, columns) {
|
|
46633
|
+
return projection === "slim" ? { columns } : {};
|
|
46634
|
+
}
|
|
46560
46635
|
buildFilter(q) {
|
|
46561
46636
|
const filter = {
|
|
46562
46637
|
where: { deviceId: q.deviceId },
|
|
@@ -46572,7 +46647,8 @@ var EventStore = class {
|
|
|
46572
46647
|
async queryMotion(q) {
|
|
46573
46648
|
const rows = await this.store.query.query({
|
|
46574
46649
|
collection: MOTION_EVENTS_COLLECTION,
|
|
46575
|
-
filter: this.buildFilter(q)
|
|
46650
|
+
filter: this.buildFilter(q),
|
|
46651
|
+
...this.slimColumns(q.projection, SLIM_MOTION_COLUMNS)
|
|
46576
46652
|
});
|
|
46577
46653
|
if (q.projection === "slim") return rows.map((r) => slimMotion(r.id, stripNulls$1(r.data)));
|
|
46578
46654
|
return rows.map((r) => {
|
|
@@ -46642,7 +46718,8 @@ var EventStore = class {
|
|
|
46642
46718
|
}
|
|
46643
46719
|
const rows = await this.store.query.query({
|
|
46644
46720
|
collection: OBJECT_EVENTS_COLLECTION,
|
|
46645
|
-
filter
|
|
46721
|
+
filter,
|
|
46722
|
+
...this.slimColumns(q.projection, SLIM_OBJECT_COLUMNS)
|
|
46646
46723
|
});
|
|
46647
46724
|
if (q.projection === "slim") return rows.map((r) => slimObject(r.id, stripNulls$1(r.data)));
|
|
46648
46725
|
return rows.map((r) => {
|
|
@@ -49301,6 +49378,16 @@ function trackColumnsFor(projection, needsPositions = false) {
|
|
|
49301
49378
|
if (projection !== "slim") return void 0;
|
|
49302
49379
|
return needsPositions ? [...SLIM_TRACK_COLUMN_NAMES, "positions"] : SLIM_TRACK_COLUMN_NAMES;
|
|
49303
49380
|
}
|
|
49381
|
+
/**
|
|
49382
|
+
* The SELECT list for an IDS-ONLY recent page: the primary key (free, always
|
|
49383
|
+
* returned) plus `lastSeen`.
|
|
49384
|
+
*
|
|
49385
|
+
* `lastSeen` is not an optimisation to shave off later — {@link
|
|
49386
|
+
* TrackStore.recentPage} orders on it, windows on it and breaks
|
|
49387
|
+
* same-millisecond ties with it. Reading ids without it would produce a page in
|
|
49388
|
+
* whatever order the engine felt like and a cursor that cannot resume.
|
|
49389
|
+
*/
|
|
49390
|
+
var RECENT_IDS_COLUMNS = ["lastSeen"];
|
|
49304
49391
|
/** Spreadable `columns` for a store query — omitted entirely for `full`. */
|
|
49305
49392
|
function projectionArg(projection, needsPositions = false) {
|
|
49306
49393
|
const columns = trackColumnsFor(projection, needsPositions);
|
|
@@ -50555,10 +50642,48 @@ var TrackStore = class {
|
|
|
50555
50642
|
* Errors propagate (no partial merges): a failed device query fails the
|
|
50556
50643
|
* page rather than silently returning an incomplete window.
|
|
50557
50644
|
*/
|
|
50645
|
+
/**
|
|
50646
|
+
* A page of RECENT track ids, and nothing else.
|
|
50647
|
+
*
|
|
50648
|
+
* Two callers only ever wanted the strings — `collectLiveTrackIds` (the
|
|
50649
|
+
* orphan audit's live set) and `listTrackIdPage` (the embedding rebuild's
|
|
50650
|
+
* cursor walk) — and both were reading whole tracks to build them. This reads
|
|
50651
|
+
* the primary key plus `lastSeen`, which is not a saving to be traded away:
|
|
50652
|
+
* the cursor, the descending order and the same-millisecond tie-break are all
|
|
50653
|
+
* defined on `lastSeen`, so a page without it would be unordered and
|
|
50654
|
+
* unresumable.
|
|
50655
|
+
*
|
|
50656
|
+
* It shares {@link recentPage} with {@link queryRecent} rather than repeating
|
|
50657
|
+
* the window/cursor arithmetic. Two copies of a paging rule is how one of
|
|
50658
|
+
* them learns about a new bound and the other does not.
|
|
50659
|
+
*/
|
|
50660
|
+
async queryRecentIds(params) {
|
|
50661
|
+
const { page, nextCursor } = await this.recentPage(params, RECENT_IDS_COLUMNS);
|
|
50662
|
+
return {
|
|
50663
|
+
trackIds: page.map((r) => r.id),
|
|
50664
|
+
nextCursor
|
|
50665
|
+
};
|
|
50666
|
+
}
|
|
50558
50667
|
async queryRecent(params) {
|
|
50668
|
+
const { page, nextCursor } = await this.recentPage(params, trackColumnsFor(params.projection));
|
|
50669
|
+
return {
|
|
50670
|
+
tracks: page.map((r) => this.rowToTrack(r.id, r.data, params.projection)),
|
|
50671
|
+
nextCursor
|
|
50672
|
+
};
|
|
50673
|
+
}
|
|
50674
|
+
/**
|
|
50675
|
+
* The window, cursor and fan-out arithmetic behind every "recent tracks"
|
|
50676
|
+
* read — shared so the SELECT list is the only thing a caller varies.
|
|
50677
|
+
*
|
|
50678
|
+
* `columns` is passed straight through to the engine (`undefined` = the whole
|
|
50679
|
+
* row). Everything else here is projection-agnostic on purpose: which rows a
|
|
50680
|
+
* page contains, and in what order, must not depend on how much of each row
|
|
50681
|
+
* the caller asked to read.
|
|
50682
|
+
*/
|
|
50683
|
+
async recentPage(params, columns) {
|
|
50559
50684
|
const deviceIds = [...new Set(params.deviceIds)];
|
|
50560
50685
|
if (deviceIds.length === 0) return {
|
|
50561
|
-
|
|
50686
|
+
page: [],
|
|
50562
50687
|
nextCursor: null
|
|
50563
50688
|
};
|
|
50564
50689
|
const requested = Math.min(Math.max(params.limit ?? RECENT_DEFAULT_LIMIT, 1), RECENT_MAX_LIMIT);
|
|
@@ -50582,7 +50707,7 @@ var TrackStore = class {
|
|
|
50582
50707
|
const winHi = params.until ?? Number.MAX_SAFE_INTEGER;
|
|
50583
50708
|
const hi = cursor !== null ? Math.min(cursor.lastSeen, winHi) : winHi;
|
|
50584
50709
|
if (hi < lo) return {
|
|
50585
|
-
|
|
50710
|
+
page: [],
|
|
50586
50711
|
nextCursor: null
|
|
50587
50712
|
};
|
|
50588
50713
|
const stationaryExclusion = params.excludeStationaryPromotions === true ? { whereNot: { closedReason: "stationary-promotion" } } : {};
|
|
@@ -50598,7 +50723,7 @@ var TrackStore = class {
|
|
|
50598
50723
|
},
|
|
50599
50724
|
...pageLimit !== void 0 ? { limit: pageLimit } : {}
|
|
50600
50725
|
},
|
|
50601
|
-
...
|
|
50726
|
+
...columns === void 0 ? {} : { columns }
|
|
50602
50727
|
});
|
|
50603
50728
|
const perDevice = await Promise.all(deviceIds.map(async (deviceId) => {
|
|
50604
50729
|
if (cursor === null || cursor.lastSeen > hi) return fetchRange(deviceId, [lo, hi], limit + 1);
|
|
@@ -50622,10 +50747,9 @@ var TrackStore = class {
|
|
|
50622
50747
|
}
|
|
50623
50748
|
candidates.sort(compareRecentDesc);
|
|
50624
50749
|
const page = candidates.slice(0, limit);
|
|
50625
|
-
const tracks = page.map((r) => this.rowToTrack(r.id, r.data, params.projection));
|
|
50626
50750
|
const last = page[page.length - 1];
|
|
50627
50751
|
return {
|
|
50628
|
-
|
|
50752
|
+
page,
|
|
50629
50753
|
nextCursor: candidates.length > limit && last !== void 0 ? encodeRecentCursor({
|
|
50630
50754
|
lastSeen: last.lastSeen,
|
|
50631
50755
|
trackId: last.id
|
|
@@ -56928,6 +57052,144 @@ function rowFromMetadata(id, metadata) {
|
|
|
56928
57052
|
};
|
|
56929
57053
|
}
|
|
56930
57054
|
//#endregion
|
|
57055
|
+
//#region src/pipeline-analytics/store/settings-raw-mirror.ts
|
|
57056
|
+
/** How long a mirrored entry is served before a refresh is started behind the
|
|
57057
|
+
* next reader. See the module doc for why this matches the per-kind caches'
|
|
57058
|
+
* own window rather than introducing a second staleness bound. */
|
|
57059
|
+
var RAW_SETTINGS_STALE_MS = 5e3;
|
|
57060
|
+
/**
|
|
57061
|
+
* How long a FAILED refresh is not retried.
|
|
57062
|
+
*
|
|
57063
|
+
* A refresh that throws leaves `fetchedAt` where it was, so without a
|
|
57064
|
+
* cooldown every subsequent reader would start another attempt — a broken
|
|
57065
|
+
* settings store would be hammered at frame rate by the very mechanism built
|
|
57066
|
+
* to stop that.
|
|
57067
|
+
*/
|
|
57068
|
+
var RAW_SETTINGS_MISS_COOLDOWN_MS = 5e3;
|
|
57069
|
+
/**
|
|
57070
|
+
* The addon's per-device raw blob (`pipeline-analytics:addon-devices`), kept
|
|
57071
|
+
* fresh OFF every per-kind settings cache's hot path.
|
|
57072
|
+
*
|
|
57073
|
+
* `resolve` is COLD-awaiting and single-flight: a burst of settings kinds
|
|
57074
|
+
* (or frames) missing on the same device in the same tick costs ONE read,
|
|
57075
|
+
* not one per caller — the exact failure this class exists to remove.
|
|
57076
|
+
*/
|
|
57077
|
+
var RawDeviceBlobMirror = class {
|
|
57078
|
+
deps;
|
|
57079
|
+
staleMs;
|
|
57080
|
+
missCooldownMs;
|
|
57081
|
+
entries = /* @__PURE__ */ new Map();
|
|
57082
|
+
inFlight = /* @__PURE__ */ new Map();
|
|
57083
|
+
now;
|
|
57084
|
+
constructor(deps, staleMs = RAW_SETTINGS_STALE_MS, missCooldownMs = RAW_SETTINGS_MISS_COOLDOWN_MS) {
|
|
57085
|
+
this.deps = deps;
|
|
57086
|
+
this.staleMs = staleMs;
|
|
57087
|
+
this.missCooldownMs = missCooldownMs;
|
|
57088
|
+
this.now = deps.now ?? (() => Date.now());
|
|
57089
|
+
}
|
|
57090
|
+
/**
|
|
57091
|
+
* The device's raw stored blob.
|
|
57092
|
+
*
|
|
57093
|
+
* COLD awaits — the first settings kind to touch a device has to get the
|
|
57094
|
+
* real answer. WARM returns immediately, stale or not, and starts the
|
|
57095
|
+
* refresh behind the caller.
|
|
57096
|
+
*/
|
|
57097
|
+
async resolve(deviceId) {
|
|
57098
|
+
const entry = this.entries.get(deviceId);
|
|
57099
|
+
if (entry === void 0) return this.load(deviceId);
|
|
57100
|
+
if (this.now() - entry.fetchedAt >= this.staleMs) this.load(deviceId);
|
|
57101
|
+
return entry.value;
|
|
57102
|
+
}
|
|
57103
|
+
/** Drop a device's entry. Called by every in-process writer of this
|
|
57104
|
+
* device's row, so an operator's own change is never served stale. */
|
|
57105
|
+
invalidate(deviceId) {
|
|
57106
|
+
this.entries.delete(deviceId);
|
|
57107
|
+
}
|
|
57108
|
+
invalidateAll() {
|
|
57109
|
+
this.entries.clear();
|
|
57110
|
+
}
|
|
57111
|
+
async load(deviceId) {
|
|
57112
|
+
const pending = this.inFlight.get(deviceId);
|
|
57113
|
+
if (pending !== void 0) return pending;
|
|
57114
|
+
const run = this.deps.read(deviceId).then((value) => {
|
|
57115
|
+
this.entries.set(deviceId, {
|
|
57116
|
+
value,
|
|
57117
|
+
fetchedAt: this.now()
|
|
57118
|
+
});
|
|
57119
|
+
return value;
|
|
57120
|
+
}).catch((err) => {
|
|
57121
|
+
this.deps.onRefreshFailed?.(deviceId, err);
|
|
57122
|
+
const existing = this.entries.get(deviceId);
|
|
57123
|
+
if (existing !== void 0) {
|
|
57124
|
+
existing.fetchedAt = this.now() - this.staleMs + this.missCooldownMs;
|
|
57125
|
+
return existing.value;
|
|
57126
|
+
}
|
|
57127
|
+
return {};
|
|
57128
|
+
}).finally(() => {
|
|
57129
|
+
this.inFlight.delete(deviceId);
|
|
57130
|
+
});
|
|
57131
|
+
this.inFlight.set(deviceId, run);
|
|
57132
|
+
return run;
|
|
57133
|
+
}
|
|
57134
|
+
};
|
|
57135
|
+
/**
|
|
57136
|
+
* The addon's ONE `addon_settings` row (`pipeline-analytics:addon-settings`),
|
|
57137
|
+
* kept fresh OFF every per-kind settings cache's hot path. Same shape as
|
|
57138
|
+
* {@link RawDeviceBlobMirror}, singleton rather than keyed — there is exactly
|
|
57139
|
+
* one addon store per process.
|
|
57140
|
+
*/
|
|
57141
|
+
var RawGlobalStoreMirror = class {
|
|
57142
|
+
deps;
|
|
57143
|
+
staleMs;
|
|
57144
|
+
missCooldownMs;
|
|
57145
|
+
entry = null;
|
|
57146
|
+
inFlight = null;
|
|
57147
|
+
now;
|
|
57148
|
+
constructor(deps, staleMs = RAW_SETTINGS_STALE_MS, missCooldownMs = RAW_SETTINGS_MISS_COOLDOWN_MS) {
|
|
57149
|
+
this.deps = deps;
|
|
57150
|
+
this.staleMs = staleMs;
|
|
57151
|
+
this.missCooldownMs = missCooldownMs;
|
|
57152
|
+
this.now = deps.now ?? (() => Date.now());
|
|
57153
|
+
}
|
|
57154
|
+
async resolve() {
|
|
57155
|
+
const entry = this.entry;
|
|
57156
|
+
if (entry === null) return this.load();
|
|
57157
|
+
if (this.now() - entry.fetchedAt >= this.staleMs) this.load();
|
|
57158
|
+
return entry.value;
|
|
57159
|
+
}
|
|
57160
|
+
/** Drop the mirrored store. Called by every in-process global-settings
|
|
57161
|
+
* writer (`onConfigChanged`), so an operator's own change is never served
|
|
57162
|
+
* stale. */
|
|
57163
|
+
invalidate() {
|
|
57164
|
+
this.entry = null;
|
|
57165
|
+
}
|
|
57166
|
+
async load() {
|
|
57167
|
+
if (this.inFlight !== null) return this.inFlight;
|
|
57168
|
+
const run = this.deps.read().then((value) => {
|
|
57169
|
+
this.entry = {
|
|
57170
|
+
value,
|
|
57171
|
+
fetchedAt: this.now()
|
|
57172
|
+
};
|
|
57173
|
+
return value;
|
|
57174
|
+
}).catch((err) => {
|
|
57175
|
+
this.deps.onRefreshFailed?.(err);
|
|
57176
|
+
const existing = this.entry;
|
|
57177
|
+
if (existing !== null) {
|
|
57178
|
+
this.entry = {
|
|
57179
|
+
value: existing.value,
|
|
57180
|
+
fetchedAt: this.now() - this.staleMs + this.missCooldownMs
|
|
57181
|
+
};
|
|
57182
|
+
return existing.value;
|
|
57183
|
+
}
|
|
57184
|
+
return {};
|
|
57185
|
+
}).finally(() => {
|
|
57186
|
+
this.inFlight = null;
|
|
57187
|
+
});
|
|
57188
|
+
this.inFlight = run;
|
|
57189
|
+
return run;
|
|
57190
|
+
}
|
|
57191
|
+
};
|
|
57192
|
+
//#endregion
|
|
56931
57193
|
//#region src/pipeline-analytics/track-retention-sweep.ts
|
|
56932
57194
|
/**
|
|
56933
57195
|
* Sweep every device with persisted tracks: skip retention-disabled devices,
|
|
@@ -57984,6 +58246,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
57984
58246
|
/** Event-media relocation engine (entity-routing Phase 4). */
|
|
57985
58247
|
mediaRelocate = null;
|
|
57986
58248
|
mediaLocationStorage = null;
|
|
58249
|
+
/**
|
|
58250
|
+
* Cached pick for NEW `eventMedia` writes (`pickWritableMediaLocation`).
|
|
58251
|
+
* Resolved once, never per blob (no-RPC-per-blob, same discipline as
|
|
58252
|
+
* `mediaLocationStorage`'s own root cache) — dropped together with it by
|
|
58253
|
+
* `refreshStorageLocationsForMigration`, since a location's `readOnly` /
|
|
58254
|
+
* `enabled` flags only change via operator reconfig.
|
|
58255
|
+
*/
|
|
58256
|
+
eventMediaWriteLocationCache = null;
|
|
57987
58257
|
mediaWriteGate = new MediaWriteGate();
|
|
57988
58258
|
storageMigrationLeaseId = null;
|
|
57989
58259
|
/** Per-camera history of LINKED-device sensor state changes (Part B). */
|
|
@@ -58383,6 +58653,33 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
58383
58653
|
audioEpisodeTracks = /* @__PURE__ */ new Map();
|
|
58384
58654
|
audioEpisodeLastPersistMs = /* @__PURE__ */ new Map();
|
|
58385
58655
|
settingsCaches = new DeviceSettingsCacheHub();
|
|
58656
|
+
/**
|
|
58657
|
+
* The addon-level store and per-device blob, mirrored ONCE and shared by
|
|
58658
|
+
* every settings kind above — see `store/settings-raw-mirror.ts`. Before
|
|
58659
|
+
* this, each kind's cache miss called `resolveGlobalStore()` +
|
|
58660
|
+
* `readDeviceBlob()` directly, so eight kinds expiring in the same window
|
|
58661
|
+
* paid for the same two reads eight times: `get
|
|
58662
|
+
* pipeline-analytics:addon-settings` at 547–694/min, `get
|
|
58663
|
+
* pipeline-analytics:addon-devices` at ~700/min, live 2026-08-26. Invalidated
|
|
58664
|
+
* by every in-process writer: `applyDeviceSettingsPatch` and the retired-
|
|
58665
|
+
* setting purges drop the device mirror, `onConfigChanged` (this process's
|
|
58666
|
+
* own `updateGlobalSettings`) drops the global mirror.
|
|
58667
|
+
*/
|
|
58668
|
+
rawDeviceBlobMirror = new RawDeviceBlobMirror({
|
|
58669
|
+
read: (deviceId) => this.readDeviceBlob(deviceId),
|
|
58670
|
+
onRefreshFailed: (deviceId, err) => {
|
|
58671
|
+
this.ctx.logger.debug("raw device-blob mirror refresh failed — serving last value", {
|
|
58672
|
+
tags: { deviceId },
|
|
58673
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
58674
|
+
});
|
|
58675
|
+
}
|
|
58676
|
+
});
|
|
58677
|
+
rawGlobalStoreMirror = new RawGlobalStoreMirror({
|
|
58678
|
+
read: () => this.resolveGlobalStore(),
|
|
58679
|
+
onRefreshFailed: (err) => {
|
|
58680
|
+
this.ctx.logger.debug("raw global-store mirror refresh failed — serving last value", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
58681
|
+
}
|
|
58682
|
+
});
|
|
58386
58683
|
/** Avoids a store read every ~30 Hz audio frame. */
|
|
58387
58684
|
audioSettingsCache = this.settingsCaches.registerDeviceCache("audio-detection", SETTINGS_CACHE_TTL_MS);
|
|
58388
58685
|
sensitivitySettingsCache = this.settingsCaches.registerDeviceCache("detection-sensitivity", SETTINGS_CACHE_TTL_MS);
|
|
@@ -59102,6 +59399,53 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
59102
59399
|
this.mediaLocationStorage = routed;
|
|
59103
59400
|
return routed;
|
|
59104
59401
|
}
|
|
59402
|
+
/**
|
|
59403
|
+
* Resolves which `eventMedia` location a NEW footage-class blob should be
|
|
59404
|
+
* written to (`MediaStoreDeps.resolveEventMediaLocation`). Reflects
|
|
59405
|
+
* `config.readOnly` — an operator draining a location marks it read-only,
|
|
59406
|
+
* and new writes must never land there (the relocate mover, exposed as
|
|
59407
|
+
* `relocateMedia`, empties what is already on it in the background).
|
|
59408
|
+
*
|
|
59409
|
+
* Cached after the first successful resolution — this is a per-blob write
|
|
59410
|
+
* path, and the write-side provider is a declared no-RPC-per-blob bypass
|
|
59411
|
+
* (D-note in `location-aware-media-storage.ts`); re-querying the storage
|
|
59412
|
+
* cap on every blob would undo that. Dropped by
|
|
59413
|
+
* `refreshStorageLocationsForMigration`, same trigger as
|
|
59414
|
+
* `mediaLocationStorage.invalidateRoots()`.
|
|
59415
|
+
*
|
|
59416
|
+
* Degrades to the bare `eventMedia` type ref (pre-2026-08-26 behaviour)
|
|
59417
|
+
* when the storage cap itself is unreachable — a transient RPC failure (or
|
|
59418
|
+
* a test harness that never wires `api.storage`) must not turn every media
|
|
59419
|
+
* write into a hard failure the way "no writable location" does.
|
|
59420
|
+
*/
|
|
59421
|
+
async resolveEventMediaWriteLocation(api, logger, deviceId) {
|
|
59422
|
+
const cached = this.eventMediaWriteLocationCache;
|
|
59423
|
+
if (cached !== null) return cached;
|
|
59424
|
+
let locations;
|
|
59425
|
+
try {
|
|
59426
|
+
locations = await api.storage.listLocations.query({ type: DEFAULT_MEDIA_LOCATION });
|
|
59427
|
+
} catch (err) {
|
|
59428
|
+
logger.debug("pipeline-analytics: eventMedia location list unavailable — writing through the default ref", { meta: { error: require_dist.errMsg(err) } });
|
|
59429
|
+
this.eventMediaWriteLocationCache = DEFAULT_MEDIA_LOCATION;
|
|
59430
|
+
return DEFAULT_MEDIA_LOCATION;
|
|
59431
|
+
}
|
|
59432
|
+
const picked = pickWritableMediaLocation(locations.map((l) => ({
|
|
59433
|
+
id: l.id,
|
|
59434
|
+
readOnly: l.config["readOnly"] === true,
|
|
59435
|
+
enabled: l.enabled !== false,
|
|
59436
|
+
headroomBytes: l.capacity?.availableBytes ?? 0
|
|
59437
|
+
})), locations.find((l) => l.isDefault)?.id ?? "eventMedia");
|
|
59438
|
+
if (picked === null) {
|
|
59439
|
+
const seen = locations.length > 0 ? locations.map((l) => l.id).join(", ") : "(none registered)";
|
|
59440
|
+
logger.error("pipeline-analytics: no writable eventMedia storage location — refusing the write instead of failing far downstream as a filesystem error", {
|
|
59441
|
+
tags: { deviceId },
|
|
59442
|
+
meta: { locationsSeen: seen }
|
|
59443
|
+
});
|
|
59444
|
+
throw new Error(`no writable eventMedia storage location (seen: ${seen})`);
|
|
59445
|
+
}
|
|
59446
|
+
this.eventMediaWriteLocationCache = picked;
|
|
59447
|
+
return picked;
|
|
59448
|
+
}
|
|
59105
59449
|
/** Constructs every SQLite-backed store plus the stationary/package-drop/
|
|
59106
59450
|
* sensor plumbing, in the exact pre-S7 order. Returns the non-null bundle
|
|
59107
59451
|
* so the rest of boot consumes the stores without re-narrowing. */
|
|
@@ -59160,7 +59504,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
59160
59504
|
storage,
|
|
59161
59505
|
store: api.settingsStore,
|
|
59162
59506
|
logger: logger.child("MediaStore"),
|
|
59163
|
-
writeGate: this.mediaWriteGate
|
|
59507
|
+
writeGate: this.mediaWriteGate,
|
|
59508
|
+
resolveEventMediaLocation: (deviceId) => this.resolveEventMediaWriteLocation(api, logger, deviceId)
|
|
59164
59509
|
});
|
|
59165
59510
|
this.mediaStore = mediaStore;
|
|
59166
59511
|
const retrainFrames = new RetrainFrameStore({
|
|
@@ -60468,6 +60813,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
60468
60813
|
this.forgetDeviceProcessors(data.deviceId);
|
|
60469
60814
|
this.levelStateByDevice.delete(data.deviceId);
|
|
60470
60815
|
this.settingsCaches.clearDevice(data.deviceId);
|
|
60816
|
+
this.rawDeviceBlobMirror.invalidate(data.deviceId);
|
|
60471
60817
|
}
|
|
60472
60818
|
}
|
|
60473
60819
|
/** `DeviceUnregistered` handler — subscription topology lives in
|
|
@@ -60492,6 +60838,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
60492
60838
|
}
|
|
60493
60839
|
this.audioMarkers?.forgetDevice(deviceId);
|
|
60494
60840
|
this.settingsCaches.clearDevice(deviceId);
|
|
60841
|
+
this.rawDeviceBlobMirror.invalidate(deviceId);
|
|
60495
60842
|
this.bindingCache?.invalidate(deviceId);
|
|
60496
60843
|
this.detectionCoverage?.forget(deviceId);
|
|
60497
60844
|
this.zoneAnalytics?.forgetDevice(deviceId);
|
|
@@ -60920,6 +61267,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
60920
61267
|
}] : []
|
|
60921
61268
|
];
|
|
60922
61269
|
}
|
|
61270
|
+
/**
|
|
61271
|
+
* `BaseAddon.updateGlobalSettings` calls this after the write lands —
|
|
61272
|
+
* gated to the LOCAL node's own write (a sibling-node write returns before
|
|
61273
|
+
* reaching it), which is exactly the scope `rawGlobalStoreMirror` needs to
|
|
61274
|
+
* invalidate: the mirror only ever answers for THIS process, so it only
|
|
61275
|
+
* needs to drop on THIS process's own write (D224).
|
|
61276
|
+
*/
|
|
61277
|
+
async onConfigChanged() {
|
|
61278
|
+
await super.onConfigChanged();
|
|
61279
|
+
this.rawGlobalStoreMirror.invalidate();
|
|
61280
|
+
}
|
|
60923
61281
|
async onShutdown() {
|
|
60924
61282
|
this.shuttingDown = true;
|
|
60925
61283
|
this.sceneEngine?.stop();
|
|
@@ -60975,6 +61333,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
60975
61333
|
this.gatedNodeByDevice.clear();
|
|
60976
61334
|
this.levelStateByDevice.clear();
|
|
60977
61335
|
this.settingsCaches.clearAll();
|
|
61336
|
+
this.rawDeviceBlobMirror.invalidateAll();
|
|
61337
|
+
this.rawGlobalStoreMirror.invalidate();
|
|
60978
61338
|
this.confirmationDetectorStepByNode.clear();
|
|
60979
61339
|
this.trackStore?.clearAll();
|
|
60980
61340
|
this.stationaryRegistry = null;
|
|
@@ -61687,10 +62047,16 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
61687
62047
|
* auto-assigning — see `__tests__/global-settings-cascade.spec.ts`).
|
|
61688
62048
|
* `resolveGlobalStore()` is the sanctioned BaseAddon read (per-node
|
|
61689
62049
|
* projection applied; a raw `readAddonStore` would leak `@node` keys if a
|
|
61690
|
-
* perNode field is ever declared).
|
|
62050
|
+
* perNode field is ever declared).
|
|
62051
|
+
*
|
|
62052
|
+
* Both raw reads go through `rawGlobalStoreMirror` / `rawDeviceBlobMirror`
|
|
62053
|
+
* rather than straight to the store: this method is the ONE place all
|
|
62054
|
+
* eight settings kinds funnel through on a cache miss, so calling the raw
|
|
62055
|
+
* reads directly here means eight kinds expiring in the same window each
|
|
62056
|
+
* pay for the same two reads — see `store/settings-raw-mirror.ts`. */
|
|
61691
62057
|
async readDeviceSettings(deviceId, resolve) {
|
|
61692
|
-
const globalRaw = this.ctxIfReady !== null ? await this.
|
|
61693
|
-
const deviceRaw = await this.
|
|
62058
|
+
const globalRaw = this.ctxIfReady !== null ? await this.rawGlobalStoreMirror.resolve() : {};
|
|
62059
|
+
const deviceRaw = await this.rawDeviceBlobMirror.resolve(deviceId);
|
|
61694
62060
|
return resolve({
|
|
61695
62061
|
...globalRaw,
|
|
61696
62062
|
...deviceRaw
|
|
@@ -63361,6 +63727,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
63361
63727
|
});
|
|
63362
63728
|
}
|
|
63363
63729
|
await settings.writeDeviceStore(deviceId, plan.patch);
|
|
63730
|
+
this.rawDeviceBlobMirror.invalidate(deviceId);
|
|
63364
63731
|
} catch (err) {
|
|
63365
63732
|
this.retiredSettingsChecked.delete(deviceId);
|
|
63366
63733
|
this.ctx.logger.warn("retired retention settings migration failed — will retry", {
|
|
@@ -63432,6 +63799,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
63432
63799
|
deviceId
|
|
63433
63800
|
});
|
|
63434
63801
|
await settings.writeDeviceStore(deviceId, plan.patch);
|
|
63802
|
+
this.rawDeviceBlobMirror.invalidate(deviceId);
|
|
63435
63803
|
} catch (err) {
|
|
63436
63804
|
this.storedSuggestionCheckedDevices.delete(deviceId);
|
|
63437
63805
|
this.ctx.logger.warn("face suggestion threshold purge failed — will retry", {
|
|
@@ -64417,11 +64785,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
64417
64785
|
const trackStore = this.trackStore;
|
|
64418
64786
|
if (!trackStore) return live;
|
|
64419
64787
|
for (const deviceId of deviceIds) {
|
|
64420
|
-
const page = await trackStore.
|
|
64788
|
+
const page = await trackStore.queryRecentIds({
|
|
64421
64789
|
deviceIds: [deviceId],
|
|
64422
64790
|
limit: EMBEDDING_ORPHAN_SCAN_LIMIT
|
|
64423
64791
|
});
|
|
64424
|
-
for (const
|
|
64792
|
+
for (const trackId of page.trackIds) live.add(trackId);
|
|
64425
64793
|
}
|
|
64426
64794
|
return live;
|
|
64427
64795
|
}
|
|
@@ -64744,19 +65112,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
64744
65112
|
logger: this.ctx.logger
|
|
64745
65113
|
}));
|
|
64746
65114
|
const deps = {
|
|
64747
|
-
listTrackIdPage: async (cursor) => {
|
|
64748
|
-
|
|
64749
|
-
|
|
64750
|
-
|
|
64751
|
-
|
|
64752
|
-
|
|
64753
|
-
|
|
64754
|
-
});
|
|
64755
|
-
return {
|
|
64756
|
-
trackIds: page.tracks.map((t) => t.trackId),
|
|
64757
|
-
nextCursor: page.nextCursor
|
|
64758
|
-
};
|
|
64759
|
-
},
|
|
65115
|
+
listTrackIdPage: async (cursor) => trackStore.queryRecentIds({
|
|
65116
|
+
deviceIds,
|
|
65117
|
+
...input.since !== void 0 ? { since: input.since } : {},
|
|
65118
|
+
...input.until !== void 0 ? { until: input.until } : {},
|
|
65119
|
+
...cursor !== null ? { cursor } : {},
|
|
65120
|
+
limit: REBUILD_PAGE_SIZE
|
|
65121
|
+
}),
|
|
64760
65122
|
loadSource: (trackId) => loadRebuildSource({
|
|
64761
65123
|
media: mediaStore,
|
|
64762
65124
|
events: eventStore
|
|
@@ -65584,6 +65946,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
65584
65946
|
async refreshStorageLocationsForMigration(input) {
|
|
65585
65947
|
if (this.storageMigrationLeaseId !== input.leaseId) throw new Error("event-media storage migration lease does not match");
|
|
65586
65948
|
this.mediaLocationStorage?.invalidateRoots();
|
|
65949
|
+
this.eventMediaWriteLocationCache = null;
|
|
65587
65950
|
return { refreshed: true };
|
|
65588
65951
|
}
|
|
65589
65952
|
async startStorageMigrationMove(input) {
|
|
@@ -65597,6 +65960,32 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
65597
65960
|
async cancelStorageMigrationMove(input) {
|
|
65598
65961
|
return { cancelled: this.mediaRelocate?.cancel(input.jobId) ?? false };
|
|
65599
65962
|
}
|
|
65963
|
+
/**
|
|
65964
|
+
* Move event media between locations — the OPERATOR's mover, no lease.
|
|
65965
|
+
* Twin of `recording.relocateFootage`: same engine (`this.mediaRelocate`)
|
|
65966
|
+
* the lease-gated coordinator move uses, runnable in the background
|
|
65967
|
+
* without pausing anything. `startStorageMigrationMove` above stays
|
|
65968
|
+
* lease-gated for the orchestrated migration; this method must never touch
|
|
65969
|
+
* `storageMigrationLeaseId`.
|
|
65970
|
+
*/
|
|
65971
|
+
async relocateMedia(input) {
|
|
65972
|
+
if (!this.mediaRelocate) throw new Error("media relocation unavailable");
|
|
65973
|
+
const jobId = this.mediaRelocate.start(input);
|
|
65974
|
+
this.ctx.logger.info("pipeline-analytics: media relocate job enqueued", { meta: {
|
|
65975
|
+
jobId,
|
|
65976
|
+
to: input.toLocationId
|
|
65977
|
+
} });
|
|
65978
|
+
return { jobId };
|
|
65979
|
+
}
|
|
65980
|
+
/** Every relocate job this addon knows about, newest first (in RAM: the
|
|
65981
|
+
* move is resumable, so a lost list costs nothing but the display). */
|
|
65982
|
+
async listRelocateMediaJobs() {
|
|
65983
|
+
return this.mediaRelocate?.list() ?? [];
|
|
65984
|
+
}
|
|
65985
|
+
/** Cancel a running or queued relocate job. */
|
|
65986
|
+
async cancelRelocateMedia(input) {
|
|
65987
|
+
return { cancelled: this.mediaRelocate?.cancel(input.jobId) ?? false };
|
|
65988
|
+
}
|
|
65600
65989
|
async listOpsLog(input) {
|
|
65601
65990
|
return this.queryFacade.listOpsLog(input);
|
|
65602
65991
|
}
|
|
@@ -65990,6 +66379,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
|
|
|
65990
66379
|
}
|
|
65991
66380
|
async applyDeviceSettingsPatch(input) {
|
|
65992
66381
|
await this.updateDeviceSettings(input.deviceId, input.patch);
|
|
66382
|
+
this.rawDeviceBlobMirror.invalidate(input.deviceId);
|
|
65993
66383
|
this.audioSettingsCache.delete(input.deviceId);
|
|
65994
66384
|
this.sensitivitySettingsCache.delete(input.deviceId);
|
|
65995
66385
|
this.trackingSettingsCache.delete(input.deviceId);
|