@camstack/addon-post-analysis 1.2.613 → 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/pipeline-analytics/index.js +642 -74
- package/dist/pipeline-analytics/index.mjs +641 -74
- package/package.json +1 -1
|
@@ -9491,7 +9491,9 @@ var MigrationMarkerBodySchema = object({
|
|
|
9491
9491
|
});
|
|
9492
9492
|
/** The orphan audit's resume position: one row, the whole map in one value on purpose. */
|
|
9493
9493
|
var SweepCursorBodySchema = object({
|
|
9494
|
-
|
|
9494
|
+
/** A number is an offset (a settings-store scope); a string is the records
|
|
9495
|
+
* engine's opaque keyset cursor (a scope walked through its kind). */
|
|
9496
|
+
scopes: record(string(), union([number(), string()])),
|
|
9495
9497
|
/** Absent on a row written before the walk existed — absent means "from the top". */
|
|
9496
9498
|
faceVectorCursor: number().optional(),
|
|
9497
9499
|
/** One cursor per CLIP index, keyed by index name. Absent on an older row. */
|
|
@@ -9965,7 +9967,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
|
|
|
9965
9967
|
//#endregion
|
|
9966
9968
|
//#region src/pipeline-analytics/scene/scene-config-ledger.ts
|
|
9967
9969
|
/** The engine's page cap for one query; a reseed pages by it, bounded by the ledger limit. */
|
|
9968
|
-
var PAGE_SIZE$
|
|
9970
|
+
var PAGE_SIZE$2 = 1e3;
|
|
9969
9971
|
/**
|
|
9970
9972
|
* A row whose monitor JSON no longer validates is SKIPPED, not repaired: a
|
|
9971
9973
|
* half-parsed scene would report a verdict about a region it cannot describe.
|
|
@@ -10003,7 +10005,7 @@ var SceneConfigLedger = class {
|
|
|
10003
10005
|
let seen = 0;
|
|
10004
10006
|
let cursor;
|
|
10005
10007
|
for (;;) {
|
|
10006
|
-
const limit = Math.min(PAGE_SIZE$
|
|
10008
|
+
const limit = Math.min(PAGE_SIZE$2, this.loadLimit - seen);
|
|
10007
10009
|
if (limit <= 0) break;
|
|
10008
10010
|
const page = await this.kind.query({
|
|
10009
10011
|
limit,
|
|
@@ -12050,6 +12052,28 @@ var EventLedgerCollection = class EventLedgerCollection {
|
|
|
12050
12052
|
if (mapped.limit !== void 0 && out.length > mapped.limit) return out.slice(0, mapped.limit);
|
|
12051
12053
|
return out;
|
|
12052
12054
|
}
|
|
12055
|
+
/**
|
|
12056
|
+
* One page of the whole kind in `id` order, resumed from the records
|
|
12057
|
+
* engine's OPAQUE keyset cursor (`null` = from the top). The answer's
|
|
12058
|
+
* `nextCursor` is `null` at the end. A keyset cursor names the last row
|
|
12059
|
+
* read, so deleting rows behind it never shifts what comes next — the
|
|
12060
|
+
* reason a bulk walk that reclaims as it goes needs this, not an offset.
|
|
12061
|
+
*
|
|
12062
|
+
* Records only: without `ctx.records` the caller pages the settings store
|
|
12063
|
+
* itself, and asking here throws rather than inventing a cursor.
|
|
12064
|
+
*/
|
|
12065
|
+
async pageById(cursor, limit) {
|
|
12066
|
+
if (this.kind === null) throw new Error(`event ledger ${this.collection}: pageById needs the records kind`);
|
|
12067
|
+
const page = await this.kind.query({
|
|
12068
|
+
order: "id",
|
|
12069
|
+
limit,
|
|
12070
|
+
...cursor === null ? {} : { cursor }
|
|
12071
|
+
});
|
|
12072
|
+
return {
|
|
12073
|
+
rows: page.records.map((row) => recordsRowToLegacy(row, this.legacyAtField)),
|
|
12074
|
+
nextCursor: page.nextCursor
|
|
12075
|
+
};
|
|
12076
|
+
}
|
|
12053
12077
|
async getManyByIds(ids) {
|
|
12054
12078
|
if (ids.length === 0) return [];
|
|
12055
12079
|
if (this.kind === null) return (await this.store.query.query({
|
|
@@ -13176,11 +13200,35 @@ var FaceStore = class {
|
|
|
13176
13200
|
* carries no `embeddingModelId`, and those rows were all `arcface-r100`, so
|
|
13177
13201
|
* that is what they are stamped with — a KNOWN historical fact, not a
|
|
13178
13202
|
* fallback guess. A row that names some other model keeps its own.
|
|
13203
|
+
*
|
|
13204
|
+
* On a records hub the rows are paged through the faces LEDGER: the legacy
|
|
13205
|
+
* table is dropped there, and the adoption copied each row's body whole —
|
|
13206
|
+
* an `embedding` the pass had not drained yet included — so a records-era
|
|
13207
|
+
* row can still need converting (the same shape as `IdentityStore`).
|
|
13179
13208
|
*/
|
|
13180
13209
|
conversionSpec() {
|
|
13210
|
+
const ledger = this.ledger;
|
|
13211
|
+
const recordsPaging = ledger.usesRecords ? {
|
|
13212
|
+
pageRows: (limit, offset) => ledger.query({
|
|
13213
|
+
orderBy: {
|
|
13214
|
+
field: "id",
|
|
13215
|
+
direction: "asc"
|
|
13216
|
+
},
|
|
13217
|
+
limit,
|
|
13218
|
+
offset
|
|
13219
|
+
}, [
|
|
13220
|
+
"deviceId",
|
|
13221
|
+
"timestamp",
|
|
13222
|
+
"trackId",
|
|
13223
|
+
"embeddingModelId",
|
|
13224
|
+
"embedding"
|
|
13225
|
+
]),
|
|
13226
|
+
drainEmbedding: (id) => ledger.update(id, { embedding: [] })
|
|
13227
|
+
} : {};
|
|
13181
13228
|
return {
|
|
13182
13229
|
collection: FACES_COLLECTION,
|
|
13183
13230
|
vectors: this.vectors,
|
|
13231
|
+
...recordsPaging,
|
|
13184
13232
|
write: (id, embedding, data) => {
|
|
13185
13233
|
const deviceId = data["deviceId"];
|
|
13186
13234
|
const timestamp = data["timestamp"];
|
|
@@ -67432,7 +67480,7 @@ async function settleReplaced(deps, sample, replacement, dryRun) {
|
|
|
67432
67480
|
});
|
|
67433
67481
|
return "old-removal-retried";
|
|
67434
67482
|
}
|
|
67435
|
-
function tally$
|
|
67483
|
+
function tally$2(samples, outcomes, dryRun) {
|
|
67436
67484
|
const count = (kind) => outcomes.filter((o) => o.outcome === kind).length;
|
|
67437
67485
|
const refusals = [];
|
|
67438
67486
|
for (const o of outcomes) if (o.outcome === "refused" && o.reason !== void 0) refusals.push({
|
|
@@ -67504,7 +67552,7 @@ async function reenrolPhotoSamples(deps, options) {
|
|
|
67504
67552
|
outcomes.push(outcomeOf(sample, verdict));
|
|
67505
67553
|
await deps.pause(pacingMs);
|
|
67506
67554
|
}
|
|
67507
|
-
const result = tally$
|
|
67555
|
+
const result = tally$2(samples, outcomes, dryRun);
|
|
67508
67556
|
if (!dryRun && (result.reenrolled > 0 || result.oldRemovalRetried > 0)) await deps.refreshGallery();
|
|
67509
67557
|
const { outcomes: _perSample, ...summary } = result;
|
|
67510
67558
|
deps.logger.info("face photo re-enrol finished", { meta: { ...summary } });
|
|
@@ -78919,24 +78967,11 @@ function createDiscardJob(deps) {
|
|
|
78919
78967
|
});
|
|
78920
78968
|
await sleep(0);
|
|
78921
78969
|
}
|
|
78922
|
-
|
|
78970
|
+
const retrainIds = (await deps.retrainFrameLedger.query({})).filter((row) => row.data["locationId"] === locationId).map((row) => row.id);
|
|
78971
|
+
for (let i = 0; i < retrainIds.length; i += MEDIA_PAGE) {
|
|
78923
78972
|
if (cancelRequested) return owners;
|
|
78924
|
-
const
|
|
78925
|
-
|
|
78926
|
-
columns: ["id"],
|
|
78927
|
-
filter: {
|
|
78928
|
-
where: { locationId },
|
|
78929
|
-
limit: MEDIA_PAGE
|
|
78930
|
-
}
|
|
78931
|
-
});
|
|
78932
|
-
if (rows.length === 0) break;
|
|
78933
|
-
for (const row of rows) {
|
|
78934
|
-
await deps.store.delete.mutate({
|
|
78935
|
-
collection: RETRAIN_FRAMES_COLLECTION,
|
|
78936
|
-
key: row.id
|
|
78937
|
-
});
|
|
78938
|
-
deleted += 1;
|
|
78939
|
-
}
|
|
78973
|
+
const page = retrainIds.slice(i, i + MEDIA_PAGE);
|
|
78974
|
+
deleted += await deps.retrainFrameLedger.deleteByIds(page);
|
|
78940
78975
|
report({
|
|
78941
78976
|
dbRowsDeleted: deleted,
|
|
78942
78977
|
detail: `deleted ${deleted} media/retrain rows`
|
|
@@ -78988,6 +79023,382 @@ function createDiscardJob(deps) {
|
|
|
78988
79023
|
};
|
|
78989
79024
|
}
|
|
78990
79025
|
//#endregion
|
|
79026
|
+
//#region src/pipeline-analytics/retention/media-forget-actions.ts
|
|
79027
|
+
/**
|
|
79028
|
+
* The operator door for the NARROW media-row forget (`media-row-forget.ts`).
|
|
79029
|
+
*
|
|
79030
|
+
* It forgets `pipeline-analytics:media` rows stamped with ONE location whose
|
|
79031
|
+
* file is provably gone, and nothing else: no event, track, object-event,
|
|
79032
|
+
* face, plate or identity row is read for deletion, ever. Contrast
|
|
79033
|
+
* `storage.discardLocation`, which wipes the disk AND cascades owners.
|
|
79034
|
+
*
|
|
79035
|
+
* `dryRun` defaults to `true`: an unstated intent destroys nothing. Applying
|
|
79036
|
+
* also requires `expectAbsent` — the `absent` count a dry run just reported —
|
|
79037
|
+
* so the forget only ever deletes a population a human has already read.
|
|
79038
|
+
*/
|
|
79039
|
+
/** Hard ceiling on rows one call examines — the call is synchronous. */
|
|
79040
|
+
var MEDIA_FORGET_MAX_ROWS = 5e4;
|
|
79041
|
+
var MediaForgetInputSchema = object({
|
|
79042
|
+
locationId: string().min(1),
|
|
79043
|
+
/** Restrict to these cameras. Omitted = every row stamped with the location. */
|
|
79044
|
+
deviceIds: array(number().int()).min(1).optional(),
|
|
79045
|
+
dryRun: boolean().default(true),
|
|
79046
|
+
/**
|
|
79047
|
+
* Required when `dryRun` is false: the `totals.absent` of the dry run. A
|
|
79048
|
+
* different count at apply time refuses the whole call before any delete.
|
|
79049
|
+
*/
|
|
79050
|
+
expectAbsent: number().int().nonnegative().optional(),
|
|
79051
|
+
/** Rows examined in this call. Default and max {@link MEDIA_FORGET_MAX_ROWS}. */
|
|
79052
|
+
maxRows: number().int().min(1).max(MEDIA_FORGET_MAX_ROWS).optional(),
|
|
79053
|
+
/** Detailed rows returned per verdict. Default 20. */
|
|
79054
|
+
sampleSize: number().int().min(0).max(500).optional()
|
|
79055
|
+
});
|
|
79056
|
+
var VerdictCountsSchema = object({
|
|
79057
|
+
rows: number(),
|
|
79058
|
+
/** File answered ENOENT — the only verdict that may be forgotten. */
|
|
79059
|
+
absent: number(),
|
|
79060
|
+
/** File is there: relocation is incomplete. Never forgotten. */
|
|
79061
|
+
present: number(),
|
|
79062
|
+
/** The stat failed with any errno other than ENOENT. Never forgotten. */
|
|
79063
|
+
error: number(),
|
|
79064
|
+
/** No path, a path escaping the root, or a retention-exempt owner. Never forgotten. */
|
|
79065
|
+
kept: number(),
|
|
79066
|
+
/** Rows actually deleted (0 on a dry run). */
|
|
79067
|
+
deleted: number()
|
|
79068
|
+
});
|
|
79069
|
+
var MediaForgetSampleSchema = object({
|
|
79070
|
+
mediaId: number(),
|
|
79071
|
+
deviceId: number(),
|
|
79072
|
+
kind: string(),
|
|
79073
|
+
ownerKind: string(),
|
|
79074
|
+
ownerId: string(),
|
|
79075
|
+
path: string(),
|
|
79076
|
+
/** The errno (error) or the reason (kept); absent otherwise. */
|
|
79077
|
+
reason: string().optional()
|
|
79078
|
+
});
|
|
79079
|
+
var mediaForgetActions = defineCustomActions({ "storage.forgetMissingMedia": customAction(MediaForgetInputSchema, object({
|
|
79080
|
+
locationId: string(),
|
|
79081
|
+
dryRun: boolean(),
|
|
79082
|
+
root: string(),
|
|
79083
|
+
/** Rows of this location that were not examined because `maxRows` was hit. */
|
|
79084
|
+
truncated: boolean(),
|
|
79085
|
+
totals: VerdictCountsSchema,
|
|
79086
|
+
byDevice: array(VerdictCountsSchema.extend({ deviceId: number() })),
|
|
79087
|
+
byKind: array(VerdictCountsSchema.extend({ kind: string() })),
|
|
79088
|
+
/** Every id the call deletes (dry run: would delete). The exact list. */
|
|
79089
|
+
absentIds: array(number()),
|
|
79090
|
+
samples: object({
|
|
79091
|
+
absent: array(MediaForgetSampleSchema),
|
|
79092
|
+
present: array(MediaForgetSampleSchema),
|
|
79093
|
+
error: array(MediaForgetSampleSchema),
|
|
79094
|
+
kept: array(MediaForgetSampleSchema)
|
|
79095
|
+
}),
|
|
79096
|
+
/** Absent at classification, no longer ENOENT at delete time: kept. */
|
|
79097
|
+
changedBeforeDelete: number()
|
|
79098
|
+
}), {
|
|
79099
|
+
kind: "mutation",
|
|
79100
|
+
auth: "admin"
|
|
79101
|
+
}) });
|
|
79102
|
+
//#endregion
|
|
79103
|
+
//#region src/pipeline-analytics/retention/media-row-forget.ts
|
|
79104
|
+
/**
|
|
79105
|
+
* The NARROW media-row forget: delete `pipeline-analytics:media` rows stamped
|
|
79106
|
+
* with ONE storage location whose file is provably gone — and nothing else.
|
|
79107
|
+
*
|
|
79108
|
+
* Why it exists: a location whose bytes were lost keeps rows naming it, and a
|
|
79109
|
+
* cascading cleanup is not an acceptable way to clear them. `reconcileFromDisk`
|
|
79110
|
+
* once cascaded ~132 k events away from a read it did not double-check. Here:
|
|
79111
|
+
*
|
|
79112
|
+
* - **Only media rows.** No event, track, object-event, face, plate or
|
|
79113
|
+
* identity row is deleted. A parent keeps existing with that media kind
|
|
79114
|
+
* missing — which is exactly what it already serves (`getBytesById` answers
|
|
79115
|
+
* `null` for a missing row as it does for a missing blob).
|
|
79116
|
+
* - **Only `ENOENT` is absence (D49).** Each row's file is `stat`ed under the
|
|
79117
|
+
* location's own root on THIS node. Any other errno keeps the row and is
|
|
79118
|
+
* reported; a present file keeps the row and is reported (relocation is
|
|
79119
|
+
* incomplete). A row with no path, a path that escapes the root, or a
|
|
79120
|
+
* retention-exempt owner (the enrolled gallery) is kept.
|
|
79121
|
+
* - **Only rows stamped with the id.** `locationId IS NULL` rows mean
|
|
79122
|
+
* "wherever eventMedia points now" and are never selected.
|
|
79123
|
+
* - **Only a location nothing writes to.** An active (writable) location is
|
|
79124
|
+
* refused: new rows could land while the walk runs.
|
|
79125
|
+
* - **Dry run first, then the same count.** Applying requires
|
|
79126
|
+
* `expectAbsent`; the whole population is classified before any delete,
|
|
79127
|
+
* and a different absent count refuses the call with nothing written.
|
|
79128
|
+
* Each id is re-`stat`ed right before its batch is deleted, and the delete
|
|
79129
|
+
* predicate repeats `locationId`, so a row repointed in between survives.
|
|
79130
|
+
*
|
|
79131
|
+
* Occupancy needs no separate bookkeeping: `storage-occupancy` sums
|
|
79132
|
+
* `sizeBytes WHERE locationId = ?` on demand, so the row IS the accounting.
|
|
79133
|
+
*/
|
|
79134
|
+
var PAGE_SIZE$1 = 200;
|
|
79135
|
+
var DELETE_BATCH = 200;
|
|
79136
|
+
var DEFAULT_SAMPLE_SIZE = 20;
|
|
79137
|
+
function errnoOf(err) {
|
|
79138
|
+
if (err instanceof Error && "code" in err && typeof err.code === "string") return err.code;
|
|
79139
|
+
return err instanceof Error ? err.message : String(err);
|
|
79140
|
+
}
|
|
79141
|
+
function emptyCounts() {
|
|
79142
|
+
return {
|
|
79143
|
+
rows: 0,
|
|
79144
|
+
absent: 0,
|
|
79145
|
+
present: 0,
|
|
79146
|
+
error: 0,
|
|
79147
|
+
kept: 0,
|
|
79148
|
+
deleted: 0
|
|
79149
|
+
};
|
|
79150
|
+
}
|
|
79151
|
+
function tally$1(counts, verdict) {
|
|
79152
|
+
const rows = counts.rows + 1;
|
|
79153
|
+
switch (verdict) {
|
|
79154
|
+
case "absent": return {
|
|
79155
|
+
...counts,
|
|
79156
|
+
rows,
|
|
79157
|
+
absent: counts.absent + 1
|
|
79158
|
+
};
|
|
79159
|
+
case "present": return {
|
|
79160
|
+
...counts,
|
|
79161
|
+
rows,
|
|
79162
|
+
present: counts.present + 1
|
|
79163
|
+
};
|
|
79164
|
+
case "error": return {
|
|
79165
|
+
...counts,
|
|
79166
|
+
rows,
|
|
79167
|
+
error: counts.error + 1
|
|
79168
|
+
};
|
|
79169
|
+
case "kept": return {
|
|
79170
|
+
...counts,
|
|
79171
|
+
rows,
|
|
79172
|
+
kept: counts.kept + 1
|
|
79173
|
+
};
|
|
79174
|
+
}
|
|
79175
|
+
}
|
|
79176
|
+
/** The file's absolute path under `root`, or `null` when it would escape it. */
|
|
79177
|
+
function resolveUnder(root, relativePath) {
|
|
79178
|
+
const base = path.resolve(root);
|
|
79179
|
+
const full = path.resolve(base, relativePath);
|
|
79180
|
+
return full.startsWith(base + path.sep) ? full : null;
|
|
79181
|
+
}
|
|
79182
|
+
function toSample(id, data) {
|
|
79183
|
+
return {
|
|
79184
|
+
mediaId: Number(id),
|
|
79185
|
+
deviceId: Number(data["deviceId"] ?? 0),
|
|
79186
|
+
kind: String(data["kind"] ?? ""),
|
|
79187
|
+
ownerKind: String(data["ownerKind"] ?? ""),
|
|
79188
|
+
ownerId: String(data["ownerId"] ?? ""),
|
|
79189
|
+
path: String(data["path"] ?? "")
|
|
79190
|
+
};
|
|
79191
|
+
}
|
|
79192
|
+
async function probe(stat, absolutePath) {
|
|
79193
|
+
try {
|
|
79194
|
+
await stat(absolutePath);
|
|
79195
|
+
return { verdict: "present" };
|
|
79196
|
+
} catch (err) {
|
|
79197
|
+
const code = errnoOf(err);
|
|
79198
|
+
return code === "ENOENT" ? { verdict: "absent" } : {
|
|
79199
|
+
verdict: "error",
|
|
79200
|
+
reason: code
|
|
79201
|
+
};
|
|
79202
|
+
}
|
|
79203
|
+
}
|
|
79204
|
+
async function classify(stat, root, id, data) {
|
|
79205
|
+
const sample = toSample(id, data);
|
|
79206
|
+
const ownerKind = sample.ownerKind;
|
|
79207
|
+
if (isMediaOwnerKind(ownerKind) && isRetentionExemptOwnerKind(ownerKind)) return {
|
|
79208
|
+
key: id,
|
|
79209
|
+
sample: {
|
|
79210
|
+
...sample,
|
|
79211
|
+
reason: "retention-exempt owner"
|
|
79212
|
+
},
|
|
79213
|
+
verdict: "kept",
|
|
79214
|
+
absolutePath: null
|
|
79215
|
+
};
|
|
79216
|
+
if (sample.path.length === 0) return {
|
|
79217
|
+
key: id,
|
|
79218
|
+
sample: {
|
|
79219
|
+
...sample,
|
|
79220
|
+
reason: "no path"
|
|
79221
|
+
},
|
|
79222
|
+
verdict: "kept",
|
|
79223
|
+
absolutePath: null
|
|
79224
|
+
};
|
|
79225
|
+
const absolutePath = resolveUnder(root, sample.path);
|
|
79226
|
+
if (absolutePath === null) return {
|
|
79227
|
+
key: id,
|
|
79228
|
+
sample: {
|
|
79229
|
+
...sample,
|
|
79230
|
+
reason: "path escapes the root"
|
|
79231
|
+
},
|
|
79232
|
+
verdict: "kept",
|
|
79233
|
+
absolutePath
|
|
79234
|
+
};
|
|
79235
|
+
const result = await probe(stat, absolutePath);
|
|
79236
|
+
return {
|
|
79237
|
+
key: id,
|
|
79238
|
+
sample: result.reason === void 0 ? sample : {
|
|
79239
|
+
...sample,
|
|
79240
|
+
reason: result.reason
|
|
79241
|
+
},
|
|
79242
|
+
verdict: result.verdict,
|
|
79243
|
+
absolutePath
|
|
79244
|
+
};
|
|
79245
|
+
}
|
|
79246
|
+
function resolveTarget(locations, locationId, localNodeId) {
|
|
79247
|
+
const location = locations.find((row) => row.id === locationId);
|
|
79248
|
+
if (!location) throw new Error(`forgetMissingMedia refused: unknown location ${locationId}`);
|
|
79249
|
+
if (location.type !== "eventMedia") throw new Error(`forgetMissingMedia refused: ${locationId} is ${location.type}, pipeline-analytics owns eventMedia rows only`);
|
|
79250
|
+
if (mayWriteToLocation(location)) throw new Error(`forgetMissingMedia refused: ${locationId} is still a write target — set it to drain, readonly or disabled first`);
|
|
79251
|
+
const nodeId = location.nodeId === void 0 || location.nodeId === "" ? "hub" : location.nodeId;
|
|
79252
|
+
if (nodeId !== localNodeId) throw new Error(`forgetMissingMedia refused: ${locationId} lives on node ${nodeId}, this process is ${localNodeId}`);
|
|
79253
|
+
return {
|
|
79254
|
+
location,
|
|
79255
|
+
root: locationBasePath(location)
|
|
79256
|
+
};
|
|
79257
|
+
}
|
|
79258
|
+
/**
|
|
79259
|
+
* Classify every row stamped with `input.locationId` (bounded by `maxRows`),
|
|
79260
|
+
* and — only when `dryRun` is false and the absent count equals
|
|
79261
|
+
* `expectAbsent` — delete the absent ones, per camera, in batches.
|
|
79262
|
+
*/
|
|
79263
|
+
async function forgetMissingMediaRows(deps, rawInput) {
|
|
79264
|
+
const input = MediaForgetInputSchema.parse(rawInput);
|
|
79265
|
+
if (!input.dryRun && input.expectAbsent === void 0) throw new Error("forgetMissingMedia refused: an apply needs expectAbsent (the dry run’s totals.absent)");
|
|
79266
|
+
const stat = deps.stat ?? ((p) => promises.stat(p));
|
|
79267
|
+
const pace = deps.pace ?? (() => new Promise((resolve) => setImmediate(resolve)));
|
|
79268
|
+
const maxRows = input.maxRows ?? MEDIA_FORGET_MAX_ROWS;
|
|
79269
|
+
const sampleSize = input.sampleSize ?? DEFAULT_SAMPLE_SIZE;
|
|
79270
|
+
const { root } = resolveTarget(await deps.listLocations(), input.locationId, deps.localNodeId);
|
|
79271
|
+
const classified = [];
|
|
79272
|
+
let cursor = 0;
|
|
79273
|
+
let truncated = false;
|
|
79274
|
+
for (;;) {
|
|
79275
|
+
const rows = await deps.store.query.query({
|
|
79276
|
+
collection: MEDIA_COLLECTION,
|
|
79277
|
+
columns: [
|
|
79278
|
+
"id",
|
|
79279
|
+
"deviceId",
|
|
79280
|
+
"ownerKind",
|
|
79281
|
+
"ownerId",
|
|
79282
|
+
"kind",
|
|
79283
|
+
"path"
|
|
79284
|
+
],
|
|
79285
|
+
filter: {
|
|
79286
|
+
where: { locationId: input.locationId },
|
|
79287
|
+
...input.deviceIds ? { whereIn: { deviceId: [...input.deviceIds] } } : {},
|
|
79288
|
+
whereBetween: { id: [cursor + 1, Number.MAX_SAFE_INTEGER] },
|
|
79289
|
+
orderBy: {
|
|
79290
|
+
field: "id",
|
|
79291
|
+
direction: "asc"
|
|
79292
|
+
},
|
|
79293
|
+
limit: PAGE_SIZE$1
|
|
79294
|
+
}
|
|
79295
|
+
});
|
|
79296
|
+
if (rows.length === 0) break;
|
|
79297
|
+
for (const row of rows) {
|
|
79298
|
+
if (classified.length >= maxRows) {
|
|
79299
|
+
truncated = true;
|
|
79300
|
+
break;
|
|
79301
|
+
}
|
|
79302
|
+
classified.push(await classify(stat, root, row.id, row.data));
|
|
79303
|
+
cursor = Math.max(cursor, Number(row.id));
|
|
79304
|
+
}
|
|
79305
|
+
if (truncated || rows.length < PAGE_SIZE$1) break;
|
|
79306
|
+
await pace();
|
|
79307
|
+
}
|
|
79308
|
+
const absentRows = classified.filter((c) => c.verdict === "absent");
|
|
79309
|
+
const deletedIds = /* @__PURE__ */ new Set();
|
|
79310
|
+
let changedBeforeDelete = 0;
|
|
79311
|
+
if (!input.dryRun) {
|
|
79312
|
+
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`);
|
|
79313
|
+
const byDevice = /* @__PURE__ */ new Map();
|
|
79314
|
+
for (const row of absentRows) byDevice.set(row.sample.deviceId, [...byDevice.get(row.sample.deviceId) ?? [], row]);
|
|
79315
|
+
for (const [deviceId, rows] of byDevice) for (let at = 0; at < rows.length; at += DELETE_BATCH) {
|
|
79316
|
+
const batch = [];
|
|
79317
|
+
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);
|
|
79318
|
+
else changedBeforeDelete += 1;
|
|
79319
|
+
if (batch.length === 0) continue;
|
|
79320
|
+
const { deleted } = await deps.store.deleteWhere.mutate({
|
|
79321
|
+
collection: MEDIA_COLLECTION,
|
|
79322
|
+
filter: {
|
|
79323
|
+
where: {
|
|
79324
|
+
locationId: input.locationId,
|
|
79325
|
+
deviceId
|
|
79326
|
+
},
|
|
79327
|
+
whereIn: { id: batch.map((row) => row.key) }
|
|
79328
|
+
}
|
|
79329
|
+
});
|
|
79330
|
+
for (const row of batch) deletedIds.add(row.sample.mediaId);
|
|
79331
|
+
deps.logger.info("forgetMissingMedia: forgot media rows whose file is gone", {
|
|
79332
|
+
tags: { deviceId },
|
|
79333
|
+
meta: {
|
|
79334
|
+
locationId: input.locationId,
|
|
79335
|
+
batch: batch.length,
|
|
79336
|
+
deleted,
|
|
79337
|
+
firstId: batch[0]?.sample.mediaId,
|
|
79338
|
+
lastId: batch[batch.length - 1]?.sample.mediaId
|
|
79339
|
+
}
|
|
79340
|
+
});
|
|
79341
|
+
if (deleted !== batch.length) deps.logger.warn("forgetMissingMedia: a batch deleted fewer rows than it named", {
|
|
79342
|
+
tags: { deviceId },
|
|
79343
|
+
meta: {
|
|
79344
|
+
locationId: input.locationId,
|
|
79345
|
+
batch: batch.length,
|
|
79346
|
+
deleted
|
|
79347
|
+
}
|
|
79348
|
+
});
|
|
79349
|
+
await pace();
|
|
79350
|
+
}
|
|
79351
|
+
}
|
|
79352
|
+
let totals = emptyCounts();
|
|
79353
|
+
const perDevice = /* @__PURE__ */ new Map();
|
|
79354
|
+
const perKind = /* @__PURE__ */ new Map();
|
|
79355
|
+
for (const row of classified) {
|
|
79356
|
+
const isDeleted = deletedIds.has(row.sample.mediaId);
|
|
79357
|
+
const bump = (c) => {
|
|
79358
|
+
const next = tally$1(c, row.verdict);
|
|
79359
|
+
return isDeleted ? {
|
|
79360
|
+
...next,
|
|
79361
|
+
deleted: next.deleted + 1
|
|
79362
|
+
} : next;
|
|
79363
|
+
};
|
|
79364
|
+
totals = bump(totals);
|
|
79365
|
+
perDevice.set(row.sample.deviceId, bump(perDevice.get(row.sample.deviceId) ?? emptyCounts()));
|
|
79366
|
+
perKind.set(row.sample.kind, bump(perKind.get(row.sample.kind) ?? emptyCounts()));
|
|
79367
|
+
}
|
|
79368
|
+
const samplesOf = (verdict) => classified.filter((c) => c.verdict === verdict).slice(0, sampleSize).map((c) => c.sample);
|
|
79369
|
+
for (const [deviceId, counts] of perDevice) deps.logger.info("forgetMissingMedia: per-camera verdict", {
|
|
79370
|
+
tags: { deviceId },
|
|
79371
|
+
meta: {
|
|
79372
|
+
locationId: input.locationId,
|
|
79373
|
+
dryRun: input.dryRun,
|
|
79374
|
+
...counts
|
|
79375
|
+
}
|
|
79376
|
+
});
|
|
79377
|
+
return {
|
|
79378
|
+
locationId: input.locationId,
|
|
79379
|
+
dryRun: input.dryRun,
|
|
79380
|
+
root,
|
|
79381
|
+
truncated,
|
|
79382
|
+
totals,
|
|
79383
|
+
byDevice: [...perDevice].map(([deviceId, c]) => ({
|
|
79384
|
+
deviceId,
|
|
79385
|
+
...c
|
|
79386
|
+
})),
|
|
79387
|
+
byKind: [...perKind].map(([kind, c]) => ({
|
|
79388
|
+
kind,
|
|
79389
|
+
...c
|
|
79390
|
+
})),
|
|
79391
|
+
absentIds: absentRows.map((r) => r.sample.mediaId),
|
|
79392
|
+
samples: {
|
|
79393
|
+
absent: samplesOf("absent"),
|
|
79394
|
+
present: samplesOf("present"),
|
|
79395
|
+
error: samplesOf("error"),
|
|
79396
|
+
kept: samplesOf("kept")
|
|
79397
|
+
},
|
|
79398
|
+
changedBeforeDelete
|
|
79399
|
+
};
|
|
79400
|
+
}
|
|
79401
|
+
//#endregion
|
|
78991
79402
|
//#region src/pipeline-analytics/retention/media-reclaim.ts
|
|
78992
79403
|
var MEDIA_RECLAIM_SCOPES = ["motion-still", "track-filmstrip"];
|
|
78993
79404
|
var DEFAULT_MEDIA_RECLAIM_LIMITS = {
|
|
@@ -93265,17 +93676,8 @@ function rowIsGallery(data) {
|
|
|
93265
93676
|
* D295 calls "a guard nobody believes".
|
|
93266
93677
|
*/
|
|
93267
93678
|
function collectionsFor(mode) {
|
|
93268
|
-
if (mode === "gallery") return [{
|
|
93269
|
-
|
|
93270
|
-
order: "timestamp"
|
|
93271
|
-
}];
|
|
93272
|
-
return [{
|
|
93273
|
-
name: MEDIA_COLLECTION,
|
|
93274
|
-
order: "timestamp"
|
|
93275
|
-
}, {
|
|
93276
|
-
name: RETRAIN_FRAMES_COLLECTION,
|
|
93277
|
-
order: "copiedAt"
|
|
93278
|
-
}];
|
|
93679
|
+
if (mode === "gallery") return [{ lane: "media" }];
|
|
93680
|
+
return [{ lane: "media" }, { lane: "retrainFrames" }];
|
|
93279
93681
|
}
|
|
93280
93682
|
/**
|
|
93281
93683
|
* Does this pass act on this row at all?
|
|
@@ -93360,7 +93762,7 @@ var MediaRelocateEngine = class {
|
|
|
93360
93762
|
async countUnstamped() {
|
|
93361
93763
|
try {
|
|
93362
93764
|
const media = await this.countUnstampedIn(MEDIA_COLLECTION);
|
|
93363
|
-
const retrainFrames = await this.
|
|
93765
|
+
const retrainFrames = await this.countUnstampedRetrainFrames();
|
|
93364
93766
|
return {
|
|
93365
93767
|
media,
|
|
93366
93768
|
retrainFrames,
|
|
@@ -93412,6 +93814,28 @@ var MediaRelocateEngine = class {
|
|
|
93412
93814
|
};
|
|
93413
93815
|
}
|
|
93414
93816
|
}
|
|
93817
|
+
/**
|
|
93818
|
+
* The retrain lane's unstamped population. The ledger's records `where` is
|
|
93819
|
+
* equality-only (a `null` predicate is dropped, which would count the whole
|
|
93820
|
+
* dataset), so the rows are read and judged by {@link rowIsUnstamped} — the
|
|
93821
|
+
* dataset is operator-selected and small.
|
|
93822
|
+
*/
|
|
93823
|
+
async countUnstampedRetrainFrames() {
|
|
93824
|
+
const rows = (await this.deps.retrainFrameLedger.query({})).filter((row) => rowIsUnstamped(row.data)).length;
|
|
93825
|
+
return {
|
|
93826
|
+
present: rows > 0,
|
|
93827
|
+
rows
|
|
93828
|
+
};
|
|
93829
|
+
}
|
|
93830
|
+
/** Every row of one lane, oldest-first. */
|
|
93831
|
+
rowsOf(collection) {
|
|
93832
|
+
return collection.lane === "media" ? this.pageRows(MEDIA_COLLECTION, "timestamp") : this.retrainFrameRows();
|
|
93833
|
+
}
|
|
93834
|
+
/** The retrain-frame ledger, read whole and yielded oldest copy first. */
|
|
93835
|
+
async *retrainFrameRows() {
|
|
93836
|
+
const ordered = (await this.deps.retrainFrameLedger.query({})).toSorted((a, b) => Number(a.data["copiedAt"] ?? 0) - Number(b.data["copiedAt"] ?? 0));
|
|
93837
|
+
for (const row of ordered) yield row;
|
|
93838
|
+
}
|
|
93415
93839
|
start(input) {
|
|
93416
93840
|
for (const j of this.jobs.values()) if (j.state === "running") throw new Error(`a media relocation is already running (${j.jobId})`);
|
|
93417
93841
|
this.cancelCensuses("a relocate pass started");
|
|
@@ -93515,22 +93939,22 @@ var MediaRelocateEngine = class {
|
|
|
93515
93939
|
let sealable = 0;
|
|
93516
93940
|
for (const collection of collectionsFor(mode)) {
|
|
93517
93941
|
if (stop()) return null;
|
|
93518
|
-
sealable += await this.deps.store.count.query({
|
|
93519
|
-
collection:
|
|
93942
|
+
sealable += collection.lane === "media" ? await this.deps.store.count.query({
|
|
93943
|
+
collection: MEDIA_COLLECTION,
|
|
93520
93944
|
filter: { where: { [LOCATION_ID_FIELD]: null } }
|
|
93521
|
-
});
|
|
93945
|
+
}) : (await this.countUnstampedRetrainFrames()).rows ?? 0;
|
|
93522
93946
|
}
|
|
93523
93947
|
return { rows: sealable };
|
|
93524
93948
|
}
|
|
93525
93949
|
let rows = 0;
|
|
93526
93950
|
for (const collection of collectionsFor(mode)) {
|
|
93527
|
-
const isRetrain = collection.
|
|
93528
|
-
for await (const row of this.
|
|
93951
|
+
const isRetrain = collection.lane === "retrainFrames";
|
|
93952
|
+
for await (const row of this.rowsOf(collection)) {
|
|
93529
93953
|
if (stop()) {
|
|
93530
93954
|
this.deps.logger.warn("media relocate: census stopped before it finished — reporting the residue as unknown, not as empty", { meta: {
|
|
93531
93955
|
mode,
|
|
93532
93956
|
toLocationId,
|
|
93533
|
-
collection: collection.
|
|
93957
|
+
collection: collection.lane
|
|
93534
93958
|
} });
|
|
93535
93959
|
return null;
|
|
93536
93960
|
}
|
|
@@ -93538,7 +93962,7 @@ var MediaRelocateEngine = class {
|
|
|
93538
93962
|
this.deps.logger.warn("media relocate: census exceeded its budget — this collection is larger than a walk can answer", { meta: {
|
|
93539
93963
|
mode,
|
|
93540
93964
|
toLocationId,
|
|
93541
|
-
collection: collection.
|
|
93965
|
+
collection: collection.lane,
|
|
93542
93966
|
rowsVisited: budget.rowsVisited,
|
|
93543
93967
|
rowBudget: MEDIA_CENSUS_ROW_BUDGET,
|
|
93544
93968
|
deadlineMs: MEDIA_CENSUS_DEADLINE_MS
|
|
@@ -93574,15 +93998,15 @@ var MediaRelocateEngine = class {
|
|
|
93574
93998
|
if (job.mode === "seal") {
|
|
93575
93999
|
for (const collection of collectionsFor(job.mode)) {
|
|
93576
94000
|
if (job.cancelRequested) break;
|
|
93577
|
-
await this.sealCollection(collection
|
|
94001
|
+
await this.sealCollection(collection, job);
|
|
93578
94002
|
}
|
|
93579
94003
|
job.state = job.cancelRequested ? "cancelled" : "done";
|
|
93580
94004
|
return;
|
|
93581
94005
|
}
|
|
93582
94006
|
for (const collection of collectionsFor(job.mode)) {
|
|
93583
94007
|
if (job.cancelRequested) break;
|
|
93584
|
-
const isRetrain = collection.
|
|
93585
|
-
for await (const row of this.
|
|
94008
|
+
const isRetrain = collection.lane === "retrainFrames";
|
|
94009
|
+
for await (const row of this.rowsOf(collection)) {
|
|
93586
94010
|
if (job.cancelRequested) break;
|
|
93587
94011
|
const data = row.data;
|
|
93588
94012
|
if (!selectsRow(job.mode, job.toLocationId, data, isRetrain, job.fromLocationId)) continue;
|
|
@@ -93594,7 +94018,7 @@ var MediaRelocateEngine = class {
|
|
|
93594
94018
|
}
|
|
93595
94019
|
try {
|
|
93596
94020
|
if (await this.samePhysicalLocation(from, job.toLocationId)) {
|
|
93597
|
-
await this.stamp(collection
|
|
94021
|
+
await this.stamp(collection, row.id, data, job.toLocationId);
|
|
93598
94022
|
continue;
|
|
93599
94023
|
}
|
|
93600
94024
|
const bytes = await this.deps.storage.read({
|
|
@@ -93610,7 +94034,7 @@ var MediaRelocateEngine = class {
|
|
|
93610
94034
|
location: job.toLocationId,
|
|
93611
94035
|
relativePath
|
|
93612
94036
|
})).length !== bytes.length) throw new Error(`relocated media size mismatch for ${relativePath}`);
|
|
93613
|
-
await this.stamp(collection
|
|
94037
|
+
await this.stamp(collection, row.id, data, job.toLocationId);
|
|
93614
94038
|
await this.deps.storage.delete({
|
|
93615
94039
|
location: from,
|
|
93616
94040
|
relativePath
|
|
@@ -93621,7 +94045,7 @@ var MediaRelocateEngine = class {
|
|
|
93621
94045
|
} catch (err) {
|
|
93622
94046
|
failedRows++;
|
|
93623
94047
|
this.deps.logger.debug("media relocate row failed", { meta: {
|
|
93624
|
-
collection: collection.
|
|
94048
|
+
collection: collection.lane,
|
|
93625
94049
|
key: row.id,
|
|
93626
94050
|
error: String(err)
|
|
93627
94051
|
} });
|
|
@@ -93694,7 +94118,17 @@ var MediaRelocateEngine = class {
|
|
|
93694
94118
|
* reports a completed seal having stamped nothing. Zero unstamped rows is the
|
|
93695
94119
|
* single value the migration gate reads as "safe to cut over".
|
|
93696
94120
|
*/
|
|
93697
|
-
async sealCollection(
|
|
94121
|
+
async sealCollection(lane, job) {
|
|
94122
|
+
if (lane.lane === "retrainFrames") {
|
|
94123
|
+
for (const row of await this.deps.retrainFrameLedger.query({})) {
|
|
94124
|
+
if (job.cancelRequested) return;
|
|
94125
|
+
if (!rowIsUnstamped(row.data)) continue;
|
|
94126
|
+
await this.deps.retrainFrameLedger.update(row.id, { [LOCATION_ID_FIELD]: job.toLocationId });
|
|
94127
|
+
job.filesMoved += 1;
|
|
94128
|
+
}
|
|
94129
|
+
return;
|
|
94130
|
+
}
|
|
94131
|
+
const collection = MEDIA_COLLECTION;
|
|
93698
94132
|
for (;;) {
|
|
93699
94133
|
if (job.cancelRequested) return;
|
|
93700
94134
|
const batch = await this.deps.store.query.query({
|
|
@@ -93715,9 +94149,13 @@ var MediaRelocateEngine = class {
|
|
|
93715
94149
|
job.filesMoved += updated;
|
|
93716
94150
|
}
|
|
93717
94151
|
}
|
|
93718
|
-
async stamp(
|
|
94152
|
+
async stamp(lane, key, data, locationId) {
|
|
94153
|
+
if (lane.lane === "retrainFrames") {
|
|
94154
|
+
await this.deps.retrainFrameLedger.update(key, { [LOCATION_ID_FIELD]: locationId });
|
|
94155
|
+
return;
|
|
94156
|
+
}
|
|
93719
94157
|
await this.deps.store.set.mutate({
|
|
93720
|
-
collection,
|
|
94158
|
+
collection: MEDIA_COLLECTION,
|
|
93721
94159
|
key,
|
|
93722
94160
|
value: {
|
|
93723
94161
|
...data,
|
|
@@ -102164,6 +102602,18 @@ function decodeCursorMap(raw) {
|
|
|
102164
102602
|
function nonNegativeInt(value) {
|
|
102165
102603
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : null;
|
|
102166
102604
|
}
|
|
102605
|
+
function decodeScopeCursors(raw) {
|
|
102606
|
+
const out = {};
|
|
102607
|
+
if (raw !== null && typeof raw === "object") for (const [key, cursor] of Object.entries({ ...raw })) {
|
|
102608
|
+
if (typeof cursor === "string" && cursor.length > 0) {
|
|
102609
|
+
out[key] = cursor;
|
|
102610
|
+
continue;
|
|
102611
|
+
}
|
|
102612
|
+
const parsed = nonNegativeInt(cursor);
|
|
102613
|
+
if (parsed !== null) out[key] = parsed;
|
|
102614
|
+
}
|
|
102615
|
+
return out;
|
|
102616
|
+
}
|
|
102167
102617
|
/** Read a stored row back into a cursor map, ignoring anything unreadable. */
|
|
102168
102618
|
function decodeOrphanAuditCursors(value) {
|
|
102169
102619
|
const empty = {
|
|
@@ -102175,7 +102625,7 @@ function decodeOrphanAuditCursors(value) {
|
|
|
102175
102625
|
if (value === null || typeof value !== "object") return empty;
|
|
102176
102626
|
const row = { ...value };
|
|
102177
102627
|
return {
|
|
102178
|
-
scopes:
|
|
102628
|
+
scopes: decodeScopeCursors(row["scopes"]),
|
|
102179
102629
|
faceVectorCursor: nonNegativeInt(row["faceVectorCursor"]) ?? 0,
|
|
102180
102630
|
clipIndexCursors: decodeCursorMap(row["clipIndexCursors"]),
|
|
102181
102631
|
updatedAtMs: nonNegativeInt(row["updatedAtMs"]) ?? 0
|
|
@@ -103748,7 +104198,9 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
103748
104198
|
};
|
|
103749
104199
|
const deviceIds = /* @__PURE__ */ new Set();
|
|
103750
104200
|
const graceFloor = deps.now() - limits.graceMs;
|
|
103751
|
-
let offset = Math.max(0, cursor);
|
|
104201
|
+
let offset = typeof cursor === "number" ? Math.max(0, cursor) : 0;
|
|
104202
|
+
let keysetCursor = typeof cursor === "string" ? cursor : null;
|
|
104203
|
+
let keyed = keysetCursor !== null;
|
|
103752
104204
|
let stoppedBy = "exhausted";
|
|
103753
104205
|
let collectionSize = null;
|
|
103754
104206
|
try {
|
|
@@ -103765,9 +104217,16 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
103765
104217
|
stoppedBy = "row-ceiling";
|
|
103766
104218
|
break;
|
|
103767
104219
|
}
|
|
103768
|
-
|
|
104220
|
+
if (deps.stop?.() === true) {
|
|
104221
|
+
stoppedBy = "stopped";
|
|
104222
|
+
break;
|
|
104223
|
+
}
|
|
104224
|
+
const keyedPage = deps.pageKeyset === void 0 ? null : await deps.pageKeyset(scope.collection, keysetCursor, limits.pageSize);
|
|
104225
|
+
keyed = keyedPage !== null;
|
|
104226
|
+
const rows = keyedPage !== null ? keyedPage.rows : await deps.page(scope.collection, scope.shard, offset, limits.pageSize);
|
|
103769
104227
|
if (rows.length === 0) {
|
|
103770
104228
|
offset = 0;
|
|
104229
|
+
keysetCursor = null;
|
|
103771
104230
|
break;
|
|
103772
104231
|
}
|
|
103773
104232
|
const refs = rows.map((row) => scope.resolve(row));
|
|
@@ -103854,9 +104313,17 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
103854
104313
|
if (deps.breathe) await deps.breathe();
|
|
103855
104314
|
if (ceilingHit) {
|
|
103856
104315
|
stoppedBy = "reclaim-ceiling";
|
|
103857
|
-
offset += rows.length - reclaimedInPage;
|
|
104316
|
+
if (!keyed) offset += rows.length - reclaimedInPage;
|
|
103858
104317
|
break;
|
|
103859
104318
|
}
|
|
104319
|
+
if (keyedPage !== null) {
|
|
104320
|
+
if (keyedPage.nextCursor === null) {
|
|
104321
|
+
keysetCursor = null;
|
|
104322
|
+
break;
|
|
104323
|
+
}
|
|
104324
|
+
keysetCursor = keyedPage.nextCursor;
|
|
104325
|
+
continue;
|
|
104326
|
+
}
|
|
103860
104327
|
const advanced = rows.length - reclaimedInPage;
|
|
103861
104328
|
if (advanced === 0 && rows.length < limits.pageSize) {
|
|
103862
104329
|
offset = 0;
|
|
@@ -103872,7 +104339,7 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
103872
104339
|
collectionSize,
|
|
103873
104340
|
coveragePercent: Math.round(tally.examined / collectionSize * 1e3) / 10,
|
|
103874
104341
|
stoppedBy,
|
|
103875
|
-
nextCursor: offset
|
|
104342
|
+
nextCursor: keyed ? keysetCursor ?? 0 : offset
|
|
103876
104343
|
} });
|
|
103877
104344
|
const freedNothing = scope.sizeField === null ? tally.reclaimed === 0 : tally.bytesReclaimed === 0;
|
|
103878
104345
|
if (limits.mode === "reclaim" && tally.orphans > 0 && freedNothing) deps.logger.warn("orphan audit: reclaim pass freed no bytes against a standing backlog", { meta: {
|
|
@@ -103899,7 +104366,7 @@ async function auditScope(scope, deps, limits, cursor) {
|
|
|
103899
104366
|
unresolvable: tally.unresolvable,
|
|
103900
104367
|
deviceIds: [...deviceIds],
|
|
103901
104368
|
stoppedBy,
|
|
103902
|
-
nextCursor: offset
|
|
104369
|
+
nextCursor: keyed ? keysetCursor ?? 0 : offset
|
|
103903
104370
|
};
|
|
103904
104371
|
}
|
|
103905
104372
|
/**
|
|
@@ -103991,7 +104458,8 @@ var ORPHAN_AUDIT_DEFAULTS = {
|
|
|
103991
104458
|
//#endregion
|
|
103992
104459
|
//#region src/pipeline-analytics/retention/orphan-reclaimers.ts
|
|
103993
104460
|
var OBJECT_EVENTS_COLLECTION = "pipeline-analytics:object-events";
|
|
103994
|
-
var
|
|
104461
|
+
var PLATES_COLLECTION = "pipeline-analytics:plates";
|
|
104462
|
+
var RAW_DELETE_COLLECTIONS = new Set([OBJECT_EVENTS_COLLECTION]);
|
|
103995
104463
|
/**
|
|
103996
104464
|
* The `deleteRow` port: ONE row by its primary key `id` (what every
|
|
103997
104465
|
* raw-delete collection above declares), answering whether a row went.
|
|
@@ -104065,6 +104533,13 @@ function buildOrphanReclaimer(ports) {
|
|
|
104065
104533
|
rows: await ports.deleteTrackAlias(key) ? 1 : 0,
|
|
104066
104534
|
bytes: null
|
|
104067
104535
|
};
|
|
104536
|
+
if (collection === PLATES_COLLECTION) {
|
|
104537
|
+
const assert = trackIdAssertOf(data);
|
|
104538
|
+
return {
|
|
104539
|
+
rows: await ports.deletePlate(key, assert === null ? void 0 : assert) ? 1 : 0,
|
|
104540
|
+
bytes: null
|
|
104541
|
+
};
|
|
104542
|
+
}
|
|
104068
104543
|
if (RAW_DELETE_COLLECTIONS.has(collection)) {
|
|
104069
104544
|
const assert = trackIdAssertOf(data);
|
|
104070
104545
|
if (collection === OBJECT_EVENTS_COLLECTION && assert === null) {
|
|
@@ -104086,6 +104561,87 @@ function buildOrphanReclaimer(ports) {
|
|
|
104086
104561
|
};
|
|
104087
104562
|
}
|
|
104088
104563
|
//#endregion
|
|
104564
|
+
//#region src/pipeline-analytics/retention/orphan-audit-ledgers.ts
|
|
104565
|
+
/** Audited collection → its records-kind ledger. Empty without records. */
|
|
104566
|
+
function orphanAuditLedgers(wiring) {
|
|
104567
|
+
if (wiring.records === void 0) return /* @__PURE__ */ new Map();
|
|
104568
|
+
return new Map([
|
|
104569
|
+
[FACES_COLLECTION, wireFaceLedger(wiring)],
|
|
104570
|
+
[PLATES_COLLECTION$1, wirePlateLedger(wiring)],
|
|
104571
|
+
[TRACK_ALIASES_COLLECTION, wireTrackAliasLedger(wiring)]
|
|
104572
|
+
]);
|
|
104573
|
+
}
|
|
104574
|
+
/** {@link OrphanAuditDeps.pageKeyset} over those ledgers; `null` for any other collection. */
|
|
104575
|
+
function keysetPager(ledgers) {
|
|
104576
|
+
return async (collection, cursor, limit) => {
|
|
104577
|
+
const ledger = ledgers.get(collection);
|
|
104578
|
+
if (ledger === void 0) return null;
|
|
104579
|
+
const page = await ledger.pageById(cursor, limit);
|
|
104580
|
+
return {
|
|
104581
|
+
rows: page.rows.map((row) => ({
|
|
104582
|
+
key: row.id,
|
|
104583
|
+
data: row.data
|
|
104584
|
+
})),
|
|
104585
|
+
nextCursor: page.nextCursor
|
|
104586
|
+
};
|
|
104587
|
+
};
|
|
104588
|
+
}
|
|
104589
|
+
/** The kind's count for a ledger-backed collection, `undefined` for any other. */
|
|
104590
|
+
async function ledgerSize(ledgers, collection) {
|
|
104591
|
+
const ledger = ledgers.get(collection);
|
|
104592
|
+
return ledger === void 0 ? void 0 : ledger.count();
|
|
104593
|
+
}
|
|
104594
|
+
/**
|
|
104595
|
+
* The plate reclaim through the plate ledger — records on a hub, the legacy
|
|
104596
|
+
* collection only without records. With `assert`, a row whose `trackId` is not
|
|
104597
|
+
* the dead track's is KEPT (logged), never deleted.
|
|
104598
|
+
*/
|
|
104599
|
+
function plateDeleter(plates, logger) {
|
|
104600
|
+
return async (plateId, assert) => {
|
|
104601
|
+
const row = await plates.get(plateId);
|
|
104602
|
+
if (row === null) return false;
|
|
104603
|
+
if (assert !== void 0 && row.data["trackId"] !== assert.trackId) {
|
|
104604
|
+
const deviceId = row.data["deviceId"];
|
|
104605
|
+
logger.warn("orphan audit: plate row now names another track — kept", {
|
|
104606
|
+
...typeof deviceId === "number" ? { tags: { deviceId } } : {},
|
|
104607
|
+
meta: {
|
|
104608
|
+
plateId,
|
|
104609
|
+
expectedTrackId: assert.trackId
|
|
104610
|
+
}
|
|
104611
|
+
});
|
|
104612
|
+
return false;
|
|
104613
|
+
}
|
|
104614
|
+
await plates.deleteKey(plateId);
|
|
104615
|
+
return true;
|
|
104616
|
+
};
|
|
104617
|
+
}
|
|
104618
|
+
function auditCollectionPorts(store, ledgers) {
|
|
104619
|
+
return {
|
|
104620
|
+
pageKeyset: keysetPager(ledgers),
|
|
104621
|
+
pageByOffset: async (collection, offset, limit) => {
|
|
104622
|
+
return (await store.query.query({
|
|
104623
|
+
collection,
|
|
104624
|
+
filter: {
|
|
104625
|
+
orderBy: {
|
|
104626
|
+
field: "id",
|
|
104627
|
+
direction: "asc"
|
|
104628
|
+
},
|
|
104629
|
+
limit,
|
|
104630
|
+
offset
|
|
104631
|
+
}
|
|
104632
|
+
})).map((row) => ({
|
|
104633
|
+
key: row.id,
|
|
104634
|
+
data: row.data
|
|
104635
|
+
}));
|
|
104636
|
+
},
|
|
104637
|
+
size: async (collection) => {
|
|
104638
|
+
const counted = await ledgerSize(ledgers, collection);
|
|
104639
|
+
if (counted !== void 0) return counted;
|
|
104640
|
+
return store.count.query({ collection });
|
|
104641
|
+
}
|
|
104642
|
+
};
|
|
104643
|
+
}
|
|
104644
|
+
//#endregion
|
|
104089
104645
|
//#region src/pipeline-analytics/retention/retired-device-settings.ts
|
|
104090
104646
|
/**
|
|
104091
104647
|
* Every retired knob, with the sentence that explains it. The list is the
|
|
@@ -114843,6 +115399,7 @@ var customActions = {
|
|
|
114843
115399
|
...orphanAuditActions,
|
|
114844
115400
|
...mediaReclaimActions,
|
|
114845
115401
|
...locationDiscardActions,
|
|
115402
|
+
...mediaForgetActions,
|
|
114846
115403
|
...photoEnrollActions,
|
|
114847
115404
|
...faceRescoreActions,
|
|
114848
115405
|
...stillnessCalibrationActions
|
|
@@ -116633,6 +117190,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
116633
117190
|
orphanAuditState = { ...IDLE_ORPHAN_AUDIT };
|
|
116634
117191
|
/** Frozen-location leftover wipe (events/ on a drained HDD). Detached. */
|
|
116635
117192
|
discardJob = null;
|
|
117193
|
+
/** The narrow media-row forget's wiring (`storage.forgetMissingMedia`). */
|
|
117194
|
+
mediaForgetDeps = null;
|
|
116636
117195
|
/**
|
|
116637
117196
|
* The CLIP indexes' walks (D649), one resume cursor per index, kept in their
|
|
116638
117197
|
* OWN field rather than in {@link orphanAuditCursors}: `runOrphanAudit`
|
|
@@ -116841,10 +117400,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
116841
117400
|
const isHub = ownNodeId === "hub";
|
|
116842
117401
|
const ncHandlers = isHub ? this.buildNcActionHandlers(api) : void 0;
|
|
116843
117402
|
if (isHub) this.reconcileTapePinsInBackground();
|
|
117403
|
+
this.mediaForgetDeps = {
|
|
117404
|
+
listLocations: async () => api.storage.listLocations.query({}),
|
|
117405
|
+
localNodeId: ownNodeId,
|
|
117406
|
+
store: api.settingsStore,
|
|
117407
|
+
logger: logger.child("MediaForget")
|
|
117408
|
+
};
|
|
116844
117409
|
this.discardJob = createDiscardJob({
|
|
116845
117410
|
listLocations: async () => api.storage.listLocations.query({}),
|
|
116846
117411
|
localNodeId: ownNodeId,
|
|
116847
117412
|
store: api.settingsStore,
|
|
117413
|
+
retrainFrameLedger: wireRetrainFrameLedger(this.eventLedgerWiring(api.settingsStore)),
|
|
116848
117414
|
deleteTrackEvents: async (trackId) => {
|
|
116849
117415
|
await this.eventStore?.deleteByTrack(trackId);
|
|
116850
117416
|
},
|
|
@@ -116865,6 +117431,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
116865
117431
|
...orphanAuditActions,
|
|
116866
117432
|
...mediaReclaimActions,
|
|
116867
117433
|
...locationDiscardActions,
|
|
117434
|
+
...mediaForgetActions,
|
|
116868
117435
|
...photoEnrollActions,
|
|
116869
117436
|
...faceRescoreActions,
|
|
116870
117437
|
...stillnessCalibrationActions
|
|
@@ -117460,6 +118027,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
117460
118027
|
});
|
|
117461
118028
|
this.mediaRelocate = new MediaRelocateEngine({
|
|
117462
118029
|
store: api.settingsStore,
|
|
118030
|
+
retrainFrameLedger: wireRetrainFrameLedger(eventLedgerWiring),
|
|
117463
118031
|
storage,
|
|
117464
118032
|
logger: logger.child("MediaRelocate"),
|
|
117465
118033
|
now: () => Date.now(),
|
|
@@ -125202,36 +125770,29 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
125202
125770
|
const trackStore = this.trackStore;
|
|
125203
125771
|
const eventStore = this.eventStore;
|
|
125204
125772
|
const mediaStore = this.mediaStore;
|
|
125205
|
-
|
|
125773
|
+
const settingsStore = this.settingsStore;
|
|
125774
|
+
if (!trackStore || !eventStore || !mediaStore || !settingsStore) {
|
|
125206
125775
|
if (!alreadyHeld) this.orphanAuditRunning = false;
|
|
125207
125776
|
return null;
|
|
125208
125777
|
}
|
|
125209
125778
|
try {
|
|
125210
125779
|
const limits = await this.orphanAuditLimits(overrides);
|
|
125780
|
+
const auditLedgers = orphanAuditLedgers(this.eventLedgerWiring(settingsStore));
|
|
125781
|
+
const plateLedger = wirePlateLedger(this.eventLedgerWiring(settingsStore));
|
|
125782
|
+
const collectionPorts = auditCollectionPorts(settingsStore, auditLedgers);
|
|
125211
125783
|
const deps = {
|
|
125784
|
+
pageKeyset: collectionPorts.pageKeyset,
|
|
125785
|
+
stop: () => this.shuttingDown,
|
|
125212
125786
|
page: async (collection, shard, offset, limit) => {
|
|
125213
125787
|
if (collection === MEDIA_COLLECTION) return (await mediaStore.pageForAudit(shard?.value ?? "", offset, limit)).map((row) => ({
|
|
125214
125788
|
key: String(row.mediaId),
|
|
125215
125789
|
data: row.data
|
|
125216
125790
|
}));
|
|
125217
|
-
return (
|
|
125218
|
-
collection,
|
|
125219
|
-
filter: {
|
|
125220
|
-
orderBy: {
|
|
125221
|
-
field: "id",
|
|
125222
|
-
direction: "asc"
|
|
125223
|
-
},
|
|
125224
|
-
limit,
|
|
125225
|
-
offset
|
|
125226
|
-
}
|
|
125227
|
-
}) ?? []).map((row) => ({
|
|
125228
|
-
key: row.id,
|
|
125229
|
-
data: row.data
|
|
125230
|
-
}));
|
|
125791
|
+
return collectionPorts.pageByOffset(collection, offset, limit);
|
|
125231
125792
|
},
|
|
125232
125793
|
size: async (collection, shard) => {
|
|
125233
125794
|
if (collection === MEDIA_COLLECTION) return mediaStore.countForAudit(shard?.value ?? "");
|
|
125234
|
-
return
|
|
125795
|
+
return collectionPorts.size(collection);
|
|
125235
125796
|
},
|
|
125236
125797
|
liveTrackIds: (ids) => this.liveTrackIdsThroughAliases(ids, trackStore),
|
|
125237
125798
|
liveEventIds: (table, ids) => eventStore.existingEventIdsIn(table, ids),
|
|
@@ -125254,6 +125815,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
125254
125815
|
if (aliasStore === null) throw new Error("track alias store not bound");
|
|
125255
125816
|
return await aliasStore.deleteAlias(aliasId);
|
|
125256
125817
|
},
|
|
125818
|
+
deletePlate: plateDeleter(plateLedger, this.ctx.logger),
|
|
125257
125819
|
deleteRow: settingsRowDeleterOf(() => this.settingsStore),
|
|
125258
125820
|
logger: this.ctx.logger
|
|
125259
125821
|
});
|
|
@@ -126675,7 +127237,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
126675
127237
|
jobId: null
|
|
126676
127238
|
},
|
|
126677
127239
|
"storage.discardLocationStatus": async () => this.discardJob?.status() ?? IDLE_DISCARD,
|
|
126678
|
-
"storage.discardLocationCancel": async () => this.discardJob?.cancel() ?? { requested: false }
|
|
127240
|
+
"storage.discardLocationCancel": async () => this.discardJob?.cancel() ?? { requested: false },
|
|
127241
|
+
"storage.forgetMissingMedia": async (input) => {
|
|
127242
|
+
const deps = this.mediaForgetDeps;
|
|
127243
|
+
if (deps === null) throw new Error("storage.forgetMissingMedia: not initialised");
|
|
127244
|
+
return forgetMissingMediaRows(deps, input);
|
|
127245
|
+
}
|
|
126679
127246
|
};
|
|
126680
127247
|
}
|
|
126681
127248
|
/**
|