@camstack/addon-post-analysis 1.2.306 → 1.2.307
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 +233 -14
- package/dist/pipeline-analytics/index.mjs +233 -14
- package/package.json +1 -1
|
@@ -20071,6 +20071,114 @@ function ncGroupSubjectText(texts, input) {
|
|
|
20071
20071
|
return texts.join([...names, ...others]);
|
|
20072
20072
|
}
|
|
20073
20073
|
//#endregion
|
|
20074
|
+
//#region src/notification-center/outbox-key.ts
|
|
20075
|
+
/**
|
|
20076
|
+
* The record kinds whose `recordId` is a per-table SQLite rowid (D474), and so
|
|
20077
|
+
* may be reused after a delete. `device-event` is NOT one: sensor events are
|
|
20078
|
+
* TEXT-keyed with a producer-minted id (`sensor-event-store.ts`).
|
|
20079
|
+
*/
|
|
20080
|
+
var ROWID_RECORD_KINDS = new Set([
|
|
20081
|
+
"object-event",
|
|
20082
|
+
"package-event",
|
|
20083
|
+
"audio-event"
|
|
20084
|
+
]);
|
|
20085
|
+
/** Is this kind's record id a reusable rowid? See {@link ROWID_RECORD_KINDS}. */
|
|
20086
|
+
function isRowidRecordKind(kind) {
|
|
20087
|
+
return ROWID_RECORD_KINDS.has(kind);
|
|
20088
|
+
}
|
|
20089
|
+
/** The record identity an outbox key input speaks for. */
|
|
20090
|
+
function outboxRecordIdentity(input) {
|
|
20091
|
+
return {
|
|
20092
|
+
recordId: input.recordId,
|
|
20093
|
+
deviceId: input.deviceId,
|
|
20094
|
+
recordAt: input.payload.subject.timestamp
|
|
20095
|
+
};
|
|
20096
|
+
}
|
|
20097
|
+
/**
|
|
20098
|
+
* The record GENERATION ref — `<recordId>@<deviceId>:<recordAt>`. Exported for
|
|
20099
|
+
* the specs and for a caller that must name a rowid record outside the outbox
|
|
20100
|
+
* (none today); a key is always built through {@link outboxEntryId}.
|
|
20101
|
+
*/
|
|
20102
|
+
function recordGenerationRef(record) {
|
|
20103
|
+
return `${record.recordId}@${String(record.deviceId)}:${String(record.recordAt)}`;
|
|
20104
|
+
}
|
|
20105
|
+
/**
|
|
20106
|
+
* The middle segment of the dedup id:
|
|
20107
|
+
*
|
|
20108
|
+
* 1. an explicit `dedupRef` (upgrade / burst revision);
|
|
20109
|
+
* 2. the track id — `maxPerTrack: 1` (a track id is random, never reused);
|
|
20110
|
+
* 3. for a rowid record, its generation ref;
|
|
20111
|
+
* 4. the record id as is (synthetic ids already carry device + instant).
|
|
20112
|
+
*/
|
|
20113
|
+
function outboxDedupRef(input) {
|
|
20114
|
+
if (input.dedupRef !== void 0) return input.dedupRef;
|
|
20115
|
+
if (input.trackId !== void 0) return input.trackId;
|
|
20116
|
+
if (isRowidRecordKind(input.recordKind)) return recordGenerationRef(outboxRecordIdentity(input));
|
|
20117
|
+
return input.recordId;
|
|
20118
|
+
}
|
|
20119
|
+
/** The outbox row id — the durable per-track / per-record fire guarantee. */
|
|
20120
|
+
function outboxEntryId(input) {
|
|
20121
|
+
return `${input.ruleId}:${outboxDedupRef(input)}:${input.targetId}`;
|
|
20122
|
+
}
|
|
20123
|
+
/**
|
|
20124
|
+
* The id this input had BEFORE D694 — but only when it differs from today's,
|
|
20125
|
+
* i.e. for a rowid record keyed on its record id. `null` otherwise: there is no
|
|
20126
|
+
* older spelling to look for.
|
|
20127
|
+
*/
|
|
20128
|
+
function legacyOutboxEntryId(input) {
|
|
20129
|
+
if (input.dedupRef !== void 0 || input.trackId !== void 0) return null;
|
|
20130
|
+
if (!isRowidRecordKind(input.recordKind)) return null;
|
|
20131
|
+
return `${input.ruleId}:${input.recordId}:${input.targetId}`;
|
|
20132
|
+
}
|
|
20133
|
+
/**
|
|
20134
|
+
* A STORED row's id as it would be built today, when the row was written under
|
|
20135
|
+
* the pre-D694 spelling; `null` when the stored id is already current (or was
|
|
20136
|
+
* built from an explicit dedup ref, which a row does not record and which never
|
|
20137
|
+
* involved the record id).
|
|
20138
|
+
*/
|
|
20139
|
+
function translatedLegacyEntryId(storedId, row) {
|
|
20140
|
+
const legacy = legacyOutboxEntryId(row);
|
|
20141
|
+
if (legacy === null || legacy !== storedId) return null;
|
|
20142
|
+
return outboxEntryId(row);
|
|
20143
|
+
}
|
|
20144
|
+
/**
|
|
20145
|
+
* Does a STORED row's payload really carry its record timestamp?
|
|
20146
|
+
*
|
|
20147
|
+
* The type says it always does, but a stored payload is checked only as an
|
|
20148
|
+
* object on the way back from the store (`rowToEntry`), so a malformed or very
|
|
20149
|
+
* old row can lack `subject`. Every read of {@link outboxRecordIdentity} on a
|
|
20150
|
+
* STORED row is preceded by this check. Takes the payload as `unknown` on
|
|
20151
|
+
* purpose: the static type cannot answer the question.
|
|
20152
|
+
*/
|
|
20153
|
+
function hasRecordTimestamp(payload) {
|
|
20154
|
+
if (typeof payload !== "object" || payload === null || !("subject" in payload)) return false;
|
|
20155
|
+
const subject = payload.subject;
|
|
20156
|
+
return typeof subject === "object" && subject !== null && "timestamp" in subject && typeof subject.timestamp === "number";
|
|
20157
|
+
}
|
|
20158
|
+
/** Do two rows speak for the same RECORD? Id, camera and record timestamp. */
|
|
20159
|
+
function isSameOutboxRecord(a, b) {
|
|
20160
|
+
return a.recordId === b.recordId && a.deviceId === b.deviceId && a.recordAt === b.recordAt;
|
|
20161
|
+
}
|
|
20162
|
+
/**
|
|
20163
|
+
* The scope of the event a payload's `eventId` names.
|
|
20164
|
+
*
|
|
20165
|
+
* Usually the row's own subject. A COMBINED alarm row is the exception: its
|
|
20166
|
+
* subject is the alarm transition (its own instant), while its `eventId` is the
|
|
20167
|
+
* triggering rule's event — so the generation is the alarm SOURCE's camera and
|
|
20168
|
+
* timestamp (`NcRuleFireRef`), never the transition's.
|
|
20169
|
+
*/
|
|
20170
|
+
function eventMediaScopeOf(subject) {
|
|
20171
|
+
const source = subject.systemEvent?.alarmSource;
|
|
20172
|
+
if (source !== void 0) return {
|
|
20173
|
+
deviceId: source.deviceId,
|
|
20174
|
+
recordAt: source.timestamp
|
|
20175
|
+
};
|
|
20176
|
+
return {
|
|
20177
|
+
deviceId: subject.deviceId,
|
|
20178
|
+
recordAt: subject.timestamp
|
|
20179
|
+
};
|
|
20180
|
+
}
|
|
20181
|
+
//#endregion
|
|
20074
20182
|
//#region src/notification-center/footage-source.ts
|
|
20075
20183
|
/**
|
|
20076
20184
|
* Decide where this window must be cut from. Pure and total.
|
|
@@ -22036,7 +22144,7 @@ var NcDispatcher = class {
|
|
|
22036
22144
|
});
|
|
22037
22145
|
if (policy === "keyFrame" || frame === "full" || frame === "boxed") owners.reverse();
|
|
22038
22146
|
for (const owner of owners) try {
|
|
22039
|
-
const files = await this.deps.getMediaForOwner(owner.kind, owner.id);
|
|
22147
|
+
const files = owner.kind === "object" ? await this.deps.getMediaForOwner(owner.kind, owner.id, eventMediaScopeOf(subject)) : await this.deps.getMediaForOwner(owner.kind, owner.id);
|
|
22040
22148
|
if (files.length === 0) continue;
|
|
22041
22149
|
const preference = frame !== void 0 ? framePreference(frame, owner.kind) : policy === "best-matching" ? bestMatchingKindPreference(signal, owner.kind) : attachmentKindPreference(policy, owner.kind);
|
|
22042
22150
|
const ordered = [];
|
|
@@ -24321,11 +24429,6 @@ var DEFAULT_MAX_ATTEMPTS = 8;
|
|
|
24321
24429
|
var DEFAULT_DRAIN_BATCH = 20;
|
|
24322
24430
|
/** Dedup ids remembered at boot — bounded to recent rows (dedup horizon). */
|
|
24323
24431
|
var DEDUP_HYDRATE_WINDOW_MS = 168 * 36e5;
|
|
24324
|
-
/** Stable dedup id — the durable per-track/per-record fire guarantee. */
|
|
24325
|
-
function outboxEntryId(input) {
|
|
24326
|
-
const dedupRef = input.dedupRef ?? input.trackId ?? input.recordId;
|
|
24327
|
-
return `${input.ruleId}:${dedupRef}:${input.targetId}`;
|
|
24328
|
-
}
|
|
24329
24432
|
var NcOutbox = class {
|
|
24330
24433
|
pending = /* @__PURE__ */ new Map();
|
|
24331
24434
|
/** Every id ever enqueued within the dedup horizon (incl. sent/dead). */
|
|
@@ -24402,20 +24505,27 @@ var NcOutbox = class {
|
|
|
24402
24505
|
}
|
|
24403
24506
|
});
|
|
24404
24507
|
let superseded = 0;
|
|
24508
|
+
let translated = 0;
|
|
24405
24509
|
for (const row of rows) {
|
|
24406
|
-
|
|
24510
|
+
const entry = rowToEntry$1(row.id, row.data);
|
|
24511
|
+
const current = entry === null ? null : this.translateLegacy(row.id, entry);
|
|
24512
|
+
if (this.knownIds.has(row.id) || current !== null && this.knownIds.has(current)) {
|
|
24407
24513
|
superseded += 1;
|
|
24408
24514
|
continue;
|
|
24409
24515
|
}
|
|
24410
24516
|
this.knownIds.add(row.id);
|
|
24411
|
-
|
|
24517
|
+
if (current !== null) {
|
|
24518
|
+
this.knownIds.add(current);
|
|
24519
|
+
translated += 1;
|
|
24520
|
+
}
|
|
24412
24521
|
if (entry !== null && entry.status === "pending") this.pending.set(entry.id, entry);
|
|
24413
24522
|
}
|
|
24414
24523
|
this.horizonLoaded = true;
|
|
24415
24524
|
this.logger.info("notification outbox loaded", { meta: {
|
|
24416
24525
|
pending: this.pending.size,
|
|
24417
24526
|
known: this.knownIds.size,
|
|
24418
|
-
superseded
|
|
24527
|
+
superseded,
|
|
24528
|
+
translated
|
|
24419
24529
|
} });
|
|
24420
24530
|
} catch (err) {
|
|
24421
24531
|
this.logger.warn("notification outbox load failed", { meta: { error: String(err) } });
|
|
@@ -24439,7 +24549,7 @@ var NcOutbox = class {
|
|
|
24439
24549
|
for (const input of inputs) {
|
|
24440
24550
|
const id = outboxEntryId(input);
|
|
24441
24551
|
if (this.knownIds.has(id)) continue;
|
|
24442
|
-
if (!this.horizonLoaded && await this.
|
|
24552
|
+
if (!this.horizonLoaded && await this.alreadyStored(id, input)) {
|
|
24443
24553
|
this.knownIds.add(id);
|
|
24444
24554
|
this.logger.info("outbox enqueue deduped against the store — the horizon is still cold", {
|
|
24445
24555
|
tags: { deviceId: input.deviceId },
|
|
@@ -24656,7 +24766,7 @@ var NcOutbox = class {
|
|
|
24656
24766
|
*/
|
|
24657
24767
|
async supersede(input) {
|
|
24658
24768
|
const wanted = new Set(input.targetIds);
|
|
24659
|
-
const victims = [...this.pending.values()].filter((e) => e.ruleId === input.ruleId && e.
|
|
24769
|
+
const victims = [...this.pending.values()].filter((e) => e.ruleId === input.ruleId && wanted.has(e.targetId) && hasRecordTimestamp(e.payload) && isSameOutboxRecord(outboxRecordIdentity(e), input));
|
|
24660
24770
|
let dropped = 0;
|
|
24661
24771
|
for (const entry of victims) try {
|
|
24662
24772
|
await this.store.delete.mutate({
|
|
@@ -24772,6 +24882,81 @@ var NcOutbox = class {
|
|
|
24772
24882
|
await this.mutate(retry);
|
|
24773
24883
|
}
|
|
24774
24884
|
/**
|
|
24885
|
+
* Cold-horizon dedup: is this intent already a row in the store?
|
|
24886
|
+
*
|
|
24887
|
+
* Today's id first. Then, for a rowid record, the pre-D694 spelling — which
|
|
24888
|
+
* is accepted ONLY when the row found speaks for the SAME record (camera and
|
|
24889
|
+
* record timestamp too). A legacy row of a different record that reused the
|
|
24890
|
+
* rowid is exactly the collision D694 removes, and must not swallow this one.
|
|
24891
|
+
*/
|
|
24892
|
+
async alreadyStored(id, input) {
|
|
24893
|
+
if (await this.rowExists(id)) return true;
|
|
24894
|
+
const legacy = legacyOutboxEntryId(input);
|
|
24895
|
+
if (legacy === null) return false;
|
|
24896
|
+
const row = await this.readRow(legacy);
|
|
24897
|
+
if (row === null) return false;
|
|
24898
|
+
if (!hasRecordTimestamp(row.payload)) {
|
|
24899
|
+
this.logger.warn("outbox: a pre-D694 row carries no record timestamp — not used for dedup", {
|
|
24900
|
+
tags: { deviceId: row.deviceId },
|
|
24901
|
+
meta: {
|
|
24902
|
+
id,
|
|
24903
|
+
legacyId: legacy,
|
|
24904
|
+
ruleId: input.ruleId
|
|
24905
|
+
}
|
|
24906
|
+
});
|
|
24907
|
+
return false;
|
|
24908
|
+
}
|
|
24909
|
+
if (isSameOutboxRecord(outboxRecordIdentity(row), outboxRecordIdentity(input))) return true;
|
|
24910
|
+
this.logger.info("outbox: a pre-D694 row shares this rowid but is a different record — not a duplicate", {
|
|
24911
|
+
tags: { deviceId: input.deviceId },
|
|
24912
|
+
meta: {
|
|
24913
|
+
id,
|
|
24914
|
+
legacyId: legacy,
|
|
24915
|
+
legacyDeviceId: row.deviceId,
|
|
24916
|
+
ruleId: input.ruleId
|
|
24917
|
+
}
|
|
24918
|
+
});
|
|
24919
|
+
return false;
|
|
24920
|
+
}
|
|
24921
|
+
/**
|
|
24922
|
+
* The id a stored pre-D694 row would have today, or `null`.
|
|
24923
|
+
*
|
|
24924
|
+
* Guarded per row: a stored payload is validated only as an object
|
|
24925
|
+
* (`rowToEntry`), and a row with no `subject.timestamp` would otherwise throw
|
|
24926
|
+
* inside {@link load} and leave the WHOLE dedup horizon cold. Such a row is
|
|
24927
|
+
* still loaded under its stored id; it just gets no translated twin.
|
|
24928
|
+
*/
|
|
24929
|
+
translateLegacy(storedId, entry) {
|
|
24930
|
+
if (legacyOutboxEntryId(entry) !== storedId) return null;
|
|
24931
|
+
if (!hasRecordTimestamp(entry.payload)) {
|
|
24932
|
+
this.logger.warn("outbox: a pre-D694 row carries no record timestamp — loaded without translation", {
|
|
24933
|
+
tags: { deviceId: entry.deviceId },
|
|
24934
|
+
meta: {
|
|
24935
|
+
id: storedId,
|
|
24936
|
+
ruleId: entry.ruleId
|
|
24937
|
+
}
|
|
24938
|
+
});
|
|
24939
|
+
return null;
|
|
24940
|
+
}
|
|
24941
|
+
return translatedLegacyEntryId(storedId, entry);
|
|
24942
|
+
}
|
|
24943
|
+
/** One stored row by id, or `null` (absent, unreadable, or a failed read). */
|
|
24944
|
+
async readRow(id) {
|
|
24945
|
+
try {
|
|
24946
|
+
const row = await this.store.get.query({
|
|
24947
|
+
collection: NC_OUTBOX_COLLECTION,
|
|
24948
|
+
key: id
|
|
24949
|
+
});
|
|
24950
|
+
return isStoredRow(row) ? rowToEntry$1(id, row) : null;
|
|
24951
|
+
} catch (err) {
|
|
24952
|
+
this.logger.debug("outbox cold-horizon dedup read failed", { meta: {
|
|
24953
|
+
id,
|
|
24954
|
+
error: String(err)
|
|
24955
|
+
} });
|
|
24956
|
+
return null;
|
|
24957
|
+
}
|
|
24958
|
+
}
|
|
24959
|
+
/**
|
|
24775
24960
|
* Does a row already exist under this dedup id?
|
|
24776
24961
|
*
|
|
24777
24962
|
* Asked ONLY while {@link horizonLoaded} is false. {@link persist} writes
|
|
@@ -24865,6 +25050,10 @@ var OUTBOX_RECORD_KINDS = new Set([
|
|
|
24865
25050
|
function isOutboxRecordKind(x) {
|
|
24866
25051
|
return typeof x === "string" && OUTBOX_RECORD_KINDS.has(x);
|
|
24867
25052
|
}
|
|
25053
|
+
/** `settingsStore.get` answers `unknown` — a guard, never a cast. */
|
|
25054
|
+
function isStoredRow(x) {
|
|
25055
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
25056
|
+
}
|
|
24868
25057
|
/** A stored row back to an entry — a guard, never a cast. Exported for the
|
|
24869
25058
|
* specs that read the store a centre wrote to. */
|
|
24870
25059
|
function rowToEntry$1(id, data) {
|
|
@@ -33231,7 +33420,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
33231
33420
|
...deps.dispatcher,
|
|
33232
33421
|
logger: this.logger.child("dispatch"),
|
|
33233
33422
|
texts: this.texts,
|
|
33234
|
-
getMediaForOwner: async (ownerKind, ownerId) => {
|
|
33423
|
+
getMediaForOwner: async (ownerKind, ownerId, eventScope) => {
|
|
33235
33424
|
const held = isSyntheticId(ownerId) ? this.syntheticMedia.get(ownerId) : void 0;
|
|
33236
33425
|
if (held !== void 0) return held.files;
|
|
33237
33426
|
const shelved = isStillShelfId(ownerId) ? this.stills?.get(ownerId) : void 0;
|
|
@@ -33240,7 +33429,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
33240
33429
|
if (linked !== void 0) return linked;
|
|
33241
33430
|
const departed = ownerKind === "track" && this.departureFallbacks.has(ownerId);
|
|
33242
33431
|
const owner = departed ? await this.resolveDepartedTrack(ownerId) : ownerId;
|
|
33243
|
-
const files = await deps.dispatcher.getMediaForOwner(ownerKind, owner);
|
|
33432
|
+
const files = await deps.dispatcher.getMediaForOwner(ownerKind, owner, eventScope);
|
|
33244
33433
|
if (files.length > 0 || !departed) return files;
|
|
33245
33434
|
return this.departureFallbackMedia(ownerId) ?? files;
|
|
33246
33435
|
},
|
|
@@ -35387,6 +35576,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
35387
35576
|
const dropped = await this.outbox.supersede({
|
|
35388
35577
|
ruleId: source.ruleId,
|
|
35389
35578
|
recordId: source.recordId,
|
|
35579
|
+
deviceId: source.deviceId,
|
|
35580
|
+
recordAt: source.timestamp,
|
|
35390
35581
|
targetIds: [...combined]
|
|
35391
35582
|
});
|
|
35392
35583
|
this.logger.info("alarm notification combined with the rule that triggered it", {
|
|
@@ -96128,6 +96319,30 @@ async function encodeClipQuery(encode, text, modelId, warn) {
|
|
|
96128
96319
|
}
|
|
96129
96320
|
}
|
|
96130
96321
|
//#endregion
|
|
96322
|
+
//#region src/pipeline-analytics/nc-owner-media-reader.ts
|
|
96323
|
+
/** Keep only the rows written for THIS generation of the event id. */
|
|
96324
|
+
function ofGeneration(files, scope) {
|
|
96325
|
+
return files.filter((f) => f.timestamp === scope.recordAt);
|
|
96326
|
+
}
|
|
96327
|
+
function makeNcOwnerMediaReader(deps) {
|
|
96328
|
+
return async (ownerKind, ownerId, eventScope) => {
|
|
96329
|
+
if (ownerKind === "track") return deps.media.listByOwner("track", await deps.resolveTrackOwnerId(ownerId));
|
|
96330
|
+
if (eventScope === void 0) return deps.media.listByOwner("object", ownerId);
|
|
96331
|
+
const onCamera = await deps.media.listByOwner("object", ownerId, eventScope.deviceId);
|
|
96332
|
+
const kept = ofGeneration(onCamera, eventScope);
|
|
96333
|
+
if (kept.length < onCamera.length) deps.logger.warn("event media of another record on a reused event id — not used", {
|
|
96334
|
+
tags: { deviceId: eventScope.deviceId },
|
|
96335
|
+
meta: {
|
|
96336
|
+
ownerId,
|
|
96337
|
+
recordAt: eventScope.recordAt,
|
|
96338
|
+
refused: onCamera.length - kept.length,
|
|
96339
|
+
kept: kept.length
|
|
96340
|
+
}
|
|
96341
|
+
});
|
|
96342
|
+
return kept;
|
|
96343
|
+
};
|
|
96344
|
+
}
|
|
96345
|
+
//#endregion
|
|
96131
96346
|
//#region src/pipeline-analytics/store/settings-raw-mirror.ts
|
|
96132
96347
|
/** How long a mirrored entry is served before a refresh is started behind the
|
|
96133
96348
|
* next reader. See the module doc for why this matches the per-kind caches'
|
|
@@ -100452,7 +100667,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
100452
100667
|
}
|
|
100453
100668
|
});
|
|
100454
100669
|
},
|
|
100455
|
-
getMediaForOwner:
|
|
100670
|
+
getMediaForOwner: makeNcOwnerMediaReader({
|
|
100671
|
+
media: stores.mediaStore,
|
|
100672
|
+
resolveTrackOwnerId: (trackId) => resolveMediaOwnerId("track", trackId, (id) => this.trackAliasStore?.resolve(id) ?? Promise.resolve(null), (failure) => this.aliasResolveFailureLogger?.(failure), "getMediaForOwner"),
|
|
100673
|
+
logger: this.ctx.logger.child("nc-media")
|
|
100674
|
+
}),
|
|
100456
100675
|
getSubjectTrail: async (deviceId, trackId) => {
|
|
100457
100676
|
const dims = this.lastFrameDimsByDevice.get(deviceId);
|
|
100458
100677
|
if (dims === void 0 || dims.w <= 0 || dims.h <= 0) return [];
|
|
@@ -20048,6 +20048,114 @@ function ncGroupSubjectText(texts, input) {
|
|
|
20048
20048
|
return texts.join([...names, ...others]);
|
|
20049
20049
|
}
|
|
20050
20050
|
//#endregion
|
|
20051
|
+
//#region src/notification-center/outbox-key.ts
|
|
20052
|
+
/**
|
|
20053
|
+
* The record kinds whose `recordId` is a per-table SQLite rowid (D474), and so
|
|
20054
|
+
* may be reused after a delete. `device-event` is NOT one: sensor events are
|
|
20055
|
+
* TEXT-keyed with a producer-minted id (`sensor-event-store.ts`).
|
|
20056
|
+
*/
|
|
20057
|
+
var ROWID_RECORD_KINDS = new Set([
|
|
20058
|
+
"object-event",
|
|
20059
|
+
"package-event",
|
|
20060
|
+
"audio-event"
|
|
20061
|
+
]);
|
|
20062
|
+
/** Is this kind's record id a reusable rowid? See {@link ROWID_RECORD_KINDS}. */
|
|
20063
|
+
function isRowidRecordKind(kind) {
|
|
20064
|
+
return ROWID_RECORD_KINDS.has(kind);
|
|
20065
|
+
}
|
|
20066
|
+
/** The record identity an outbox key input speaks for. */
|
|
20067
|
+
function outboxRecordIdentity(input) {
|
|
20068
|
+
return {
|
|
20069
|
+
recordId: input.recordId,
|
|
20070
|
+
deviceId: input.deviceId,
|
|
20071
|
+
recordAt: input.payload.subject.timestamp
|
|
20072
|
+
};
|
|
20073
|
+
}
|
|
20074
|
+
/**
|
|
20075
|
+
* The record GENERATION ref — `<recordId>@<deviceId>:<recordAt>`. Exported for
|
|
20076
|
+
* the specs and for a caller that must name a rowid record outside the outbox
|
|
20077
|
+
* (none today); a key is always built through {@link outboxEntryId}.
|
|
20078
|
+
*/
|
|
20079
|
+
function recordGenerationRef(record) {
|
|
20080
|
+
return `${record.recordId}@${String(record.deviceId)}:${String(record.recordAt)}`;
|
|
20081
|
+
}
|
|
20082
|
+
/**
|
|
20083
|
+
* The middle segment of the dedup id:
|
|
20084
|
+
*
|
|
20085
|
+
* 1. an explicit `dedupRef` (upgrade / burst revision);
|
|
20086
|
+
* 2. the track id — `maxPerTrack: 1` (a track id is random, never reused);
|
|
20087
|
+
* 3. for a rowid record, its generation ref;
|
|
20088
|
+
* 4. the record id as is (synthetic ids already carry device + instant).
|
|
20089
|
+
*/
|
|
20090
|
+
function outboxDedupRef(input) {
|
|
20091
|
+
if (input.dedupRef !== void 0) return input.dedupRef;
|
|
20092
|
+
if (input.trackId !== void 0) return input.trackId;
|
|
20093
|
+
if (isRowidRecordKind(input.recordKind)) return recordGenerationRef(outboxRecordIdentity(input));
|
|
20094
|
+
return input.recordId;
|
|
20095
|
+
}
|
|
20096
|
+
/** The outbox row id — the durable per-track / per-record fire guarantee. */
|
|
20097
|
+
function outboxEntryId(input) {
|
|
20098
|
+
return `${input.ruleId}:${outboxDedupRef(input)}:${input.targetId}`;
|
|
20099
|
+
}
|
|
20100
|
+
/**
|
|
20101
|
+
* The id this input had BEFORE D694 — but only when it differs from today's,
|
|
20102
|
+
* i.e. for a rowid record keyed on its record id. `null` otherwise: there is no
|
|
20103
|
+
* older spelling to look for.
|
|
20104
|
+
*/
|
|
20105
|
+
function legacyOutboxEntryId(input) {
|
|
20106
|
+
if (input.dedupRef !== void 0 || input.trackId !== void 0) return null;
|
|
20107
|
+
if (!isRowidRecordKind(input.recordKind)) return null;
|
|
20108
|
+
return `${input.ruleId}:${input.recordId}:${input.targetId}`;
|
|
20109
|
+
}
|
|
20110
|
+
/**
|
|
20111
|
+
* A STORED row's id as it would be built today, when the row was written under
|
|
20112
|
+
* the pre-D694 spelling; `null` when the stored id is already current (or was
|
|
20113
|
+
* built from an explicit dedup ref, which a row does not record and which never
|
|
20114
|
+
* involved the record id).
|
|
20115
|
+
*/
|
|
20116
|
+
function translatedLegacyEntryId(storedId, row) {
|
|
20117
|
+
const legacy = legacyOutboxEntryId(row);
|
|
20118
|
+
if (legacy === null || legacy !== storedId) return null;
|
|
20119
|
+
return outboxEntryId(row);
|
|
20120
|
+
}
|
|
20121
|
+
/**
|
|
20122
|
+
* Does a STORED row's payload really carry its record timestamp?
|
|
20123
|
+
*
|
|
20124
|
+
* The type says it always does, but a stored payload is checked only as an
|
|
20125
|
+
* object on the way back from the store (`rowToEntry`), so a malformed or very
|
|
20126
|
+
* old row can lack `subject`. Every read of {@link outboxRecordIdentity} on a
|
|
20127
|
+
* STORED row is preceded by this check. Takes the payload as `unknown` on
|
|
20128
|
+
* purpose: the static type cannot answer the question.
|
|
20129
|
+
*/
|
|
20130
|
+
function hasRecordTimestamp(payload) {
|
|
20131
|
+
if (typeof payload !== "object" || payload === null || !("subject" in payload)) return false;
|
|
20132
|
+
const subject = payload.subject;
|
|
20133
|
+
return typeof subject === "object" && subject !== null && "timestamp" in subject && typeof subject.timestamp === "number";
|
|
20134
|
+
}
|
|
20135
|
+
/** Do two rows speak for the same RECORD? Id, camera and record timestamp. */
|
|
20136
|
+
function isSameOutboxRecord(a, b) {
|
|
20137
|
+
return a.recordId === b.recordId && a.deviceId === b.deviceId && a.recordAt === b.recordAt;
|
|
20138
|
+
}
|
|
20139
|
+
/**
|
|
20140
|
+
* The scope of the event a payload's `eventId` names.
|
|
20141
|
+
*
|
|
20142
|
+
* Usually the row's own subject. A COMBINED alarm row is the exception: its
|
|
20143
|
+
* subject is the alarm transition (its own instant), while its `eventId` is the
|
|
20144
|
+
* triggering rule's event — so the generation is the alarm SOURCE's camera and
|
|
20145
|
+
* timestamp (`NcRuleFireRef`), never the transition's.
|
|
20146
|
+
*/
|
|
20147
|
+
function eventMediaScopeOf(subject) {
|
|
20148
|
+
const source = subject.systemEvent?.alarmSource;
|
|
20149
|
+
if (source !== void 0) return {
|
|
20150
|
+
deviceId: source.deviceId,
|
|
20151
|
+
recordAt: source.timestamp
|
|
20152
|
+
};
|
|
20153
|
+
return {
|
|
20154
|
+
deviceId: subject.deviceId,
|
|
20155
|
+
recordAt: subject.timestamp
|
|
20156
|
+
};
|
|
20157
|
+
}
|
|
20158
|
+
//#endregion
|
|
20051
20159
|
//#region src/notification-center/footage-source.ts
|
|
20052
20160
|
/**
|
|
20053
20161
|
* Decide where this window must be cut from. Pure and total.
|
|
@@ -22013,7 +22121,7 @@ var NcDispatcher = class {
|
|
|
22013
22121
|
});
|
|
22014
22122
|
if (policy === "keyFrame" || frame === "full" || frame === "boxed") owners.reverse();
|
|
22015
22123
|
for (const owner of owners) try {
|
|
22016
|
-
const files = await this.deps.getMediaForOwner(owner.kind, owner.id);
|
|
22124
|
+
const files = owner.kind === "object" ? await this.deps.getMediaForOwner(owner.kind, owner.id, eventMediaScopeOf(subject)) : await this.deps.getMediaForOwner(owner.kind, owner.id);
|
|
22017
22125
|
if (files.length === 0) continue;
|
|
22018
22126
|
const preference = frame !== void 0 ? framePreference(frame, owner.kind) : policy === "best-matching" ? bestMatchingKindPreference(signal, owner.kind) : attachmentKindPreference(policy, owner.kind);
|
|
22019
22127
|
const ordered = [];
|
|
@@ -24298,11 +24406,6 @@ var DEFAULT_MAX_ATTEMPTS = 8;
|
|
|
24298
24406
|
var DEFAULT_DRAIN_BATCH = 20;
|
|
24299
24407
|
/** Dedup ids remembered at boot — bounded to recent rows (dedup horizon). */
|
|
24300
24408
|
var DEDUP_HYDRATE_WINDOW_MS = 168 * 36e5;
|
|
24301
|
-
/** Stable dedup id — the durable per-track/per-record fire guarantee. */
|
|
24302
|
-
function outboxEntryId(input) {
|
|
24303
|
-
const dedupRef = input.dedupRef ?? input.trackId ?? input.recordId;
|
|
24304
|
-
return `${input.ruleId}:${dedupRef}:${input.targetId}`;
|
|
24305
|
-
}
|
|
24306
24409
|
var NcOutbox = class {
|
|
24307
24410
|
pending = /* @__PURE__ */ new Map();
|
|
24308
24411
|
/** Every id ever enqueued within the dedup horizon (incl. sent/dead). */
|
|
@@ -24379,20 +24482,27 @@ var NcOutbox = class {
|
|
|
24379
24482
|
}
|
|
24380
24483
|
});
|
|
24381
24484
|
let superseded = 0;
|
|
24485
|
+
let translated = 0;
|
|
24382
24486
|
for (const row of rows) {
|
|
24383
|
-
|
|
24487
|
+
const entry = rowToEntry$1(row.id, row.data);
|
|
24488
|
+
const current = entry === null ? null : this.translateLegacy(row.id, entry);
|
|
24489
|
+
if (this.knownIds.has(row.id) || current !== null && this.knownIds.has(current)) {
|
|
24384
24490
|
superseded += 1;
|
|
24385
24491
|
continue;
|
|
24386
24492
|
}
|
|
24387
24493
|
this.knownIds.add(row.id);
|
|
24388
|
-
|
|
24494
|
+
if (current !== null) {
|
|
24495
|
+
this.knownIds.add(current);
|
|
24496
|
+
translated += 1;
|
|
24497
|
+
}
|
|
24389
24498
|
if (entry !== null && entry.status === "pending") this.pending.set(entry.id, entry);
|
|
24390
24499
|
}
|
|
24391
24500
|
this.horizonLoaded = true;
|
|
24392
24501
|
this.logger.info("notification outbox loaded", { meta: {
|
|
24393
24502
|
pending: this.pending.size,
|
|
24394
24503
|
known: this.knownIds.size,
|
|
24395
|
-
superseded
|
|
24504
|
+
superseded,
|
|
24505
|
+
translated
|
|
24396
24506
|
} });
|
|
24397
24507
|
} catch (err) {
|
|
24398
24508
|
this.logger.warn("notification outbox load failed", { meta: { error: String(err) } });
|
|
@@ -24416,7 +24526,7 @@ var NcOutbox = class {
|
|
|
24416
24526
|
for (const input of inputs) {
|
|
24417
24527
|
const id = outboxEntryId(input);
|
|
24418
24528
|
if (this.knownIds.has(id)) continue;
|
|
24419
|
-
if (!this.horizonLoaded && await this.
|
|
24529
|
+
if (!this.horizonLoaded && await this.alreadyStored(id, input)) {
|
|
24420
24530
|
this.knownIds.add(id);
|
|
24421
24531
|
this.logger.info("outbox enqueue deduped against the store — the horizon is still cold", {
|
|
24422
24532
|
tags: { deviceId: input.deviceId },
|
|
@@ -24633,7 +24743,7 @@ var NcOutbox = class {
|
|
|
24633
24743
|
*/
|
|
24634
24744
|
async supersede(input) {
|
|
24635
24745
|
const wanted = new Set(input.targetIds);
|
|
24636
|
-
const victims = [...this.pending.values()].filter((e) => e.ruleId === input.ruleId && e.
|
|
24746
|
+
const victims = [...this.pending.values()].filter((e) => e.ruleId === input.ruleId && wanted.has(e.targetId) && hasRecordTimestamp(e.payload) && isSameOutboxRecord(outboxRecordIdentity(e), input));
|
|
24637
24747
|
let dropped = 0;
|
|
24638
24748
|
for (const entry of victims) try {
|
|
24639
24749
|
await this.store.delete.mutate({
|
|
@@ -24749,6 +24859,81 @@ var NcOutbox = class {
|
|
|
24749
24859
|
await this.mutate(retry);
|
|
24750
24860
|
}
|
|
24751
24861
|
/**
|
|
24862
|
+
* Cold-horizon dedup: is this intent already a row in the store?
|
|
24863
|
+
*
|
|
24864
|
+
* Today's id first. Then, for a rowid record, the pre-D694 spelling — which
|
|
24865
|
+
* is accepted ONLY when the row found speaks for the SAME record (camera and
|
|
24866
|
+
* record timestamp too). A legacy row of a different record that reused the
|
|
24867
|
+
* rowid is exactly the collision D694 removes, and must not swallow this one.
|
|
24868
|
+
*/
|
|
24869
|
+
async alreadyStored(id, input) {
|
|
24870
|
+
if (await this.rowExists(id)) return true;
|
|
24871
|
+
const legacy = legacyOutboxEntryId(input);
|
|
24872
|
+
if (legacy === null) return false;
|
|
24873
|
+
const row = await this.readRow(legacy);
|
|
24874
|
+
if (row === null) return false;
|
|
24875
|
+
if (!hasRecordTimestamp(row.payload)) {
|
|
24876
|
+
this.logger.warn("outbox: a pre-D694 row carries no record timestamp — not used for dedup", {
|
|
24877
|
+
tags: { deviceId: row.deviceId },
|
|
24878
|
+
meta: {
|
|
24879
|
+
id,
|
|
24880
|
+
legacyId: legacy,
|
|
24881
|
+
ruleId: input.ruleId
|
|
24882
|
+
}
|
|
24883
|
+
});
|
|
24884
|
+
return false;
|
|
24885
|
+
}
|
|
24886
|
+
if (isSameOutboxRecord(outboxRecordIdentity(row), outboxRecordIdentity(input))) return true;
|
|
24887
|
+
this.logger.info("outbox: a pre-D694 row shares this rowid but is a different record — not a duplicate", {
|
|
24888
|
+
tags: { deviceId: input.deviceId },
|
|
24889
|
+
meta: {
|
|
24890
|
+
id,
|
|
24891
|
+
legacyId: legacy,
|
|
24892
|
+
legacyDeviceId: row.deviceId,
|
|
24893
|
+
ruleId: input.ruleId
|
|
24894
|
+
}
|
|
24895
|
+
});
|
|
24896
|
+
return false;
|
|
24897
|
+
}
|
|
24898
|
+
/**
|
|
24899
|
+
* The id a stored pre-D694 row would have today, or `null`.
|
|
24900
|
+
*
|
|
24901
|
+
* Guarded per row: a stored payload is validated only as an object
|
|
24902
|
+
* (`rowToEntry`), and a row with no `subject.timestamp` would otherwise throw
|
|
24903
|
+
* inside {@link load} and leave the WHOLE dedup horizon cold. Such a row is
|
|
24904
|
+
* still loaded under its stored id; it just gets no translated twin.
|
|
24905
|
+
*/
|
|
24906
|
+
translateLegacy(storedId, entry) {
|
|
24907
|
+
if (legacyOutboxEntryId(entry) !== storedId) return null;
|
|
24908
|
+
if (!hasRecordTimestamp(entry.payload)) {
|
|
24909
|
+
this.logger.warn("outbox: a pre-D694 row carries no record timestamp — loaded without translation", {
|
|
24910
|
+
tags: { deviceId: entry.deviceId },
|
|
24911
|
+
meta: {
|
|
24912
|
+
id: storedId,
|
|
24913
|
+
ruleId: entry.ruleId
|
|
24914
|
+
}
|
|
24915
|
+
});
|
|
24916
|
+
return null;
|
|
24917
|
+
}
|
|
24918
|
+
return translatedLegacyEntryId(storedId, entry);
|
|
24919
|
+
}
|
|
24920
|
+
/** One stored row by id, or `null` (absent, unreadable, or a failed read). */
|
|
24921
|
+
async readRow(id) {
|
|
24922
|
+
try {
|
|
24923
|
+
const row = await this.store.get.query({
|
|
24924
|
+
collection: NC_OUTBOX_COLLECTION,
|
|
24925
|
+
key: id
|
|
24926
|
+
});
|
|
24927
|
+
return isStoredRow(row) ? rowToEntry$1(id, row) : null;
|
|
24928
|
+
} catch (err) {
|
|
24929
|
+
this.logger.debug("outbox cold-horizon dedup read failed", { meta: {
|
|
24930
|
+
id,
|
|
24931
|
+
error: String(err)
|
|
24932
|
+
} });
|
|
24933
|
+
return null;
|
|
24934
|
+
}
|
|
24935
|
+
}
|
|
24936
|
+
/**
|
|
24752
24937
|
* Does a row already exist under this dedup id?
|
|
24753
24938
|
*
|
|
24754
24939
|
* Asked ONLY while {@link horizonLoaded} is false. {@link persist} writes
|
|
@@ -24842,6 +25027,10 @@ var OUTBOX_RECORD_KINDS = new Set([
|
|
|
24842
25027
|
function isOutboxRecordKind(x) {
|
|
24843
25028
|
return typeof x === "string" && OUTBOX_RECORD_KINDS.has(x);
|
|
24844
25029
|
}
|
|
25030
|
+
/** `settingsStore.get` answers `unknown` — a guard, never a cast. */
|
|
25031
|
+
function isStoredRow(x) {
|
|
25032
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
25033
|
+
}
|
|
24845
25034
|
/** A stored row back to an entry — a guard, never a cast. Exported for the
|
|
24846
25035
|
* specs that read the store a centre wrote to. */
|
|
24847
25036
|
function rowToEntry$1(id, data) {
|
|
@@ -33208,7 +33397,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
33208
33397
|
...deps.dispatcher,
|
|
33209
33398
|
logger: this.logger.child("dispatch"),
|
|
33210
33399
|
texts: this.texts,
|
|
33211
|
-
getMediaForOwner: async (ownerKind, ownerId) => {
|
|
33400
|
+
getMediaForOwner: async (ownerKind, ownerId, eventScope) => {
|
|
33212
33401
|
const held = isSyntheticId(ownerId) ? this.syntheticMedia.get(ownerId) : void 0;
|
|
33213
33402
|
if (held !== void 0) return held.files;
|
|
33214
33403
|
const shelved = isStillShelfId(ownerId) ? this.stills?.get(ownerId) : void 0;
|
|
@@ -33217,7 +33406,7 @@ var NotificationCenter = class NotificationCenter {
|
|
|
33217
33406
|
if (linked !== void 0) return linked;
|
|
33218
33407
|
const departed = ownerKind === "track" && this.departureFallbacks.has(ownerId);
|
|
33219
33408
|
const owner = departed ? await this.resolveDepartedTrack(ownerId) : ownerId;
|
|
33220
|
-
const files = await deps.dispatcher.getMediaForOwner(ownerKind, owner);
|
|
33409
|
+
const files = await deps.dispatcher.getMediaForOwner(ownerKind, owner, eventScope);
|
|
33221
33410
|
if (files.length > 0 || !departed) return files;
|
|
33222
33411
|
return this.departureFallbackMedia(ownerId) ?? files;
|
|
33223
33412
|
},
|
|
@@ -35364,6 +35553,8 @@ var NotificationCenter = class NotificationCenter {
|
|
|
35364
35553
|
const dropped = await this.outbox.supersede({
|
|
35365
35554
|
ruleId: source.ruleId,
|
|
35366
35555
|
recordId: source.recordId,
|
|
35556
|
+
deviceId: source.deviceId,
|
|
35557
|
+
recordAt: source.timestamp,
|
|
35367
35558
|
targetIds: [...combined]
|
|
35368
35559
|
});
|
|
35369
35560
|
this.logger.info("alarm notification combined with the rule that triggered it", {
|
|
@@ -96033,6 +96224,30 @@ async function encodeClipQuery(encode, text, modelId, warn) {
|
|
|
96033
96224
|
}
|
|
96034
96225
|
}
|
|
96035
96226
|
//#endregion
|
|
96227
|
+
//#region src/pipeline-analytics/nc-owner-media-reader.ts
|
|
96228
|
+
/** Keep only the rows written for THIS generation of the event id. */
|
|
96229
|
+
function ofGeneration(files, scope) {
|
|
96230
|
+
return files.filter((f) => f.timestamp === scope.recordAt);
|
|
96231
|
+
}
|
|
96232
|
+
function makeNcOwnerMediaReader(deps) {
|
|
96233
|
+
return async (ownerKind, ownerId, eventScope) => {
|
|
96234
|
+
if (ownerKind === "track") return deps.media.listByOwner("track", await deps.resolveTrackOwnerId(ownerId));
|
|
96235
|
+
if (eventScope === void 0) return deps.media.listByOwner("object", ownerId);
|
|
96236
|
+
const onCamera = await deps.media.listByOwner("object", ownerId, eventScope.deviceId);
|
|
96237
|
+
const kept = ofGeneration(onCamera, eventScope);
|
|
96238
|
+
if (kept.length < onCamera.length) deps.logger.warn("event media of another record on a reused event id — not used", {
|
|
96239
|
+
tags: { deviceId: eventScope.deviceId },
|
|
96240
|
+
meta: {
|
|
96241
|
+
ownerId,
|
|
96242
|
+
recordAt: eventScope.recordAt,
|
|
96243
|
+
refused: onCamera.length - kept.length,
|
|
96244
|
+
kept: kept.length
|
|
96245
|
+
}
|
|
96246
|
+
});
|
|
96247
|
+
return kept;
|
|
96248
|
+
};
|
|
96249
|
+
}
|
|
96250
|
+
//#endregion
|
|
96036
96251
|
//#region src/pipeline-analytics/store/settings-raw-mirror.ts
|
|
96037
96252
|
/** How long a mirrored entry is served before a refresh is started behind the
|
|
96038
96253
|
* next reader. See the module doc for why this matches the per-kind caches'
|
|
@@ -100357,7 +100572,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
|
|
|
100357
100572
|
}
|
|
100358
100573
|
});
|
|
100359
100574
|
},
|
|
100360
|
-
getMediaForOwner:
|
|
100575
|
+
getMediaForOwner: makeNcOwnerMediaReader({
|
|
100576
|
+
media: stores.mediaStore,
|
|
100577
|
+
resolveTrackOwnerId: (trackId) => resolveMediaOwnerId("track", trackId, (id) => this.trackAliasStore?.resolve(id) ?? Promise.resolve(null), (failure) => this.aliasResolveFailureLogger?.(failure), "getMediaForOwner"),
|
|
100578
|
+
logger: this.ctx.logger.child("nc-media")
|
|
100579
|
+
}),
|
|
100361
100580
|
getSubjectTrail: async (deviceId, trackId) => {
|
|
100362
100581
|
const dims = this.lastFrameDimsByDevice.get(deviceId);
|
|
100363
100582
|
if (dims === void 0 || dims.w <= 0 || dims.h <= 0) return [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-post-analysis",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.307",
|
|
4
4
|
"description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|