@camstack/addon-post-analysis 1.2.9 → 1.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{dist-Bezh0l-m.js → dist-B5fLGGEF.js} +7566 -7398
- package/dist/{dist-Dm1I_IVp.mjs → dist-EaH6ywaC.mjs} +7566 -7398
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/{node-DsYWuKgE.js → node-PvutBRd4.js} +1 -1
- package/dist/pipeline-analytics/_stub.js +1 -1
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BfZn4SxE.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CziJ_lMq.mjs} +2 -2
- package/dist/pipeline-analytics/{_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CbYZbG3H.mjs → _virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DCdWQhcm.mjs} +1 -1
- package/dist/pipeline-analytics/{hostInit-PN20an9o.mjs → hostInit-DQHgLTeH.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +635 -605
- package/dist/pipeline-analytics/index.mjs +634 -604
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_dist = require("../dist-
|
|
5
|
+
const require_dist = require("../dist-B5fLGGEF.js");
|
|
6
6
|
let node_crypto = require("node:crypto");
|
|
7
7
|
let sharp = require("sharp");
|
|
8
8
|
sharp = require_dist.__toESM(sharp);
|
|
@@ -4347,17 +4347,25 @@ var NcDispatcher = class {
|
|
|
4347
4347
|
};
|
|
4348
4348
|
function buildTemplateVars(entry, deviceName) {
|
|
4349
4349
|
const subject = entry.payload.subject;
|
|
4350
|
+
const occupancy = subject.occupancy;
|
|
4350
4351
|
return {
|
|
4351
4352
|
camera: deviceName,
|
|
4352
4353
|
class: subject.className,
|
|
4353
4354
|
label: subject.label ?? "",
|
|
4354
4355
|
zones: subject.zones.join(", "),
|
|
4355
|
-
zone: subject.zones[0] ?? "",
|
|
4356
|
+
zone: occupancy?.zone ?? subject.zones[0] ?? "",
|
|
4356
4357
|
confidence: subject.confidence !== void 0 ? `${Math.round(subject.confidence * 100)}%` : "",
|
|
4357
4358
|
time: new Date(subject.timestamp).toLocaleTimeString(),
|
|
4358
|
-
rule: entry.payload.ruleName
|
|
4359
|
+
rule: entry.payload.ruleName,
|
|
4360
|
+
count: occupancy !== void 0 ? `${occupancy.count}` : "",
|
|
4361
|
+
capacity: occupancy !== void 0 ? `${occupancy.capacity}` : "",
|
|
4362
|
+
op: occupancy !== void 0 ? occupancyOpWord(occupancy.occupied) : ""
|
|
4359
4363
|
};
|
|
4360
4364
|
}
|
|
4365
|
+
/** The human-readable edge polarity used by `{{op}}` and the default body. */
|
|
4366
|
+
function occupancyOpWord(occupied) {
|
|
4367
|
+
return occupied ? "occupied" : "free";
|
|
4368
|
+
}
|
|
4361
4369
|
/** `{{var}}` interpolation; missing vars render empty. Null template → null. */
|
|
4362
4370
|
function renderTemplate(template, vars) {
|
|
4363
4371
|
if (template === void 0 || template.trim().length === 0) return null;
|
|
@@ -4365,6 +4373,8 @@ function renderTemplate(template, vars) {
|
|
|
4365
4373
|
}
|
|
4366
4374
|
function defaultBody(entry, deviceName) {
|
|
4367
4375
|
const subject = entry.payload.subject;
|
|
4376
|
+
const occupancy = subject.occupancy;
|
|
4377
|
+
if (occupancy !== void 0) return `${occupancy.zone ?? deviceName} ${occupancyOpWord(occupancy.occupied)} (${occupancy.count}/${occupancy.capacity})`;
|
|
4368
4378
|
const label = subject.label !== void 0 ? ` (${subject.label})` : "";
|
|
4369
4379
|
const zones = subject.zones.length > 0 ? ` in ${subject.zones.join(", ")}` : "";
|
|
4370
4380
|
const suffix = entry.recordKind === "track-end" ? " — visit ended" : "";
|
|
@@ -4387,63 +4397,236 @@ function clampPriority(priority) {
|
|
|
4387
4397
|
return Math.max(1, Math.min(5, Math.round(priority)));
|
|
4388
4398
|
}
|
|
4389
4399
|
//#endregion
|
|
4390
|
-
//#region src/notification-center/
|
|
4391
|
-
|
|
4392
|
-
var
|
|
4393
|
-
var
|
|
4394
|
-
|
|
4400
|
+
//#region src/notification-center/occupancy-watcher.ts
|
|
4401
|
+
/** Sentinel key segments for the "no zone" (whole-frame) and "no class" scopes. */
|
|
4402
|
+
var FRAME_SCOPE = "@frame";
|
|
4403
|
+
var ALL_CLASSES = "@all";
|
|
4404
|
+
/** Prefix marking the trailing threshold segment (`t<n>`) — makes the grammar
|
|
4405
|
+
* self-describing so a legacy three-segment key can never be mis-parsed as a
|
|
4406
|
+
* four-segment one (the last segment of a legacy key is a className, never a
|
|
4407
|
+
* `t<digits>` token). */
|
|
4408
|
+
var THRESHOLD_PREFIX = "t";
|
|
4409
|
+
var THRESHOLD_SEGMENT = /^t(\d+)$/;
|
|
4410
|
+
function occupancyKey(deviceId, zoneId, className, threshold) {
|
|
4411
|
+
return `${deviceId}|${zoneId ?? FRAME_SCOPE}|${className ?? ALL_CLASSES}|${THRESHOLD_PREFIX}${threshold}`;
|
|
4412
|
+
}
|
|
4413
|
+
/**
|
|
4414
|
+
* Inverse of {@link occupancyKey} — the ONE place the key grammar is decoded, so
|
|
4415
|
+
* the durable store (whose column schema drops the derivable
|
|
4416
|
+
* `zoneId`/`className`/`threshold`) can reconstruct the full scope on reseed.
|
|
4417
|
+
*
|
|
4418
|
+
* The four-segment grammar is asserted EXACTLY: at least four segments AND a
|
|
4419
|
+
* trailing `t<digits>` threshold marker. A legacy three-segment key (no marker)
|
|
4420
|
+
* is REJECTED (`null`) rather than positionally mis-parsed — the durable store
|
|
4421
|
+
* then SKIPS that row, so a pre-amendment persisted row cold-re-observes instead
|
|
4422
|
+
* of hydrating a corrupt scope (className←threshold, zoneId←class). `deviceId`
|
|
4423
|
+
* is the leading numeric segment, `threshold` the trailing `t<n>`, `className`
|
|
4424
|
+
* the segment before it, and the (possibly `|`-containing) `zoneId` everything
|
|
4425
|
+
* between. Exact per-segment count is impossible because a `zoneId` may itself
|
|
4426
|
+
* contain `|`; the trailing marker is the unambiguous grammar discriminator.
|
|
4427
|
+
*/
|
|
4428
|
+
function parseOccupancyKey(key) {
|
|
4429
|
+
const parts = key.split("|");
|
|
4430
|
+
if (parts.length < 4) return null;
|
|
4431
|
+
const deviceId = Number(parts[0]);
|
|
4432
|
+
if (!Number.isFinite(deviceId)) return null;
|
|
4433
|
+
const thresholdMatch = THRESHOLD_SEGMENT.exec(parts[parts.length - 1] ?? "");
|
|
4434
|
+
if (thresholdMatch === null) return null;
|
|
4435
|
+
const threshold = Number(thresholdMatch[1]);
|
|
4436
|
+
const className = parts[parts.length - 2] ?? ALL_CLASSES;
|
|
4437
|
+
const zoneId = parts.slice(1, -2).join("|");
|
|
4438
|
+
return {
|
|
4439
|
+
deviceId,
|
|
4440
|
+
...zoneId !== FRAME_SCOPE ? { zoneId } : {},
|
|
4441
|
+
...className !== ALL_CLASSES ? { className } : {},
|
|
4442
|
+
threshold
|
|
4443
|
+
};
|
|
4444
|
+
}
|
|
4445
|
+
/** Device-agnostic partial key (`zone|class|t<threshold>`) — the merge/watch
|
|
4446
|
+
* granularity. Distinct thresholds are distinct partial keys (per-threshold). */
|
|
4447
|
+
function partialKey(zoneId, className, threshold) {
|
|
4448
|
+
return `${zoneId ?? FRAME_SCOPE}|${className ?? ALL_CLASSES}|${THRESHOLD_PREFIX}${threshold}`;
|
|
4449
|
+
}
|
|
4450
|
+
/** The device-agnostic partial key of a full state/edge key — everything after
|
|
4451
|
+
* the leading `deviceId|` segment. */
|
|
4452
|
+
function partialKeyOf(key) {
|
|
4453
|
+
const idx = key.indexOf("|");
|
|
4454
|
+
return idx < 0 ? key : key.slice(idx + 1);
|
|
4455
|
+
}
|
|
4456
|
+
var OccupancyWatcher = class {
|
|
4457
|
+
/** Watched specs, keyed by their device-agnostic partial key. */
|
|
4458
|
+
watched = /* @__PURE__ */ new Map();
|
|
4459
|
+
/** Confirmed + pending state, keyed by the full {@link OccupancyKey}. */
|
|
4460
|
+
states = /* @__PURE__ */ new Map();
|
|
4461
|
+
/**
|
|
4462
|
+
* Replace the watched key set (rule-driven — recomputed on rule change).
|
|
4463
|
+
* Each distinct `(zone, class, threshold)` is its OWN watched partial key
|
|
4464
|
+
* (per-threshold edges — NO min-threshold merge). Specs that collide on the
|
|
4465
|
+
* SAME `(zone, class, threshold)` merge to the MAX sustain (longest debounce),
|
|
4466
|
+
* so one watcher serves those co-threshold rules. Confirmed state for a key
|
|
4467
|
+
* that is no longer watched is DROPPED (bounds the RAM map + lets the caller
|
|
4468
|
+
* prune the durable row to the active set); a still-watched key retains its
|
|
4469
|
+
* level (re-evaluated on the next `observe`).
|
|
4470
|
+
*/
|
|
4471
|
+
setWatchedKeys(specs) {
|
|
4472
|
+
this.watched.clear();
|
|
4473
|
+
for (const spec of specs) {
|
|
4474
|
+
const pk = partialKey(spec.zoneId, spec.className, spec.threshold);
|
|
4475
|
+
const existing = this.watched.get(pk);
|
|
4476
|
+
if (existing === void 0) {
|
|
4477
|
+
this.watched.set(pk, {
|
|
4478
|
+
...spec.zoneId !== void 0 ? { zoneId: spec.zoneId } : {},
|
|
4479
|
+
...spec.className !== void 0 ? { className: spec.className } : {},
|
|
4480
|
+
threshold: spec.threshold,
|
|
4481
|
+
sustainSeconds: spec.sustainSeconds
|
|
4482
|
+
});
|
|
4483
|
+
continue;
|
|
4484
|
+
}
|
|
4485
|
+
this.watched.set(pk, {
|
|
4486
|
+
...existing,
|
|
4487
|
+
sustainSeconds: Math.max(existing.sustainSeconds, spec.sustainSeconds)
|
|
4488
|
+
});
|
|
4489
|
+
}
|
|
4490
|
+
for (const key of [...this.states.keys()]) if (!this.watched.has(partialKeyOf(key))) this.states.delete(key);
|
|
4491
|
+
}
|
|
4492
|
+
/**
|
|
4493
|
+
* Feed one camera snapshot at time `now`, returning the edges that COMMIT on
|
|
4494
|
+
* this tick (usually none). Every watched key is evaluated for `deviceId`;
|
|
4495
|
+
* fail-closed keys (absent zone) are skipped and hold no state.
|
|
4496
|
+
*/
|
|
4497
|
+
observe(deviceId, snapshot, now) {
|
|
4498
|
+
const edges = [];
|
|
4499
|
+
for (const spec of this.watched.values()) {
|
|
4500
|
+
const resolved = resolveScope(snapshot, spec);
|
|
4501
|
+
if (resolved === null) continue;
|
|
4502
|
+
const key = occupancyKey(deviceId, spec.zoneId, spec.className, spec.threshold);
|
|
4503
|
+
const edge = step(this.stateFor(key, deviceId, spec, now), spec, resolved, now);
|
|
4504
|
+
if (edge !== null) edges.push(edge);
|
|
4505
|
+
}
|
|
4506
|
+
return edges;
|
|
4507
|
+
}
|
|
4508
|
+
/** Reseed confirmed state from durable rows (boot). Only rows whose key is
|
|
4509
|
+
* currently WATCHED are restored — an orphaned durable row (its rule gone)
|
|
4510
|
+
* is dropped, keeping the RAM map bounded to the active set. Pending edges
|
|
4511
|
+
* are not restored (fail-closed — they re-open on the next snapshots). */
|
|
4512
|
+
hydrate(rows) {
|
|
4513
|
+
for (const row of rows) {
|
|
4514
|
+
if (!this.watched.has(partialKeyOf(row.key))) continue;
|
|
4515
|
+
this.states.set(row.key, {
|
|
4516
|
+
deviceId: row.deviceId,
|
|
4517
|
+
...row.zoneId !== void 0 ? { zoneId: row.zoneId } : {},
|
|
4518
|
+
...row.className !== void 0 ? { className: row.className } : {},
|
|
4519
|
+
threshold: row.threshold,
|
|
4520
|
+
confirmedCount: row.confirmedCount,
|
|
4521
|
+
occupied: row.occupied,
|
|
4522
|
+
lastChangeAt: row.lastChangeAt
|
|
4523
|
+
});
|
|
4524
|
+
}
|
|
4525
|
+
}
|
|
4526
|
+
/** Snapshot the CONFIRMED state for durable persistence (pending excluded). */
|
|
4527
|
+
snapshotState() {
|
|
4528
|
+
const rows = [];
|
|
4529
|
+
for (const [key, state] of this.states) rows.push({
|
|
4530
|
+
key,
|
|
4531
|
+
deviceId: state.deviceId,
|
|
4532
|
+
...state.zoneId !== void 0 ? { zoneId: state.zoneId } : {},
|
|
4533
|
+
...state.className !== void 0 ? { className: state.className } : {},
|
|
4534
|
+
threshold: state.threshold,
|
|
4535
|
+
confirmedCount: state.confirmedCount,
|
|
4536
|
+
occupied: state.occupied,
|
|
4537
|
+
lastChangeAt: state.lastChangeAt,
|
|
4538
|
+
updatedAt: state.lastChangeAt
|
|
4539
|
+
});
|
|
4540
|
+
return rows;
|
|
4541
|
+
}
|
|
4542
|
+
stateFor(key, deviceId, spec, now) {
|
|
4543
|
+
const existing = this.states.get(key);
|
|
4544
|
+
if (existing !== void 0) return existing;
|
|
4545
|
+
const fresh = {
|
|
4546
|
+
deviceId,
|
|
4547
|
+
...spec.zoneId !== void 0 ? { zoneId: spec.zoneId } : {},
|
|
4548
|
+
...spec.className !== void 0 ? { className: spec.className } : {},
|
|
4549
|
+
threshold: spec.threshold,
|
|
4550
|
+
confirmedCount: 0,
|
|
4551
|
+
occupied: false,
|
|
4552
|
+
lastChangeAt: now
|
|
4553
|
+
};
|
|
4554
|
+
this.states.set(key, fresh);
|
|
4555
|
+
return fresh;
|
|
4556
|
+
}
|
|
4557
|
+
};
|
|
4558
|
+
/** Read the count (+ zone name) for a spec, or `null` when fail-closed. */
|
|
4559
|
+
function resolveScope(snapshot, spec) {
|
|
4560
|
+
if (spec.zoneId === void 0) return { count: spec.className === void 0 ? snapshot.frame.totalObjects : snapshot.frame.byClass[spec.className] ?? 0 };
|
|
4561
|
+
const zone = snapshot.zones.find((z) => z.zoneId === spec.zoneId);
|
|
4562
|
+
if (zone === void 0) return null;
|
|
4563
|
+
return {
|
|
4564
|
+
count: spec.className === void 0 ? zone.totalObjects : zone.byClass[spec.className] ?? 0,
|
|
4565
|
+
zoneName: zone.zoneName
|
|
4566
|
+
};
|
|
4567
|
+
}
|
|
4568
|
+
/**
|
|
4569
|
+
* Advance one key's state by one observation. Mutates `state` in place (the
|
|
4570
|
+
* watcher owns it) and returns a committed edge, or `null`.
|
|
4571
|
+
*/
|
|
4572
|
+
function step(state, spec, resolved, now) {
|
|
4573
|
+
const rawOccupied = resolved.count >= spec.threshold;
|
|
4574
|
+
const sustainMs = spec.sustainSeconds * 1e3;
|
|
4575
|
+
if (rawOccupied === state.occupied) {
|
|
4576
|
+
state.pendingTargetOccupied = void 0;
|
|
4577
|
+
state.pendingSince = void 0;
|
|
4578
|
+
return null;
|
|
4579
|
+
}
|
|
4580
|
+
if (state.pendingTargetOccupied !== rawOccupied) {
|
|
4581
|
+
state.pendingTargetOccupied = rawOccupied;
|
|
4582
|
+
state.pendingSince = now;
|
|
4583
|
+
}
|
|
4584
|
+
if (now - (state.pendingSince ?? now) < sustainMs) return null;
|
|
4585
|
+
const previousCount = state.confirmedCount;
|
|
4586
|
+
state.confirmedCount = resolved.count;
|
|
4587
|
+
state.occupied = rawOccupied;
|
|
4588
|
+
state.lastChangeAt = now;
|
|
4589
|
+
state.pendingTargetOccupied = void 0;
|
|
4590
|
+
state.pendingSince = void 0;
|
|
4591
|
+
return {
|
|
4592
|
+
deviceId: state.deviceId,
|
|
4593
|
+
...spec.zoneId !== void 0 ? { zoneId: spec.zoneId } : {},
|
|
4594
|
+
...resolved.zoneName !== void 0 ? { zoneName: resolved.zoneName } : {},
|
|
4595
|
+
...spec.className !== void 0 ? { className: spec.className } : {},
|
|
4596
|
+
count: resolved.count,
|
|
4597
|
+
previousCount,
|
|
4598
|
+
occupied: rawOccupied,
|
|
4599
|
+
threshold: spec.threshold,
|
|
4600
|
+
timestamp: now
|
|
4601
|
+
};
|
|
4602
|
+
}
|
|
4603
|
+
//#endregion
|
|
4604
|
+
//#region src/notification-center/occupancy-store.ts
|
|
4605
|
+
var NC_OCCUPANCY_COLLECTION = "notification-center:occupancy";
|
|
4606
|
+
var NC_OCCUPANCY_COLUMNS = [
|
|
4395
4607
|
{
|
|
4396
|
-
name: "
|
|
4608
|
+
name: "key",
|
|
4397
4609
|
type: "TEXT",
|
|
4398
4610
|
primaryKey: true,
|
|
4399
4611
|
notNull: true
|
|
4400
4612
|
},
|
|
4401
|
-
{
|
|
4402
|
-
name: "ruleId",
|
|
4403
|
-
type: "TEXT",
|
|
4404
|
-
notNull: true
|
|
4405
|
-
},
|
|
4406
|
-
{
|
|
4407
|
-
name: "targetId",
|
|
4408
|
-
type: "TEXT",
|
|
4409
|
-
notNull: true
|
|
4410
|
-
},
|
|
4411
4613
|
{
|
|
4412
4614
|
name: "deviceId",
|
|
4413
4615
|
type: "INTEGER",
|
|
4414
4616
|
notNull: true
|
|
4415
4617
|
},
|
|
4416
4618
|
{
|
|
4417
|
-
name: "
|
|
4418
|
-
type: "TEXT",
|
|
4419
|
-
notNull: true
|
|
4420
|
-
},
|
|
4421
|
-
{
|
|
4422
|
-
name: "recordId",
|
|
4423
|
-
type: "TEXT",
|
|
4424
|
-
notNull: true
|
|
4425
|
-
},
|
|
4426
|
-
{
|
|
4427
|
-
name: "trackId",
|
|
4428
|
-
type: "TEXT"
|
|
4429
|
-
},
|
|
4430
|
-
{
|
|
4431
|
-
name: "status",
|
|
4432
|
-
type: "TEXT",
|
|
4433
|
-
notNull: true
|
|
4434
|
-
},
|
|
4435
|
-
{
|
|
4436
|
-
name: "attempts",
|
|
4619
|
+
name: "confirmedCount",
|
|
4437
4620
|
type: "INTEGER",
|
|
4438
4621
|
notNull: true
|
|
4439
4622
|
},
|
|
4440
4623
|
{
|
|
4441
|
-
name: "
|
|
4442
|
-
type: "
|
|
4624
|
+
name: "occupied",
|
|
4625
|
+
type: "BOOLEAN",
|
|
4443
4626
|
notNull: true
|
|
4444
4627
|
},
|
|
4445
4628
|
{
|
|
4446
|
-
name: "
|
|
4629
|
+
name: "lastChangeAt",
|
|
4447
4630
|
type: "INTEGER",
|
|
4448
4631
|
notNull: true
|
|
4449
4632
|
},
|
|
@@ -4451,10 +4634,216 @@ var NC_OUTBOX_COLUMNS = [
|
|
|
4451
4634
|
name: "updatedAt",
|
|
4452
4635
|
type: "INTEGER",
|
|
4453
4636
|
notNull: true
|
|
4454
|
-
}
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4637
|
+
}
|
|
4638
|
+
];
|
|
4639
|
+
var NC_OCCUPANCY_INDEXES = [{
|
|
4640
|
+
name: "idx_nc_occupancy_device",
|
|
4641
|
+
columns: ["deviceId"]
|
|
4642
|
+
}];
|
|
4643
|
+
/** Query cap — a per-(device, zone, class) key set is small; this is a
|
|
4644
|
+
* generous ceiling that still bounds a pathological read. */
|
|
4645
|
+
var LOAD_LIMIT = 1e5;
|
|
4646
|
+
var OccupancyStore = class {
|
|
4647
|
+
cache = /* @__PURE__ */ new Map();
|
|
4648
|
+
store;
|
|
4649
|
+
logger;
|
|
4650
|
+
constructor(deps) {
|
|
4651
|
+
this.store = deps.store;
|
|
4652
|
+
this.logger = deps.logger;
|
|
4653
|
+
}
|
|
4654
|
+
static async declare(store) {
|
|
4655
|
+
await store.declareCollection.mutate({
|
|
4656
|
+
collection: NC_OCCUPANCY_COLLECTION,
|
|
4657
|
+
columns: [...NC_OCCUPANCY_COLUMNS],
|
|
4658
|
+
indexes: [...NC_OCCUPANCY_INDEXES]
|
|
4659
|
+
});
|
|
4660
|
+
}
|
|
4661
|
+
/**
|
|
4662
|
+
* Reseed the confirmed edge-state from the store (boot) — replaces the cache
|
|
4663
|
+
* wholesale and returns the rows for {@link OccupancyWatcher.hydrate}. A row
|
|
4664
|
+
* whose scalars/key no longer parse is skipped with a warning (a degraded row
|
|
4665
|
+
* must never crash the reseed). Best-effort: a store error yields `[]` and a
|
|
4666
|
+
* cold watcher, never a throw into boot.
|
|
4667
|
+
*/
|
|
4668
|
+
async load() {
|
|
4669
|
+
try {
|
|
4670
|
+
const records = await this.store.query.query({
|
|
4671
|
+
collection: NC_OCCUPANCY_COLLECTION,
|
|
4672
|
+
filter: { limit: LOAD_LIMIT }
|
|
4673
|
+
});
|
|
4674
|
+
this.cache.clear();
|
|
4675
|
+
let skipped = 0;
|
|
4676
|
+
for (const record of records) {
|
|
4677
|
+
const row = recordToRow(record.id, record.data);
|
|
4678
|
+
if (row === null) {
|
|
4679
|
+
skipped += 1;
|
|
4680
|
+
continue;
|
|
4681
|
+
}
|
|
4682
|
+
this.cache.set(row.key, row);
|
|
4683
|
+
}
|
|
4684
|
+
this.logger.debug("occupancy state loaded", { meta: {
|
|
4685
|
+
keys: this.cache.size,
|
|
4686
|
+
...skipped > 0 ? { skippedInvalid: skipped } : {}
|
|
4687
|
+
} });
|
|
4688
|
+
return [...this.cache.values()];
|
|
4689
|
+
} catch (err) {
|
|
4690
|
+
this.logger.warn("occupancy state load failed", { meta: { error: String(err) } });
|
|
4691
|
+
return [];
|
|
4692
|
+
}
|
|
4693
|
+
}
|
|
4694
|
+
/** The in-RAM confirmed-state mirror (post-{@link load}/{@link persist}). */
|
|
4695
|
+
snapshot() {
|
|
4696
|
+
return [...this.cache.values()];
|
|
4697
|
+
}
|
|
4698
|
+
/**
|
|
4699
|
+
* Durably upsert one confirmed edge-state row (write-through: the store FIRST,
|
|
4700
|
+
* then the cache — a failed persist never leaves a phantom in-RAM level). The
|
|
4701
|
+
* `key` is the PK, so re-persisting a key advances it in place.
|
|
4702
|
+
*/
|
|
4703
|
+
async persist(row) {
|
|
4704
|
+
await this.store.set.mutate({
|
|
4705
|
+
collection: NC_OCCUPANCY_COLLECTION,
|
|
4706
|
+
key: row.key,
|
|
4707
|
+
value: rowToValue(row)
|
|
4708
|
+
});
|
|
4709
|
+
this.cache.set(row.key, row);
|
|
4710
|
+
}
|
|
4711
|
+
/**
|
|
4712
|
+
* Prune every persisted key NOT in `activeKeys` (the currently watched set) —
|
|
4713
|
+
* the bounded-row-count guarantee when rules stop watching a key. Returns the
|
|
4714
|
+
* number of rows dropped. Best-effort per row: a failed delete is logged and
|
|
4715
|
+
* the key retained (retried next prune) rather than aborting the sweep.
|
|
4716
|
+
*/
|
|
4717
|
+
async pruneExcept(activeKeys) {
|
|
4718
|
+
let pruned = 0;
|
|
4719
|
+
for (const key of [...this.cache.keys()]) {
|
|
4720
|
+
if (activeKeys.has(key)) continue;
|
|
4721
|
+
try {
|
|
4722
|
+
await this.store.delete.mutate({
|
|
4723
|
+
collection: NC_OCCUPANCY_COLLECTION,
|
|
4724
|
+
key
|
|
4725
|
+
});
|
|
4726
|
+
this.cache.delete(key);
|
|
4727
|
+
pruned += 1;
|
|
4728
|
+
} catch (err) {
|
|
4729
|
+
this.logger.debug("occupancy prune delete failed", { meta: {
|
|
4730
|
+
key,
|
|
4731
|
+
error: String(err)
|
|
4732
|
+
} });
|
|
4733
|
+
}
|
|
4734
|
+
}
|
|
4735
|
+
return pruned;
|
|
4736
|
+
}
|
|
4737
|
+
};
|
|
4738
|
+
/** The persisted column map for a row (the `key` PK is passed separately). */
|
|
4739
|
+
function rowToValue(row) {
|
|
4740
|
+
return {
|
|
4741
|
+
deviceId: row.deviceId,
|
|
4742
|
+
confirmedCount: row.confirmedCount,
|
|
4743
|
+
occupied: row.occupied,
|
|
4744
|
+
lastChangeAt: row.lastChangeAt,
|
|
4745
|
+
updatedAt: row.updatedAt
|
|
4746
|
+
};
|
|
4747
|
+
}
|
|
4748
|
+
/**
|
|
4749
|
+
* Structurally validate a persisted record and reconstruct the full
|
|
4750
|
+
* {@link OccupancyStateRow} (deriving `zoneId`/`className`/`threshold` from the
|
|
4751
|
+
* key). Returns `null` for any malformed row — including a legacy
|
|
4752
|
+
* three-segment key with no `t<n>` threshold marker, which
|
|
4753
|
+
* {@link parseOccupancyKey} rejects — so the caller skips it (cold re-observe)
|
|
4754
|
+
* rather than hydrating a mis-parsed scope.
|
|
4755
|
+
*/
|
|
4756
|
+
function recordToRow(key, data) {
|
|
4757
|
+
const scope = parseOccupancyKey(key);
|
|
4758
|
+
if (scope === null) return null;
|
|
4759
|
+
const deviceId = Number(data["deviceId"]);
|
|
4760
|
+
const confirmedCount = Number(data["confirmedCount"]);
|
|
4761
|
+
const lastChangeAt = Number(data["lastChangeAt"]);
|
|
4762
|
+
const updatedAt = Number(data["updatedAt"]);
|
|
4763
|
+
if (!Number.isFinite(deviceId) || !Number.isFinite(confirmedCount) || !Number.isFinite(lastChangeAt) || !Number.isFinite(updatedAt)) return null;
|
|
4764
|
+
const rawOccupied = data["occupied"];
|
|
4765
|
+
const occupied = rawOccupied === true || rawOccupied === 1;
|
|
4766
|
+
return {
|
|
4767
|
+
key,
|
|
4768
|
+
deviceId,
|
|
4769
|
+
...scope.zoneId !== void 0 ? { zoneId: scope.zoneId } : {},
|
|
4770
|
+
...scope.className !== void 0 ? { className: scope.className } : {},
|
|
4771
|
+
threshold: scope.threshold,
|
|
4772
|
+
confirmedCount,
|
|
4773
|
+
occupied,
|
|
4774
|
+
lastChangeAt,
|
|
4775
|
+
updatedAt
|
|
4776
|
+
};
|
|
4777
|
+
}
|
|
4778
|
+
//#endregion
|
|
4779
|
+
//#region src/notification-center/outbox.ts
|
|
4780
|
+
var NC_OUTBOX_COLLECTION = "notification-center:outbox";
|
|
4781
|
+
var NC_META_COLLECTION = "notification-center:meta";
|
|
4782
|
+
var NC_WATERMARK_KEY = "watermark";
|
|
4783
|
+
var NC_OUTBOX_COLUMNS = [
|
|
4784
|
+
{
|
|
4785
|
+
name: "id",
|
|
4786
|
+
type: "TEXT",
|
|
4787
|
+
primaryKey: true,
|
|
4788
|
+
notNull: true
|
|
4789
|
+
},
|
|
4790
|
+
{
|
|
4791
|
+
name: "ruleId",
|
|
4792
|
+
type: "TEXT",
|
|
4793
|
+
notNull: true
|
|
4794
|
+
},
|
|
4795
|
+
{
|
|
4796
|
+
name: "targetId",
|
|
4797
|
+
type: "TEXT",
|
|
4798
|
+
notNull: true
|
|
4799
|
+
},
|
|
4800
|
+
{
|
|
4801
|
+
name: "deviceId",
|
|
4802
|
+
type: "INTEGER",
|
|
4803
|
+
notNull: true
|
|
4804
|
+
},
|
|
4805
|
+
{
|
|
4806
|
+
name: "recordKind",
|
|
4807
|
+
type: "TEXT",
|
|
4808
|
+
notNull: true
|
|
4809
|
+
},
|
|
4810
|
+
{
|
|
4811
|
+
name: "recordId",
|
|
4812
|
+
type: "TEXT",
|
|
4813
|
+
notNull: true
|
|
4814
|
+
},
|
|
4815
|
+
{
|
|
4816
|
+
name: "trackId",
|
|
4817
|
+
type: "TEXT"
|
|
4818
|
+
},
|
|
4819
|
+
{
|
|
4820
|
+
name: "status",
|
|
4821
|
+
type: "TEXT",
|
|
4822
|
+
notNull: true
|
|
4823
|
+
},
|
|
4824
|
+
{
|
|
4825
|
+
name: "attempts",
|
|
4826
|
+
type: "INTEGER",
|
|
4827
|
+
notNull: true
|
|
4828
|
+
},
|
|
4829
|
+
{
|
|
4830
|
+
name: "nextAttemptAt",
|
|
4831
|
+
type: "INTEGER",
|
|
4832
|
+
notNull: true
|
|
4833
|
+
},
|
|
4834
|
+
{
|
|
4835
|
+
name: "createdAt",
|
|
4836
|
+
type: "INTEGER",
|
|
4837
|
+
notNull: true
|
|
4838
|
+
},
|
|
4839
|
+
{
|
|
4840
|
+
name: "updatedAt",
|
|
4841
|
+
type: "INTEGER",
|
|
4842
|
+
notNull: true
|
|
4843
|
+
},
|
|
4844
|
+
{
|
|
4845
|
+
name: "lastError",
|
|
4846
|
+
type: "TEXT"
|
|
4458
4847
|
},
|
|
4459
4848
|
{
|
|
4460
4849
|
name: "payload",
|
|
@@ -4659,586 +5048,207 @@ var NcOutbox = class {
|
|
|
4659
5048
|
field: "createdAt",
|
|
4660
5049
|
direction: "desc"
|
|
4661
5050
|
},
|
|
4662
|
-
limit: query.limit
|
|
4663
|
-
}
|
|
4664
|
-
});
|
|
4665
|
-
const out = [];
|
|
4666
|
-
for (const row of rows) {
|
|
4667
|
-
const entry = rowToEntry$1(row.id, row.data);
|
|
4668
|
-
if (entry !== null) out.push(entry);
|
|
4669
|
-
}
|
|
4670
|
-
return out;
|
|
4671
|
-
} catch (err) {
|
|
4672
|
-
this.logger.debug("outbox history query failed", { meta: { error: String(err) } });
|
|
4673
|
-
return [];
|
|
4674
|
-
}
|
|
4675
|
-
}
|
|
4676
|
-
async getWatermark() {
|
|
4677
|
-
try {
|
|
4678
|
-
const row = await this.store.get.query({
|
|
4679
|
-
collection: NC_META_COLLECTION,
|
|
4680
|
-
key: NC_WATERMARK_KEY
|
|
4681
|
-
});
|
|
4682
|
-
if (row === null || row === void 0 || typeof row !== "object") return null;
|
|
4683
|
-
const value = row["value"];
|
|
4684
|
-
if (value === null || typeof value !== "object") return null;
|
|
4685
|
-
const ts = value["ts"];
|
|
4686
|
-
return typeof ts === "number" && Number.isFinite(ts) ? ts : null;
|
|
4687
|
-
} catch {
|
|
4688
|
-
return null;
|
|
4689
|
-
}
|
|
4690
|
-
}
|
|
4691
|
-
async setWatermark(ts) {
|
|
4692
|
-
try {
|
|
4693
|
-
await this.store.set.mutate({
|
|
4694
|
-
collection: NC_META_COLLECTION,
|
|
4695
|
-
key: NC_WATERMARK_KEY,
|
|
4696
|
-
value: { value: { ts } }
|
|
4697
|
-
});
|
|
4698
|
-
} catch (err) {
|
|
4699
|
-
this.logger.debug("outbox watermark write failed", { meta: { error: String(err) } });
|
|
4700
|
-
}
|
|
4701
|
-
}
|
|
4702
|
-
/** Drop terminal rows older than `cutoffMs` (retention hygiene). */
|
|
4703
|
-
async pruneBefore(cutoffMs) {
|
|
4704
|
-
let pruned = 0;
|
|
4705
|
-
try {
|
|
4706
|
-
const rows = await this.store.query.query({
|
|
4707
|
-
collection: NC_OUTBOX_COLLECTION,
|
|
4708
|
-
filter: {
|
|
4709
|
-
whereBetween: { createdAt: [0, cutoffMs] },
|
|
4710
|
-
limit: 5e3
|
|
4711
|
-
}
|
|
4712
|
-
});
|
|
4713
|
-
for (const row of rows) {
|
|
4714
|
-
const status = row.data["status"];
|
|
4715
|
-
if (status !== "sent" && status !== "dead") continue;
|
|
4716
|
-
await this.store.delete.mutate({
|
|
4717
|
-
collection: NC_OUTBOX_COLLECTION,
|
|
4718
|
-
key: row.id
|
|
4719
|
-
});
|
|
4720
|
-
this.knownIds.delete(row.id);
|
|
4721
|
-
pruned += 1;
|
|
4722
|
-
}
|
|
4723
|
-
} catch (err) {
|
|
4724
|
-
this.logger.debug("outbox prune failed", { meta: { error: String(err) } });
|
|
4725
|
-
}
|
|
4726
|
-
return pruned;
|
|
4727
|
-
}
|
|
4728
|
-
backoffMs(attempts) {
|
|
4729
|
-
return Math.min(this.backoffBaseMs * 2 ** Math.max(0, attempts - 1), this.backoffCapMs);
|
|
4730
|
-
}
|
|
4731
|
-
async attempt(entry) {
|
|
4732
|
-
let result;
|
|
4733
|
-
try {
|
|
4734
|
-
result = await this.deliver(entry);
|
|
4735
|
-
} catch (err) {
|
|
4736
|
-
result = {
|
|
4737
|
-
ok: false,
|
|
4738
|
-
error: String(err),
|
|
4739
|
-
permanent: false
|
|
4740
|
-
};
|
|
4741
|
-
}
|
|
4742
|
-
const now = this.now();
|
|
4743
|
-
if (result.ok) {
|
|
4744
|
-
const sent = {
|
|
4745
|
-
...entry,
|
|
4746
|
-
status: "sent",
|
|
4747
|
-
updatedAt: now
|
|
4748
|
-
};
|
|
4749
|
-
this.pending.delete(entry.id);
|
|
4750
|
-
await this.mutate(sent);
|
|
4751
|
-
return;
|
|
4752
|
-
}
|
|
4753
|
-
const attempts = entry.attempts + 1;
|
|
4754
|
-
const exhausted = attempts >= this.maxAttempts;
|
|
4755
|
-
if (result.permanent || exhausted) {
|
|
4756
|
-
const dead = {
|
|
4757
|
-
...entry,
|
|
4758
|
-
status: "dead",
|
|
4759
|
-
attempts,
|
|
4760
|
-
updatedAt: now,
|
|
4761
|
-
lastError: result.error
|
|
4762
|
-
};
|
|
4763
|
-
this.pending.delete(entry.id);
|
|
4764
|
-
await this.mutate(dead);
|
|
4765
|
-
this.logger.warn("outbox entry dead-lettered", {
|
|
4766
|
-
tags: { deviceId: entry.deviceId },
|
|
4767
|
-
meta: {
|
|
4768
|
-
id: entry.id,
|
|
4769
|
-
ruleId: entry.ruleId,
|
|
4770
|
-
targetId: entry.targetId,
|
|
4771
|
-
attempts,
|
|
4772
|
-
permanent: result.permanent,
|
|
4773
|
-
error: result.error
|
|
4774
|
-
}
|
|
4775
|
-
});
|
|
4776
|
-
return;
|
|
4777
|
-
}
|
|
4778
|
-
const retry = {
|
|
4779
|
-
...entry,
|
|
4780
|
-
attempts,
|
|
4781
|
-
nextAttemptAt: now + this.backoffMs(attempts),
|
|
4782
|
-
updatedAt: now,
|
|
4783
|
-
lastError: result.error
|
|
4784
|
-
};
|
|
4785
|
-
this.pending.set(entry.id, retry);
|
|
4786
|
-
await this.mutate(retry);
|
|
4787
|
-
}
|
|
4788
|
-
async persist(entry) {
|
|
4789
|
-
await this.store.set.mutate({
|
|
4790
|
-
collection: NC_OUTBOX_COLLECTION,
|
|
4791
|
-
key: entry.id,
|
|
4792
|
-
value: entryToRow(entry)
|
|
4793
|
-
});
|
|
4794
|
-
}
|
|
4795
|
-
async mutate(entry) {
|
|
4796
|
-
try {
|
|
4797
|
-
await this.store.update.mutate({
|
|
4798
|
-
collection: NC_OUTBOX_COLLECTION,
|
|
4799
|
-
id: entry.id,
|
|
4800
|
-
data: entryToRow(entry)
|
|
4801
|
-
});
|
|
4802
|
-
} catch (err) {
|
|
4803
|
-
this.logger.debug("outbox row update failed", { meta: {
|
|
4804
|
-
id: entry.id,
|
|
4805
|
-
error: String(err)
|
|
4806
|
-
} });
|
|
4807
|
-
}
|
|
4808
|
-
}
|
|
4809
|
-
};
|
|
4810
|
-
function entryToRow(entry) {
|
|
4811
|
-
return {
|
|
4812
|
-
ruleId: entry.ruleId,
|
|
4813
|
-
targetId: entry.targetId,
|
|
4814
|
-
deviceId: entry.deviceId,
|
|
4815
|
-
recordKind: entry.recordKind,
|
|
4816
|
-
recordId: entry.recordId,
|
|
4817
|
-
...entry.trackId !== void 0 ? { trackId: entry.trackId } : {},
|
|
4818
|
-
status: entry.status,
|
|
4819
|
-
attempts: entry.attempts,
|
|
4820
|
-
nextAttemptAt: entry.nextAttemptAt,
|
|
4821
|
-
createdAt: entry.createdAt,
|
|
4822
|
-
updatedAt: entry.updatedAt,
|
|
4823
|
-
...entry.lastError !== void 0 ? { lastError: entry.lastError } : {},
|
|
4824
|
-
payload: entry.payload
|
|
4825
|
-
};
|
|
4826
|
-
}
|
|
4827
|
-
var OUTBOX_RECORD_KINDS = new Set([
|
|
4828
|
-
"object-event",
|
|
4829
|
-
"track-end",
|
|
4830
|
-
"device-event",
|
|
4831
|
-
"package-event",
|
|
4832
|
-
"audio-event"
|
|
4833
|
-
]);
|
|
4834
|
-
function isOutboxRecordKind(x) {
|
|
4835
|
-
return typeof x === "string" && OUTBOX_RECORD_KINDS.has(x);
|
|
4836
|
-
}
|
|
4837
|
-
function rowToEntry$1(id, data) {
|
|
4838
|
-
const ruleId = data["ruleId"];
|
|
4839
|
-
const targetId = data["targetId"];
|
|
4840
|
-
const deviceId = Number(data["deviceId"]);
|
|
4841
|
-
const recordKind = data["recordKind"];
|
|
4842
|
-
const recordId = data["recordId"];
|
|
4843
|
-
const status = data["status"];
|
|
4844
|
-
const payload = data["payload"];
|
|
4845
|
-
if (typeof ruleId !== "string" || typeof targetId !== "string" || !Number.isFinite(deviceId) || !isOutboxRecordKind(recordKind) || typeof recordId !== "string" || status !== "pending" && status !== "sent" && status !== "dead" || payload === null || typeof payload !== "object") return null;
|
|
4846
|
-
const trackId = data["trackId"];
|
|
4847
|
-
const lastError = data["lastError"];
|
|
4848
|
-
return {
|
|
4849
|
-
id,
|
|
4850
|
-
ruleId,
|
|
4851
|
-
targetId,
|
|
4852
|
-
deviceId,
|
|
4853
|
-
recordKind,
|
|
4854
|
-
recordId,
|
|
4855
|
-
...typeof trackId === "string" ? { trackId } : {},
|
|
4856
|
-
status,
|
|
4857
|
-
attempts: Number(data["attempts"] ?? 0),
|
|
4858
|
-
nextAttemptAt: Number(data["nextAttemptAt"] ?? 0),
|
|
4859
|
-
createdAt: Number(data["createdAt"] ?? 0),
|
|
4860
|
-
updatedAt: Number(data["updatedAt"] ?? 0),
|
|
4861
|
-
...typeof lastError === "string" ? { lastError } : {},
|
|
4862
|
-
payload
|
|
4863
|
-
};
|
|
4864
|
-
}
|
|
4865
|
-
//#endregion
|
|
4866
|
-
//#region src/notification-center/occupancy-watcher.ts
|
|
4867
|
-
/** Sentinel key segments for the "no zone" (whole-frame) and "no class" scopes. */
|
|
4868
|
-
var FRAME_SCOPE = "@frame";
|
|
4869
|
-
var ALL_CLASSES = "@all";
|
|
4870
|
-
/** Prefix marking the trailing threshold segment (`t<n>`) — makes the grammar
|
|
4871
|
-
* self-describing so a legacy three-segment key can never be mis-parsed as a
|
|
4872
|
-
* four-segment one (the last segment of a legacy key is a className, never a
|
|
4873
|
-
* `t<digits>` token). */
|
|
4874
|
-
var THRESHOLD_PREFIX = "t";
|
|
4875
|
-
var THRESHOLD_SEGMENT = /^t(\d+)$/;
|
|
4876
|
-
function occupancyKey(deviceId, zoneId, className, threshold) {
|
|
4877
|
-
return `${deviceId}|${zoneId ?? FRAME_SCOPE}|${className ?? ALL_CLASSES}|${THRESHOLD_PREFIX}${threshold}`;
|
|
4878
|
-
}
|
|
4879
|
-
/**
|
|
4880
|
-
* Inverse of {@link occupancyKey} — the ONE place the key grammar is decoded, so
|
|
4881
|
-
* the durable store (whose column schema drops the derivable
|
|
4882
|
-
* `zoneId`/`className`/`threshold`) can reconstruct the full scope on reseed.
|
|
4883
|
-
*
|
|
4884
|
-
* The four-segment grammar is asserted EXACTLY: at least four segments AND a
|
|
4885
|
-
* trailing `t<digits>` threshold marker. A legacy three-segment key (no marker)
|
|
4886
|
-
* is REJECTED (`null`) rather than positionally mis-parsed — the durable store
|
|
4887
|
-
* then SKIPS that row, so a pre-amendment persisted row cold-re-observes instead
|
|
4888
|
-
* of hydrating a corrupt scope (className←threshold, zoneId←class). `deviceId`
|
|
4889
|
-
* is the leading numeric segment, `threshold` the trailing `t<n>`, `className`
|
|
4890
|
-
* the segment before it, and the (possibly `|`-containing) `zoneId` everything
|
|
4891
|
-
* between. Exact per-segment count is impossible because a `zoneId` may itself
|
|
4892
|
-
* contain `|`; the trailing marker is the unambiguous grammar discriminator.
|
|
4893
|
-
*/
|
|
4894
|
-
function parseOccupancyKey(key) {
|
|
4895
|
-
const parts = key.split("|");
|
|
4896
|
-
if (parts.length < 4) return null;
|
|
4897
|
-
const deviceId = Number(parts[0]);
|
|
4898
|
-
if (!Number.isFinite(deviceId)) return null;
|
|
4899
|
-
const thresholdMatch = THRESHOLD_SEGMENT.exec(parts[parts.length - 1] ?? "");
|
|
4900
|
-
if (thresholdMatch === null) return null;
|
|
4901
|
-
const threshold = Number(thresholdMatch[1]);
|
|
4902
|
-
const className = parts[parts.length - 2] ?? ALL_CLASSES;
|
|
4903
|
-
const zoneId = parts.slice(1, -2).join("|");
|
|
4904
|
-
return {
|
|
4905
|
-
deviceId,
|
|
4906
|
-
...zoneId !== FRAME_SCOPE ? { zoneId } : {},
|
|
4907
|
-
...className !== ALL_CLASSES ? { className } : {},
|
|
4908
|
-
threshold
|
|
4909
|
-
};
|
|
4910
|
-
}
|
|
4911
|
-
/** Device-agnostic partial key (`zone|class|t<threshold>`) — the merge/watch
|
|
4912
|
-
* granularity. Distinct thresholds are distinct partial keys (per-threshold). */
|
|
4913
|
-
function partialKey(zoneId, className, threshold) {
|
|
4914
|
-
return `${zoneId ?? FRAME_SCOPE}|${className ?? ALL_CLASSES}|${THRESHOLD_PREFIX}${threshold}`;
|
|
4915
|
-
}
|
|
4916
|
-
/** The device-agnostic partial key of a full state/edge key — everything after
|
|
4917
|
-
* the leading `deviceId|` segment. */
|
|
4918
|
-
function partialKeyOf(key) {
|
|
4919
|
-
const idx = key.indexOf("|");
|
|
4920
|
-
return idx < 0 ? key : key.slice(idx + 1);
|
|
4921
|
-
}
|
|
4922
|
-
var OccupancyWatcher = class {
|
|
4923
|
-
/** Watched specs, keyed by their device-agnostic partial key. */
|
|
4924
|
-
watched = /* @__PURE__ */ new Map();
|
|
4925
|
-
/** Confirmed + pending state, keyed by the full {@link OccupancyKey}. */
|
|
4926
|
-
states = /* @__PURE__ */ new Map();
|
|
4927
|
-
/**
|
|
4928
|
-
* Replace the watched key set (rule-driven — recomputed on rule change).
|
|
4929
|
-
* Each distinct `(zone, class, threshold)` is its OWN watched partial key
|
|
4930
|
-
* (per-threshold edges — NO min-threshold merge). Specs that collide on the
|
|
4931
|
-
* SAME `(zone, class, threshold)` merge to the MAX sustain (longest debounce),
|
|
4932
|
-
* so one watcher serves those co-threshold rules. Confirmed state for a key
|
|
4933
|
-
* that is no longer watched is DROPPED (bounds the RAM map + lets the caller
|
|
4934
|
-
* prune the durable row to the active set); a still-watched key retains its
|
|
4935
|
-
* level (re-evaluated on the next `observe`).
|
|
4936
|
-
*/
|
|
4937
|
-
setWatchedKeys(specs) {
|
|
4938
|
-
this.watched.clear();
|
|
4939
|
-
for (const spec of specs) {
|
|
4940
|
-
const pk = partialKey(spec.zoneId, spec.className, spec.threshold);
|
|
4941
|
-
const existing = this.watched.get(pk);
|
|
4942
|
-
if (existing === void 0) {
|
|
4943
|
-
this.watched.set(pk, {
|
|
4944
|
-
...spec.zoneId !== void 0 ? { zoneId: spec.zoneId } : {},
|
|
4945
|
-
...spec.className !== void 0 ? { className: spec.className } : {},
|
|
4946
|
-
threshold: spec.threshold,
|
|
4947
|
-
sustainSeconds: spec.sustainSeconds
|
|
4948
|
-
});
|
|
4949
|
-
continue;
|
|
4950
|
-
}
|
|
4951
|
-
this.watched.set(pk, {
|
|
4952
|
-
...existing,
|
|
4953
|
-
sustainSeconds: Math.max(existing.sustainSeconds, spec.sustainSeconds)
|
|
4954
|
-
});
|
|
4955
|
-
}
|
|
4956
|
-
for (const key of [...this.states.keys()]) if (!this.watched.has(partialKeyOf(key))) this.states.delete(key);
|
|
4957
|
-
}
|
|
4958
|
-
/**
|
|
4959
|
-
* Feed one camera snapshot at time `now`, returning the edges that COMMIT on
|
|
4960
|
-
* this tick (usually none). Every watched key is evaluated for `deviceId`;
|
|
4961
|
-
* fail-closed keys (absent zone) are skipped and hold no state.
|
|
4962
|
-
*/
|
|
4963
|
-
observe(deviceId, snapshot, now) {
|
|
4964
|
-
const edges = [];
|
|
4965
|
-
for (const spec of this.watched.values()) {
|
|
4966
|
-
const resolved = resolveScope(snapshot, spec);
|
|
4967
|
-
if (resolved === null) continue;
|
|
4968
|
-
const key = occupancyKey(deviceId, spec.zoneId, spec.className, spec.threshold);
|
|
4969
|
-
const edge = step(this.stateFor(key, deviceId, spec, now), spec, resolved, now);
|
|
4970
|
-
if (edge !== null) edges.push(edge);
|
|
4971
|
-
}
|
|
4972
|
-
return edges;
|
|
4973
|
-
}
|
|
4974
|
-
/** Reseed confirmed state from durable rows (boot). Only rows whose key is
|
|
4975
|
-
* currently WATCHED are restored — an orphaned durable row (its rule gone)
|
|
4976
|
-
* is dropped, keeping the RAM map bounded to the active set. Pending edges
|
|
4977
|
-
* are not restored (fail-closed — they re-open on the next snapshots). */
|
|
4978
|
-
hydrate(rows) {
|
|
4979
|
-
for (const row of rows) {
|
|
4980
|
-
if (!this.watched.has(partialKeyOf(row.key))) continue;
|
|
4981
|
-
this.states.set(row.key, {
|
|
4982
|
-
deviceId: row.deviceId,
|
|
4983
|
-
...row.zoneId !== void 0 ? { zoneId: row.zoneId } : {},
|
|
4984
|
-
...row.className !== void 0 ? { className: row.className } : {},
|
|
4985
|
-
threshold: row.threshold,
|
|
4986
|
-
confirmedCount: row.confirmedCount,
|
|
4987
|
-
occupied: row.occupied,
|
|
4988
|
-
lastChangeAt: row.lastChangeAt
|
|
4989
|
-
});
|
|
4990
|
-
}
|
|
4991
|
-
}
|
|
4992
|
-
/** Snapshot the CONFIRMED state for durable persistence (pending excluded). */
|
|
4993
|
-
snapshotState() {
|
|
4994
|
-
const rows = [];
|
|
4995
|
-
for (const [key, state] of this.states) rows.push({
|
|
4996
|
-
key,
|
|
4997
|
-
deviceId: state.deviceId,
|
|
4998
|
-
...state.zoneId !== void 0 ? { zoneId: state.zoneId } : {},
|
|
4999
|
-
...state.className !== void 0 ? { className: state.className } : {},
|
|
5000
|
-
threshold: state.threshold,
|
|
5001
|
-
confirmedCount: state.confirmedCount,
|
|
5002
|
-
occupied: state.occupied,
|
|
5003
|
-
lastChangeAt: state.lastChangeAt,
|
|
5004
|
-
updatedAt: state.lastChangeAt
|
|
5005
|
-
});
|
|
5006
|
-
return rows;
|
|
5007
|
-
}
|
|
5008
|
-
stateFor(key, deviceId, spec, now) {
|
|
5009
|
-
const existing = this.states.get(key);
|
|
5010
|
-
if (existing !== void 0) return existing;
|
|
5011
|
-
const fresh = {
|
|
5012
|
-
deviceId,
|
|
5013
|
-
...spec.zoneId !== void 0 ? { zoneId: spec.zoneId } : {},
|
|
5014
|
-
...spec.className !== void 0 ? { className: spec.className } : {},
|
|
5015
|
-
threshold: spec.threshold,
|
|
5016
|
-
confirmedCount: 0,
|
|
5017
|
-
occupied: false,
|
|
5018
|
-
lastChangeAt: now
|
|
5019
|
-
};
|
|
5020
|
-
this.states.set(key, fresh);
|
|
5021
|
-
return fresh;
|
|
5022
|
-
}
|
|
5023
|
-
};
|
|
5024
|
-
/** Read the count (+ zone name) for a spec, or `null` when fail-closed. */
|
|
5025
|
-
function resolveScope(snapshot, spec) {
|
|
5026
|
-
if (spec.zoneId === void 0) return { count: spec.className === void 0 ? snapshot.frame.totalObjects : snapshot.frame.byClass[spec.className] ?? 0 };
|
|
5027
|
-
const zone = snapshot.zones.find((z) => z.zoneId === spec.zoneId);
|
|
5028
|
-
if (zone === void 0) return null;
|
|
5029
|
-
return {
|
|
5030
|
-
count: spec.className === void 0 ? zone.totalObjects : zone.byClass[spec.className] ?? 0,
|
|
5031
|
-
zoneName: zone.zoneName
|
|
5032
|
-
};
|
|
5033
|
-
}
|
|
5034
|
-
/**
|
|
5035
|
-
* Advance one key's state by one observation. Mutates `state` in place (the
|
|
5036
|
-
* watcher owns it) and returns a committed edge, or `null`.
|
|
5037
|
-
*/
|
|
5038
|
-
function step(state, spec, resolved, now) {
|
|
5039
|
-
const rawOccupied = resolved.count >= spec.threshold;
|
|
5040
|
-
const sustainMs = spec.sustainSeconds * 1e3;
|
|
5041
|
-
if (rawOccupied === state.occupied) {
|
|
5042
|
-
state.pendingTargetOccupied = void 0;
|
|
5043
|
-
state.pendingSince = void 0;
|
|
5044
|
-
return null;
|
|
5045
|
-
}
|
|
5046
|
-
if (state.pendingTargetOccupied !== rawOccupied) {
|
|
5047
|
-
state.pendingTargetOccupied = rawOccupied;
|
|
5048
|
-
state.pendingSince = now;
|
|
5049
|
-
}
|
|
5050
|
-
if (now - (state.pendingSince ?? now) < sustainMs) return null;
|
|
5051
|
-
const previousCount = state.confirmedCount;
|
|
5052
|
-
state.confirmedCount = resolved.count;
|
|
5053
|
-
state.occupied = rawOccupied;
|
|
5054
|
-
state.lastChangeAt = now;
|
|
5055
|
-
state.pendingTargetOccupied = void 0;
|
|
5056
|
-
state.pendingSince = void 0;
|
|
5057
|
-
return {
|
|
5058
|
-
deviceId: state.deviceId,
|
|
5059
|
-
...spec.zoneId !== void 0 ? { zoneId: spec.zoneId } : {},
|
|
5060
|
-
...resolved.zoneName !== void 0 ? { zoneName: resolved.zoneName } : {},
|
|
5061
|
-
...spec.className !== void 0 ? { className: spec.className } : {},
|
|
5062
|
-
count: resolved.count,
|
|
5063
|
-
previousCount,
|
|
5064
|
-
occupied: rawOccupied,
|
|
5065
|
-
threshold: spec.threshold,
|
|
5066
|
-
timestamp: now
|
|
5067
|
-
};
|
|
5068
|
-
}
|
|
5069
|
-
//#endregion
|
|
5070
|
-
//#region src/notification-center/occupancy-store.ts
|
|
5071
|
-
var NC_OCCUPANCY_COLLECTION = "notification-center:occupancy";
|
|
5072
|
-
var NC_OCCUPANCY_COLUMNS = [
|
|
5073
|
-
{
|
|
5074
|
-
name: "key",
|
|
5075
|
-
type: "TEXT",
|
|
5076
|
-
primaryKey: true,
|
|
5077
|
-
notNull: true
|
|
5078
|
-
},
|
|
5079
|
-
{
|
|
5080
|
-
name: "deviceId",
|
|
5081
|
-
type: "INTEGER",
|
|
5082
|
-
notNull: true
|
|
5083
|
-
},
|
|
5084
|
-
{
|
|
5085
|
-
name: "confirmedCount",
|
|
5086
|
-
type: "INTEGER",
|
|
5087
|
-
notNull: true
|
|
5088
|
-
},
|
|
5089
|
-
{
|
|
5090
|
-
name: "occupied",
|
|
5091
|
-
type: "BOOLEAN",
|
|
5092
|
-
notNull: true
|
|
5093
|
-
},
|
|
5094
|
-
{
|
|
5095
|
-
name: "lastChangeAt",
|
|
5096
|
-
type: "INTEGER",
|
|
5097
|
-
notNull: true
|
|
5098
|
-
},
|
|
5099
|
-
{
|
|
5100
|
-
name: "updatedAt",
|
|
5101
|
-
type: "INTEGER",
|
|
5102
|
-
notNull: true
|
|
5103
|
-
}
|
|
5104
|
-
];
|
|
5105
|
-
var NC_OCCUPANCY_INDEXES = [{
|
|
5106
|
-
name: "idx_nc_occupancy_device",
|
|
5107
|
-
columns: ["deviceId"]
|
|
5108
|
-
}];
|
|
5109
|
-
/** Query cap — a per-(device, zone, class) key set is small; this is a
|
|
5110
|
-
* generous ceiling that still bounds a pathological read. */
|
|
5111
|
-
var LOAD_LIMIT = 1e5;
|
|
5112
|
-
var OccupancyStore = class {
|
|
5113
|
-
cache = /* @__PURE__ */ new Map();
|
|
5114
|
-
store;
|
|
5115
|
-
logger;
|
|
5116
|
-
constructor(deps) {
|
|
5117
|
-
this.store = deps.store;
|
|
5118
|
-
this.logger = deps.logger;
|
|
5119
|
-
}
|
|
5120
|
-
static async declare(store) {
|
|
5121
|
-
await store.declareCollection.mutate({
|
|
5122
|
-
collection: NC_OCCUPANCY_COLLECTION,
|
|
5123
|
-
columns: [...NC_OCCUPANCY_COLUMNS],
|
|
5124
|
-
indexes: [...NC_OCCUPANCY_INDEXES]
|
|
5125
|
-
});
|
|
5126
|
-
}
|
|
5127
|
-
/**
|
|
5128
|
-
* Reseed the confirmed edge-state from the store (boot) — replaces the cache
|
|
5129
|
-
* wholesale and returns the rows for {@link OccupancyWatcher.hydrate}. A row
|
|
5130
|
-
* whose scalars/key no longer parse is skipped with a warning (a degraded row
|
|
5131
|
-
* must never crash the reseed). Best-effort: a store error yields `[]` and a
|
|
5132
|
-
* cold watcher, never a throw into boot.
|
|
5133
|
-
*/
|
|
5134
|
-
async load() {
|
|
5135
|
-
try {
|
|
5136
|
-
const records = await this.store.query.query({
|
|
5137
|
-
collection: NC_OCCUPANCY_COLLECTION,
|
|
5138
|
-
filter: { limit: LOAD_LIMIT }
|
|
5139
|
-
});
|
|
5140
|
-
this.cache.clear();
|
|
5141
|
-
let skipped = 0;
|
|
5142
|
-
for (const record of records) {
|
|
5143
|
-
const row = recordToRow(record.id, record.data);
|
|
5144
|
-
if (row === null) {
|
|
5145
|
-
skipped += 1;
|
|
5146
|
-
continue;
|
|
5051
|
+
limit: query.limit
|
|
5147
5052
|
}
|
|
5148
|
-
|
|
5053
|
+
});
|
|
5054
|
+
const out = [];
|
|
5055
|
+
for (const row of rows) {
|
|
5056
|
+
const entry = rowToEntry$1(row.id, row.data);
|
|
5057
|
+
if (entry !== null) out.push(entry);
|
|
5149
5058
|
}
|
|
5150
|
-
|
|
5151
|
-
keys: this.cache.size,
|
|
5152
|
-
...skipped > 0 ? { skippedInvalid: skipped } : {}
|
|
5153
|
-
} });
|
|
5154
|
-
return [...this.cache.values()];
|
|
5059
|
+
return out;
|
|
5155
5060
|
} catch (err) {
|
|
5156
|
-
this.logger.
|
|
5061
|
+
this.logger.debug("outbox history query failed", { meta: { error: String(err) } });
|
|
5157
5062
|
return [];
|
|
5158
5063
|
}
|
|
5159
5064
|
}
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5065
|
+
async getWatermark() {
|
|
5066
|
+
try {
|
|
5067
|
+
const row = await this.store.get.query({
|
|
5068
|
+
collection: NC_META_COLLECTION,
|
|
5069
|
+
key: NC_WATERMARK_KEY
|
|
5070
|
+
});
|
|
5071
|
+
if (row === null || row === void 0 || typeof row !== "object") return null;
|
|
5072
|
+
const value = row["value"];
|
|
5073
|
+
if (value === null || typeof value !== "object") return null;
|
|
5074
|
+
const ts = value["ts"];
|
|
5075
|
+
return typeof ts === "number" && Number.isFinite(ts) ? ts : null;
|
|
5076
|
+
} catch {
|
|
5077
|
+
return null;
|
|
5078
|
+
}
|
|
5163
5079
|
}
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
});
|
|
5175
|
-
this.cache.set(row.key, row);
|
|
5080
|
+
async setWatermark(ts) {
|
|
5081
|
+
try {
|
|
5082
|
+
await this.store.set.mutate({
|
|
5083
|
+
collection: NC_META_COLLECTION,
|
|
5084
|
+
key: NC_WATERMARK_KEY,
|
|
5085
|
+
value: { value: { ts } }
|
|
5086
|
+
});
|
|
5087
|
+
} catch (err) {
|
|
5088
|
+
this.logger.debug("outbox watermark write failed", { meta: { error: String(err) } });
|
|
5089
|
+
}
|
|
5176
5090
|
}
|
|
5177
|
-
/**
|
|
5178
|
-
|
|
5179
|
-
* the bounded-row-count guarantee when rules stop watching a key. Returns the
|
|
5180
|
-
* number of rows dropped. Best-effort per row: a failed delete is logged and
|
|
5181
|
-
* the key retained (retried next prune) rather than aborting the sweep.
|
|
5182
|
-
*/
|
|
5183
|
-
async pruneExcept(activeKeys) {
|
|
5091
|
+
/** Drop terminal rows older than `cutoffMs` (retention hygiene). */
|
|
5092
|
+
async pruneBefore(cutoffMs) {
|
|
5184
5093
|
let pruned = 0;
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5094
|
+
try {
|
|
5095
|
+
const rows = await this.store.query.query({
|
|
5096
|
+
collection: NC_OUTBOX_COLLECTION,
|
|
5097
|
+
filter: {
|
|
5098
|
+
whereBetween: { createdAt: [0, cutoffMs] },
|
|
5099
|
+
limit: 5e3
|
|
5100
|
+
}
|
|
5101
|
+
});
|
|
5102
|
+
for (const row of rows) {
|
|
5103
|
+
const status = row.data["status"];
|
|
5104
|
+
if (status !== "sent" && status !== "dead") continue;
|
|
5188
5105
|
await this.store.delete.mutate({
|
|
5189
|
-
collection:
|
|
5190
|
-
key
|
|
5106
|
+
collection: NC_OUTBOX_COLLECTION,
|
|
5107
|
+
key: row.id
|
|
5191
5108
|
});
|
|
5192
|
-
this.
|
|
5109
|
+
this.knownIds.delete(row.id);
|
|
5193
5110
|
pruned += 1;
|
|
5194
|
-
} catch (err) {
|
|
5195
|
-
this.logger.debug("occupancy prune delete failed", { meta: {
|
|
5196
|
-
key,
|
|
5197
|
-
error: String(err)
|
|
5198
|
-
} });
|
|
5199
5111
|
}
|
|
5112
|
+
} catch (err) {
|
|
5113
|
+
this.logger.debug("outbox prune failed", { meta: { error: String(err) } });
|
|
5200
5114
|
}
|
|
5201
5115
|
return pruned;
|
|
5202
5116
|
}
|
|
5117
|
+
backoffMs(attempts) {
|
|
5118
|
+
return Math.min(this.backoffBaseMs * 2 ** Math.max(0, attempts - 1), this.backoffCapMs);
|
|
5119
|
+
}
|
|
5120
|
+
async attempt(entry) {
|
|
5121
|
+
let result;
|
|
5122
|
+
try {
|
|
5123
|
+
result = await this.deliver(entry);
|
|
5124
|
+
} catch (err) {
|
|
5125
|
+
result = {
|
|
5126
|
+
ok: false,
|
|
5127
|
+
error: String(err),
|
|
5128
|
+
permanent: false
|
|
5129
|
+
};
|
|
5130
|
+
}
|
|
5131
|
+
const now = this.now();
|
|
5132
|
+
if (result.ok) {
|
|
5133
|
+
const sent = {
|
|
5134
|
+
...entry,
|
|
5135
|
+
status: "sent",
|
|
5136
|
+
updatedAt: now
|
|
5137
|
+
};
|
|
5138
|
+
this.pending.delete(entry.id);
|
|
5139
|
+
await this.mutate(sent);
|
|
5140
|
+
return;
|
|
5141
|
+
}
|
|
5142
|
+
const attempts = entry.attempts + 1;
|
|
5143
|
+
const exhausted = attempts >= this.maxAttempts;
|
|
5144
|
+
if (result.permanent || exhausted) {
|
|
5145
|
+
const dead = {
|
|
5146
|
+
...entry,
|
|
5147
|
+
status: "dead",
|
|
5148
|
+
attempts,
|
|
5149
|
+
updatedAt: now,
|
|
5150
|
+
lastError: result.error
|
|
5151
|
+
};
|
|
5152
|
+
this.pending.delete(entry.id);
|
|
5153
|
+
await this.mutate(dead);
|
|
5154
|
+
this.logger.warn("outbox entry dead-lettered", {
|
|
5155
|
+
tags: { deviceId: entry.deviceId },
|
|
5156
|
+
meta: {
|
|
5157
|
+
id: entry.id,
|
|
5158
|
+
ruleId: entry.ruleId,
|
|
5159
|
+
targetId: entry.targetId,
|
|
5160
|
+
attempts,
|
|
5161
|
+
permanent: result.permanent,
|
|
5162
|
+
error: result.error
|
|
5163
|
+
}
|
|
5164
|
+
});
|
|
5165
|
+
return;
|
|
5166
|
+
}
|
|
5167
|
+
const retry = {
|
|
5168
|
+
...entry,
|
|
5169
|
+
attempts,
|
|
5170
|
+
nextAttemptAt: now + this.backoffMs(attempts),
|
|
5171
|
+
updatedAt: now,
|
|
5172
|
+
lastError: result.error
|
|
5173
|
+
};
|
|
5174
|
+
this.pending.set(entry.id, retry);
|
|
5175
|
+
await this.mutate(retry);
|
|
5176
|
+
}
|
|
5177
|
+
async persist(entry) {
|
|
5178
|
+
await this.store.set.mutate({
|
|
5179
|
+
collection: NC_OUTBOX_COLLECTION,
|
|
5180
|
+
key: entry.id,
|
|
5181
|
+
value: entryToRow(entry)
|
|
5182
|
+
});
|
|
5183
|
+
}
|
|
5184
|
+
async mutate(entry) {
|
|
5185
|
+
try {
|
|
5186
|
+
await this.store.update.mutate({
|
|
5187
|
+
collection: NC_OUTBOX_COLLECTION,
|
|
5188
|
+
id: entry.id,
|
|
5189
|
+
data: entryToRow(entry)
|
|
5190
|
+
});
|
|
5191
|
+
} catch (err) {
|
|
5192
|
+
this.logger.debug("outbox row update failed", { meta: {
|
|
5193
|
+
id: entry.id,
|
|
5194
|
+
error: String(err)
|
|
5195
|
+
} });
|
|
5196
|
+
}
|
|
5197
|
+
}
|
|
5203
5198
|
};
|
|
5204
|
-
|
|
5205
|
-
function rowToValue(row) {
|
|
5199
|
+
function entryToRow(entry) {
|
|
5206
5200
|
return {
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5201
|
+
ruleId: entry.ruleId,
|
|
5202
|
+
targetId: entry.targetId,
|
|
5203
|
+
deviceId: entry.deviceId,
|
|
5204
|
+
recordKind: entry.recordKind,
|
|
5205
|
+
recordId: entry.recordId,
|
|
5206
|
+
...entry.trackId !== void 0 ? { trackId: entry.trackId } : {},
|
|
5207
|
+
status: entry.status,
|
|
5208
|
+
attempts: entry.attempts,
|
|
5209
|
+
nextAttemptAt: entry.nextAttemptAt,
|
|
5210
|
+
createdAt: entry.createdAt,
|
|
5211
|
+
updatedAt: entry.updatedAt,
|
|
5212
|
+
...entry.lastError !== void 0 ? { lastError: entry.lastError } : {},
|
|
5213
|
+
payload: entry.payload
|
|
5212
5214
|
};
|
|
5213
5215
|
}
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5216
|
+
var OUTBOX_RECORD_KINDS = new Set([
|
|
5217
|
+
"object-event",
|
|
5218
|
+
"track-end",
|
|
5219
|
+
"device-event",
|
|
5220
|
+
"package-event",
|
|
5221
|
+
"audio-event"
|
|
5222
|
+
]);
|
|
5223
|
+
function isOutboxRecordKind(x) {
|
|
5224
|
+
return typeof x === "string" && OUTBOX_RECORD_KINDS.has(x);
|
|
5225
|
+
}
|
|
5226
|
+
function rowToEntry$1(id, data) {
|
|
5227
|
+
const ruleId = data["ruleId"];
|
|
5228
|
+
const targetId = data["targetId"];
|
|
5225
5229
|
const deviceId = Number(data["deviceId"]);
|
|
5226
|
-
const
|
|
5227
|
-
const
|
|
5228
|
-
const
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
const
|
|
5230
|
+
const recordKind = data["recordKind"];
|
|
5231
|
+
const recordId = data["recordId"];
|
|
5232
|
+
const status = data["status"];
|
|
5233
|
+
const payload = data["payload"];
|
|
5234
|
+
if (typeof ruleId !== "string" || typeof targetId !== "string" || !Number.isFinite(deviceId) || !isOutboxRecordKind(recordKind) || typeof recordId !== "string" || status !== "pending" && status !== "sent" && status !== "dead" || payload === null || typeof payload !== "object") return null;
|
|
5235
|
+
const trackId = data["trackId"];
|
|
5236
|
+
const lastError = data["lastError"];
|
|
5232
5237
|
return {
|
|
5233
|
-
|
|
5238
|
+
id,
|
|
5239
|
+
ruleId,
|
|
5240
|
+
targetId,
|
|
5234
5241
|
deviceId,
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
|
|
5242
|
+
recordKind,
|
|
5243
|
+
recordId,
|
|
5244
|
+
...typeof trackId === "string" ? { trackId } : {},
|
|
5245
|
+
status,
|
|
5246
|
+
attempts: Number(data["attempts"] ?? 0),
|
|
5247
|
+
nextAttemptAt: Number(data["nextAttemptAt"] ?? 0),
|
|
5248
|
+
createdAt: Number(data["createdAt"] ?? 0),
|
|
5249
|
+
updatedAt: Number(data["updatedAt"] ?? 0),
|
|
5250
|
+
...typeof lastError === "string" ? { lastError } : {},
|
|
5251
|
+
payload
|
|
5242
5252
|
};
|
|
5243
5253
|
}
|
|
5244
5254
|
//#endregion
|
|
@@ -5478,6 +5488,25 @@ function occupancySpecFromCondition(occ) {
|
|
|
5478
5488
|
sustainSeconds: occ.sustainSeconds
|
|
5479
5489
|
};
|
|
5480
5490
|
}
|
|
5491
|
+
/**
|
|
5492
|
+
* Freeze the matched occupancy edge onto the outbox payload subject (A5) — the
|
|
5493
|
+
* source of the dispatcher's `{{zone}}`/`{{count}}`/`{{capacity}}` vars.
|
|
5494
|
+
*
|
|
5495
|
+
* `capacity` is the RULE's configured `occupancy.count`, NOT the edge's key
|
|
5496
|
+
* threshold: a `<=C` rule keys on threshold `C+1`, so the threshold would
|
|
5497
|
+
* misreport the operator's capacity by one. The rule condition is present by
|
|
5498
|
+
* construction (the engine's occupancy branch fires only when it is); the
|
|
5499
|
+
* threshold fallback keeps the freeze total without a cast, and is exact for
|
|
5500
|
+
* every op but `<=`.
|
|
5501
|
+
*/
|
|
5502
|
+
function frozenOccupancy(rule, occ) {
|
|
5503
|
+
return {
|
|
5504
|
+
...occ.zoneName !== void 0 ? { zone: occ.zoneName } : {},
|
|
5505
|
+
count: occ.count,
|
|
5506
|
+
capacity: rule.conditions.occupancy?.count ?? occ.threshold,
|
|
5507
|
+
occupied: occ.occupied
|
|
5508
|
+
};
|
|
5509
|
+
}
|
|
5481
5510
|
/** Classify an object-event row as a package delivery / pick-up, or `null` when
|
|
5482
5511
|
* it is an ordinary detection. */
|
|
5483
5512
|
function packagePhaseOf(ev) {
|
|
@@ -5869,7 +5898,8 @@ var NotificationCenter = class {
|
|
|
5869
5898
|
...subject.label !== void 0 ? { label: subject.label } : {},
|
|
5870
5899
|
...subject.confidence !== void 0 ? { confidence: subject.confidence } : {},
|
|
5871
5900
|
zones: subject.zones,
|
|
5872
|
-
timestamp: subject.timestamp
|
|
5901
|
+
timestamp: subject.timestamp,
|
|
5902
|
+
...subject.occupancy !== void 0 ? { occupancy: frozenOccupancy(rule, subject.occupancy) } : {}
|
|
5873
5903
|
}
|
|
5874
5904
|
};
|
|
5875
5905
|
return {
|
|
@@ -18695,7 +18725,7 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
|
|
|
18695
18725
|
let storage = this.ctx.kernel.storage;
|
|
18696
18726
|
const mediaRoot = process.env.CAMSTACK_MEDIA_ROOT?.trim();
|
|
18697
18727
|
if (mediaRoot) {
|
|
18698
|
-
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-
|
|
18728
|
+
const { FilesystemStorageProvider } = await Promise.resolve().then(() => require("../node-PvutBRd4.js"));
|
|
18699
18729
|
storage = new FilesystemStorageProvider(mediaRoot);
|
|
18700
18730
|
logger.info("pipeline-analytics: event media rooted at CAMSTACK_MEDIA_ROOT", { meta: { mediaRoot } });
|
|
18701
18731
|
}
|