@camstack/addon-pipeline 1.2.375 → 1.2.602
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/audio-analyzer/index.js +3 -3
- package/dist/audio-analyzer/index.mjs +3 -3
- package/dist/{custom-model-catalog-DF5qs8u4.mjs → custom-model-catalog--ms2wSDI.mjs} +2 -2
- package/dist/{custom-model-catalog-gbLoUFki.js → custom-model-catalog-CVvdKnH2.js} +2 -2
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-C0Mg8e3I.js → dist-B4HyxJe1.js} +703 -28
- package/dist/{dist-QyeZXkAO.mjs → dist-BmSyTuy4.mjs} +698 -29
- package/dist/{model-catalogs-BTuX-P0k.js → model-catalogs-Bj0f2Obs.js} +1 -1
- package/dist/{model-catalogs-CIE4Dz4x.mjs → model-catalogs-OnXVUgQY.mjs} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-ndjp9DQO.mjs → node--4HtlkFo.mjs} +38 -3
- package/dist/{node-CifGKjsd.js → node-DfuKhWUZ.js} +42 -1
- package/dist/pipeline-runner/index.js +4 -4
- package/dist/pipeline-runner/index.mjs +4 -4
- package/dist/{process-memory-BMfSP5-i.js → process-memory-BZ1_q3g3.js} +1 -1
- package/dist/{process-memory-CaMI-Ry2.mjs → process-memory-uf7ZfkSz.mjs} +1 -1
- package/dist/recorder/index.js +446 -518
- package/dist/recorder/index.mjs +446 -518
- package/dist/{segment-demux-js-LhwVw18z.js → segment-demux-js-C7fMqBGZ.js} +1 -1
- package/dist/{segment-demux-js-CLXBgUTo.mjs → segment-demux-js-CURD3CEv.mjs} +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B4MD_B4k.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Cd6rjANN.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DUPQbxcH.mjs +26 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CGcD3ujP.mjs +26 -0
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-DBDp5YHD.mjs → hostInit-CV-FMXnr.mjs} +3 -3
- package/dist/stream-broker/index.js +10 -9
- package/dist/stream-broker/index.mjs +10 -9
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BvMrdOCD.mjs +0 -26
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-JVFs9LgI.mjs +0 -26
package/dist/recorder/index.js
CHANGED
|
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../chunk-emK7D4bc.js");
|
|
6
|
-
const require_dist = require("../dist-
|
|
7
|
-
const require_node = require("../node-
|
|
6
|
+
const require_dist = require("../dist-B4HyxJe1.js");
|
|
7
|
+
const require_node = require("../node-DfuKhWUZ.js");
|
|
8
8
|
const require_hub_hostname = require("../hub-hostname-DAJXlOgV.js");
|
|
9
9
|
const require_restream_intent = require("../restream-intent-7uDNqMKs.js");
|
|
10
10
|
let node_crypto = require("node:crypto");
|
|
@@ -5203,29 +5203,6 @@ function activeModeForConfig(config) {
|
|
|
5203
5203
|
* pays for ten minutes, not until the next restart.
|
|
5204
5204
|
*/
|
|
5205
5205
|
var CENSUS_MAX_WINDOW_MS = 10 * 6e4;
|
|
5206
|
-
/** Store procedures worth attributing. Reads only: a write is an operator
|
|
5207
|
-
* gesture or a pipeline product, and neither is a mystery. */
|
|
5208
|
-
var CENSUSED_PROCEDURES = new Set([
|
|
5209
|
-
"query",
|
|
5210
|
-
"count",
|
|
5211
|
-
"aggregate",
|
|
5212
|
-
"get",
|
|
5213
|
-
"histogram",
|
|
5214
|
-
"getAll"
|
|
5215
|
-
]);
|
|
5216
|
-
/**
|
|
5217
|
-
* Can `new Proxy` wrap this, and is it worth wrapping?
|
|
5218
|
-
*
|
|
5219
|
-
* Functions count. The generated store client is a **tRPC proxy** and every
|
|
5220
|
-
* intermediate node of one is CALLABLE — `typeof client.query` is `'function'`,
|
|
5221
|
-
* not `'object'`. A wrapper that tested only for `'object'` handed the whole
|
|
5222
|
-
* client back unwrapped, deployed, and reported zero `query` and zero `count`
|
|
5223
|
-
* call sites while the engine was recording thousands a minute against the same
|
|
5224
|
-
* tables. Silence read as "nobody in this addon does it".
|
|
5225
|
-
*/
|
|
5226
|
-
function isProxyable(value) {
|
|
5227
|
-
return typeof value === "object" && value !== null || typeof value === "function";
|
|
5228
|
-
}
|
|
5229
5206
|
function rowsOf(value) {
|
|
5230
5207
|
if (Array.isArray(value)) return value.length;
|
|
5231
5208
|
if (typeof value === "number") return value;
|
|
@@ -5349,34 +5326,6 @@ var StoreReadCensus = class {
|
|
|
5349
5326
|
})).sort((a, b) => b.calls - a.calls);
|
|
5350
5327
|
}
|
|
5351
5328
|
};
|
|
5352
|
-
/**
|
|
5353
|
-
* Wrap a settings-store client so every read is counted.
|
|
5354
|
-
*
|
|
5355
|
-
* A `Proxy` over the generated tRPC client rather than a hand-written facade:
|
|
5356
|
-
* the client has a procedure per cap method and a facade would need updating
|
|
5357
|
-
* every time the cap gains one - which is how a diagnostic quietly stops seeing
|
|
5358
|
-
* half the traffic. Unrecognised procedures pass straight through.
|
|
5359
|
-
*/
|
|
5360
|
-
function censusStoreClient(client, census) {
|
|
5361
|
-
return new Proxy(client, { get(target, prop, receiver) {
|
|
5362
|
-
const procedure = Reflect.get(target, prop, receiver);
|
|
5363
|
-
if (typeof prop !== "string" || !CENSUSED_PROCEDURES.has(prop)) return procedure;
|
|
5364
|
-
if (!isProxyable(procedure)) return procedure;
|
|
5365
|
-
const op = prop;
|
|
5366
|
-
return new Proxy(procedure, { get(procTarget, procProp, procReceiver) {
|
|
5367
|
-
const call = Reflect.get(procTarget, procProp, procReceiver);
|
|
5368
|
-
if (procProp !== "query" && procProp !== "mutate") return call;
|
|
5369
|
-
if (typeof call !== "function") return call;
|
|
5370
|
-
const wrapped = async (...args) => {
|
|
5371
|
-
const site = census.siteOf(wrapped);
|
|
5372
|
-
const result = await Reflect.apply(call, procTarget, args);
|
|
5373
|
-
census.record(op, site, result);
|
|
5374
|
-
return result;
|
|
5375
|
-
};
|
|
5376
|
-
return wrapped;
|
|
5377
|
-
} });
|
|
5378
|
-
} });
|
|
5379
|
-
}
|
|
5380
5329
|
/** Log the census as one line, including when it collected nothing - silence
|
|
5381
5330
|
* after an arm is indistinguishable from a census that never armed. */
|
|
5382
5331
|
function logCensus(census, logger, topN) {
|
|
@@ -6787,6 +6736,94 @@ Object.freeze({
|
|
|
6787
6736
|
"path": "online",
|
|
6788
6737
|
"valueType": "boolean"
|
|
6789
6738
|
}],
|
|
6739
|
+
"dishwasher": [
|
|
6740
|
+
{
|
|
6741
|
+
"path": "state",
|
|
6742
|
+
"valueType": "enum",
|
|
6743
|
+
"enumValues": [
|
|
6744
|
+
"idle",
|
|
6745
|
+
"running",
|
|
6746
|
+
"paused",
|
|
6747
|
+
"finished",
|
|
6748
|
+
"error",
|
|
6749
|
+
"unavailable"
|
|
6750
|
+
]
|
|
6751
|
+
},
|
|
6752
|
+
{
|
|
6753
|
+
"path": "program",
|
|
6754
|
+
"valueType": "string"
|
|
6755
|
+
},
|
|
6756
|
+
{
|
|
6757
|
+
"path": "remainingMinutes",
|
|
6758
|
+
"valueType": "number"
|
|
6759
|
+
},
|
|
6760
|
+
{
|
|
6761
|
+
"path": "progressPercent",
|
|
6762
|
+
"valueType": "number"
|
|
6763
|
+
},
|
|
6764
|
+
{
|
|
6765
|
+
"path": "doorOpen",
|
|
6766
|
+
"valueType": "boolean"
|
|
6767
|
+
},
|
|
6768
|
+
{
|
|
6769
|
+
"path": "remoteStartAllowed",
|
|
6770
|
+
"valueType": "boolean"
|
|
6771
|
+
},
|
|
6772
|
+
{
|
|
6773
|
+
"path": "powerOn",
|
|
6774
|
+
"valueType": "boolean"
|
|
6775
|
+
},
|
|
6776
|
+
{
|
|
6777
|
+
"path": "saltLevel",
|
|
6778
|
+
"valueType": "enum",
|
|
6779
|
+
"enumValues": [
|
|
6780
|
+
"ok",
|
|
6781
|
+
"low",
|
|
6782
|
+
"empty"
|
|
6783
|
+
]
|
|
6784
|
+
},
|
|
6785
|
+
{
|
|
6786
|
+
"path": "rinseAidLevel",
|
|
6787
|
+
"valueType": "enum",
|
|
6788
|
+
"enumValues": [
|
|
6789
|
+
"ok",
|
|
6790
|
+
"low",
|
|
6791
|
+
"empty"
|
|
6792
|
+
]
|
|
6793
|
+
},
|
|
6794
|
+
{
|
|
6795
|
+
"path": "halfLoad",
|
|
6796
|
+
"valueType": "boolean"
|
|
6797
|
+
},
|
|
6798
|
+
{
|
|
6799
|
+
"path": "hygienePlus",
|
|
6800
|
+
"valueType": "boolean"
|
|
6801
|
+
},
|
|
6802
|
+
{
|
|
6803
|
+
"path": "extraDry",
|
|
6804
|
+
"valueType": "boolean"
|
|
6805
|
+
},
|
|
6806
|
+
{
|
|
6807
|
+
"path": "intensiveZone",
|
|
6808
|
+
"valueType": "boolean"
|
|
6809
|
+
},
|
|
6810
|
+
{
|
|
6811
|
+
"path": "varioSpeedPlus",
|
|
6812
|
+
"valueType": "boolean"
|
|
6813
|
+
},
|
|
6814
|
+
{
|
|
6815
|
+
"path": "childLock",
|
|
6816
|
+
"valueType": "boolean"
|
|
6817
|
+
},
|
|
6818
|
+
{
|
|
6819
|
+
"path": "errorCode",
|
|
6820
|
+
"valueType": "number"
|
|
6821
|
+
},
|
|
6822
|
+
{
|
|
6823
|
+
"path": "errorLabel",
|
|
6824
|
+
"valueType": "string"
|
|
6825
|
+
}
|
|
6826
|
+
],
|
|
6790
6827
|
"doorbell": [{
|
|
6791
6828
|
"path": "lastPressedAt",
|
|
6792
6829
|
"valueType": "number"
|
|
@@ -6877,6 +6914,10 @@ Object.freeze({
|
|
|
6877
6914
|
"path": "detected",
|
|
6878
6915
|
"valueType": "boolean"
|
|
6879
6916
|
}],
|
|
6917
|
+
"heating-valve": [{
|
|
6918
|
+
"path": "position",
|
|
6919
|
+
"valueType": "number"
|
|
6920
|
+
}],
|
|
6880
6921
|
"humidifier": [
|
|
6881
6922
|
{
|
|
6882
6923
|
"path": "on",
|
|
@@ -8369,259 +8410,76 @@ function buildExportArgs(input) {
|
|
|
8369
8410
|
outPath
|
|
8370
8411
|
];
|
|
8371
8412
|
}
|
|
8372
|
-
var
|
|
8373
|
-
var RowMapStore = class RowMapStore {
|
|
8374
|
-
spec;
|
|
8375
|
-
store;
|
|
8376
|
-
logger;
|
|
8377
|
-
constructor(deps) {
|
|
8378
|
-
this.spec = deps.spec;
|
|
8379
|
-
this.store = deps.store;
|
|
8380
|
-
this.logger = deps.logger;
|
|
8381
|
-
}
|
|
8382
|
-
/** The collection this store owns — for the caller's own log lines. */
|
|
8383
|
-
get collection() {
|
|
8384
|
-
return this.spec.collection;
|
|
8385
|
-
}
|
|
8413
|
+
var recorderRecordKinds = require_dist.defineRecordKinds({
|
|
8386
8414
|
/**
|
|
8387
|
-
*
|
|
8388
|
-
*
|
|
8389
|
-
|
|
8390
|
-
|
|
8391
|
-
return store.declareCollection.mutate({
|
|
8392
|
-
collection: spec.collection,
|
|
8393
|
-
columns: [
|
|
8394
|
-
{
|
|
8395
|
-
name: "id",
|
|
8396
|
-
type: "TEXT",
|
|
8397
|
-
primaryKey: true,
|
|
8398
|
-
notNull: true
|
|
8399
|
-
},
|
|
8400
|
-
...spec.columns,
|
|
8401
|
-
{
|
|
8402
|
-
name: ROW_MAP_VALUE_COLUMN,
|
|
8403
|
-
type: "JSON",
|
|
8404
|
-
notNull: true
|
|
8405
|
-
}
|
|
8406
|
-
],
|
|
8407
|
-
...spec.indexes !== void 0 ? { indexes: [...spec.indexes] } : {}
|
|
8408
|
-
});
|
|
8409
|
-
}
|
|
8410
|
-
declare() {
|
|
8411
|
-
return RowMapStore.declare(this.store, this.spec);
|
|
8412
|
-
}
|
|
8413
|
-
/** One entry, by key. `null` when absent OR unparseable (which is said aloud). */
|
|
8414
|
-
async read(key) {
|
|
8415
|
-
const raw = await this.store.get.query({
|
|
8416
|
-
collection: this.spec.collection,
|
|
8417
|
-
key
|
|
8418
|
-
});
|
|
8419
|
-
if (!isRecord(raw)) return null;
|
|
8420
|
-
const row = raw;
|
|
8421
|
-
const parsed = this.spec.schema.safeParse(row[ROW_MAP_VALUE_COLUMN]);
|
|
8422
|
-
if (!parsed.success) {
|
|
8423
|
-
this.logger.warn("row-map row skipped as unparseable", {
|
|
8424
|
-
...this.deviceTags(row),
|
|
8425
|
-
meta: {
|
|
8426
|
-
collection: this.spec.collection,
|
|
8427
|
-
key,
|
|
8428
|
-
error: parsed.error.message
|
|
8429
|
-
}
|
|
8430
|
-
});
|
|
8431
|
-
return null;
|
|
8432
|
-
}
|
|
8433
|
-
return parsed.data;
|
|
8434
|
-
}
|
|
8435
|
-
/**
|
|
8436
|
-
* Every entry, keyed. Unparseable rows are skipped — one bad entry costs that
|
|
8437
|
-
* entry, never the map.
|
|
8438
|
-
*/
|
|
8439
|
-
async readAll() {
|
|
8440
|
-
const records = await this.store.query.query({
|
|
8441
|
-
collection: this.spec.collection,
|
|
8442
|
-
filter: { limit: this.spec.loadLimit }
|
|
8443
|
-
});
|
|
8444
|
-
const out = /* @__PURE__ */ new Map();
|
|
8445
|
-
let skipped = 0;
|
|
8446
|
-
for (const record of records) {
|
|
8447
|
-
const parsed = this.spec.schema.safeParse(record.data[ROW_MAP_VALUE_COLUMN]);
|
|
8448
|
-
if (!parsed.success) {
|
|
8449
|
-
skipped += 1;
|
|
8450
|
-
continue;
|
|
8451
|
-
}
|
|
8452
|
-
out.set(record.id, parsed.data);
|
|
8453
|
-
}
|
|
8454
|
-
if (skipped > 0) this.logger.warn("row-map rows skipped as unparseable", { meta: {
|
|
8455
|
-
collection: this.spec.collection,
|
|
8456
|
-
skipped,
|
|
8457
|
-
kept: out.size
|
|
8458
|
-
} });
|
|
8459
|
-
if (records.length >= this.spec.loadLimit) this.logger.warn("row-map read hit its own bound — the map may be truncated", { meta: {
|
|
8460
|
-
collection: this.spec.collection,
|
|
8461
|
-
loadLimit: this.spec.loadLimit
|
|
8462
|
-
} });
|
|
8463
|
-
return out;
|
|
8464
|
-
}
|
|
8465
|
-
/**
|
|
8466
|
-
* Every entry as it was WRITTEN, unvalidated and unparsed — the read a
|
|
8467
|
-
* MIGRATION needs.
|
|
8468
|
-
*
|
|
8469
|
-
* {@link readAll} answers through the CURRENT schema, which is exactly what a
|
|
8470
|
-
* migration must not do: a retired key is stripped by a non-strict `z.object`
|
|
8471
|
-
* before the migration could see it, so the row it is there to rescue looks
|
|
8472
|
-
* like a row that never needed rescuing. Only a migration should call this;
|
|
8473
|
-
* everything else wants the validated read.
|
|
8474
|
-
*/
|
|
8475
|
-
async readAllRaw() {
|
|
8476
|
-
const records = await this.store.query.query({
|
|
8477
|
-
collection: this.spec.collection,
|
|
8478
|
-
filter: { limit: this.spec.loadLimit }
|
|
8479
|
-
});
|
|
8480
|
-
const out = /* @__PURE__ */ new Map();
|
|
8481
|
-
for (const record of records) out.set(record.id, record.data[ROW_MAP_VALUE_COLUMN]);
|
|
8482
|
-
return out;
|
|
8483
|
-
}
|
|
8484
|
-
/** Insert or replace ONE entry. Validates at the boundary. */
|
|
8485
|
-
async write(key, value) {
|
|
8486
|
-
const validated = this.spec.schema.parse(value);
|
|
8487
|
-
const columns = this.spec.project(key, validated);
|
|
8488
|
-
await this.store.set.mutate({
|
|
8489
|
-
collection: this.spec.collection,
|
|
8490
|
-
key,
|
|
8491
|
-
value: {
|
|
8492
|
-
...columns,
|
|
8493
|
-
[ROW_MAP_VALUE_COLUMN]: validated
|
|
8494
|
-
}
|
|
8495
|
-
});
|
|
8496
|
-
}
|
|
8497
|
-
/** Drop ONE entry. Idempotent. */
|
|
8498
|
-
async remove(key) {
|
|
8499
|
-
await this.store.delete.mutate({
|
|
8500
|
-
collection: this.spec.collection,
|
|
8501
|
-
key
|
|
8502
|
-
});
|
|
8503
|
-
}
|
|
8504
|
-
/**
|
|
8505
|
-
* Make the collection hold exactly `next` — the write path for an owner whose
|
|
8506
|
-
* in-memory map IS the authority (a persister handed the complete set).
|
|
8415
|
+
* One row per export job, inserted by `createExport` and updated as the
|
|
8416
|
+
* render progresses. The system NEVER deletes a row — it is the export AUDIT.
|
|
8417
|
+
* `deleteExport` and the janitor flip `state` and clear the file, keeping
|
|
8418
|
+
* the row. The record's `at` field maps to `createdAt`.
|
|
8507
8419
|
*
|
|
8508
|
-
*
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
dropped: doomed.length,
|
|
8524
|
-
keys: doomed.slice(0, 20).map((r) => r.id),
|
|
8525
|
-
remaining: next.size
|
|
8526
|
-
} });
|
|
8527
|
-
return doomed.length;
|
|
8528
|
-
}
|
|
8420
|
+
* Indexed by `deviceId` to serve the per-device listing efficiently.
|
|
8421
|
+
*/
|
|
8422
|
+
exports: {
|
|
8423
|
+
schema: require_dist.ExportRecordSchema,
|
|
8424
|
+
class: "ledger",
|
|
8425
|
+
writeTrigger: "one row per export job, inserted by createExport; rewritten by the render engine (progress / state patches) and the download hook",
|
|
8426
|
+
device: "none",
|
|
8427
|
+
at: "required",
|
|
8428
|
+
indexes: [{
|
|
8429
|
+
name: "device",
|
|
8430
|
+
fields: ["deviceId"]
|
|
8431
|
+
}],
|
|
8432
|
+
retention: { none: "operator intent — a row is the export AUDIT and survives forever; deleteExport and the janitor flip state and remove the file without removing the row; removeExport is a deliberate operator purge" },
|
|
8433
|
+
adoptsFrom: "recorder:exports"
|
|
8434
|
+
},
|
|
8529
8435
|
/**
|
|
8530
|
-
*
|
|
8531
|
-
*
|
|
8532
|
-
*
|
|
8533
|
-
*
|
|
8534
|
-
* parse of the whole map followed by a filter in JS.
|
|
8436
|
+
* A bounded ring of per-operation audit rows. One row per recordings
|
|
8437
|
+
* operation (prune, evict, relocate, export, …), appended best-effort by
|
|
8438
|
+
* `createRecordingOpsLogSink().append`. The ring is trimmed to the newest
|
|
8439
|
+
* `OPS_LOG_RING_DEFAULT_MAX` (500) rows after every append.
|
|
8535
8440
|
*
|
|
8536
|
-
*
|
|
8537
|
-
*
|
|
8538
|
-
*/
|
|
8539
|
-
async readWhere(filter) {
|
|
8540
|
-
const records = await this.store.query.query({
|
|
8541
|
-
collection: this.spec.collection,
|
|
8542
|
-
filter: {
|
|
8543
|
-
...filter.where !== void 0 ? { where: filter.where } : {},
|
|
8544
|
-
...filter.whereIn !== void 0 ? { whereIn: filter.whereIn } : {},
|
|
8545
|
-
...filter.whereBetween !== void 0 ? { whereBetween: filter.whereBetween } : {},
|
|
8546
|
-
...filter.orderBy !== void 0 ? { orderBy: filter.orderBy } : {},
|
|
8547
|
-
...filter.offset !== void 0 ? { offset: filter.offset } : {},
|
|
8548
|
-
limit: filter.limit ?? this.spec.loadLimit
|
|
8549
|
-
}
|
|
8550
|
-
});
|
|
8551
|
-
const out = [];
|
|
8552
|
-
let skipped = 0;
|
|
8553
|
-
for (const record of records) {
|
|
8554
|
-
const parsed = this.spec.schema.safeParse(record.data[ROW_MAP_VALUE_COLUMN]);
|
|
8555
|
-
if (!parsed.success) {
|
|
8556
|
-
skipped += 1;
|
|
8557
|
-
continue;
|
|
8558
|
-
}
|
|
8559
|
-
out.push({
|
|
8560
|
-
key: record.id,
|
|
8561
|
-
value: parsed.data
|
|
8562
|
-
});
|
|
8563
|
-
}
|
|
8564
|
-
if (skipped > 0) this.logger.warn("row-map rows skipped as unparseable", { meta: {
|
|
8565
|
-
collection: this.spec.collection,
|
|
8566
|
-
skipped,
|
|
8567
|
-
kept: out.length
|
|
8568
|
-
} });
|
|
8569
|
-
return out;
|
|
8570
|
-
}
|
|
8571
|
-
/** How many rows match, without materialising one. */
|
|
8572
|
-
async count(filter) {
|
|
8573
|
-
return this.store.count.query({
|
|
8574
|
-
collection: this.spec.collection,
|
|
8575
|
-
...filter !== void 0 ? { filter: {
|
|
8576
|
-
...filter.where !== void 0 ? { where: filter.where } : {},
|
|
8577
|
-
...filter.whereIn !== void 0 ? { whereIn: filter.whereIn } : {},
|
|
8578
|
-
...filter.whereBetween !== void 0 ? { whereBetween: filter.whereBetween } : {}
|
|
8579
|
-
} } : {}
|
|
8580
|
-
});
|
|
8581
|
-
}
|
|
8582
|
-
/**
|
|
8583
|
-
* Delete every row matching a predicate, in ONE statement. Returns how many
|
|
8584
|
-
* went, and says so — a retention trim that silently drops rows is
|
|
8585
|
-
* indistinguishable from one that had nothing to do.
|
|
8441
|
+
* Indexed by `deviceId` so a per-device listing is an indexed query, not a
|
|
8442
|
+
* full scan of the ring.
|
|
8586
8443
|
*
|
|
8587
|
-
* The
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
8596
|
-
|
|
8597
|
-
|
|
8598
|
-
|
|
8599
|
-
|
|
8600
|
-
|
|
8601
|
-
return deleted;
|
|
8602
|
-
}
|
|
8603
|
-
/** `tags: { deviceId }` off the PROJECTED column, when the spec names one. */
|
|
8604
|
-
deviceTags(row) {
|
|
8605
|
-
const column = this.spec.deviceIdColumn;
|
|
8606
|
-
if (column === void 0) return {};
|
|
8607
|
-
const raw = row[column];
|
|
8608
|
-
return typeof raw === "number" && Number.isInteger(raw) ? { tags: { deviceId: raw } } : {};
|
|
8444
|
+
* The record's `at` field maps to `OpsLogEntry.at`.
|
|
8445
|
+
*/
|
|
8446
|
+
"ops-log": {
|
|
8447
|
+
schema: require_dist.OpsLogEntrySchema,
|
|
8448
|
+
class: "ledger",
|
|
8449
|
+
writeTrigger: "one row per recordings operation (prune, evict, relocate, export, …), appended best-effort by createRecordingOpsLogSink().append; the ring is trimmed to the newest 500 rows in application code after every write",
|
|
8450
|
+
device: "none",
|
|
8451
|
+
at: "required",
|
|
8452
|
+
indexes: [{
|
|
8453
|
+
name: "device",
|
|
8454
|
+
fields: ["deviceId"]
|
|
8455
|
+
}],
|
|
8456
|
+
retention: { none: "a bounded ring of the newest 500 rows; older rows are removed in application code (trimOpsLog) after each append — owner-pruned" },
|
|
8457
|
+
adoptsFrom: "recorder:ops-log"
|
|
8609
8458
|
}
|
|
8610
|
-
};
|
|
8611
|
-
/** A store payload that is a plain object, or `null`. A type guard, not a cast. */
|
|
8612
|
-
function isRecord(value) {
|
|
8613
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8614
|
-
}
|
|
8459
|
+
});
|
|
8615
8460
|
//#endregion
|
|
8616
8461
|
//#region src/recorder/addon/export-store.ts
|
|
8617
8462
|
/**
|
|
8618
8463
|
* Recording-export persistence for the recorder addon.
|
|
8619
8464
|
*
|
|
8620
|
-
* ONE ROW PER EXPORT in `recorder
|
|
8621
|
-
* rows are never removed by the system
|
|
8622
|
-
* flip `state` and clear the file,
|
|
8465
|
+
* ONE ROW PER EXPORT in the `recorder` / `exports` record kind, through a
|
|
8466
|
+
* {@link RecordKindHandle}. History rows are never removed by the system
|
|
8467
|
+
* (audit) — `deleteExport` / the janitor flip `state` and clear the file,
|
|
8468
|
+
* keeping the row.
|
|
8469
|
+
*
|
|
8470
|
+
* ## Records migration (phase 2, step 25)
|
|
8623
8471
|
*
|
|
8624
|
-
*
|
|
8472
|
+
* The store was previously backed by a {@link RowMapStore} over a SettingsStore
|
|
8473
|
+
* collection named `recorder:exports`. It is now backed by a
|
|
8474
|
+
* `RecordKindHandle<ExportRecord>` obtained from `ctx.records`. The record's
|
|
8475
|
+
* `at` field maps to `ExportRecord.createdAt`.
|
|
8476
|
+
*
|
|
8477
|
+
* The per-ID serialiser ({@link serialize}) is preserved: two overlapping
|
|
8478
|
+
* patches on the SAME export row are still read-modify-write, so they must
|
|
8479
|
+
* be ordered. The `RecordKindHandle` (an object) is used as the WeakMap key,
|
|
8480
|
+
* keeping the same compaction semantics.
|
|
8481
|
+
*
|
|
8482
|
+
* ## What the RowMapStore replaced, and why it mattered
|
|
8625
8483
|
*
|
|
8626
8484
|
* Every job used to live in one key of the recorder's `addon-settings` / `root`
|
|
8627
8485
|
* blob — 201 833 bytes on the reference hub, of which the export history was
|
|
@@ -8636,11 +8494,6 @@ function isRecord(value) {
|
|
|
8636
8494
|
* erased, and the poll answered `unknown export "547ea182-…"`; the scheduler
|
|
8637
8495
|
* read the vanished row as a failed render and re-queued forever. A per-row
|
|
8638
8496
|
* upsert makes that class of loss unrepresentable — a write names ONE row.
|
|
8639
|
-
*
|
|
8640
|
-
* What is still needed is a per-ID serialiser: two patches to the SAME export
|
|
8641
|
-
* still read-modify-write the same row. That is what {@link serialize} bounds,
|
|
8642
|
-
* and it is now per (store, id) rather than per store, so two exports never
|
|
8643
|
-
* queue behind each other.
|
|
8644
8497
|
*/
|
|
8645
8498
|
/**
|
|
8646
8499
|
* The storage class export MP4s live in: `artifacts`, SHARED with the
|
|
@@ -8685,76 +8538,26 @@ function exportOutPathFor(root, deviceId, exportId) {
|
|
|
8685
8538
|
return node_path.default.join(exportTenantDirFor(root, deviceId), `${exportId}.mp4`);
|
|
8686
8539
|
}
|
|
8687
8540
|
/**
|
|
8688
|
-
*
|
|
8689
|
-
*
|
|
8690
|
-
*
|
|
8691
|
-
* retention="the SYSTEM never deletes a row — it is the export AUDIT. `deleteExport`
|
|
8692
|
-
* and the janitor flip `state` to 'deleted'/'expired' and remove the FILE, keeping
|
|
8693
|
-
* the row. `removeExport` exists for an operator-driven purge and nothing calls it
|
|
8694
|
-
* on a schedule."
|
|
8541
|
+
* Bound for the whole-history read. Explicit because a records query without
|
|
8542
|
+
* an explicit limit uses the default cap (100); an export list that silently
|
|
8543
|
+
* loses its oldest entries looks exactly like a purge.
|
|
8695
8544
|
*/
|
|
8696
|
-
var
|
|
8545
|
+
var RECORDING_EXPORTS_LIMIT = 2e4;
|
|
8697
8546
|
/**
|
|
8698
|
-
*
|
|
8699
|
-
*
|
|
8700
|
-
*
|
|
8547
|
+
* Obtain the `recorder` / `exports` record kind handle.
|
|
8548
|
+
* `ctx.records.declare(recorderRecordKinds)` MUST have run before any read or
|
|
8549
|
+
* write (called at boot in `onInitialize`).
|
|
8701
8550
|
*/
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
collection: RECORDING_EXPORTS_COLLECTION,
|
|
8705
|
-
schema: require_dist.ExportRecordSchema,
|
|
8706
|
-
columns: [
|
|
8707
|
-
{
|
|
8708
|
-
name: "deviceId",
|
|
8709
|
-
type: "INTEGER",
|
|
8710
|
-
notNull: true
|
|
8711
|
-
},
|
|
8712
|
-
{
|
|
8713
|
-
name: "createdAt",
|
|
8714
|
-
type: "INTEGER",
|
|
8715
|
-
notNull: true
|
|
8716
|
-
},
|
|
8717
|
-
{
|
|
8718
|
-
name: "state",
|
|
8719
|
-
type: "TEXT",
|
|
8720
|
-
notNull: true
|
|
8721
|
-
}
|
|
8722
|
-
],
|
|
8723
|
-
indexes: [{
|
|
8724
|
-
name: "idx_recorder_exports_device",
|
|
8725
|
-
columns: ["deviceId"]
|
|
8726
|
-
}, {
|
|
8727
|
-
name: "idx_recorder_exports_created",
|
|
8728
|
-
columns: ["createdAt"]
|
|
8729
|
-
}],
|
|
8730
|
-
project: (_key, value) => ({
|
|
8731
|
-
deviceId: value.deviceId,
|
|
8732
|
-
createdAt: value.createdAt,
|
|
8733
|
-
state: value.state
|
|
8734
|
-
}),
|
|
8735
|
-
deviceIdColumn: "deviceId",
|
|
8736
|
-
loadLimit: RECORDING_EXPORTS_LIMIT
|
|
8737
|
-
};
|
|
8738
|
-
/** Build the handle. `declare()` MUST run once at boot before any read/write. */
|
|
8739
|
-
function recordingExportsState(store, logger) {
|
|
8740
|
-
return new RowMapStore({
|
|
8741
|
-
spec: RECORDING_EXPORTS_SPEC,
|
|
8742
|
-
store,
|
|
8743
|
-
logger
|
|
8744
|
-
});
|
|
8551
|
+
function recordingExportsState(records) {
|
|
8552
|
+
return records.kind(recorderRecordKinds["exports"]);
|
|
8745
8553
|
}
|
|
8746
8554
|
/**
|
|
8747
8555
|
* One in-flight mutation chain per (handle, export id).
|
|
8748
8556
|
*
|
|
8749
|
-
*
|
|
8750
|
-
*
|
|
8751
|
-
*
|
|
8752
|
-
*
|
|
8753
|
-
* unrelated exports fully concurrent (the old chain was per HANDLE, so a slow
|
|
8754
|
-
* patch queued every other export behind it).
|
|
8755
|
-
*
|
|
8756
|
-
* A `WeakMap` keyed by the handle keeps a disposed store collectable; the inner
|
|
8757
|
-
* map is keyed by export id.
|
|
8557
|
+
* Two overlapping patches on the SAME export still read-modify-write one row.
|
|
8558
|
+
* Serialising per id keeps them atomic while leaving unrelated exports fully
|
|
8559
|
+
* concurrent. A `WeakMap` keyed by the handle keeps a disposed handle
|
|
8560
|
+
* collectable; the inner map is keyed by export id.
|
|
8758
8561
|
*/
|
|
8759
8562
|
var mutationChains = /* @__PURE__ */ new WeakMap();
|
|
8760
8563
|
/** Run `work` after every mutation already queued for `(state, id)`, never before. */
|
|
@@ -8774,7 +8577,11 @@ async function serialize(state, id, work) {
|
|
|
8774
8577
|
async function upsertExport(state, record) {
|
|
8775
8578
|
const validated = require_dist.ExportRecordSchema.parse(record);
|
|
8776
8579
|
return serialize(state, validated.id, async () => {
|
|
8777
|
-
await state.
|
|
8580
|
+
await state.put({
|
|
8581
|
+
id: validated.id,
|
|
8582
|
+
at: validated.createdAt,
|
|
8583
|
+
data: validated
|
|
8584
|
+
});
|
|
8778
8585
|
return validated;
|
|
8779
8586
|
});
|
|
8780
8587
|
}
|
|
@@ -8784,13 +8591,17 @@ async function upsertExport(state, record) {
|
|
|
8784
8591
|
*/
|
|
8785
8592
|
async function patchExport(state, id, patch) {
|
|
8786
8593
|
return serialize(state, id, async () => {
|
|
8787
|
-
const
|
|
8788
|
-
if (
|
|
8594
|
+
const row = await state.get(id);
|
|
8595
|
+
if (row === null) return null;
|
|
8789
8596
|
const merged = require_dist.ExportRecordSchema.parse({
|
|
8790
|
-
...
|
|
8597
|
+
...row.data,
|
|
8791
8598
|
...patch
|
|
8792
8599
|
});
|
|
8793
|
-
await state.
|
|
8600
|
+
await state.put({
|
|
8601
|
+
id,
|
|
8602
|
+
at: merged.createdAt,
|
|
8603
|
+
data: merged
|
|
8604
|
+
});
|
|
8794
8605
|
return merged;
|
|
8795
8606
|
});
|
|
8796
8607
|
}
|
|
@@ -8801,17 +8612,21 @@ async function patchExport(state, id, patch) {
|
|
|
8801
8612
|
*/
|
|
8802
8613
|
async function patchExportUnless(state, id, patch, refuse) {
|
|
8803
8614
|
return serialize(state, id, async () => {
|
|
8804
|
-
const
|
|
8805
|
-
if (
|
|
8806
|
-
if (refuse(
|
|
8615
|
+
const row = await state.get(id);
|
|
8616
|
+
if (row === null) return { kind: "missing" };
|
|
8617
|
+
if (refuse(row.data)) return {
|
|
8807
8618
|
kind: "refused",
|
|
8808
|
-
record:
|
|
8619
|
+
record: row.data
|
|
8809
8620
|
};
|
|
8810
8621
|
const merged = require_dist.ExportRecordSchema.parse({
|
|
8811
|
-
...
|
|
8622
|
+
...row.data,
|
|
8812
8623
|
...patch
|
|
8813
8624
|
});
|
|
8814
|
-
await state.
|
|
8625
|
+
await state.put({
|
|
8626
|
+
id,
|
|
8627
|
+
at: merged.createdAt,
|
|
8628
|
+
data: merged
|
|
8629
|
+
});
|
|
8815
8630
|
return {
|
|
8816
8631
|
kind: "patched",
|
|
8817
8632
|
record: merged
|
|
@@ -8820,24 +8635,22 @@ async function patchExportUnless(state, id, patch, refuse) {
|
|
|
8820
8635
|
}
|
|
8821
8636
|
/** Read one export row by id — ONE keyed row read. */
|
|
8822
8637
|
async function getExportRecord(state, id) {
|
|
8823
|
-
|
|
8638
|
+
const row = await state.get(id);
|
|
8639
|
+
return row === null ? null : row.data;
|
|
8824
8640
|
}
|
|
8825
8641
|
/**
|
|
8826
|
-
* List export rows, newest-first
|
|
8642
|
+
* List export rows, newest-first (by `createdAt` / record `at`), optionally
|
|
8643
|
+
* scoped to one device.
|
|
8827
8644
|
*
|
|
8828
|
-
* Scoped to a device this is an INDEXED query on the
|
|
8829
|
-
*
|
|
8830
|
-
* in JS.
|
|
8645
|
+
* Scoped to a device this is an INDEXED query on the `device` index — not a
|
|
8646
|
+
* parse of every export in the installation followed by a filter in JS.
|
|
8831
8647
|
*/
|
|
8832
8648
|
async function listExportsFor(state, deviceId) {
|
|
8833
|
-
return (await state.
|
|
8649
|
+
return (await state.query({
|
|
8834
8650
|
...deviceId === void 0 ? {} : { where: { deviceId } },
|
|
8835
|
-
|
|
8836
|
-
field: "createdAt",
|
|
8837
|
-
direction: "desc"
|
|
8838
|
-
},
|
|
8651
|
+
desc: true,
|
|
8839
8652
|
limit: RECORDING_EXPORTS_LIMIT
|
|
8840
|
-
})).map((
|
|
8653
|
+
})).records.map((r) => r.data);
|
|
8841
8654
|
}
|
|
8842
8655
|
//#endregion
|
|
8843
8656
|
//#region src/recorder/addon/export-timelapse-chunks.ts
|
|
@@ -11795,36 +11608,125 @@ function runFfmpeg(renderDeps, ffmpegPath, args, ext) {
|
|
|
11795
11608
|
});
|
|
11796
11609
|
}
|
|
11797
11610
|
//#endregion
|
|
11611
|
+
//#region src/durable/durable-legacy-kind-handle.ts
|
|
11612
|
+
function notImpl(method) {
|
|
11613
|
+
throw new Error(`DurableLegacyKindHandle.${method}() is not available on the legacy (agent) path`);
|
|
11614
|
+
}
|
|
11615
|
+
function isRecord(value) {
|
|
11616
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11617
|
+
}
|
|
11618
|
+
function toRecordRow(id, data) {
|
|
11619
|
+
return {
|
|
11620
|
+
id,
|
|
11621
|
+
data,
|
|
11622
|
+
deviceId: null,
|
|
11623
|
+
at: null,
|
|
11624
|
+
expiresAt: null,
|
|
11625
|
+
rev: 0,
|
|
11626
|
+
updatedAt: 0
|
|
11627
|
+
};
|
|
11628
|
+
}
|
|
11629
|
+
var DurableLegacyKindHandle = class {
|
|
11630
|
+
name;
|
|
11631
|
+
spec;
|
|
11632
|
+
store;
|
|
11633
|
+
constructor(opts) {
|
|
11634
|
+
this.name = opts.spec.collection;
|
|
11635
|
+
this.spec = opts.spec;
|
|
11636
|
+
this.store = opts.store;
|
|
11637
|
+
}
|
|
11638
|
+
async get(id) {
|
|
11639
|
+
const row = (await this.store.query.query({
|
|
11640
|
+
collection: this.spec.collection,
|
|
11641
|
+
filter: {
|
|
11642
|
+
where: { [this.pkColumn()]: id },
|
|
11643
|
+
limit: 1
|
|
11644
|
+
}
|
|
11645
|
+
}))[0];
|
|
11646
|
+
if (row === void 0 || !isRecord(row.data)) return null;
|
|
11647
|
+
const parsed = this.spec.fromRecord(row.id, row.data);
|
|
11648
|
+
if (parsed === null) return null;
|
|
11649
|
+
return toRecordRow(row.id, this.spec.toValue(parsed));
|
|
11650
|
+
}
|
|
11651
|
+
async query(q) {
|
|
11652
|
+
const rows = await this.store.query.query({
|
|
11653
|
+
collection: this.spec.collection,
|
|
11654
|
+
filter: { limit: q.limit ?? 1e4 }
|
|
11655
|
+
});
|
|
11656
|
+
const records = [];
|
|
11657
|
+
for (const row of rows) {
|
|
11658
|
+
if (!isRecord(row.data)) continue;
|
|
11659
|
+
const parsed = this.spec.fromRecord(row.id, row.data);
|
|
11660
|
+
if (parsed === null) continue;
|
|
11661
|
+
records.push(toRecordRow(row.id, this.spec.toValue(parsed)));
|
|
11662
|
+
}
|
|
11663
|
+
return {
|
|
11664
|
+
records,
|
|
11665
|
+
nextCursor: null
|
|
11666
|
+
};
|
|
11667
|
+
}
|
|
11668
|
+
async put(input) {
|
|
11669
|
+
await this.store.set.mutate({
|
|
11670
|
+
collection: this.spec.collection,
|
|
11671
|
+
key: input.id,
|
|
11672
|
+
value: input.data
|
|
11673
|
+
});
|
|
11674
|
+
return { rev: 0 };
|
|
11675
|
+
}
|
|
11676
|
+
async delete(id) {
|
|
11677
|
+
await this.store.delete.mutate({
|
|
11678
|
+
collection: this.spec.collection,
|
|
11679
|
+
key: id
|
|
11680
|
+
});
|
|
11681
|
+
return true;
|
|
11682
|
+
}
|
|
11683
|
+
getMany = () => notImpl("getMany");
|
|
11684
|
+
getResolved = () => notImpl("getResolved");
|
|
11685
|
+
putMany = () => notImpl("putMany");
|
|
11686
|
+
patch = () => notImpl("patch");
|
|
11687
|
+
deleteMany = () => notImpl("deleteMany");
|
|
11688
|
+
count = () => notImpl("count");
|
|
11689
|
+
histogram = () => notImpl("histogram");
|
|
11690
|
+
aggregate = () => notImpl("aggregate");
|
|
11691
|
+
deleteWhere = () => notImpl("deleteWhere");
|
|
11692
|
+
allocateId = () => notImpl("allocateId");
|
|
11693
|
+
pkColumn() {
|
|
11694
|
+
return this.spec.columns.find((c) => c.primaryKey === true)?.name ?? "id";
|
|
11695
|
+
}
|
|
11696
|
+
};
|
|
11697
|
+
/** Legacy settings-store handle for one {@link DurableLedgerSpec}. */
|
|
11698
|
+
function durableLegacyKind(spec, store) {
|
|
11699
|
+
return new DurableLegacyKindHandle({
|
|
11700
|
+
spec,
|
|
11701
|
+
store
|
|
11702
|
+
});
|
|
11703
|
+
}
|
|
11704
|
+
/** Agent boot: declare the SQLite collection behind a durable ledger spec. */
|
|
11705
|
+
function declareDurableLegacyCollection(store, spec) {
|
|
11706
|
+
return store.declareCollection.mutate({
|
|
11707
|
+
collection: spec.collection,
|
|
11708
|
+
columns: [...spec.columns],
|
|
11709
|
+
...spec.indexes !== void 0 ? { indexes: [...spec.indexes] } : {}
|
|
11710
|
+
});
|
|
11711
|
+
}
|
|
11712
|
+
//#endregion
|
|
11798
11713
|
//#region src/durable/durable-ledger.ts
|
|
11799
11714
|
/** Default reseed cap — every current consumer's row set is installation-bounded. */
|
|
11800
11715
|
var DEFAULT_LOAD_LIMIT = 1e5;
|
|
11801
|
-
|
|
11716
|
+
/** Page size for records reseed queries. */
|
|
11717
|
+
var QUERY_PAGE = 1e3;
|
|
11718
|
+
var DurableLedger = class {
|
|
11802
11719
|
mirror = /* @__PURE__ */ new Map();
|
|
11803
11720
|
/** Whether ANY load has read the store successfully — see {@link hasAnswered}. */
|
|
11804
11721
|
answered = false;
|
|
11805
11722
|
spec;
|
|
11806
|
-
|
|
11723
|
+
kind;
|
|
11807
11724
|
logger;
|
|
11808
11725
|
constructor(deps) {
|
|
11809
11726
|
this.spec = deps.spec;
|
|
11810
|
-
this.
|
|
11727
|
+
this.kind = deps.kind;
|
|
11811
11728
|
this.logger = deps.logger;
|
|
11812
11729
|
}
|
|
11813
|
-
/**
|
|
11814
|
-
* Register the collection. MUST run at boot, before any read or write: the
|
|
11815
|
-
* SQLite backend answers 412 for an undeclared collection and takes the whole
|
|
11816
|
-
* runner down with it (the addon-ai boot-crash lesson).
|
|
11817
|
-
*/
|
|
11818
|
-
static declare(store, spec) {
|
|
11819
|
-
return store.declareCollection.mutate({
|
|
11820
|
-
collection: spec.collection,
|
|
11821
|
-
columns: [...spec.columns],
|
|
11822
|
-
...spec.indexes !== void 0 ? { indexes: [...spec.indexes] } : {}
|
|
11823
|
-
});
|
|
11824
|
-
}
|
|
11825
|
-
declare() {
|
|
11826
|
-
return DurableLedger.declare(this.store, this.spec);
|
|
11827
|
-
}
|
|
11828
11730
|
/** The collection this ledger owns — for the caller's own log lines. */
|
|
11829
11731
|
get collection() {
|
|
11830
11732
|
return this.spec.collection;
|
|
@@ -11841,31 +11743,43 @@ var DurableLedger = class DurableLedger {
|
|
|
11841
11743
|
*/
|
|
11842
11744
|
async load() {
|
|
11843
11745
|
try {
|
|
11844
|
-
const records = await this.store.query.query({
|
|
11845
|
-
collection: this.spec.collection,
|
|
11846
|
-
filter: { limit: this.spec.loadLimit ?? DEFAULT_LOAD_LIMIT }
|
|
11847
|
-
});
|
|
11848
11746
|
const next = /* @__PURE__ */ new Map();
|
|
11849
11747
|
let skipped = 0;
|
|
11850
|
-
|
|
11851
|
-
|
|
11852
|
-
|
|
11853
|
-
|
|
11854
|
-
|
|
11748
|
+
let seen = 0;
|
|
11749
|
+
const cap = this.spec.loadLimit ?? DEFAULT_LOAD_LIMIT;
|
|
11750
|
+
let cursor;
|
|
11751
|
+
for (;;) {
|
|
11752
|
+
const limit = Math.min(QUERY_PAGE, cap - seen);
|
|
11753
|
+
if (limit <= 0) break;
|
|
11754
|
+
const page = await this.kind.query({
|
|
11755
|
+
limit,
|
|
11756
|
+
...cursor === void 0 ? {} : { cursor }
|
|
11757
|
+
});
|
|
11758
|
+
seen += page.records.length;
|
|
11759
|
+
for (const record of page.records) {
|
|
11760
|
+
const row = this.spec.fromRecord(record.id, record.data);
|
|
11761
|
+
if (row === null) {
|
|
11762
|
+
skipped += 1;
|
|
11763
|
+
continue;
|
|
11764
|
+
}
|
|
11765
|
+
next.set(this.spec.keyOf(row), row);
|
|
11855
11766
|
}
|
|
11856
|
-
|
|
11767
|
+
if (page.nextCursor === null) break;
|
|
11768
|
+
cursor = page.nextCursor;
|
|
11857
11769
|
}
|
|
11858
11770
|
this.mirror.clear();
|
|
11859
11771
|
for (const [key, row] of next) this.mirror.set(key, row);
|
|
11860
11772
|
this.answered = true;
|
|
11861
11773
|
if (skipped > 0) this.logger.warn("durable rows skipped as malformed — they gate NOTHING", { meta: {
|
|
11862
11774
|
collection: this.spec.collection,
|
|
11775
|
+
kind: this.kind.name,
|
|
11863
11776
|
skipped
|
|
11864
11777
|
} });
|
|
11865
11778
|
return [...this.mirror.values()];
|
|
11866
11779
|
} catch (err) {
|
|
11867
11780
|
this.logger.warn("durable load failed — keeping the state already in memory", { meta: {
|
|
11868
11781
|
collection: this.spec.collection,
|
|
11782
|
+
kind: this.kind.name,
|
|
11869
11783
|
error: String(err),
|
|
11870
11784
|
held: this.mirror.size
|
|
11871
11785
|
} });
|
|
@@ -11930,16 +11844,13 @@ var DurableLedger = class DurableLedger {
|
|
|
11930
11844
|
*/
|
|
11931
11845
|
async put(row) {
|
|
11932
11846
|
const key = this.spec.keyOf(row);
|
|
11847
|
+
const body = this.spec.toValue(row);
|
|
11933
11848
|
if (this.spec.writeMode === "write-behind") {
|
|
11934
11849
|
this.mirror.set(key, row);
|
|
11935
|
-
await this.
|
|
11850
|
+
await this.persistRow(key, body, row);
|
|
11936
11851
|
return;
|
|
11937
11852
|
}
|
|
11938
|
-
await this.
|
|
11939
|
-
collection: this.spec.collection,
|
|
11940
|
-
key,
|
|
11941
|
-
value: this.spec.toValue(row)
|
|
11942
|
-
});
|
|
11853
|
+
await this.kind.put(this.buildPut(key, body, row));
|
|
11943
11854
|
this.mirror.set(key, row);
|
|
11944
11855
|
}
|
|
11945
11856
|
/**
|
|
@@ -11979,13 +11890,11 @@ var DurableLedger = class DurableLedger {
|
|
|
11979
11890
|
async forget(key) {
|
|
11980
11891
|
this.mirror.delete(key);
|
|
11981
11892
|
try {
|
|
11982
|
-
await this.
|
|
11983
|
-
collection: this.spec.collection,
|
|
11984
|
-
key
|
|
11985
|
-
});
|
|
11893
|
+
await this.kind.delete(key);
|
|
11986
11894
|
} catch (err) {
|
|
11987
11895
|
this.logger.debug("durable delete failed", { meta: {
|
|
11988
11896
|
collection: this.spec.collection,
|
|
11897
|
+
kind: this.kind.name,
|
|
11989
11898
|
key,
|
|
11990
11899
|
error: String(err)
|
|
11991
11900
|
} });
|
|
@@ -12021,15 +11930,13 @@ var DurableLedger = class DurableLedger {
|
|
|
12021
11930
|
for (const key of [...this.mirror.keys()]) {
|
|
12022
11931
|
if (activeKeys.has(key)) continue;
|
|
12023
11932
|
try {
|
|
12024
|
-
await this.
|
|
12025
|
-
collection: this.spec.collection,
|
|
12026
|
-
key
|
|
12027
|
-
});
|
|
11933
|
+
await this.kind.delete(key);
|
|
12028
11934
|
this.mirror.delete(key);
|
|
12029
11935
|
pruned += 1;
|
|
12030
11936
|
} catch (err) {
|
|
12031
11937
|
this.logger.debug("durable prune delete failed", { meta: {
|
|
12032
11938
|
collection: this.spec.collection,
|
|
11939
|
+
kind: this.kind.name,
|
|
12033
11940
|
key,
|
|
12034
11941
|
error: String(err)
|
|
12035
11942
|
} });
|
|
@@ -12042,25 +11949,46 @@ var DurableLedger = class DurableLedger {
|
|
|
12042
11949
|
* decision path: the mirror already holds the truth for this process, and the
|
|
12043
11950
|
* worst a lost write can do is one silent re-seed after the next restart.
|
|
12044
11951
|
*/
|
|
12045
|
-
|
|
12046
|
-
const
|
|
11952
|
+
persist(row) {
|
|
11953
|
+
const key = this.spec.keyOf(row);
|
|
11954
|
+
this.persistRow(key, this.spec.toValue(row), row);
|
|
11955
|
+
}
|
|
11956
|
+
buildPut(key, body, row) {
|
|
11957
|
+
const deviceId = this.resolveRecordDeviceId(row, body);
|
|
11958
|
+
return {
|
|
11959
|
+
id: key,
|
|
11960
|
+
data: body,
|
|
11961
|
+
...deviceId !== void 0 ? { deviceId } : {}
|
|
11962
|
+
};
|
|
11963
|
+
}
|
|
11964
|
+
resolveRecordDeviceId(row, body) {
|
|
11965
|
+
if (row !== void 0) {
|
|
11966
|
+
const fromSpec = this.spec.deviceIdOf?.(row);
|
|
11967
|
+
if (fromSpec !== void 0 && Number.isFinite(fromSpec)) return fromSpec;
|
|
11968
|
+
}
|
|
11969
|
+
return this.deviceIdFromBody(body);
|
|
11970
|
+
}
|
|
11971
|
+
async persistRow(key, body, row) {
|
|
11972
|
+
const deviceId = this.resolveRecordDeviceId(row, body);
|
|
12047
11973
|
try {
|
|
12048
|
-
await this.
|
|
12049
|
-
collection: this.spec.collection,
|
|
12050
|
-
key: this.spec.keyOf(row),
|
|
12051
|
-
value: this.spec.toValue(row)
|
|
12052
|
-
});
|
|
11974
|
+
await this.kind.put(this.buildPut(key, body, row));
|
|
12053
11975
|
} catch (err) {
|
|
12054
11976
|
this.logger.warn("durable persist failed — this key may re-seed on boot", {
|
|
12055
11977
|
...deviceId !== void 0 ? { tags: { deviceId } } : {},
|
|
12056
11978
|
meta: {
|
|
12057
11979
|
collection: this.spec.collection,
|
|
12058
|
-
|
|
11980
|
+
kind: this.kind.name,
|
|
11981
|
+
key,
|
|
12059
11982
|
error: String(err)
|
|
12060
11983
|
}
|
|
12061
11984
|
});
|
|
12062
11985
|
}
|
|
12063
11986
|
}
|
|
11987
|
+
deviceIdFromBody(body) {
|
|
11988
|
+
const raw = body["deviceId"];
|
|
11989
|
+
const n = typeof raw === "number" ? raw : Number(raw);
|
|
11990
|
+
return Number.isFinite(n) ? n : void 0;
|
|
11991
|
+
}
|
|
12064
11992
|
};
|
|
12065
11993
|
//#endregion
|
|
12066
11994
|
//#region src/recorder/addon/segment-hour-ledger.ts
|
|
@@ -12621,15 +12549,15 @@ var SegmentHourLedger = class SegmentHourLedger {
|
|
|
12621
12549
|
this.rowCap = deps.loadPacing?.rowCap ?? 25e4;
|
|
12622
12550
|
this.ledger = new DurableLedger({
|
|
12623
12551
|
spec: SPEC,
|
|
12624
|
-
|
|
12552
|
+
kind: durableLegacyKind(SPEC, deps.store),
|
|
12625
12553
|
logger: deps.logger
|
|
12626
12554
|
});
|
|
12627
12555
|
}
|
|
12628
12556
|
static declare(store) {
|
|
12629
|
-
return
|
|
12557
|
+
return declareDurableLegacyCollection(store, SPEC);
|
|
12630
12558
|
}
|
|
12631
12559
|
declare() {
|
|
12632
|
-
return this.
|
|
12560
|
+
return declareDurableLegacyCollection(this.store, SPEC);
|
|
12633
12561
|
}
|
|
12634
12562
|
/**
|
|
12635
12563
|
* Boot load — every hour row, in one query.
|
|
@@ -15040,63 +14968,61 @@ var LocationRefresher = class {
|
|
|
15040
14968
|
}
|
|
15041
14969
|
}
|
|
15042
14970
|
};
|
|
15043
|
-
|
|
15044
|
-
|
|
15045
|
-
|
|
15046
|
-
|
|
15047
|
-
|
|
15048
|
-
|
|
15049
|
-
|
|
15050
|
-
|
|
15051
|
-
|
|
15052
|
-
|
|
15053
|
-
|
|
15054
|
-
|
|
15055
|
-
|
|
15056
|
-
|
|
15057
|
-
|
|
15058
|
-
|
|
15059
|
-
|
|
15060
|
-
|
|
15061
|
-
|
|
15062
|
-
|
|
15063
|
-
|
|
15064
|
-
|
|
15065
|
-
|
|
15066
|
-
|
|
15067
|
-
|
|
15068
|
-
|
|
15069
|
-
|
|
15070
|
-
|
|
15071
|
-
|
|
15072
|
-
|
|
15073
|
-
|
|
15074
|
-
|
|
14971
|
+
//#endregion
|
|
14972
|
+
//#region src/recorder/addon/ops-log-store.ts
|
|
14973
|
+
/**
|
|
14974
|
+
* Recordings ops-log persistence for the recorder addon.
|
|
14975
|
+
*
|
|
14976
|
+
* ONE ROW PER OPERATION in the `recorder` / `ops-log` record kind, through a
|
|
14977
|
+
* {@link RecordKindHandle}. The ring is bounded — `maxEntries` newest rows
|
|
14978
|
+
* survive — trimmed with one `deleteWhere({ atBefore })` per append.
|
|
14979
|
+
*
|
|
14980
|
+
* ## Records migration (phase 2, step 26)
|
|
14981
|
+
*
|
|
14982
|
+
* The store was previously backed by a {@link RowMapStore} over a SettingsStore
|
|
14983
|
+
* collection named `recorder:ops-log`. It is now backed by a
|
|
14984
|
+
* `RecordKindHandle<OpsLogEntry>` obtained from `ctx.records`. The record's
|
|
14985
|
+
* `at` field maps to `OpsLogEntry.at`.
|
|
14986
|
+
*
|
|
14987
|
+
* ## What the RowMapStore replaced
|
|
14988
|
+
*
|
|
14989
|
+
* The ring lived in ONE key of the recorder's `addon-settings` / `root` blob,
|
|
14990
|
+
* as a flat JSON array. On the reference hub it was 147 437 bytes of that row's
|
|
14991
|
+
* 201 833 — so appending one audit line read, parsed, re-serialised and rewrote
|
|
14992
|
+
* ~200 KB, and so did every unrelated reader of that row. The ops log is the
|
|
14993
|
+
* CHEAPEST thing in the recorder and it was paying for the most expensive.
|
|
14994
|
+
*
|
|
14995
|
+
* Writes stay BEST-EFFORT: `createRecordingOpsLogSink().append` never throws —
|
|
14996
|
+
* a failed audit write must not fail the operation it records.
|
|
14997
|
+
*/
|
|
14998
|
+
/**
|
|
14999
|
+
* Obtain the `recorder` / `ops-log` record kind handle.
|
|
15000
|
+
* `ctx.records.declare(recorderRecordKinds)` MUST have run before any read or
|
|
15001
|
+
* write (called at boot in `onInitialize`).
|
|
15002
|
+
*/
|
|
15003
|
+
function recordingOpsLogState(records) {
|
|
15004
|
+
return records.kind(recorderRecordKinds["ops-log"]);
|
|
15075
15005
|
}
|
|
15076
15006
|
/**
|
|
15077
15007
|
* Trim the ring to the newest `maxEntries` rows. Returns how many went.
|
|
15078
15008
|
*
|
|
15079
15009
|
* The cursor is the OLDEST row that must SURVIVE — offset `maxEntries - 1`
|
|
15080
|
-
* newest-first — and the delete
|
|
15010
|
+
* newest-first — and the delete removes everything strictly BEFORE its `at`
|
|
15011
|
+
* (`atBefore: cursor.at` deletes rows where `at < cursor.at`).
|
|
15081
15012
|
*
|
|
15082
|
-
* Strictly,
|
|
15083
|
-
*
|
|
15084
|
-
* and
|
|
15085
|
-
* Erring this way keeps a few EXTRA audit rows on a tie and never fewer than
|
|
15086
|
-
* the cap.
|
|
15013
|
+
* Strictly, to avoid dropping the cursor row itself and any rows tied on its
|
|
15014
|
+
* millisecond. The ring may hold a few MORE than the cap on a millisecond tie
|
|
15015
|
+
* and never FEWER.
|
|
15087
15016
|
*/
|
|
15088
15017
|
async function trimOpsLog(state, maxEntries = 500) {
|
|
15089
|
-
if (await state.count() <= maxEntries) return 0;
|
|
15090
|
-
const
|
|
15091
|
-
|
|
15092
|
-
|
|
15093
|
-
|
|
15094
|
-
|
|
15095
|
-
|
|
15096
|
-
|
|
15097
|
-
}))[0];
|
|
15098
|
-
if (oldestKept === void 0) return 0;
|
|
15099
|
-
return state.deleteWhere({ whereBetween: { at: [0, oldestKept.value.at - 1] } });
|
|
15018
|
+
if (await state.count({}) <= maxEntries) return 0;
|
|
15019
|
+
const page = await state.query({
|
|
15020
|
+
desc: true,
|
|
15021
|
+
limit: maxEntries
|
|
15022
|
+
});
|
|
15023
|
+
const oldest = page.records[page.records.length - 1];
|
|
15024
|
+
if (oldest === void 0) return 0;
|
|
15025
|
+
return state.deleteWhere({ atBefore: oldest.at ?? 0 });
|
|
15100
15026
|
}
|
|
15101
15027
|
/**
|
|
15102
15028
|
* Append one row and persist, then trim the ring. Validates at the boundary.
|
|
@@ -15104,7 +15030,11 @@ async function trimOpsLog(state, maxEntries = 500) {
|
|
|
15104
15030
|
*/
|
|
15105
15031
|
async function appendOpsLog(state, entry, maxEntries = 500) {
|
|
15106
15032
|
const validated = require_dist.OpsLogEntrySchema.parse(entry);
|
|
15107
|
-
await state.
|
|
15033
|
+
await state.put({
|
|
15034
|
+
id: validated.id,
|
|
15035
|
+
at: validated.at,
|
|
15036
|
+
data: validated
|
|
15037
|
+
});
|
|
15108
15038
|
await trimOpsLog(state, maxEntries);
|
|
15109
15039
|
return validated;
|
|
15110
15040
|
}
|
|
@@ -15112,18 +15042,15 @@ async function appendOpsLog(state, entry, maxEntries = 500) {
|
|
|
15112
15042
|
* List ops-log rows newest-first, optionally scoped to one device, capped at
|
|
15113
15043
|
* `limit` (default {@link OPS_LOG_DEFAULT_LIMIT}).
|
|
15114
15044
|
*
|
|
15115
|
-
* Scoped to a device this is an INDEXED query
|
|
15116
|
-
* followed by a filter and a sort in JS.
|
|
15045
|
+
* Scoped to a device this is an INDEXED query on the `device` index — not a
|
|
15046
|
+
* read of the whole ring followed by a filter and a sort in JS.
|
|
15117
15047
|
*/
|
|
15118
15048
|
async function listOpsLog(state, query) {
|
|
15119
|
-
return (await state.
|
|
15049
|
+
return (await state.query({
|
|
15120
15050
|
...query.deviceId === void 0 ? {} : { where: { deviceId: query.deviceId } },
|
|
15121
|
-
|
|
15122
|
-
field: "at",
|
|
15123
|
-
direction: "desc"
|
|
15124
|
-
},
|
|
15051
|
+
desc: true,
|
|
15125
15052
|
limit: query.limit ?? 200
|
|
15126
|
-
})).map((
|
|
15053
|
+
})).records.map((r) => r.data);
|
|
15127
15054
|
}
|
|
15128
15055
|
/**
|
|
15129
15056
|
* Build the recorder's ops-log sink. `append` stamps `domain:'recording'`,
|
|
@@ -23581,7 +23508,6 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
23581
23508
|
readCensus = new StoreReadCensus();
|
|
23582
23509
|
/** Memoised so every `configStore()` / `exportsStore()` / `opsLogStore()`
|
|
23583
23510
|
* call shares ONE census instead of each wrapping the raw client afresh. */
|
|
23584
|
-
censusedSettingsStoreClient = null;
|
|
23585
23511
|
constructor() {
|
|
23586
23512
|
super({ ...DEFAULT_CONFIG });
|
|
23587
23513
|
}
|
|
@@ -23618,6 +23544,9 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
23618
23544
|
} });
|
|
23619
23545
|
return { providers: [] };
|
|
23620
23546
|
}
|
|
23547
|
+
const records = this.ctx.records;
|
|
23548
|
+
if (records === void 0) throw new Error("recorder (recording node) needs ctx.records — recording node must be the hub");
|
|
23549
|
+
await records.declare(recorderRecordKinds);
|
|
23621
23550
|
await this.declareRowCollections();
|
|
23622
23551
|
this.segmentHours = new SegmentHourLedger({
|
|
23623
23552
|
store: this.ctx.api.settingsStore,
|
|
@@ -25438,26 +25367,16 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
25438
25367
|
} });
|
|
25439
25368
|
}
|
|
25440
25369
|
/**
|
|
25441
|
-
* Declare the
|
|
25442
|
-
* `
|
|
25443
|
-
*
|
|
25370
|
+
* Declare the remaining RowMapStore collection that replaced the
|
|
25371
|
+
* `recordingConfigs` blob key. Idempotent — a re-declaration of the same
|
|
25372
|
+
* shape is a no-op in the engine.
|
|
25373
|
+
*
|
|
25374
|
+
* `recorder:exports` and `recorder:ops-log` are now record kinds declared
|
|
25375
|
+
* via `ctx.records.declare(recorderRecordKinds)` at the top of
|
|
25376
|
+
* `onInitialize`.
|
|
25444
25377
|
*/
|
|
25445
25378
|
async declareRowCollections() {
|
|
25446
25379
|
await this.configStore().declare();
|
|
25447
|
-
await this.exportsStore().declare();
|
|
25448
|
-
await this.opsLogStore().declare();
|
|
25449
|
-
}
|
|
25450
|
-
/**
|
|
25451
|
-
* `ctx.api.settingsStore`, wrapped ONCE through {@link censusStoreClient}.
|
|
25452
|
-
*
|
|
25453
|
-
* LAZY (see `calendar` above): `ctx` is not assigned until `initialize()`
|
|
25454
|
-
* runs, so this cannot be a field initialiser. The wrap is permanent and
|
|
25455
|
-
* cheap when disarmed (one `get` trap and a comparison) — what is armed and
|
|
25456
|
-
* disarmed is `this.readCensus`, through `debug.storeCensus`, not this seam.
|
|
25457
|
-
*/
|
|
25458
|
-
censusedSettingsStore() {
|
|
25459
|
-
this.censusedSettingsStoreClient ??= censusStoreClient(this.ctx.api.settingsStore, this.readCensus);
|
|
25460
|
-
return this.censusedSettingsStoreClient;
|
|
25461
25380
|
}
|
|
25462
25381
|
configsState = null;
|
|
25463
25382
|
/** A camera's recording config — its per-device settings (D865), migrated on read. */
|
|
@@ -25470,14 +25389,23 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
25470
25389
|
});
|
|
25471
25390
|
return this.configsState;
|
|
25472
25391
|
}
|
|
25473
|
-
/**
|
|
25474
|
-
* wrapper as the config store). */
|
|
25392
|
+
/** Record kind handle over the `recorder` / `exports` kind (hub recording node only). */
|
|
25475
25393
|
exportsStore() {
|
|
25476
|
-
return recordingExportsState(this.
|
|
25394
|
+
return recordingExportsState(this.requireRecords());
|
|
25477
25395
|
}
|
|
25478
|
-
/**
|
|
25396
|
+
/** Record kind handle over the `recorder` / `ops-log` kind (hub recording node only). */
|
|
25479
25397
|
opsLogStore() {
|
|
25480
|
-
return recordingOpsLogState(this.
|
|
25398
|
+
return recordingOpsLogState(this.requireRecords());
|
|
25399
|
+
}
|
|
25400
|
+
/**
|
|
25401
|
+
* Returns `ctx.records`, throwing clearly when it is absent.
|
|
25402
|
+
* Only called on the designated recording node (hub by default); an agent
|
|
25403
|
+
* exits before `onInitialize` reaches any store call.
|
|
25404
|
+
*/
|
|
25405
|
+
requireRecords() {
|
|
25406
|
+
const records = this.ctx.records;
|
|
25407
|
+
if (records === void 0) throw new Error("recorder (recording node) needs ctx.records — recording node must be the hub");
|
|
25408
|
+
return records;
|
|
25481
25409
|
}
|
|
25482
25410
|
/** Durable handle over the recorder-owned placement blob (assignments + volume ids). */
|
|
25483
25411
|
placementStore() {
|