@camstack/addon-post-analysis 1.2.612 → 1.2.614
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/{clip-model-registry-C7WMvftu.js → clip-model-registry-CM-m6e1l.js} +16 -1
- package/dist/{clip-model-registry-JH4cTI06.mjs → clip-model-registry-D6tdLEFB.mjs} +16 -1
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +559 -1180
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DQE_Wu6_.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-PyXxg2Wg.mjs} +3 -3
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-7dLQKt_H.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-mydSlNUz.mjs} +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-BB2kgLY1.mjs +26 -0
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.js-C0AuF9av.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare__react__loadShare__.js-Cx9AsA5q.mjs} +1 -1
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-Bm-iyjmq.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare__react_mf_1_jsx_mf_2_runtime__loadShare__.js-CkwmHEi0.mjs} +8 -8
- package/dist/pipeline-analytics/{hostInit-Bl6Zvb1p.mjs → hostInit-RCMuxWxC.mjs} +3 -3
- package/dist/pipeline-analytics/index.js +643 -75
- package/dist/pipeline-analytics/index.mjs +642 -75
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DbUPeg6m.mjs +0 -26
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_clip_model_registry = require("../clip-model-registry-
|
|
5
|
+
const require_clip_model_registry = require("../clip-model-registry-CM-m6e1l.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_clip_model_registry.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -9499,7 +9499,9 @@ var MigrationMarkerBodySchema = require_clip_model_registry.object({
|
|
|
9499
9499
|
});
|
|
9500
9500
|
/** The orphan audit's resume position: one row, the whole map in one value on purpose. */
|
|
9501
9501
|
var SweepCursorBodySchema = require_clip_model_registry.object({
|
|
9502
|
-
|
|
9502
|
+
/** A number is an offset (a settings-store scope); a string is the records
|
|
9503
|
+
* engine's opaque keyset cursor (a scope walked through its kind). */
|
|
9504
|
+
scopes: require_clip_model_registry.record(require_clip_model_registry.string(), require_clip_model_registry.union([require_clip_model_registry.number(), require_clip_model_registry.string()])),
|
|
9503
9505
|
/** Absent on a row written before the walk existed — absent means "from the top". */
|
|
9504
9506
|
faceVectorCursor: require_clip_model_registry.number().optional(),
|
|
9505
9507
|
/** One cursor per CLIP index, keyed by index name. Absent on an older row. */
|
|
@@ -9973,7 +9975,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
|
|
|
9973
9975
|
//#endregion
|
|
9974
9976
|
//#region src/pipeline-analytics/scene/scene-config-ledger.ts
|
|
9975
9977
|
/** The engine's page cap for one query; a reseed pages by it, bounded by the ledger limit. */
|
|
9976
|
-
var PAGE_SIZE$
|
|
9978
|
+
var PAGE_SIZE$2 = 1e3;
|
|
9977
9979
|
/**
|
|
9978
9980
|
* A row whose monitor JSON no longer validates is SKIPPED, not repaired: a
|
|
9979
9981
|
* half-parsed scene would report a verdict about a region it cannot describe.
|
|
@@ -10011,7 +10013,7 @@ var SceneConfigLedger = class {
|
|
|
10011
10013
|
let seen = 0;
|
|
10012
10014
|
let cursor;
|
|
10013
10015
|
for (;;) {
|
|
10014
|
-
const limit = Math.min(PAGE_SIZE$
|
|
10016
|
+
const limit = Math.min(PAGE_SIZE$2, this.loadLimit - seen);
|
|
10015
10017
|
if (limit <= 0) break;
|
|
10016
10018
|
const page = await this.kind.query({
|
|
10017
10019
|
limit,
|
|
@@ -12058,6 +12060,28 @@ var EventLedgerCollection = class EventLedgerCollection {
|
|
|
12058
12060
|
if (mapped.limit !== void 0 && out.length > mapped.limit) return out.slice(0, mapped.limit);
|
|
12059
12061
|
return out;
|
|
12060
12062
|
}
|
|
12063
|
+
/**
|
|
12064
|
+
* One page of the whole kind in `id` order, resumed from the records
|
|
12065
|
+
* engine's OPAQUE keyset cursor (`null` = from the top). The answer's
|
|
12066
|
+
* `nextCursor` is `null` at the end. A keyset cursor names the last row
|
|
12067
|
+
* read, so deleting rows behind it never shifts what comes next — the
|
|
12068
|
+
* reason a bulk walk that reclaims as it goes needs this, not an offset.
|
|
12069
|
+
*
|
|
12070
|
+
* Records only: without `ctx.records` the caller pages the settings store
|
|
12071
|
+
* itself, and asking here throws rather than inventing a cursor.
|
|
12072
|
+
*/
|
|
12073
|
+
async pageById(cursor, limit) {
|
|
12074
|
+
if (this.kind === null) throw new Error(`event ledger ${this.collection}: pageById needs the records kind`);
|
|
12075
|
+
const page = await this.kind.query({
|
|
12076
|
+
order: "id",
|
|
12077
|
+
limit,
|
|
12078
|
+
...cursor === null ? {} : { cursor }
|
|
12079
|
+
});
|
|
12080
|
+
return {
|
|
12081
|
+
rows: page.records.map((row) => recordsRowToLegacy(row, this.legacyAtField)),
|
|
12082
|
+
nextCursor: page.nextCursor
|
|
12083
|
+
};
|
|
12084
|
+
}
|
|
12061
12085
|
async getManyByIds(ids) {
|
|
12062
12086
|
if (ids.length === 0) return [];
|
|
12063
12087
|
if (this.kind === null) return (await this.store.query.query({
|
|
@@ -13184,11 +13208,35 @@ var FaceStore = class {
|
|
|
13184
13208
|
* carries no `embeddingModelId`, and those rows were all `arcface-r100`, so
|
|
13185
13209
|
* that is what they are stamped with — a KNOWN historical fact, not a
|
|
13186
13210
|
* fallback guess. A row that names some other model keeps its own.
|
|
13211
|
+
*
|
|
13212
|
+
* On a records hub the rows are paged through the faces LEDGER: the legacy
|
|
13213
|
+
* table is dropped there, and the adoption copied each row's body whole —
|
|
13214
|
+
* an `embedding` the pass had not drained yet included — so a records-era
|
|
13215
|
+
* row can still need converting (the same shape as `IdentityStore`).
|
|
13187
13216
|
*/
|
|
13188
13217
|
conversionSpec() {
|
|
13218
|
+
const ledger = this.ledger;
|
|
13219
|
+
const recordsPaging = ledger.usesRecords ? {
|
|
13220
|
+
pageRows: (limit, offset) => ledger.query({
|
|
13221
|
+
orderBy: {
|
|
13222
|
+
field: "id",
|
|
13223
|
+
direction: "asc"
|
|
13224
|
+
},
|
|
13225
|
+
limit,
|
|
13226
|
+
offset
|
|
13227
|
+
}, [
|
|
13228
|
+
"deviceId",
|
|
13229
|
+
"timestamp",
|
|
13230
|
+
"trackId",
|
|
13231
|
+
"embeddingModelId",
|
|
13232
|
+
"embedding"
|
|
13233
|
+
]),
|
|
13234
|
+
drainEmbedding: (id) => ledger.update(id, { embedding: [] })
|
|
13235
|
+
} : {};
|
|
13189
13236
|
return {
|
|
13190
13237
|
collection: FACES_COLLECTION,
|
|
13191
13238
|
vectors: this.vectors,
|
|
13239
|
+
...recordsPaging,
|
|
13192
13240
|
write: (id, embedding, data) => {
|
|
13193
13241
|
const deviceId = data["deviceId"];
|
|
13194
13242
|
const timestamp = data["timestamp"];
|
|
@@ -67474,7 +67522,7 @@ async function settleReplaced(deps, sample, replacement, dryRun) {
|
|
|
67474
67522
|
});
|
|
67475
67523
|
return "old-removal-retried";
|
|
67476
67524
|
}
|
|
67477
|
-
function tally$
|
|
67525
|
+
function tally$2(samples, outcomes, dryRun) {
|
|
67478
67526
|
const count = (kind) => outcomes.filter((o) => o.outcome === kind).length;
|
|
67479
67527
|
const refusals = [];
|
|
67480
67528
|
for (const o of outcomes) if (o.outcome === "refused" && o.reason !== void 0) refusals.push({
|
|
@@ -67546,7 +67594,7 @@ async function reenrolPhotoSamples(deps, options) {
|
|
|
67546
67594
|
outcomes.push(outcomeOf(sample, verdict));
|
|
67547
67595
|
await deps.pause(pacingMs);
|
|
67548
67596
|
}
|
|
67549
|
-
const result = tally$
|
|
67597
|
+
const result = tally$2(samples, outcomes, dryRun);
|
|
67550
67598
|
if (!dryRun && (result.reenrolled > 0 || result.oldRemovalRetried > 0)) await deps.refreshGallery();
|
|
67551
67599
|
const { outcomes: _perSample, ...summary } = result;
|
|
67552
67600
|
deps.logger.info("face photo re-enrol finished", { meta: { ...summary } });
|
|
@@ -78963,24 +79011,11 @@ function createDiscardJob(deps) {
|
|
|
78963
79011
|
});
|
|
78964
79012
|
await sleep(0);
|
|
78965
79013
|
}
|
|
78966
|
-
|
|
79014
|
+
const retrainIds = (await deps.retrainFrameLedger.query({})).filter((row) => row.data["locationId"] === locationId).map((row) => row.id);
|
|
79015
|
+
for (let i = 0; i < retrainIds.length; i += MEDIA_PAGE) {
|
|
78967
79016
|
if (cancelRequested) return owners;
|
|
78968
|
-
const
|
|
78969
|
-
|
|
78970
|
-
columns: ["id"],
|
|
78971
|
-
filter: {
|
|
78972
|
-
where: { locationId },
|
|
78973
|
-
limit: MEDIA_PAGE
|
|
78974
|
-
}
|
|
78975
|
-
});
|
|
78976
|
-
if (rows.length === 0) break;
|
|
78977
|
-
for (const row of rows) {
|
|
78978
|
-
await deps.store.delete.mutate({
|
|
78979
|
-
collection: RETRAIN_FRAMES_COLLECTION,
|
|
78980
|
-
key: row.id
|
|
78981
|
-
});
|
|
78982
|
-
deleted += 1;
|
|
78983
|
-
}
|
|
79017
|
+
const page = retrainIds.slice(i, i + MEDIA_PAGE);
|
|
79018
|
+
deleted += await deps.retrainFrameLedger.deleteByIds(page);
|
|
78984
79019
|
report({
|
|
78985
79020
|
dbRowsDeleted: deleted,
|
|
78986
79021
|
detail: `deleted ${deleted} media/retrain rows`
|
|
@@ -79032,6 +79067,383 @@ function createDiscardJob(deps) {
|
|
|
79032
79067
|
};
|
|
79033
79068
|
}
|
|
79034
79069
|
//#endregion
|
|
79070
|
+
//#region src/pipeline-analytics/retention/media-forget-actions.ts
|
|
79071
|
+
/**
|
|
79072
|
+
* The operator door for the NARROW media-row forget (`media-row-forget.ts`).
|
|
79073
|
+
*
|
|
79074
|
+
* It forgets `pipeline-analytics:media` rows stamped with ONE location whose
|
|
79075
|
+
* file is provably gone, and nothing else: no event, track, object-event,
|
|
79076
|
+
* face, plate or identity row is read for deletion, ever. Contrast
|
|
79077
|
+
* `storage.discardLocation`, which wipes the disk AND cascades owners.
|
|
79078
|
+
*
|
|
79079
|
+
* `dryRun` defaults to `true`: an unstated intent destroys nothing. Applying
|
|
79080
|
+
* also requires `expectAbsent` — the `absent` count a dry run just reported —
|
|
79081
|
+
* so the forget only ever deletes a population a human has already read.
|
|
79082
|
+
*/
|
|
79083
|
+
/** Hard ceiling on rows one call examines — the call is synchronous. */
|
|
79084
|
+
var MEDIA_FORGET_MAX_ROWS = 5e4;
|
|
79085
|
+
var MediaForgetInputSchema = require_clip_model_registry.object({
|
|
79086
|
+
locationId: require_clip_model_registry.string().min(1),
|
|
79087
|
+
/** Restrict to these cameras. Omitted = every row stamped with the location. */
|
|
79088
|
+
deviceIds: require_clip_model_registry.array(require_clip_model_registry.number().int()).min(1).optional(),
|
|
79089
|
+
dryRun: require_clip_model_registry.boolean().default(true),
|
|
79090
|
+
/**
|
|
79091
|
+
* Required when `dryRun` is false: the `totals.absent` of the dry run. A
|
|
79092
|
+
* different count at apply time refuses the whole call before any delete.
|
|
79093
|
+
*/
|
|
79094
|
+
expectAbsent: require_clip_model_registry.number().int().nonnegative().optional(),
|
|
79095
|
+
/** Rows examined in this call. Default and max {@link MEDIA_FORGET_MAX_ROWS}. */
|
|
79096
|
+
maxRows: require_clip_model_registry.number().int().min(1).max(MEDIA_FORGET_MAX_ROWS).optional(),
|
|
79097
|
+
/** Detailed rows returned per verdict. Default 20. */
|
|
79098
|
+
sampleSize: require_clip_model_registry.number().int().min(0).max(500).optional()
|
|
79099
|
+
});
|
|
79100
|
+
var VerdictCountsSchema = require_clip_model_registry.object({
|
|
79101
|
+
rows: require_clip_model_registry.number(),
|
|
79102
|
+
/** File answered ENOENT — the only verdict that may be forgotten. */
|
|
79103
|
+
absent: require_clip_model_registry.number(),
|
|
79104
|
+
/** File is there: relocation is incomplete. Never forgotten. */
|
|
79105
|
+
present: require_clip_model_registry.number(),
|
|
79106
|
+
/** The stat failed with any errno other than ENOENT. Never forgotten. */
|
|
79107
|
+
error: require_clip_model_registry.number(),
|
|
79108
|
+
/** No path, a path escaping the root, or a retention-exempt owner. Never forgotten. */
|
|
79109
|
+
kept: require_clip_model_registry.number(),
|
|
79110
|
+
/** Rows actually deleted (0 on a dry run). */
|
|
79111
|
+
deleted: require_clip_model_registry.number()
|
|
79112
|
+
});
|
|
79113
|
+
var MediaForgetSampleSchema = require_clip_model_registry.object({
|
|
79114
|
+
mediaId: require_clip_model_registry.number(),
|
|
79115
|
+
deviceId: require_clip_model_registry.number(),
|
|
79116
|
+
kind: require_clip_model_registry.string(),
|
|
79117
|
+
ownerKind: require_clip_model_registry.string(),
|
|
79118
|
+
ownerId: require_clip_model_registry.string(),
|
|
79119
|
+
path: require_clip_model_registry.string(),
|
|
79120
|
+
/** The errno (error) or the reason (kept); absent otherwise. */
|
|
79121
|
+
reason: require_clip_model_registry.string().optional()
|
|
79122
|
+
});
|
|
79123
|
+
var MediaForgetResultSchema = require_clip_model_registry.object({
|
|
79124
|
+
locationId: require_clip_model_registry.string(),
|
|
79125
|
+
dryRun: require_clip_model_registry.boolean(),
|
|
79126
|
+
root: require_clip_model_registry.string(),
|
|
79127
|
+
/** Rows of this location that were not examined because `maxRows` was hit. */
|
|
79128
|
+
truncated: require_clip_model_registry.boolean(),
|
|
79129
|
+
totals: VerdictCountsSchema,
|
|
79130
|
+
byDevice: require_clip_model_registry.array(VerdictCountsSchema.extend({ deviceId: require_clip_model_registry.number() })),
|
|
79131
|
+
byKind: require_clip_model_registry.array(VerdictCountsSchema.extend({ kind: require_clip_model_registry.string() })),
|
|
79132
|
+
/** Every id the call deletes (dry run: would delete). The exact list. */
|
|
79133
|
+
absentIds: require_clip_model_registry.array(require_clip_model_registry.number()),
|
|
79134
|
+
samples: require_clip_model_registry.object({
|
|
79135
|
+
absent: require_clip_model_registry.array(MediaForgetSampleSchema),
|
|
79136
|
+
present: require_clip_model_registry.array(MediaForgetSampleSchema),
|
|
79137
|
+
error: require_clip_model_registry.array(MediaForgetSampleSchema),
|
|
79138
|
+
kept: require_clip_model_registry.array(MediaForgetSampleSchema)
|
|
79139
|
+
}),
|
|
79140
|
+
/** Absent at classification, no longer ENOENT at delete time: kept. */
|
|
79141
|
+
changedBeforeDelete: require_clip_model_registry.number()
|
|
79142
|
+
});
|
|
79143
|
+
var mediaForgetActions = require_clip_model_registry.defineCustomActions({ "storage.forgetMissingMedia": require_clip_model_registry.customAction(MediaForgetInputSchema, MediaForgetResultSchema, {
|
|
79144
|
+
kind: "mutation",
|
|
79145
|
+
auth: "admin"
|
|
79146
|
+
}) });
|
|
79147
|
+
//#endregion
|
|
79148
|
+
//#region src/pipeline-analytics/retention/media-row-forget.ts
|
|
79149
|
+
/**
|
|
79150
|
+
* The NARROW media-row forget: delete `pipeline-analytics:media` rows stamped
|
|
79151
|
+
* with ONE storage location whose file is provably gone — and nothing else.
|
|
79152
|
+
*
|
|
79153
|
+
* Why it exists: a location whose bytes were lost keeps rows naming it, and a
|
|
79154
|
+
* cascading cleanup is not an acceptable way to clear them. `reconcileFromDisk`
|
|
79155
|
+
* once cascaded ~132 k events away from a read it did not double-check. Here:
|
|
79156
|
+
*
|
|
79157
|
+
* - **Only media rows.** No event, track, object-event, face, plate or
|
|
79158
|
+
* identity row is deleted. A parent keeps existing with that media kind
|
|
79159
|
+
* missing — which is exactly what it already serves (`getBytesById` answers
|
|
79160
|
+
* `null` for a missing row as it does for a missing blob).
|
|
79161
|
+
* - **Only `ENOENT` is absence (D49).** Each row's file is `stat`ed under the
|
|
79162
|
+
* location's own root on THIS node. Any other errno keeps the row and is
|
|
79163
|
+
* reported; a present file keeps the row and is reported (relocation is
|
|
79164
|
+
* incomplete). A row with no path, a path that escapes the root, or a
|
|
79165
|
+
* retention-exempt owner (the enrolled gallery) is kept.
|
|
79166
|
+
* - **Only rows stamped with the id.** `locationId IS NULL` rows mean
|
|
79167
|
+
* "wherever eventMedia points now" and are never selected.
|
|
79168
|
+
* - **Only a location nothing writes to.** An active (writable) location is
|
|
79169
|
+
* refused: new rows could land while the walk runs.
|
|
79170
|
+
* - **Dry run first, then the same count.** Applying requires
|
|
79171
|
+
* `expectAbsent`; the whole population is classified before any delete,
|
|
79172
|
+
* and a different absent count refuses the call with nothing written.
|
|
79173
|
+
* Each id is re-`stat`ed right before its batch is deleted, and the delete
|
|
79174
|
+
* predicate repeats `locationId`, so a row repointed in between survives.
|
|
79175
|
+
*
|
|
79176
|
+
* Occupancy needs no separate bookkeeping: `storage-occupancy` sums
|
|
79177
|
+
* `sizeBytes WHERE locationId = ?` on demand, so the row IS the accounting.
|
|
79178
|
+
*/
|
|
79179
|
+
var PAGE_SIZE$1 = 200;
|
|
79180
|
+
var DELETE_BATCH = 200;
|
|
79181
|
+
var DEFAULT_SAMPLE_SIZE = 20;
|
|
79182
|
+
function errnoOf(err) {
|
|
79183
|
+
if (err instanceof Error && "code" in err && typeof err.code === "string") return err.code;
|
|
79184
|
+
return err instanceof Error ? err.message : String(err);
|
|
79185
|
+
}
|
|
79186
|
+
function emptyCounts() {
|
|
79187
|
+
return {
|
|
79188
|
+
rows: 0,
|
|
79189
|
+
absent: 0,
|
|
79190
|
+
present: 0,
|
|
79191
|
+
error: 0,
|
|
79192
|
+
kept: 0,
|
|
79193
|
+
deleted: 0
|
|
79194
|
+
};
|
|
79195
|
+
}
|
|
79196
|
+
function tally$1(counts, verdict) {
|
|
79197
|
+
const rows = counts.rows + 1;
|
|
79198
|
+
switch (verdict) {
|
|
79199
|
+
case "absent": return {
|
|
79200
|
+
...counts,
|
|
79201
|
+
rows,
|
|
79202
|
+
absent: counts.absent + 1
|
|
79203
|
+
};
|
|
79204
|
+
case "present": return {
|
|
79205
|
+
...counts,
|
|
79206
|
+
rows,
|
|
79207
|
+
present: counts.present + 1
|
|
79208
|
+
};
|
|
79209
|
+
case "error": return {
|
|
79210
|
+
...counts,
|
|
79211
|
+
rows,
|
|
79212
|
+
error: counts.error + 1
|
|
79213
|
+
};
|
|
79214
|
+
case "kept": return {
|
|
79215
|
+
...counts,
|
|
79216
|
+
rows,
|
|
79217
|
+
kept: counts.kept + 1
|
|
79218
|
+
};
|
|
79219
|
+
}
|
|
79220
|
+
}
|
|
79221
|
+
/** The file's absolute path under `root`, or `null` when it would escape it. */
|
|
79222
|
+
function resolveUnder(root, relativePath) {
|
|
79223
|
+
const base = node_path.default.resolve(root);
|
|
79224
|
+
const full = node_path.default.resolve(base, relativePath);
|
|
79225
|
+
return full.startsWith(base + node_path.default.sep) ? full : null;
|
|
79226
|
+
}
|
|
79227
|
+
function toSample(id, data) {
|
|
79228
|
+
return {
|
|
79229
|
+
mediaId: Number(id),
|
|
79230
|
+
deviceId: Number(data["deviceId"] ?? 0),
|
|
79231
|
+
kind: String(data["kind"] ?? ""),
|
|
79232
|
+
ownerKind: String(data["ownerKind"] ?? ""),
|
|
79233
|
+
ownerId: String(data["ownerId"] ?? ""),
|
|
79234
|
+
path: String(data["path"] ?? "")
|
|
79235
|
+
};
|
|
79236
|
+
}
|
|
79237
|
+
async function probe(stat, absolutePath) {
|
|
79238
|
+
try {
|
|
79239
|
+
await stat(absolutePath);
|
|
79240
|
+
return { verdict: "present" };
|
|
79241
|
+
} catch (err) {
|
|
79242
|
+
const code = errnoOf(err);
|
|
79243
|
+
return code === "ENOENT" ? { verdict: "absent" } : {
|
|
79244
|
+
verdict: "error",
|
|
79245
|
+
reason: code
|
|
79246
|
+
};
|
|
79247
|
+
}
|
|
79248
|
+
}
|
|
79249
|
+
async function classify(stat, root, id, data) {
|
|
79250
|
+
const sample = toSample(id, data);
|
|
79251
|
+
const ownerKind = sample.ownerKind;
|
|
79252
|
+
if (isMediaOwnerKind(ownerKind) && isRetentionExemptOwnerKind(ownerKind)) return {
|
|
79253
|
+
key: id,
|
|
79254
|
+
sample: {
|
|
79255
|
+
...sample,
|
|
79256
|
+
reason: "retention-exempt owner"
|
|
79257
|
+
},
|
|
79258
|
+
verdict: "kept",
|
|
79259
|
+
absolutePath: null
|
|
79260
|
+
};
|
|
79261
|
+
if (sample.path.length === 0) return {
|
|
79262
|
+
key: id,
|
|
79263
|
+
sample: {
|
|
79264
|
+
...sample,
|
|
79265
|
+
reason: "no path"
|
|
79266
|
+
},
|
|
79267
|
+
verdict: "kept",
|
|
79268
|
+
absolutePath: null
|
|
79269
|
+
};
|
|
79270
|
+
const absolutePath = resolveUnder(root, sample.path);
|
|
79271
|
+
if (absolutePath === null) return {
|
|
79272
|
+
key: id,
|
|
79273
|
+
sample: {
|
|
79274
|
+
...sample,
|
|
79275
|
+
reason: "path escapes the root"
|
|
79276
|
+
},
|
|
79277
|
+
verdict: "kept",
|
|
79278
|
+
absolutePath
|
|
79279
|
+
};
|
|
79280
|
+
const result = await probe(stat, absolutePath);
|
|
79281
|
+
return {
|
|
79282
|
+
key: id,
|
|
79283
|
+
sample: result.reason === void 0 ? sample : {
|
|
79284
|
+
...sample,
|
|
79285
|
+
reason: result.reason
|
|
79286
|
+
},
|
|
79287
|
+
verdict: result.verdict,
|
|
79288
|
+
absolutePath
|
|
79289
|
+
};
|
|
79290
|
+
}
|
|
79291
|
+
function resolveTarget(locations, locationId, localNodeId) {
|
|
79292
|
+
const location = locations.find((row) => row.id === locationId);
|
|
79293
|
+
if (!location) throw new Error(`forgetMissingMedia refused: unknown location ${locationId}`);
|
|
79294
|
+
if (location.type !== "eventMedia") throw new Error(`forgetMissingMedia refused: ${locationId} is ${location.type}, pipeline-analytics owns eventMedia rows only`);
|
|
79295
|
+
if (require_clip_model_registry.mayWriteToLocation(location)) throw new Error(`forgetMissingMedia refused: ${locationId} is still a write target — set it to drain, readonly or disabled first`);
|
|
79296
|
+
const nodeId = location.nodeId === void 0 || location.nodeId === "" ? "hub" : location.nodeId;
|
|
79297
|
+
if (nodeId !== localNodeId) throw new Error(`forgetMissingMedia refused: ${locationId} lives on node ${nodeId}, this process is ${localNodeId}`);
|
|
79298
|
+
return {
|
|
79299
|
+
location,
|
|
79300
|
+
root: locationBasePath(location)
|
|
79301
|
+
};
|
|
79302
|
+
}
|
|
79303
|
+
/**
|
|
79304
|
+
* Classify every row stamped with `input.locationId` (bounded by `maxRows`),
|
|
79305
|
+
* and — only when `dryRun` is false and the absent count equals
|
|
79306
|
+
* `expectAbsent` — delete the absent ones, per camera, in batches.
|
|
79307
|
+
*/
|
|
79308
|
+
async function forgetMissingMediaRows(deps, rawInput) {
|
|
79309
|
+
const input = MediaForgetInputSchema.parse(rawInput);
|
|
79310
|
+
if (!input.dryRun && input.expectAbsent === void 0) throw new Error("forgetMissingMedia refused: an apply needs expectAbsent (the dry run’s totals.absent)");
|
|
79311
|
+
const stat = deps.stat ?? ((p) => node_fs.promises.stat(p));
|
|
79312
|
+
const pace = deps.pace ?? (() => new Promise((resolve) => setImmediate(resolve)));
|
|
79313
|
+
const maxRows = input.maxRows ?? MEDIA_FORGET_MAX_ROWS;
|
|
79314
|
+
const sampleSize = input.sampleSize ?? DEFAULT_SAMPLE_SIZE;
|
|
79315
|
+
const { root } = resolveTarget(await deps.listLocations(), input.locationId, deps.localNodeId);
|
|
79316
|
+
const classified = [];
|
|
79317
|
+
let cursor = 0;
|
|
79318
|
+
let truncated = false;
|
|
79319
|
+
for (;;) {
|
|
79320
|
+
const rows = await deps.store.query.query({
|
|
79321
|
+
collection: MEDIA_COLLECTION,
|
|
79322
|
+
columns: [
|
|
79323
|
+
"id",
|
|
79324
|
+
"deviceId",
|
|
79325
|
+
"ownerKind",
|
|
79326
|
+
"ownerId",
|
|
79327
|
+
"kind",
|
|
79328
|
+
"path"
|
|
79329
|
+
],
|
|
79330
|
+
filter: {
|
|
79331
|
+
where: { locationId: input.locationId },
|
|
79332
|
+
...input.deviceIds ? { whereIn: { deviceId: [...input.deviceIds] } } : {},
|
|
79333
|
+
whereBetween: { id: [cursor + 1, Number.MAX_SAFE_INTEGER] },
|
|
79334
|
+
orderBy: {
|
|
79335
|
+
field: "id",
|
|
79336
|
+
direction: "asc"
|
|
79337
|
+
},
|
|
79338
|
+
limit: PAGE_SIZE$1
|
|
79339
|
+
}
|
|
79340
|
+
});
|
|
79341
|
+
if (rows.length === 0) break;
|
|
79342
|
+
for (const row of rows) {
|
|
79343
|
+
if (classified.length >= maxRows) {
|
|
79344
|
+
truncated = true;
|
|
79345
|
+
break;
|
|
79346
|
+
}
|
|
79347
|
+
classified.push(await classify(stat, root, row.id, row.data));
|
|
79348
|
+
cursor = Math.max(cursor, Number(row.id));
|
|
79349
|
+
}
|
|
79350
|
+
if (truncated || rows.length < PAGE_SIZE$1) break;
|
|
79351
|
+
await pace();
|
|
79352
|
+
}
|
|
79353
|
+
const absentRows = classified.filter((c) => c.verdict === "absent");
|
|
79354
|
+
const deletedIds = /* @__PURE__ */ new Set();
|
|
79355
|
+
let changedBeforeDelete = 0;
|
|
79356
|
+
if (!input.dryRun) {
|
|
79357
|
+
if (absentRows.length !== input.expectAbsent) throw new Error(`forgetMissingMedia refused: ${String(absentRows.length)} rows are absent now, expectAbsent was ${String(input.expectAbsent)} — run the dry run again; nothing was deleted`);
|
|
79358
|
+
const byDevice = /* @__PURE__ */ new Map();
|
|
79359
|
+
for (const row of absentRows) byDevice.set(row.sample.deviceId, [...byDevice.get(row.sample.deviceId) ?? [], row]);
|
|
79360
|
+
for (const [deviceId, rows] of byDevice) for (let at = 0; at < rows.length; at += DELETE_BATCH) {
|
|
79361
|
+
const batch = [];
|
|
79362
|
+
for (const row of rows.slice(at, at + DELETE_BATCH)) if ((row.absolutePath === null ? null : await probe(stat, row.absolutePath))?.verdict === "absent") batch.push(row);
|
|
79363
|
+
else changedBeforeDelete += 1;
|
|
79364
|
+
if (batch.length === 0) continue;
|
|
79365
|
+
const { deleted } = await deps.store.deleteWhere.mutate({
|
|
79366
|
+
collection: MEDIA_COLLECTION,
|
|
79367
|
+
filter: {
|
|
79368
|
+
where: {
|
|
79369
|
+
locationId: input.locationId,
|
|
79370
|
+
deviceId
|
|
79371
|
+
},
|
|
79372
|
+
whereIn: { id: batch.map((row) => row.key) }
|
|
79373
|
+
}
|
|
79374
|
+
});
|
|
79375
|
+
for (const row of batch) deletedIds.add(row.sample.mediaId);
|
|
79376
|
+
deps.logger.info("forgetMissingMedia: forgot media rows whose file is gone", {
|
|
79377
|
+
tags: { deviceId },
|
|
79378
|
+
meta: {
|
|
79379
|
+
locationId: input.locationId,
|
|
79380
|
+
batch: batch.length,
|
|
79381
|
+
deleted,
|
|
79382
|
+
firstId: batch[0]?.sample.mediaId,
|
|
79383
|
+
lastId: batch[batch.length - 1]?.sample.mediaId
|
|
79384
|
+
}
|
|
79385
|
+
});
|
|
79386
|
+
if (deleted !== batch.length) deps.logger.warn("forgetMissingMedia: a batch deleted fewer rows than it named", {
|
|
79387
|
+
tags: { deviceId },
|
|
79388
|
+
meta: {
|
|
79389
|
+
locationId: input.locationId,
|
|
79390
|
+
batch: batch.length,
|
|
79391
|
+
deleted
|
|
79392
|
+
}
|
|
79393
|
+
});
|
|
79394
|
+
await pace();
|
|
79395
|
+
}
|
|
79396
|
+
}
|
|
79397
|
+
let totals = emptyCounts();
|
|
79398
|
+
const perDevice = /* @__PURE__ */ new Map();
|
|
79399
|
+
const perKind = /* @__PURE__ */ new Map();
|
|
79400
|
+
for (const row of classified) {
|
|
79401
|
+
const isDeleted = deletedIds.has(row.sample.mediaId);
|
|
79402
|
+
const bump = (c) => {
|
|
79403
|
+
const next = tally$1(c, row.verdict);
|
|
79404
|
+
return isDeleted ? {
|
|
79405
|
+
...next,
|
|
79406
|
+
deleted: next.deleted + 1
|
|
79407
|
+
} : next;
|
|
79408
|
+
};
|
|
79409
|
+
totals = bump(totals);
|
|
79410
|
+
perDevice.set(row.sample.deviceId, bump(perDevice.get(row.sample.deviceId) ?? emptyCounts()));
|
|
79411
|
+
perKind.set(row.sample.kind, bump(perKind.get(row.sample.kind) ?? emptyCounts()));
|
|
79412
|
+
}
|
|
79413
|
+
const samplesOf = (verdict) => classified.filter((c) => c.verdict === verdict).slice(0, sampleSize).map((c) => c.sample);
|
|
79414
|
+
for (const [deviceId, counts] of perDevice) deps.logger.info("forgetMissingMedia: per-camera verdict", {
|
|
79415
|
+
tags: { deviceId },
|
|
79416
|
+
meta: {
|
|
79417
|
+
locationId: input.locationId,
|
|
79418
|
+
dryRun: input.dryRun,
|
|
79419
|
+
...counts
|
|
79420
|
+
}
|
|
79421
|
+
});
|
|
79422
|
+
return {
|
|
79423
|
+
locationId: input.locationId,
|
|
79424
|
+
dryRun: input.dryRun,
|
|
79425
|
+
root,
|
|
79426
|
+
truncated,
|
|
79427
|
+
totals,
|
|
79428
|
+
byDevice: [...perDevice].map(([deviceId, c]) => ({
|
|
79429
|
+
deviceId,
|
|
79430
|
+
...c
|
|
79431
|
+
})),
|
|
79432
|
+
byKind: [...perKind].map(([kind, c]) => ({
|
|
79433
|
+
kind,
|
|
79434
|
+
...c
|
|
79435
|
+
})),
|
|
79436
|
+
absentIds: absentRows.map((r) => r.sample.mediaId),
|
|
79437
|
+
samples: {
|
|
79438
|
+
absent: samplesOf("absent"),
|
|
79439
|
+
present: samplesOf("present"),
|
|
79440
|
+
error: samplesOf("error"),
|
|
79441
|
+
kept: samplesOf("kept")
|
|
79442
|
+
},
|
|
79443
|
+
changedBeforeDelete
|
|
79444
|
+
};
|
|
79445
|
+
}
|
|
79446
|
+
//#endregion
|
|
79035
79447
|
//#region src/pipeline-analytics/retention/media-reclaim.ts
|
|
79036
79448
|
var MEDIA_RECLAIM_SCOPES = ["motion-still", "track-filmstrip"];
|
|
79037
79449
|
var DEFAULT_MEDIA_RECLAIM_LIMITS = {
|
|
@@ -93359,17 +93771,8 @@ function rowIsGallery(data) {
|
|
|
93359
93771
|
* D295 calls "a guard nobody believes".
|
|
93360
93772
|
*/
|
|
93361
93773
|
function collectionsFor(mode) {
|
|
93362
|
-
if (mode === "gallery") return [{
|
|
93363
|
-
|
|
93364
|
-
order: "timestamp"
|
|
93365
|
-
}];
|
|
93366
|
-
return [{
|
|
93367
|
-
name: MEDIA_COLLECTION,
|
|
93368
|
-
order: "timestamp"
|
|
93369
|
-
}, {
|
|
93370
|
-
name: RETRAIN_FRAMES_COLLECTION,
|
|
93371
|
-
order: "copiedAt"
|
|
93372
|
-
}];
|
|
93774
|
+
if (mode === "gallery") return [{ lane: "media" }];
|
|
93775
|
+
return [{ lane: "media" }, { lane: "retrainFrames" }];
|
|
93373
93776
|
}
|
|
93374
93777
|
/**
|
|
93375
93778
|
* Does this pass act on this row at all?
|
|
@@ -93454,7 +93857,7 @@ var MediaRelocateEngine = class {
|
|
|
93454
93857
|
async countUnstamped() {
|
|
93455
93858
|
try {
|
|
93456
93859
|
const media = await this.countUnstampedIn(MEDIA_COLLECTION);
|
|
93457
|
-
const retrainFrames = await this.
|
|
93860
|
+
const retrainFrames = await this.countUnstampedRetrainFrames();
|
|
93458
93861
|
return {
|
|
93459
93862
|
media,
|
|
93460
93863
|
retrainFrames,
|
|
@@ -93506,6 +93909,28 @@ var MediaRelocateEngine = class {
|
|
|
93506
93909
|
};
|
|
93507
93910
|
}
|
|
93508
93911
|
}
|
|
93912
|
+
/**
|
|
93913
|
+
* The retrain lane's unstamped population. The ledger's records `where` is
|
|
93914
|
+
* equality-only (a `null` predicate is dropped, which would count the whole
|
|
93915
|
+
* dataset), so the rows are read and judged by {@link rowIsUnstamped} — the
|
|
93916
|
+
* dataset is operator-selected and small.
|
|
93917
|
+
*/
|
|
93918
|
+
async countUnstampedRetrainFrames() {
|
|
93919
|
+
const rows = (await this.deps.retrainFrameLedger.query({})).filter((row) => rowIsUnstamped(row.data)).length;
|
|
93920
|
+
return {
|
|
93921
|
+
present: rows > 0,
|
|
93922
|
+
rows
|
|
93923
|
+
};
|
|
93924
|
+
}
|
|
93925
|
+
/** Every row of one lane, oldest-first. */
|
|
93926
|
+
rowsOf(collection) {
|
|
93927
|
+
return collection.lane === "media" ? this.pageRows(MEDIA_COLLECTION, "timestamp") : this.retrainFrameRows();
|
|
93928
|
+
}
|
|
93929
|
+
/** The retrain-frame ledger, read whole and yielded oldest copy first. */
|
|
93930
|
+
async *retrainFrameRows() {
|
|
93931
|
+
const ordered = (await this.deps.retrainFrameLedger.query({})).toSorted((a, b) => Number(a.data["copiedAt"] ?? 0) - Number(b.data["copiedAt"] ?? 0));
|
|
93932
|
+
for (const row of ordered) yield row;
|
|
93933
|
+
}
|
|
93509
93934
|
start(input) {
|
|
93510
93935
|
for (const j of this.jobs.values()) if (j.state === "running") throw new Error(`a media relocation is already running (${j.jobId})`);
|
|
93511
93936
|
this.cancelCensuses("a relocate pass started");
|
|
@@ -93609,22 +94034,22 @@ var MediaRelocateEngine = class {
|
|
|
93609
94034
|
let sealable = 0;
|
|
93610
94035
|
for (const collection of collectionsFor(mode)) {
|
|
93611
94036
|
if (stop()) return null;
|
|
93612
|
-
sealable += await this.deps.store.count.query({
|
|
93613
|
-
collection:
|
|
94037
|
+
sealable += collection.lane === "media" ? await this.deps.store.count.query({
|
|
94038
|
+
collection: MEDIA_COLLECTION,
|
|
93614
94039
|
filter: { where: { [LOCATION_ID_FIELD]: null } }
|
|
93615
|
-
});
|
|
94040
|
+
}) : (await this.countUnstampedRetrainFrames()).rows ?? 0;
|
|
93616
94041
|
}
|
|
93617
94042
|
return { rows: sealable };
|
|
93618
94043
|
}
|
|
93619
94044
|
let rows = 0;
|
|
93620
94045
|
for (const collection of collectionsFor(mode)) {
|
|
93621
|
-
const isRetrain = collection.
|
|
93622
|
-
for await (const row of this.
|
|
94046
|
+
const isRetrain = collection.lane === "retrainFrames";
|
|
94047
|
+
for await (const row of this.rowsOf(collection)) {
|
|
93623
94048
|
if (stop()) {
|
|
93624
94049
|
this.deps.logger.warn("media relocate: census stopped before it finished — reporting the residue as unknown, not as empty", { meta: {
|
|
93625
94050
|
mode,
|
|
93626
94051
|
toLocationId,
|
|
93627
|
-
collection: collection.
|
|
94052
|
+
collection: collection.lane
|
|
93628
94053
|
} });
|
|
93629
94054
|
return null;
|
|
93630
94055
|
}
|
|
@@ -93632,7 +94057,7 @@ var MediaRelocateEngine = class {
|
|
|
93632
94057
|
this.deps.logger.warn("media relocate: census exceeded its budget — this collection is larger than a walk can answer", { meta: {
|
|
93633
94058
|
mode,
|
|
93634
94059
|
toLocationId,
|
|
93635
|
-
collection: collection.
|
|
94060
|
+
collection: collection.lane,
|
|
93636
94061
|
rowsVisited: budget.rowsVisited,
|
|
93637
94062
|
rowBudget: MEDIA_CENSUS_ROW_BUDGET,
|
|
93638
94063
|
deadlineMs: MEDIA_CENSUS_DEADLINE_MS
|
|
@@ -93668,15 +94093,15 @@ var MediaRelocateEngine = class {
|
|
|
93668
94093
|
if (job.mode === "seal") {
|
|
93669
94094
|
for (const collection of collectionsFor(job.mode)) {
|
|
93670
94095
|
if (job.cancelRequested) break;
|
|
93671
|
-
await this.sealCollection(collection
|
|
94096
|
+
await this.sealCollection(collection, job);
|
|
93672
94097
|
}
|
|
93673
94098
|
job.state = job.cancelRequested ? "cancelled" : "done";
|
|
93674
94099
|
return;
|
|
93675
94100
|
}
|
|
93676
94101
|
for (const collection of collectionsFor(job.mode)) {
|
|
93677
94102
|
if (job.cancelRequested) break;
|
|
93678
|
-
const isRetrain = collection.
|
|
93679
|
-
for await (const row of this.
|
|
94103
|
+
const isRetrain = collection.lane === "retrainFrames";
|
|
94104
|
+
for await (const row of this.rowsOf(collection)) {
|
|
93680
94105
|
if (job.cancelRequested) break;
|
|
93681
94106
|
const data = row.data;
|
|
93682
94107
|
if (!selectsRow(job.mode, job.toLocationId, data, isRetrain, job.fromLocationId)) continue;
|
|
@@ -93688,7 +94113,7 @@ var MediaRelocateEngine = class {
|
|
|
93688
94113
|
}
|
|
93689
94114
|
try {
|
|
93690
94115
|
if (await this.samePhysicalLocation(from, job.toLocationId)) {
|
|
93691
|
-
await this.stamp(collection
|
|
94116
|
+
await this.stamp(collection, row.id, data, job.toLocationId);
|
|
93692
94117
|
continue;
|
|
93693
94118
|
}
|
|
93694
94119
|
const bytes = await this.deps.storage.read({
|
|
@@ -93704,7 +94129,7 @@ var MediaRelocateEngine = class {
|
|
|
93704
94129
|
location: job.toLocationId,
|
|
93705
94130
|
relativePath
|
|
93706
94131
|
})).length !== bytes.length) throw new Error(`relocated media size mismatch for ${relativePath}`);
|
|
93707
|
-
await this.stamp(collection
|
|
94132
|
+
await this.stamp(collection, row.id, data, job.toLocationId);
|
|
93708
94133
|
await this.deps.storage.delete({
|
|
93709
94134
|
location: from,
|
|
93710
94135
|
relativePath
|
|
@@ -93715,7 +94140,7 @@ var MediaRelocateEngine = class {
|
|
|
93715
94140
|
} catch (err) {
|
|
93716
94141
|
failedRows++;
|
|
93717
94142
|
this.deps.logger.debug("media relocate row failed", { meta: {
|
|
93718
|
-
collection: collection.
|
|
94143
|
+
collection: collection.lane,
|
|
93719
94144
|
key: row.id,
|
|
93720
94145
|
error: String(err)
|
|
93721
94146
|
} });
|
|
@@ -93788,7 +94213,17 @@ var MediaRelocateEngine = class {
|
|
|
93788
94213
|
* reports a completed seal having stamped nothing. Zero unstamped rows is the
|
|
93789
94214
|
* single value the migration gate reads as "safe to cut over".
|
|
93790
94215
|
*/
|
|
93791
|
-
async sealCollection(
|
|
94216
|
+
async sealCollection(lane, job) {
|
|
94217
|
+
if (lane.lane === "retrainFrames") {
|
|
94218
|
+
for (const row of await this.deps.retrainFrameLedger.query({})) {
|
|
94219
|
+
if (job.cancelRequested) return;
|
|
94220
|
+
if (!rowIsUnstamped(row.data)) continue;
|
|
94221
|
+
await this.deps.retrainFrameLedger.update(row.id, { [LOCATION_ID_FIELD]: job.toLocationId });
|
|
94222
|
+
job.filesMoved += 1;
|
|
94223
|
+
}
|
|
94224
|
+
return;
|
|
94225
|
+
}
|
|
94226
|
+
const collection = MEDIA_COLLECTION;
|
|
93792
94227
|
for (;;) {
|
|
93793
94228
|
if (job.cancelRequested) return;
|
|
93794
94229
|
const batch = await this.deps.store.query.query({
|
|
@@ -93809,9 +94244,13 @@ var MediaRelocateEngine = class {
|
|
|
93809
94244
|
job.filesMoved += updated;
|
|
93810
94245
|
}
|
|
93811
94246
|
}
|
|
93812
|
-
async stamp(
|
|
94247
|
+
async stamp(lane, key, data, locationId) {
|
|
94248
|
+
if (lane.lane === "retrainFrames") {
|
|
94249
|
+
await this.deps.retrainFrameLedger.update(key, { [LOCATION_ID_FIELD]: locationId });
|
|
94250
|
+
return;
|
|
94251
|
+
}
|
|
93813
94252
|
await this.deps.store.set.mutate({
|
|
93814
|
-
collection,
|
|
94253
|
+
collection: MEDIA_COLLECTION,
|
|
93815
94254
|
key,
|
|
93816
94255
|
value: {
|
|
93817
94256
|
...data,
|
|
@@ -102258,6 +102697,18 @@ function decodeCursorMap(raw) {
|
|
|
102258
102697
|
function nonNegativeInt(value) {
|
|
102259
102698
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : null;
|
|
102260
102699
|
}
|
|
102700
|
+
function decodeScopeCursors(raw) {
|
|
102701
|
+
const out = {};
|
|
102702
|
+
if (raw !== null && typeof raw === "object") for (const [key, cursor] of Object.entries({ ...raw })) {
|
|
102703
|
+
if (typeof cursor === "string" && cursor.length > 0) {
|
|
102704
|
+
out[key] = cursor;
|
|
102705
|
+
continue;
|
|
102706
|
+
}
|
|
102707
|
+
const parsed = nonNegativeInt(cursor);
|
|
102708
|
+
if (parsed !== null) out[key] = parsed;
|
|
102709
|
+
}
|
|
102710
|
+
return out;
|
|
102711
|
+
}
|
|
102261
102712
|
/** Read a stored row back into a cursor map, ignoring anything unreadable. */
|
|
102262
102713
|
function decodeOrphanAuditCursors(value) {
|
|
102263
102714
|
const empty = {
|
|
@@ -102269,7 +102720,7 @@ function decodeOrphanAuditCursors(value) {
|
|
|
102269
102720
|
if (value === null || typeof value !== "object") return empty;
|
|
102270
102721
|
const row = { ...value };
|
|
102271
102722
|
return {
|
|
102272
|
-
scopes:
|
|
102723
|
+
scopes: decodeScopeCursors(row["scopes"]),
|
|
102273
102724
|
faceVectorCursor: nonNegativeInt(row["faceVectorCursor"]) ?? 0,
|
|
102274
102725
|
clipIndexCursors: decodeCursorMap(row["clipIndexCursors"]),
|
|
102275
102726
|
updatedAtMs: nonNegativeInt(row["updatedAtMs"]) ?? 0
|
|
@@ -103842,7 +104293,9 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
103842
104293
|
};
|
|
103843
104294
|
const deviceIds = /* @__PURE__ */ new Set();
|
|
103844
104295
|
const graceFloor = deps.now() - limits.graceMs;
|
|
103845
|
-
let offset = Math.max(0, cursor);
|
|
104296
|
+
let offset = typeof cursor === "number" ? Math.max(0, cursor) : 0;
|
|
104297
|
+
let keysetCursor = typeof cursor === "string" ? cursor : null;
|
|
104298
|
+
let keyed = keysetCursor !== null;
|
|
103846
104299
|
let stoppedBy = "exhausted";
|
|
103847
104300
|
let collectionSize = null;
|
|
103848
104301
|
try {
|
|
@@ -103859,9 +104312,16 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
103859
104312
|
stoppedBy = "row-ceiling";
|
|
103860
104313
|
break;
|
|
103861
104314
|
}
|
|
103862
|
-
|
|
104315
|
+
if (deps.stop?.() === true) {
|
|
104316
|
+
stoppedBy = "stopped";
|
|
104317
|
+
break;
|
|
104318
|
+
}
|
|
104319
|
+
const keyedPage = deps.pageKeyset === void 0 ? null : await deps.pageKeyset(scope.collection, keysetCursor, limits.pageSize);
|
|
104320
|
+
keyed = keyedPage !== null;
|
|
104321
|
+
const rows = keyedPage !== null ? keyedPage.rows : await deps.page(scope.collection, scope.shard, offset, limits.pageSize);
|
|
103863
104322
|
if (rows.length === 0) {
|
|
103864
104323
|
offset = 0;
|
|
104324
|
+
keysetCursor = null;
|
|
103865
104325
|
break;
|
|
103866
104326
|
}
|
|
103867
104327
|
const refs = rows.map((row) => scope.resolve(row));
|
|
@@ -103948,9 +104408,17 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
103948
104408
|
if (deps.breathe) await deps.breathe();
|
|
103949
104409
|
if (ceilingHit) {
|
|
103950
104410
|
stoppedBy = "reclaim-ceiling";
|
|
103951
|
-
offset += rows.length - reclaimedInPage;
|
|
104411
|
+
if (!keyed) offset += rows.length - reclaimedInPage;
|
|
103952
104412
|
break;
|
|
103953
104413
|
}
|
|
104414
|
+
if (keyedPage !== null) {
|
|
104415
|
+
if (keyedPage.nextCursor === null) {
|
|
104416
|
+
keysetCursor = null;
|
|
104417
|
+
break;
|
|
104418
|
+
}
|
|
104419
|
+
keysetCursor = keyedPage.nextCursor;
|
|
104420
|
+
continue;
|
|
104421
|
+
}
|
|
103954
104422
|
const advanced = rows.length - reclaimedInPage;
|
|
103955
104423
|
if (advanced === 0 && rows.length < limits.pageSize) {
|
|
103956
104424
|
offset = 0;
|
|
@@ -103966,7 +104434,7 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
103966
104434
|
collectionSize,
|
|
103967
104435
|
coveragePercent: Math.round(tally.examined / collectionSize * 1e3) / 10,
|
|
103968
104436
|
stoppedBy,
|
|
103969
|
-
nextCursor: offset
|
|
104437
|
+
nextCursor: keyed ? keysetCursor ?? 0 : offset
|
|
103970
104438
|
} });
|
|
103971
104439
|
const freedNothing = scope.sizeField === null ? tally.reclaimed === 0 : tally.bytesReclaimed === 0;
|
|
103972
104440
|
if (limits.mode === "reclaim" && tally.orphans > 0 && freedNothing) deps.logger.warn("orphan audit: reclaim pass freed no bytes against a standing backlog", { meta: {
|
|
@@ -103993,7 +104461,7 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
103993
104461
|
unresolvable: tally.unresolvable,
|
|
103994
104462
|
deviceIds: [...deviceIds],
|
|
103995
104463
|
stoppedBy,
|
|
103996
|
-
nextCursor: offset
|
|
104464
|
+
nextCursor: keyed ? keysetCursor ?? 0 : offset
|
|
103997
104465
|
};
|
|
103998
104466
|
}
|
|
103999
104467
|
/**
|
|
@@ -104085,7 +104553,8 @@ var ORPHAN_AUDIT_DEFAULTS = {
|
|
|
104085
104553
|
//#endregion
|
|
104086
104554
|
//#region src/pipeline-analytics/retention/orphan-reclaimers.ts
|
|
104087
104555
|
var OBJECT_EVENTS_COLLECTION = "pipeline-analytics:object-events";
|
|
104088
|
-
var
|
|
104556
|
+
var PLATES_COLLECTION = "pipeline-analytics:plates";
|
|
104557
|
+
var RAW_DELETE_COLLECTIONS = new Set([OBJECT_EVENTS_COLLECTION]);
|
|
104089
104558
|
/**
|
|
104090
104559
|
* The `deleteRow` port: ONE row by its primary key `id` (what every
|
|
104091
104560
|
* raw-delete collection above declares), answering whether a row went.
|
|
@@ -104159,6 +104628,13 @@ function buildOrphanReclaimer(ports) {
|
|
|
104159
104628
|
rows: await ports.deleteTrackAlias(key) ? 1 : 0,
|
|
104160
104629
|
bytes: null
|
|
104161
104630
|
};
|
|
104631
|
+
if (collection === PLATES_COLLECTION) {
|
|
104632
|
+
const assert = trackIdAssertOf(data);
|
|
104633
|
+
return {
|
|
104634
|
+
rows: await ports.deletePlate(key, assert === null ? void 0 : assert) ? 1 : 0,
|
|
104635
|
+
bytes: null
|
|
104636
|
+
};
|
|
104637
|
+
}
|
|
104162
104638
|
if (RAW_DELETE_COLLECTIONS.has(collection)) {
|
|
104163
104639
|
const assert = trackIdAssertOf(data);
|
|
104164
104640
|
if (collection === OBJECT_EVENTS_COLLECTION && assert === null) {
|
|
@@ -104180,6 +104656,87 @@ function buildOrphanReclaimer(ports) {
|
|
|
104180
104656
|
};
|
|
104181
104657
|
}
|
|
104182
104658
|
//#endregion
|
|
104659
|
+
//#region src/pipeline-analytics/retention/orphan-audit-ledgers.ts
|
|
104660
|
+
/** Audited collection → its records-kind ledger. Empty without records. */
|
|
104661
|
+
function orphanAuditLedgers(wiring) {
|
|
104662
|
+
if (wiring.records === void 0) return /* @__PURE__ */ new Map();
|
|
104663
|
+
return new Map([
|
|
104664
|
+
[FACES_COLLECTION, wireFaceLedger(wiring)],
|
|
104665
|
+
[PLATES_COLLECTION$1, wirePlateLedger(wiring)],
|
|
104666
|
+
[TRACK_ALIASES_COLLECTION, wireTrackAliasLedger(wiring)]
|
|
104667
|
+
]);
|
|
104668
|
+
}
|
|
104669
|
+
/** {@link OrphanAuditDeps.pageKeyset} over those ledgers; `null` for any other collection. */
|
|
104670
|
+
function keysetPager(ledgers) {
|
|
104671
|
+
return async (collection, cursor, limit) => {
|
|
104672
|
+
const ledger = ledgers.get(collection);
|
|
104673
|
+
if (ledger === void 0) return null;
|
|
104674
|
+
const page = await ledger.pageById(cursor, limit);
|
|
104675
|
+
return {
|
|
104676
|
+
rows: page.rows.map((row) => ({
|
|
104677
|
+
key: row.id,
|
|
104678
|
+
data: row.data
|
|
104679
|
+
})),
|
|
104680
|
+
nextCursor: page.nextCursor
|
|
104681
|
+
};
|
|
104682
|
+
};
|
|
104683
|
+
}
|
|
104684
|
+
/** The kind's count for a ledger-backed collection, `undefined` for any other. */
|
|
104685
|
+
async function ledgerSize(ledgers, collection) {
|
|
104686
|
+
const ledger = ledgers.get(collection);
|
|
104687
|
+
return ledger === void 0 ? void 0 : ledger.count();
|
|
104688
|
+
}
|
|
104689
|
+
/**
|
|
104690
|
+
* The plate reclaim through the plate ledger — records on a hub, the legacy
|
|
104691
|
+
* collection only without records. With `assert`, a row whose `trackId` is not
|
|
104692
|
+
* the dead track's is KEPT (logged), never deleted.
|
|
104693
|
+
*/
|
|
104694
|
+
function plateDeleter(plates, logger) {
|
|
104695
|
+
return async (plateId, assert) => {
|
|
104696
|
+
const row = await plates.get(plateId);
|
|
104697
|
+
if (row === null) return false;
|
|
104698
|
+
if (assert !== void 0 && row.data["trackId"] !== assert.trackId) {
|
|
104699
|
+
const deviceId = row.data["deviceId"];
|
|
104700
|
+
logger.warn("orphan audit: plate row now names another track — kept", {
|
|
104701
|
+
...typeof deviceId === "number" ? { tags: { deviceId } } : {},
|
|
104702
|
+
meta: {
|
|
104703
|
+
plateId,
|
|
104704
|
+
expectedTrackId: assert.trackId
|
|
104705
|
+
}
|
|
104706
|
+
});
|
|
104707
|
+
return false;
|
|
104708
|
+
}
|
|
104709
|
+
await plates.deleteKey(plateId);
|
|
104710
|
+
return true;
|
|
104711
|
+
};
|
|
104712
|
+
}
|
|
104713
|
+
function auditCollectionPorts(store, ledgers) {
|
|
104714
|
+
return {
|
|
104715
|
+
pageKeyset: keysetPager(ledgers),
|
|
104716
|
+
pageByOffset: async (collection, offset, limit) => {
|
|
104717
|
+
return (await store.query.query({
|
|
104718
|
+
collection,
|
|
104719
|
+
filter: {
|
|
104720
|
+
orderBy: {
|
|
104721
|
+
field: "id",
|
|
104722
|
+
direction: "asc"
|
|
104723
|
+
},
|
|
104724
|
+
limit,
|
|
104725
|
+
offset
|
|
104726
|
+
}
|
|
104727
|
+
})).map((row) => ({
|
|
104728
|
+
key: row.id,
|
|
104729
|
+
data: row.data
|
|
104730
|
+
}));
|
|
104731
|
+
},
|
|
104732
|
+
size: async (collection) => {
|
|
104733
|
+
const counted = await ledgerSize(ledgers, collection);
|
|
104734
|
+
if (counted !== void 0) return counted;
|
|
104735
|
+
return store.count.query({ collection });
|
|
104736
|
+
}
|
|
104737
|
+
};
|
|
104738
|
+
}
|
|
104739
|
+
//#endregion
|
|
104183
104740
|
//#region src/pipeline-analytics/retention/retired-device-settings.ts
|
|
104184
104741
|
/**
|
|
104185
104742
|
* Every retired knob, with the sentence that explains it. The list is the
|
|
@@ -114937,6 +115494,7 @@ var customActions = {
|
|
|
114937
115494
|
...orphanAuditActions,
|
|
114938
115495
|
...mediaReclaimActions,
|
|
114939
115496
|
...locationDiscardActions,
|
|
115497
|
+
...mediaForgetActions,
|
|
114940
115498
|
...photoEnrollActions,
|
|
114941
115499
|
...faceRescoreActions,
|
|
114942
115500
|
...stillnessCalibrationActions
|
|
@@ -116727,6 +117285,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
116727
117285
|
orphanAuditState = { ...IDLE_ORPHAN_AUDIT };
|
|
116728
117286
|
/** Frozen-location leftover wipe (events/ on a drained HDD). Detached. */
|
|
116729
117287
|
discardJob = null;
|
|
117288
|
+
/** The narrow media-row forget's wiring (`storage.forgetMissingMedia`). */
|
|
117289
|
+
mediaForgetDeps = null;
|
|
116730
117290
|
/**
|
|
116731
117291
|
* The CLIP indexes' walks (D649), one resume cursor per index, kept in their
|
|
116732
117292
|
* OWN field rather than in {@link orphanAuditCursors}: `runOrphanAudit`
|
|
@@ -116935,10 +117495,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
116935
117495
|
const isHub = ownNodeId === "hub";
|
|
116936
117496
|
const ncHandlers = isHub ? this.buildNcActionHandlers(api) : void 0;
|
|
116937
117497
|
if (isHub) this.reconcileTapePinsInBackground();
|
|
117498
|
+
this.mediaForgetDeps = {
|
|
117499
|
+
listLocations: async () => api.storage.listLocations.query({}),
|
|
117500
|
+
localNodeId: ownNodeId,
|
|
117501
|
+
store: api.settingsStore,
|
|
117502
|
+
logger: logger.child("MediaForget")
|
|
117503
|
+
};
|
|
116938
117504
|
this.discardJob = createDiscardJob({
|
|
116939
117505
|
listLocations: async () => api.storage.listLocations.query({}),
|
|
116940
117506
|
localNodeId: ownNodeId,
|
|
116941
117507
|
store: api.settingsStore,
|
|
117508
|
+
retrainFrameLedger: wireRetrainFrameLedger(this.eventLedgerWiring(api.settingsStore)),
|
|
116942
117509
|
deleteTrackEvents: async (trackId) => {
|
|
116943
117510
|
await this.eventStore?.deleteByTrack(trackId);
|
|
116944
117511
|
},
|
|
@@ -116959,6 +117526,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
116959
117526
|
...orphanAuditActions,
|
|
116960
117527
|
...mediaReclaimActions,
|
|
116961
117528
|
...locationDiscardActions,
|
|
117529
|
+
...mediaForgetActions,
|
|
116962
117530
|
...photoEnrollActions,
|
|
116963
117531
|
...faceRescoreActions,
|
|
116964
117532
|
...stillnessCalibrationActions
|
|
@@ -117554,6 +118122,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
117554
118122
|
});
|
|
117555
118123
|
this.mediaRelocate = new MediaRelocateEngine({
|
|
117556
118124
|
store: api.settingsStore,
|
|
118125
|
+
retrainFrameLedger: wireRetrainFrameLedger(eventLedgerWiring),
|
|
117557
118126
|
storage,
|
|
117558
118127
|
logger: logger.child("MediaRelocate"),
|
|
117559
118128
|
now: () => Date.now(),
|
|
@@ -125296,36 +125865,29 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
125296
125865
|
const trackStore = this.trackStore;
|
|
125297
125866
|
const eventStore = this.eventStore;
|
|
125298
125867
|
const mediaStore = this.mediaStore;
|
|
125299
|
-
|
|
125868
|
+
const settingsStore = this.settingsStore;
|
|
125869
|
+
if (!trackStore || !eventStore || !mediaStore || !settingsStore) {
|
|
125300
125870
|
if (!alreadyHeld) this.orphanAuditRunning = false;
|
|
125301
125871
|
return null;
|
|
125302
125872
|
}
|
|
125303
125873
|
try {
|
|
125304
125874
|
const limits = await this.orphanAuditLimits(overrides);
|
|
125875
|
+
const auditLedgers = orphanAuditLedgers(this.eventLedgerWiring(settingsStore));
|
|
125876
|
+
const plateLedger = wirePlateLedger(this.eventLedgerWiring(settingsStore));
|
|
125877
|
+
const collectionPorts = auditCollectionPorts(settingsStore, auditLedgers);
|
|
125305
125878
|
const deps = {
|
|
125879
|
+
pageKeyset: collectionPorts.pageKeyset,
|
|
125880
|
+
stop: () => this.shuttingDown,
|
|
125306
125881
|
page: async (collection, shard, offset, limit) => {
|
|
125307
125882
|
if (collection === MEDIA_COLLECTION) return (await mediaStore.pageForAudit(shard?.value ?? "", offset, limit)).map((row) => ({
|
|
125308
125883
|
key: String(row.mediaId),
|
|
125309
125884
|
data: row.data
|
|
125310
125885
|
}));
|
|
125311
|
-
return (
|
|
125312
|
-
collection,
|
|
125313
|
-
filter: {
|
|
125314
|
-
orderBy: {
|
|
125315
|
-
field: "id",
|
|
125316
|
-
direction: "asc"
|
|
125317
|
-
},
|
|
125318
|
-
limit,
|
|
125319
|
-
offset
|
|
125320
|
-
}
|
|
125321
|
-
}) ?? []).map((row) => ({
|
|
125322
|
-
key: row.id,
|
|
125323
|
-
data: row.data
|
|
125324
|
-
}));
|
|
125886
|
+
return collectionPorts.pageByOffset(collection, offset, limit);
|
|
125325
125887
|
},
|
|
125326
125888
|
size: async (collection, shard) => {
|
|
125327
125889
|
if (collection === MEDIA_COLLECTION) return mediaStore.countForAudit(shard?.value ?? "");
|
|
125328
|
-
return
|
|
125890
|
+
return collectionPorts.size(collection);
|
|
125329
125891
|
},
|
|
125330
125892
|
liveTrackIds: (ids) => this.liveTrackIdsThroughAliases(ids, trackStore),
|
|
125331
125893
|
liveEventIds: (table, ids) => eventStore.existingEventIdsIn(table, ids),
|
|
@@ -125348,6 +125910,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
125348
125910
|
if (aliasStore === null) throw new Error("track alias store not bound");
|
|
125349
125911
|
return await aliasStore.deleteAlias(aliasId);
|
|
125350
125912
|
},
|
|
125913
|
+
deletePlate: plateDeleter(plateLedger, this.ctx.logger),
|
|
125351
125914
|
deleteRow: settingsRowDeleterOf(() => this.settingsStore),
|
|
125352
125915
|
logger: this.ctx.logger
|
|
125353
125916
|
});
|
|
@@ -126769,7 +127332,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
126769
127332
|
jobId: null
|
|
126770
127333
|
},
|
|
126771
127334
|
"storage.discardLocationStatus": async () => this.discardJob?.status() ?? IDLE_DISCARD,
|
|
126772
|
-
"storage.discardLocationCancel": async () => this.discardJob?.cancel() ?? { requested: false }
|
|
127335
|
+
"storage.discardLocationCancel": async () => this.discardJob?.cancel() ?? { requested: false },
|
|
127336
|
+
"storage.forgetMissingMedia": async (input) => {
|
|
127337
|
+
const deps = this.mediaForgetDeps;
|
|
127338
|
+
if (deps === null) throw new Error("storage.forgetMissingMedia: not initialised");
|
|
127339
|
+
return forgetMissingMediaRows(deps, input);
|
|
127340
|
+
}
|
|
126773
127341
|
};
|
|
126774
127342
|
}
|
|
126775
127343
|
/**
|