@camstack/addon-pipeline 1.2.374 → 1.2.601
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-1LDuDw8S.mjs → custom-model-catalog-CK9GK3N5.mjs} +2 -2
- package/dist/{custom-model-catalog-W0DHDhfo.js → custom-model-catalog-DgyafyuW.js} +2 -2
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-CmdLCb_F.mjs → dist-BaHsDOWa.mjs} +1124 -29
- package/dist/{dist-CBEUVjTS.js → dist-BqI10eLV.js} +1129 -28
- package/dist/{model-catalogs-BnCdB2A3.mjs → model-catalogs-DM5sVj1T.mjs} +1 -1
- package/dist/{model-catalogs-D5YksA5A.js → model-catalogs-DhGVt_sH.js} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-C3r8kiOo.mjs → node-B0a0dXhg.mjs} +38 -3
- package/dist/{node-DMgi6pND.js → node-CgwqjGz0.js} +42 -1
- package/dist/pipeline-runner/index.js +4 -4
- package/dist/pipeline-runner/index.mjs +4 -4
- package/dist/{process-memory-GFR7fxpJ.js → process-memory-C0zbWvwD.js} +1 -1
- package/dist/{process-memory-X0ws1SGJ.mjs → process-memory-sfdcLVkW.mjs} +1 -1
- package/dist/recorder/index.js +357 -516
- package/dist/recorder/index.mjs +357 -516
- package/dist/{segment-demux-js-CSQVhbFI.js → segment-demux-js-B9lL2VCC.js} +1 -1
- package/dist/{segment-demux-js-IKEO1XBz.mjs → segment-demux-js-BB4CUAB5.mjs} +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-DPFTlVoW.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-CbN9DjoK.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Boq8JqyD.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-B25q1OKG.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-BOp1_Hhk.mjs → hostInit-BSYHAfPt.mjs} +3 -3
- package/dist/stream-broker/index.js +556 -32
- package/dist/stream-broker/index.mjs +556 -32
- 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-B7wsxLrK.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-BohOb2Cl.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-BqI10eLV.js");
|
|
7
|
+
const require_node = require("../node-CgwqjGz0.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) {
|
|
@@ -6151,7 +6100,9 @@ Object.freeze({
|
|
|
6151
6100
|
},
|
|
6152
6101
|
"setDeviceConfig": { "config": { "__nested__": { "bands": [] } } }
|
|
6153
6102
|
},
|
|
6103
|
+
"records": { "query": { "limit": 100 } },
|
|
6154
6104
|
"storage": { "upsertLocation": { "isSystem": false } },
|
|
6105
|
+
"storage-admin": { "readRecords": { "limit": 100 } },
|
|
6155
6106
|
"storage-provider": {
|
|
6156
6107
|
"beginDownload": { "location": { "__nested__": { "isSystem": false } } },
|
|
6157
6108
|
"beginUpload": { "location": { "__nested__": { "isSystem": false } } },
|
|
@@ -6586,6 +6537,18 @@ Object.freeze({
|
|
|
6586
6537
|
{
|
|
6587
6538
|
"path": "swingHorizontal",
|
|
6588
6539
|
"valueType": "boolean"
|
|
6540
|
+
},
|
|
6541
|
+
{
|
|
6542
|
+
"path": "minTemp",
|
|
6543
|
+
"valueType": "number"
|
|
6544
|
+
},
|
|
6545
|
+
{
|
|
6546
|
+
"path": "maxTemp",
|
|
6547
|
+
"valueType": "number"
|
|
6548
|
+
},
|
|
6549
|
+
{
|
|
6550
|
+
"path": "tempStep",
|
|
6551
|
+
"valueType": "number"
|
|
6589
6552
|
}
|
|
6590
6553
|
],
|
|
6591
6554
|
"color": [
|
|
@@ -8355,259 +8318,76 @@ function buildExportArgs(input) {
|
|
|
8355
8318
|
outPath
|
|
8356
8319
|
];
|
|
8357
8320
|
}
|
|
8358
|
-
var
|
|
8359
|
-
var RowMapStore = class RowMapStore {
|
|
8360
|
-
spec;
|
|
8361
|
-
store;
|
|
8362
|
-
logger;
|
|
8363
|
-
constructor(deps) {
|
|
8364
|
-
this.spec = deps.spec;
|
|
8365
|
-
this.store = deps.store;
|
|
8366
|
-
this.logger = deps.logger;
|
|
8367
|
-
}
|
|
8368
|
-
/** The collection this store owns — for the caller's own log lines. */
|
|
8369
|
-
get collection() {
|
|
8370
|
-
return this.spec.collection;
|
|
8371
|
-
}
|
|
8321
|
+
var recorderRecordKinds = require_dist.defineRecordKinds({
|
|
8372
8322
|
/**
|
|
8373
|
-
*
|
|
8374
|
-
*
|
|
8375
|
-
|
|
8376
|
-
|
|
8377
|
-
return store.declareCollection.mutate({
|
|
8378
|
-
collection: spec.collection,
|
|
8379
|
-
columns: [
|
|
8380
|
-
{
|
|
8381
|
-
name: "id",
|
|
8382
|
-
type: "TEXT",
|
|
8383
|
-
primaryKey: true,
|
|
8384
|
-
notNull: true
|
|
8385
|
-
},
|
|
8386
|
-
...spec.columns,
|
|
8387
|
-
{
|
|
8388
|
-
name: ROW_MAP_VALUE_COLUMN,
|
|
8389
|
-
type: "JSON",
|
|
8390
|
-
notNull: true
|
|
8391
|
-
}
|
|
8392
|
-
],
|
|
8393
|
-
...spec.indexes !== void 0 ? { indexes: [...spec.indexes] } : {}
|
|
8394
|
-
});
|
|
8395
|
-
}
|
|
8396
|
-
declare() {
|
|
8397
|
-
return RowMapStore.declare(this.store, this.spec);
|
|
8398
|
-
}
|
|
8399
|
-
/** One entry, by key. `null` when absent OR unparseable (which is said aloud). */
|
|
8400
|
-
async read(key) {
|
|
8401
|
-
const raw = await this.store.get.query({
|
|
8402
|
-
collection: this.spec.collection,
|
|
8403
|
-
key
|
|
8404
|
-
});
|
|
8405
|
-
if (!isRecord(raw)) return null;
|
|
8406
|
-
const row = raw;
|
|
8407
|
-
const parsed = this.spec.schema.safeParse(row[ROW_MAP_VALUE_COLUMN]);
|
|
8408
|
-
if (!parsed.success) {
|
|
8409
|
-
this.logger.warn("row-map row skipped as unparseable", {
|
|
8410
|
-
...this.deviceTags(row),
|
|
8411
|
-
meta: {
|
|
8412
|
-
collection: this.spec.collection,
|
|
8413
|
-
key,
|
|
8414
|
-
error: parsed.error.message
|
|
8415
|
-
}
|
|
8416
|
-
});
|
|
8417
|
-
return null;
|
|
8418
|
-
}
|
|
8419
|
-
return parsed.data;
|
|
8420
|
-
}
|
|
8421
|
-
/**
|
|
8422
|
-
* Every entry, keyed. Unparseable rows are skipped — one bad entry costs that
|
|
8423
|
-
* entry, never the map.
|
|
8424
|
-
*/
|
|
8425
|
-
async readAll() {
|
|
8426
|
-
const records = await this.store.query.query({
|
|
8427
|
-
collection: this.spec.collection,
|
|
8428
|
-
filter: { limit: this.spec.loadLimit }
|
|
8429
|
-
});
|
|
8430
|
-
const out = /* @__PURE__ */ new Map();
|
|
8431
|
-
let skipped = 0;
|
|
8432
|
-
for (const record of records) {
|
|
8433
|
-
const parsed = this.spec.schema.safeParse(record.data[ROW_MAP_VALUE_COLUMN]);
|
|
8434
|
-
if (!parsed.success) {
|
|
8435
|
-
skipped += 1;
|
|
8436
|
-
continue;
|
|
8437
|
-
}
|
|
8438
|
-
out.set(record.id, parsed.data);
|
|
8439
|
-
}
|
|
8440
|
-
if (skipped > 0) this.logger.warn("row-map rows skipped as unparseable", { meta: {
|
|
8441
|
-
collection: this.spec.collection,
|
|
8442
|
-
skipped,
|
|
8443
|
-
kept: out.size
|
|
8444
|
-
} });
|
|
8445
|
-
if (records.length >= this.spec.loadLimit) this.logger.warn("row-map read hit its own bound — the map may be truncated", { meta: {
|
|
8446
|
-
collection: this.spec.collection,
|
|
8447
|
-
loadLimit: this.spec.loadLimit
|
|
8448
|
-
} });
|
|
8449
|
-
return out;
|
|
8450
|
-
}
|
|
8451
|
-
/**
|
|
8452
|
-
* Every entry as it was WRITTEN, unvalidated and unparsed — the read a
|
|
8453
|
-
* MIGRATION needs.
|
|
8454
|
-
*
|
|
8455
|
-
* {@link readAll} answers through the CURRENT schema, which is exactly what a
|
|
8456
|
-
* migration must not do: a retired key is stripped by a non-strict `z.object`
|
|
8457
|
-
* before the migration could see it, so the row it is there to rescue looks
|
|
8458
|
-
* like a row that never needed rescuing. Only a migration should call this;
|
|
8459
|
-
* everything else wants the validated read.
|
|
8460
|
-
*/
|
|
8461
|
-
async readAllRaw() {
|
|
8462
|
-
const records = await this.store.query.query({
|
|
8463
|
-
collection: this.spec.collection,
|
|
8464
|
-
filter: { limit: this.spec.loadLimit }
|
|
8465
|
-
});
|
|
8466
|
-
const out = /* @__PURE__ */ new Map();
|
|
8467
|
-
for (const record of records) out.set(record.id, record.data[ROW_MAP_VALUE_COLUMN]);
|
|
8468
|
-
return out;
|
|
8469
|
-
}
|
|
8470
|
-
/** Insert or replace ONE entry. Validates at the boundary. */
|
|
8471
|
-
async write(key, value) {
|
|
8472
|
-
const validated = this.spec.schema.parse(value);
|
|
8473
|
-
const columns = this.spec.project(key, validated);
|
|
8474
|
-
await this.store.set.mutate({
|
|
8475
|
-
collection: this.spec.collection,
|
|
8476
|
-
key,
|
|
8477
|
-
value: {
|
|
8478
|
-
...columns,
|
|
8479
|
-
[ROW_MAP_VALUE_COLUMN]: validated
|
|
8480
|
-
}
|
|
8481
|
-
});
|
|
8482
|
-
}
|
|
8483
|
-
/** Drop ONE entry. Idempotent. */
|
|
8484
|
-
async remove(key) {
|
|
8485
|
-
await this.store.delete.mutate({
|
|
8486
|
-
collection: this.spec.collection,
|
|
8487
|
-
key
|
|
8488
|
-
});
|
|
8489
|
-
}
|
|
8490
|
-
/**
|
|
8491
|
-
* Make the collection hold exactly `next` — the write path for an owner whose
|
|
8492
|
-
* in-memory map IS the authority (a persister handed the complete set).
|
|
8323
|
+
* One row per export job, inserted by `createExport` and updated as the
|
|
8324
|
+
* render progresses. The system NEVER deletes a row — it is the export AUDIT.
|
|
8325
|
+
* `deleteExport` and the janitor flip `state` and clear the file, keeping
|
|
8326
|
+
* the row. The record's `at` field maps to `createdAt`.
|
|
8493
8327
|
*
|
|
8494
|
-
*
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8498
|
-
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
dropped: doomed.length,
|
|
8510
|
-
keys: doomed.slice(0, 20).map((r) => r.id),
|
|
8511
|
-
remaining: next.size
|
|
8512
|
-
} });
|
|
8513
|
-
return doomed.length;
|
|
8514
|
-
}
|
|
8328
|
+
* Indexed by `deviceId` to serve the per-device listing efficiently.
|
|
8329
|
+
*/
|
|
8330
|
+
exports: {
|
|
8331
|
+
schema: require_dist.ExportRecordSchema,
|
|
8332
|
+
class: "ledger",
|
|
8333
|
+
writeTrigger: "one row per export job, inserted by createExport; rewritten by the render engine (progress / state patches) and the download hook",
|
|
8334
|
+
device: "none",
|
|
8335
|
+
at: "required",
|
|
8336
|
+
indexes: [{
|
|
8337
|
+
name: "device",
|
|
8338
|
+
fields: ["deviceId"]
|
|
8339
|
+
}],
|
|
8340
|
+
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" },
|
|
8341
|
+
adoptsFrom: "recorder:exports"
|
|
8342
|
+
},
|
|
8515
8343
|
/**
|
|
8516
|
-
*
|
|
8344
|
+
* A bounded ring of per-operation audit rows. One row per recordings
|
|
8345
|
+
* operation (prune, evict, relocate, export, …), appended best-effort by
|
|
8346
|
+
* `createRecordingOpsLogSink().append`. The ring is trimmed to the newest
|
|
8347
|
+
* `OPS_LOG_RING_DEFAULT_MAX` (500) rows after every append.
|
|
8517
8348
|
*
|
|
8518
|
-
*
|
|
8519
|
-
*
|
|
8520
|
-
* parse of the whole map followed by a filter in JS.
|
|
8521
|
-
*
|
|
8522
|
-
* A `limit` is REQUIRED — `settings-store.query` silently caps an unbounded
|
|
8523
|
-
* read at 2 000 rows and answers as if that were everything.
|
|
8524
|
-
*/
|
|
8525
|
-
async readWhere(filter) {
|
|
8526
|
-
const records = await this.store.query.query({
|
|
8527
|
-
collection: this.spec.collection,
|
|
8528
|
-
filter: {
|
|
8529
|
-
...filter.where !== void 0 ? { where: filter.where } : {},
|
|
8530
|
-
...filter.whereIn !== void 0 ? { whereIn: filter.whereIn } : {},
|
|
8531
|
-
...filter.whereBetween !== void 0 ? { whereBetween: filter.whereBetween } : {},
|
|
8532
|
-
...filter.orderBy !== void 0 ? { orderBy: filter.orderBy } : {},
|
|
8533
|
-
...filter.offset !== void 0 ? { offset: filter.offset } : {},
|
|
8534
|
-
limit: filter.limit ?? this.spec.loadLimit
|
|
8535
|
-
}
|
|
8536
|
-
});
|
|
8537
|
-
const out = [];
|
|
8538
|
-
let skipped = 0;
|
|
8539
|
-
for (const record of records) {
|
|
8540
|
-
const parsed = this.spec.schema.safeParse(record.data[ROW_MAP_VALUE_COLUMN]);
|
|
8541
|
-
if (!parsed.success) {
|
|
8542
|
-
skipped += 1;
|
|
8543
|
-
continue;
|
|
8544
|
-
}
|
|
8545
|
-
out.push({
|
|
8546
|
-
key: record.id,
|
|
8547
|
-
value: parsed.data
|
|
8548
|
-
});
|
|
8549
|
-
}
|
|
8550
|
-
if (skipped > 0) this.logger.warn("row-map rows skipped as unparseable", { meta: {
|
|
8551
|
-
collection: this.spec.collection,
|
|
8552
|
-
skipped,
|
|
8553
|
-
kept: out.length
|
|
8554
|
-
} });
|
|
8555
|
-
return out;
|
|
8556
|
-
}
|
|
8557
|
-
/** How many rows match, without materialising one. */
|
|
8558
|
-
async count(filter) {
|
|
8559
|
-
return this.store.count.query({
|
|
8560
|
-
collection: this.spec.collection,
|
|
8561
|
-
...filter !== void 0 ? { filter: {
|
|
8562
|
-
...filter.where !== void 0 ? { where: filter.where } : {},
|
|
8563
|
-
...filter.whereIn !== void 0 ? { whereIn: filter.whereIn } : {},
|
|
8564
|
-
...filter.whereBetween !== void 0 ? { whereBetween: filter.whereBetween } : {}
|
|
8565
|
-
} } : {}
|
|
8566
|
-
});
|
|
8567
|
-
}
|
|
8568
|
-
/**
|
|
8569
|
-
* Delete every row matching a predicate, in ONE statement. Returns how many
|
|
8570
|
-
* went, and says so — a retention trim that silently drops rows is
|
|
8571
|
-
* indistinguishable from one that had nothing to do.
|
|
8349
|
+
* Indexed by `deviceId` so a per-device listing is an indexed query, not a
|
|
8350
|
+
* full scan of the ring.
|
|
8572
8351
|
*
|
|
8573
|
-
* The
|
|
8574
|
-
|
|
8575
|
-
|
|
8576
|
-
|
|
8577
|
-
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
|
|
8581
|
-
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
|
|
8585
|
-
|
|
8586
|
-
|
|
8587
|
-
return deleted;
|
|
8588
|
-
}
|
|
8589
|
-
/** `tags: { deviceId }` off the PROJECTED column, when the spec names one. */
|
|
8590
|
-
deviceTags(row) {
|
|
8591
|
-
const column = this.spec.deviceIdColumn;
|
|
8592
|
-
if (column === void 0) return {};
|
|
8593
|
-
const raw = row[column];
|
|
8594
|
-
return typeof raw === "number" && Number.isInteger(raw) ? { tags: { deviceId: raw } } : {};
|
|
8352
|
+
* The record's `at` field maps to `OpsLogEntry.at`.
|
|
8353
|
+
*/
|
|
8354
|
+
"ops-log": {
|
|
8355
|
+
schema: require_dist.OpsLogEntrySchema,
|
|
8356
|
+
class: "ledger",
|
|
8357
|
+
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",
|
|
8358
|
+
device: "none",
|
|
8359
|
+
at: "required",
|
|
8360
|
+
indexes: [{
|
|
8361
|
+
name: "device",
|
|
8362
|
+
fields: ["deviceId"]
|
|
8363
|
+
}],
|
|
8364
|
+
retention: { none: "a bounded ring of the newest 500 rows; older rows are removed in application code (trimOpsLog) after each append — owner-pruned" },
|
|
8365
|
+
adoptsFrom: "recorder:ops-log"
|
|
8595
8366
|
}
|
|
8596
|
-
};
|
|
8597
|
-
/** A store payload that is a plain object, or `null`. A type guard, not a cast. */
|
|
8598
|
-
function isRecord(value) {
|
|
8599
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8600
|
-
}
|
|
8367
|
+
});
|
|
8601
8368
|
//#endregion
|
|
8602
8369
|
//#region src/recorder/addon/export-store.ts
|
|
8603
8370
|
/**
|
|
8604
8371
|
* Recording-export persistence for the recorder addon.
|
|
8605
8372
|
*
|
|
8606
|
-
* ONE ROW PER EXPORT in `recorder
|
|
8607
|
-
* rows are never removed by the system
|
|
8608
|
-
* flip `state` and clear the file,
|
|
8373
|
+
* ONE ROW PER EXPORT in the `recorder` / `exports` record kind, through a
|
|
8374
|
+
* {@link RecordKindHandle}. History rows are never removed by the system
|
|
8375
|
+
* (audit) — `deleteExport` / the janitor flip `state` and clear the file,
|
|
8376
|
+
* keeping the row.
|
|
8377
|
+
*
|
|
8378
|
+
* ## Records migration (phase 2, step 25)
|
|
8379
|
+
*
|
|
8380
|
+
* The store was previously backed by a {@link RowMapStore} over a SettingsStore
|
|
8381
|
+
* collection named `recorder:exports`. It is now backed by a
|
|
8382
|
+
* `RecordKindHandle<ExportRecord>` obtained from `ctx.records`. The record's
|
|
8383
|
+
* `at` field maps to `ExportRecord.createdAt`.
|
|
8609
8384
|
*
|
|
8610
|
-
*
|
|
8385
|
+
* The per-ID serialiser ({@link serialize}) is preserved: two overlapping
|
|
8386
|
+
* patches on the SAME export row are still read-modify-write, so they must
|
|
8387
|
+
* be ordered. The `RecordKindHandle` (an object) is used as the WeakMap key,
|
|
8388
|
+
* keeping the same compaction semantics.
|
|
8389
|
+
*
|
|
8390
|
+
* ## What the RowMapStore replaced, and why it mattered
|
|
8611
8391
|
*
|
|
8612
8392
|
* Every job used to live in one key of the recorder's `addon-settings` / `root`
|
|
8613
8393
|
* blob — 201 833 bytes on the reference hub, of which the export history was
|
|
@@ -8622,11 +8402,6 @@ function isRecord(value) {
|
|
|
8622
8402
|
* erased, and the poll answered `unknown export "547ea182-…"`; the scheduler
|
|
8623
8403
|
* read the vanished row as a failed render and re-queued forever. A per-row
|
|
8624
8404
|
* upsert makes that class of loss unrepresentable — a write names ONE row.
|
|
8625
|
-
*
|
|
8626
|
-
* What is still needed is a per-ID serialiser: two patches to the SAME export
|
|
8627
|
-
* still read-modify-write the same row. That is what {@link serialize} bounds,
|
|
8628
|
-
* and it is now per (store, id) rather than per store, so two exports never
|
|
8629
|
-
* queue behind each other.
|
|
8630
8405
|
*/
|
|
8631
8406
|
/**
|
|
8632
8407
|
* The storage class export MP4s live in: `artifacts`, SHARED with the
|
|
@@ -8671,76 +8446,26 @@ function exportOutPathFor(root, deviceId, exportId) {
|
|
|
8671
8446
|
return node_path.default.join(exportTenantDirFor(root, deviceId), `${exportId}.mp4`);
|
|
8672
8447
|
}
|
|
8673
8448
|
/**
|
|
8674
|
-
*
|
|
8675
|
-
*
|
|
8676
|
-
*
|
|
8677
|
-
* retention="the SYSTEM never deletes a row — it is the export AUDIT. `deleteExport`
|
|
8678
|
-
* and the janitor flip `state` to 'deleted'/'expired' and remove the FILE, keeping
|
|
8679
|
-
* the row. `removeExport` exists for an operator-driven purge and nothing calls it
|
|
8680
|
-
* on a schedule."
|
|
8449
|
+
* Bound for the whole-history read. Explicit because a records query without
|
|
8450
|
+
* an explicit limit uses the default cap (100); an export list that silently
|
|
8451
|
+
* loses its oldest entries looks exactly like a purge.
|
|
8681
8452
|
*/
|
|
8682
|
-
var
|
|
8453
|
+
var RECORDING_EXPORTS_LIMIT = 2e4;
|
|
8683
8454
|
/**
|
|
8684
|
-
*
|
|
8685
|
-
*
|
|
8686
|
-
*
|
|
8455
|
+
* Obtain the `recorder` / `exports` record kind handle.
|
|
8456
|
+
* `ctx.records.declare(recorderRecordKinds)` MUST have run before any read or
|
|
8457
|
+
* write (called at boot in `onInitialize`).
|
|
8687
8458
|
*/
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
collection: RECORDING_EXPORTS_COLLECTION,
|
|
8691
|
-
schema: require_dist.ExportRecordSchema,
|
|
8692
|
-
columns: [
|
|
8693
|
-
{
|
|
8694
|
-
name: "deviceId",
|
|
8695
|
-
type: "INTEGER",
|
|
8696
|
-
notNull: true
|
|
8697
|
-
},
|
|
8698
|
-
{
|
|
8699
|
-
name: "createdAt",
|
|
8700
|
-
type: "INTEGER",
|
|
8701
|
-
notNull: true
|
|
8702
|
-
},
|
|
8703
|
-
{
|
|
8704
|
-
name: "state",
|
|
8705
|
-
type: "TEXT",
|
|
8706
|
-
notNull: true
|
|
8707
|
-
}
|
|
8708
|
-
],
|
|
8709
|
-
indexes: [{
|
|
8710
|
-
name: "idx_recorder_exports_device",
|
|
8711
|
-
columns: ["deviceId"]
|
|
8712
|
-
}, {
|
|
8713
|
-
name: "idx_recorder_exports_created",
|
|
8714
|
-
columns: ["createdAt"]
|
|
8715
|
-
}],
|
|
8716
|
-
project: (_key, value) => ({
|
|
8717
|
-
deviceId: value.deviceId,
|
|
8718
|
-
createdAt: value.createdAt,
|
|
8719
|
-
state: value.state
|
|
8720
|
-
}),
|
|
8721
|
-
deviceIdColumn: "deviceId",
|
|
8722
|
-
loadLimit: RECORDING_EXPORTS_LIMIT
|
|
8723
|
-
};
|
|
8724
|
-
/** Build the handle. `declare()` MUST run once at boot before any read/write. */
|
|
8725
|
-
function recordingExportsState(store, logger) {
|
|
8726
|
-
return new RowMapStore({
|
|
8727
|
-
spec: RECORDING_EXPORTS_SPEC,
|
|
8728
|
-
store,
|
|
8729
|
-
logger
|
|
8730
|
-
});
|
|
8459
|
+
function recordingExportsState(records) {
|
|
8460
|
+
return records.kind(recorderRecordKinds["exports"]);
|
|
8731
8461
|
}
|
|
8732
8462
|
/**
|
|
8733
8463
|
* One in-flight mutation chain per (handle, export id).
|
|
8734
8464
|
*
|
|
8735
|
-
*
|
|
8736
|
-
*
|
|
8737
|
-
*
|
|
8738
|
-
*
|
|
8739
|
-
* unrelated exports fully concurrent (the old chain was per HANDLE, so a slow
|
|
8740
|
-
* patch queued every other export behind it).
|
|
8741
|
-
*
|
|
8742
|
-
* A `WeakMap` keyed by the handle keeps a disposed store collectable; the inner
|
|
8743
|
-
* map is keyed by export id.
|
|
8465
|
+
* Two overlapping patches on the SAME export still read-modify-write one row.
|
|
8466
|
+
* Serialising per id keeps them atomic while leaving unrelated exports fully
|
|
8467
|
+
* concurrent. A `WeakMap` keyed by the handle keeps a disposed handle
|
|
8468
|
+
* collectable; the inner map is keyed by export id.
|
|
8744
8469
|
*/
|
|
8745
8470
|
var mutationChains = /* @__PURE__ */ new WeakMap();
|
|
8746
8471
|
/** Run `work` after every mutation already queued for `(state, id)`, never before. */
|
|
@@ -8760,7 +8485,11 @@ async function serialize(state, id, work) {
|
|
|
8760
8485
|
async function upsertExport(state, record) {
|
|
8761
8486
|
const validated = require_dist.ExportRecordSchema.parse(record);
|
|
8762
8487
|
return serialize(state, validated.id, async () => {
|
|
8763
|
-
await state.
|
|
8488
|
+
await state.put({
|
|
8489
|
+
id: validated.id,
|
|
8490
|
+
at: validated.createdAt,
|
|
8491
|
+
data: validated
|
|
8492
|
+
});
|
|
8764
8493
|
return validated;
|
|
8765
8494
|
});
|
|
8766
8495
|
}
|
|
@@ -8770,13 +8499,17 @@ async function upsertExport(state, record) {
|
|
|
8770
8499
|
*/
|
|
8771
8500
|
async function patchExport(state, id, patch) {
|
|
8772
8501
|
return serialize(state, id, async () => {
|
|
8773
|
-
const
|
|
8774
|
-
if (
|
|
8502
|
+
const row = await state.get(id);
|
|
8503
|
+
if (row === null) return null;
|
|
8775
8504
|
const merged = require_dist.ExportRecordSchema.parse({
|
|
8776
|
-
...
|
|
8505
|
+
...row.data,
|
|
8777
8506
|
...patch
|
|
8778
8507
|
});
|
|
8779
|
-
await state.
|
|
8508
|
+
await state.put({
|
|
8509
|
+
id,
|
|
8510
|
+
at: merged.createdAt,
|
|
8511
|
+
data: merged
|
|
8512
|
+
});
|
|
8780
8513
|
return merged;
|
|
8781
8514
|
});
|
|
8782
8515
|
}
|
|
@@ -8787,17 +8520,21 @@ async function patchExport(state, id, patch) {
|
|
|
8787
8520
|
*/
|
|
8788
8521
|
async function patchExportUnless(state, id, patch, refuse) {
|
|
8789
8522
|
return serialize(state, id, async () => {
|
|
8790
|
-
const
|
|
8791
|
-
if (
|
|
8792
|
-
if (refuse(
|
|
8523
|
+
const row = await state.get(id);
|
|
8524
|
+
if (row === null) return { kind: "missing" };
|
|
8525
|
+
if (refuse(row.data)) return {
|
|
8793
8526
|
kind: "refused",
|
|
8794
|
-
record:
|
|
8527
|
+
record: row.data
|
|
8795
8528
|
};
|
|
8796
8529
|
const merged = require_dist.ExportRecordSchema.parse({
|
|
8797
|
-
...
|
|
8530
|
+
...row.data,
|
|
8798
8531
|
...patch
|
|
8799
8532
|
});
|
|
8800
|
-
await state.
|
|
8533
|
+
await state.put({
|
|
8534
|
+
id,
|
|
8535
|
+
at: merged.createdAt,
|
|
8536
|
+
data: merged
|
|
8537
|
+
});
|
|
8801
8538
|
return {
|
|
8802
8539
|
kind: "patched",
|
|
8803
8540
|
record: merged
|
|
@@ -8806,24 +8543,22 @@ async function patchExportUnless(state, id, patch, refuse) {
|
|
|
8806
8543
|
}
|
|
8807
8544
|
/** Read one export row by id — ONE keyed row read. */
|
|
8808
8545
|
async function getExportRecord(state, id) {
|
|
8809
|
-
|
|
8546
|
+
const row = await state.get(id);
|
|
8547
|
+
return row === null ? null : row.data;
|
|
8810
8548
|
}
|
|
8811
8549
|
/**
|
|
8812
|
-
* List export rows, newest-first
|
|
8550
|
+
* List export rows, newest-first (by `createdAt` / record `at`), optionally
|
|
8551
|
+
* scoped to one device.
|
|
8813
8552
|
*
|
|
8814
|
-
* Scoped to a device this is an INDEXED query on the
|
|
8815
|
-
*
|
|
8816
|
-
* in JS.
|
|
8553
|
+
* Scoped to a device this is an INDEXED query on the `device` index — not a
|
|
8554
|
+
* parse of every export in the installation followed by a filter in JS.
|
|
8817
8555
|
*/
|
|
8818
8556
|
async function listExportsFor(state, deviceId) {
|
|
8819
|
-
return (await state.
|
|
8557
|
+
return (await state.query({
|
|
8820
8558
|
...deviceId === void 0 ? {} : { where: { deviceId } },
|
|
8821
|
-
|
|
8822
|
-
field: "createdAt",
|
|
8823
|
-
direction: "desc"
|
|
8824
|
-
},
|
|
8559
|
+
desc: true,
|
|
8825
8560
|
limit: RECORDING_EXPORTS_LIMIT
|
|
8826
|
-
})).map((
|
|
8561
|
+
})).records.map((r) => r.data);
|
|
8827
8562
|
}
|
|
8828
8563
|
//#endregion
|
|
8829
8564
|
//#region src/recorder/addon/export-timelapse-chunks.ts
|
|
@@ -11781,36 +11516,125 @@ function runFfmpeg(renderDeps, ffmpegPath, args, ext) {
|
|
|
11781
11516
|
});
|
|
11782
11517
|
}
|
|
11783
11518
|
//#endregion
|
|
11519
|
+
//#region src/durable/durable-legacy-kind-handle.ts
|
|
11520
|
+
function notImpl(method) {
|
|
11521
|
+
throw new Error(`DurableLegacyKindHandle.${method}() is not available on the legacy (agent) path`);
|
|
11522
|
+
}
|
|
11523
|
+
function isRecord(value) {
|
|
11524
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11525
|
+
}
|
|
11526
|
+
function toRecordRow(id, data) {
|
|
11527
|
+
return {
|
|
11528
|
+
id,
|
|
11529
|
+
data,
|
|
11530
|
+
deviceId: null,
|
|
11531
|
+
at: null,
|
|
11532
|
+
expiresAt: null,
|
|
11533
|
+
rev: 0,
|
|
11534
|
+
updatedAt: 0
|
|
11535
|
+
};
|
|
11536
|
+
}
|
|
11537
|
+
var DurableLegacyKindHandle = class {
|
|
11538
|
+
name;
|
|
11539
|
+
spec;
|
|
11540
|
+
store;
|
|
11541
|
+
constructor(opts) {
|
|
11542
|
+
this.name = opts.spec.collection;
|
|
11543
|
+
this.spec = opts.spec;
|
|
11544
|
+
this.store = opts.store;
|
|
11545
|
+
}
|
|
11546
|
+
async get(id) {
|
|
11547
|
+
const row = (await this.store.query.query({
|
|
11548
|
+
collection: this.spec.collection,
|
|
11549
|
+
filter: {
|
|
11550
|
+
where: { [this.pkColumn()]: id },
|
|
11551
|
+
limit: 1
|
|
11552
|
+
}
|
|
11553
|
+
}))[0];
|
|
11554
|
+
if (row === void 0 || !isRecord(row.data)) return null;
|
|
11555
|
+
const parsed = this.spec.fromRecord(row.id, row.data);
|
|
11556
|
+
if (parsed === null) return null;
|
|
11557
|
+
return toRecordRow(row.id, this.spec.toValue(parsed));
|
|
11558
|
+
}
|
|
11559
|
+
async query(q) {
|
|
11560
|
+
const rows = await this.store.query.query({
|
|
11561
|
+
collection: this.spec.collection,
|
|
11562
|
+
filter: { limit: q.limit ?? 1e4 }
|
|
11563
|
+
});
|
|
11564
|
+
const records = [];
|
|
11565
|
+
for (const row of rows) {
|
|
11566
|
+
if (!isRecord(row.data)) continue;
|
|
11567
|
+
const parsed = this.spec.fromRecord(row.id, row.data);
|
|
11568
|
+
if (parsed === null) continue;
|
|
11569
|
+
records.push(toRecordRow(row.id, this.spec.toValue(parsed)));
|
|
11570
|
+
}
|
|
11571
|
+
return {
|
|
11572
|
+
records,
|
|
11573
|
+
nextCursor: null
|
|
11574
|
+
};
|
|
11575
|
+
}
|
|
11576
|
+
async put(input) {
|
|
11577
|
+
await this.store.set.mutate({
|
|
11578
|
+
collection: this.spec.collection,
|
|
11579
|
+
key: input.id,
|
|
11580
|
+
value: input.data
|
|
11581
|
+
});
|
|
11582
|
+
return { rev: 0 };
|
|
11583
|
+
}
|
|
11584
|
+
async delete(id) {
|
|
11585
|
+
await this.store.delete.mutate({
|
|
11586
|
+
collection: this.spec.collection,
|
|
11587
|
+
key: id
|
|
11588
|
+
});
|
|
11589
|
+
return true;
|
|
11590
|
+
}
|
|
11591
|
+
getMany = () => notImpl("getMany");
|
|
11592
|
+
getResolved = () => notImpl("getResolved");
|
|
11593
|
+
putMany = () => notImpl("putMany");
|
|
11594
|
+
patch = () => notImpl("patch");
|
|
11595
|
+
deleteMany = () => notImpl("deleteMany");
|
|
11596
|
+
count = () => notImpl("count");
|
|
11597
|
+
histogram = () => notImpl("histogram");
|
|
11598
|
+
aggregate = () => notImpl("aggregate");
|
|
11599
|
+
deleteWhere = () => notImpl("deleteWhere");
|
|
11600
|
+
allocateId = () => notImpl("allocateId");
|
|
11601
|
+
pkColumn() {
|
|
11602
|
+
return this.spec.columns.find((c) => c.primaryKey === true)?.name ?? "id";
|
|
11603
|
+
}
|
|
11604
|
+
};
|
|
11605
|
+
/** Legacy settings-store handle for one {@link DurableLedgerSpec}. */
|
|
11606
|
+
function durableLegacyKind(spec, store) {
|
|
11607
|
+
return new DurableLegacyKindHandle({
|
|
11608
|
+
spec,
|
|
11609
|
+
store
|
|
11610
|
+
});
|
|
11611
|
+
}
|
|
11612
|
+
/** Agent boot: declare the SQLite collection behind a durable ledger spec. */
|
|
11613
|
+
function declareDurableLegacyCollection(store, spec) {
|
|
11614
|
+
return store.declareCollection.mutate({
|
|
11615
|
+
collection: spec.collection,
|
|
11616
|
+
columns: [...spec.columns],
|
|
11617
|
+
...spec.indexes !== void 0 ? { indexes: [...spec.indexes] } : {}
|
|
11618
|
+
});
|
|
11619
|
+
}
|
|
11620
|
+
//#endregion
|
|
11784
11621
|
//#region src/durable/durable-ledger.ts
|
|
11785
11622
|
/** Default reseed cap — every current consumer's row set is installation-bounded. */
|
|
11786
11623
|
var DEFAULT_LOAD_LIMIT = 1e5;
|
|
11787
|
-
|
|
11624
|
+
/** Page size for records reseed queries. */
|
|
11625
|
+
var QUERY_PAGE = 1e3;
|
|
11626
|
+
var DurableLedger = class {
|
|
11788
11627
|
mirror = /* @__PURE__ */ new Map();
|
|
11789
11628
|
/** Whether ANY load has read the store successfully — see {@link hasAnswered}. */
|
|
11790
11629
|
answered = false;
|
|
11791
11630
|
spec;
|
|
11792
|
-
|
|
11631
|
+
kind;
|
|
11793
11632
|
logger;
|
|
11794
11633
|
constructor(deps) {
|
|
11795
11634
|
this.spec = deps.spec;
|
|
11796
|
-
this.
|
|
11635
|
+
this.kind = deps.kind;
|
|
11797
11636
|
this.logger = deps.logger;
|
|
11798
11637
|
}
|
|
11799
|
-
/**
|
|
11800
|
-
* Register the collection. MUST run at boot, before any read or write: the
|
|
11801
|
-
* SQLite backend answers 412 for an undeclared collection and takes the whole
|
|
11802
|
-
* runner down with it (the addon-ai boot-crash lesson).
|
|
11803
|
-
*/
|
|
11804
|
-
static declare(store, spec) {
|
|
11805
|
-
return store.declareCollection.mutate({
|
|
11806
|
-
collection: spec.collection,
|
|
11807
|
-
columns: [...spec.columns],
|
|
11808
|
-
...spec.indexes !== void 0 ? { indexes: [...spec.indexes] } : {}
|
|
11809
|
-
});
|
|
11810
|
-
}
|
|
11811
|
-
declare() {
|
|
11812
|
-
return DurableLedger.declare(this.store, this.spec);
|
|
11813
|
-
}
|
|
11814
11638
|
/** The collection this ledger owns — for the caller's own log lines. */
|
|
11815
11639
|
get collection() {
|
|
11816
11640
|
return this.spec.collection;
|
|
@@ -11827,31 +11651,43 @@ var DurableLedger = class DurableLedger {
|
|
|
11827
11651
|
*/
|
|
11828
11652
|
async load() {
|
|
11829
11653
|
try {
|
|
11830
|
-
const records = await this.store.query.query({
|
|
11831
|
-
collection: this.spec.collection,
|
|
11832
|
-
filter: { limit: this.spec.loadLimit ?? DEFAULT_LOAD_LIMIT }
|
|
11833
|
-
});
|
|
11834
11654
|
const next = /* @__PURE__ */ new Map();
|
|
11835
11655
|
let skipped = 0;
|
|
11836
|
-
|
|
11837
|
-
|
|
11838
|
-
|
|
11839
|
-
|
|
11840
|
-
|
|
11656
|
+
let seen = 0;
|
|
11657
|
+
const cap = this.spec.loadLimit ?? DEFAULT_LOAD_LIMIT;
|
|
11658
|
+
let cursor;
|
|
11659
|
+
for (;;) {
|
|
11660
|
+
const limit = Math.min(QUERY_PAGE, cap - seen);
|
|
11661
|
+
if (limit <= 0) break;
|
|
11662
|
+
const page = await this.kind.query({
|
|
11663
|
+
limit,
|
|
11664
|
+
...cursor === void 0 ? {} : { cursor }
|
|
11665
|
+
});
|
|
11666
|
+
seen += page.records.length;
|
|
11667
|
+
for (const record of page.records) {
|
|
11668
|
+
const row = this.spec.fromRecord(record.id, record.data);
|
|
11669
|
+
if (row === null) {
|
|
11670
|
+
skipped += 1;
|
|
11671
|
+
continue;
|
|
11672
|
+
}
|
|
11673
|
+
next.set(this.spec.keyOf(row), row);
|
|
11841
11674
|
}
|
|
11842
|
-
|
|
11675
|
+
if (page.nextCursor === null) break;
|
|
11676
|
+
cursor = page.nextCursor;
|
|
11843
11677
|
}
|
|
11844
11678
|
this.mirror.clear();
|
|
11845
11679
|
for (const [key, row] of next) this.mirror.set(key, row);
|
|
11846
11680
|
this.answered = true;
|
|
11847
11681
|
if (skipped > 0) this.logger.warn("durable rows skipped as malformed — they gate NOTHING", { meta: {
|
|
11848
11682
|
collection: this.spec.collection,
|
|
11683
|
+
kind: this.kind.name,
|
|
11849
11684
|
skipped
|
|
11850
11685
|
} });
|
|
11851
11686
|
return [...this.mirror.values()];
|
|
11852
11687
|
} catch (err) {
|
|
11853
11688
|
this.logger.warn("durable load failed — keeping the state already in memory", { meta: {
|
|
11854
11689
|
collection: this.spec.collection,
|
|
11690
|
+
kind: this.kind.name,
|
|
11855
11691
|
error: String(err),
|
|
11856
11692
|
held: this.mirror.size
|
|
11857
11693
|
} });
|
|
@@ -11916,15 +11752,15 @@ var DurableLedger = class DurableLedger {
|
|
|
11916
11752
|
*/
|
|
11917
11753
|
async put(row) {
|
|
11918
11754
|
const key = this.spec.keyOf(row);
|
|
11755
|
+
const body = this.spec.toValue(row);
|
|
11919
11756
|
if (this.spec.writeMode === "write-behind") {
|
|
11920
11757
|
this.mirror.set(key, row);
|
|
11921
|
-
await this.
|
|
11758
|
+
await this.persistRow(key, body);
|
|
11922
11759
|
return;
|
|
11923
11760
|
}
|
|
11924
|
-
await this.
|
|
11925
|
-
|
|
11926
|
-
|
|
11927
|
-
value: this.spec.toValue(row)
|
|
11761
|
+
await this.kind.put({
|
|
11762
|
+
id: key,
|
|
11763
|
+
data: body
|
|
11928
11764
|
});
|
|
11929
11765
|
this.mirror.set(key, row);
|
|
11930
11766
|
}
|
|
@@ -11965,13 +11801,11 @@ var DurableLedger = class DurableLedger {
|
|
|
11965
11801
|
async forget(key) {
|
|
11966
11802
|
this.mirror.delete(key);
|
|
11967
11803
|
try {
|
|
11968
|
-
await this.
|
|
11969
|
-
collection: this.spec.collection,
|
|
11970
|
-
key
|
|
11971
|
-
});
|
|
11804
|
+
await this.kind.delete(key);
|
|
11972
11805
|
} catch (err) {
|
|
11973
11806
|
this.logger.debug("durable delete failed", { meta: {
|
|
11974
11807
|
collection: this.spec.collection,
|
|
11808
|
+
kind: this.kind.name,
|
|
11975
11809
|
key,
|
|
11976
11810
|
error: String(err)
|
|
11977
11811
|
} });
|
|
@@ -12007,15 +11841,13 @@ var DurableLedger = class DurableLedger {
|
|
|
12007
11841
|
for (const key of [...this.mirror.keys()]) {
|
|
12008
11842
|
if (activeKeys.has(key)) continue;
|
|
12009
11843
|
try {
|
|
12010
|
-
await this.
|
|
12011
|
-
collection: this.spec.collection,
|
|
12012
|
-
key
|
|
12013
|
-
});
|
|
11844
|
+
await this.kind.delete(key);
|
|
12014
11845
|
this.mirror.delete(key);
|
|
12015
11846
|
pruned += 1;
|
|
12016
11847
|
} catch (err) {
|
|
12017
11848
|
this.logger.debug("durable prune delete failed", { meta: {
|
|
12018
11849
|
collection: this.spec.collection,
|
|
11850
|
+
kind: this.kind.name,
|
|
12019
11851
|
key,
|
|
12020
11852
|
error: String(err)
|
|
12021
11853
|
} });
|
|
@@ -12028,25 +11860,34 @@ var DurableLedger = class DurableLedger {
|
|
|
12028
11860
|
* decision path: the mirror already holds the truth for this process, and the
|
|
12029
11861
|
* worst a lost write can do is one silent re-seed after the next restart.
|
|
12030
11862
|
*/
|
|
12031
|
-
|
|
12032
|
-
const
|
|
11863
|
+
persist(row) {
|
|
11864
|
+
const key = this.spec.keyOf(row);
|
|
11865
|
+
this.persistRow(key, this.spec.toValue(row));
|
|
11866
|
+
}
|
|
11867
|
+
async persistRow(key, body) {
|
|
11868
|
+
const deviceId = this.deviceIdFromBody(body);
|
|
12033
11869
|
try {
|
|
12034
|
-
await this.
|
|
12035
|
-
|
|
12036
|
-
|
|
12037
|
-
value: this.spec.toValue(row)
|
|
11870
|
+
await this.kind.put({
|
|
11871
|
+
id: key,
|
|
11872
|
+
data: body
|
|
12038
11873
|
});
|
|
12039
11874
|
} catch (err) {
|
|
12040
11875
|
this.logger.warn("durable persist failed — this key may re-seed on boot", {
|
|
12041
11876
|
...deviceId !== void 0 ? { tags: { deviceId } } : {},
|
|
12042
11877
|
meta: {
|
|
12043
11878
|
collection: this.spec.collection,
|
|
12044
|
-
|
|
11879
|
+
kind: this.kind.name,
|
|
11880
|
+
key,
|
|
12045
11881
|
error: String(err)
|
|
12046
11882
|
}
|
|
12047
11883
|
});
|
|
12048
11884
|
}
|
|
12049
11885
|
}
|
|
11886
|
+
deviceIdFromBody(body) {
|
|
11887
|
+
const raw = body["deviceId"];
|
|
11888
|
+
const n = typeof raw === "number" ? raw : Number(raw);
|
|
11889
|
+
return Number.isFinite(n) ? n : void 0;
|
|
11890
|
+
}
|
|
12050
11891
|
};
|
|
12051
11892
|
//#endregion
|
|
12052
11893
|
//#region src/recorder/addon/segment-hour-ledger.ts
|
|
@@ -12607,15 +12448,15 @@ var SegmentHourLedger = class SegmentHourLedger {
|
|
|
12607
12448
|
this.rowCap = deps.loadPacing?.rowCap ?? 25e4;
|
|
12608
12449
|
this.ledger = new DurableLedger({
|
|
12609
12450
|
spec: SPEC,
|
|
12610
|
-
|
|
12451
|
+
kind: durableLegacyKind(SPEC, deps.store),
|
|
12611
12452
|
logger: deps.logger
|
|
12612
12453
|
});
|
|
12613
12454
|
}
|
|
12614
12455
|
static declare(store) {
|
|
12615
|
-
return
|
|
12456
|
+
return declareDurableLegacyCollection(store, SPEC);
|
|
12616
12457
|
}
|
|
12617
12458
|
declare() {
|
|
12618
|
-
return this.
|
|
12459
|
+
return declareDurableLegacyCollection(this.store, SPEC);
|
|
12619
12460
|
}
|
|
12620
12461
|
/**
|
|
12621
12462
|
* Boot load — every hour row, in one query.
|
|
@@ -15026,63 +14867,61 @@ var LocationRefresher = class {
|
|
|
15026
14867
|
}
|
|
15027
14868
|
}
|
|
15028
14869
|
};
|
|
15029
|
-
|
|
15030
|
-
|
|
15031
|
-
|
|
15032
|
-
|
|
15033
|
-
|
|
15034
|
-
|
|
15035
|
-
|
|
15036
|
-
|
|
15037
|
-
|
|
15038
|
-
|
|
15039
|
-
|
|
15040
|
-
|
|
15041
|
-
|
|
15042
|
-
|
|
15043
|
-
|
|
15044
|
-
|
|
15045
|
-
|
|
15046
|
-
|
|
15047
|
-
|
|
15048
|
-
|
|
15049
|
-
|
|
15050
|
-
|
|
15051
|
-
|
|
15052
|
-
|
|
15053
|
-
|
|
15054
|
-
|
|
15055
|
-
|
|
15056
|
-
|
|
15057
|
-
|
|
15058
|
-
|
|
15059
|
-
|
|
15060
|
-
|
|
14870
|
+
//#endregion
|
|
14871
|
+
//#region src/recorder/addon/ops-log-store.ts
|
|
14872
|
+
/**
|
|
14873
|
+
* Recordings ops-log persistence for the recorder addon.
|
|
14874
|
+
*
|
|
14875
|
+
* ONE ROW PER OPERATION in the `recorder` / `ops-log` record kind, through a
|
|
14876
|
+
* {@link RecordKindHandle}. The ring is bounded — `maxEntries` newest rows
|
|
14877
|
+
* survive — trimmed with one `deleteWhere({ atBefore })` per append.
|
|
14878
|
+
*
|
|
14879
|
+
* ## Records migration (phase 2, step 26)
|
|
14880
|
+
*
|
|
14881
|
+
* The store was previously backed by a {@link RowMapStore} over a SettingsStore
|
|
14882
|
+
* collection named `recorder:ops-log`. It is now backed by a
|
|
14883
|
+
* `RecordKindHandle<OpsLogEntry>` obtained from `ctx.records`. The record's
|
|
14884
|
+
* `at` field maps to `OpsLogEntry.at`.
|
|
14885
|
+
*
|
|
14886
|
+
* ## What the RowMapStore replaced
|
|
14887
|
+
*
|
|
14888
|
+
* The ring lived in ONE key of the recorder's `addon-settings` / `root` blob,
|
|
14889
|
+
* as a flat JSON array. On the reference hub it was 147 437 bytes of that row's
|
|
14890
|
+
* 201 833 — so appending one audit line read, parsed, re-serialised and rewrote
|
|
14891
|
+
* ~200 KB, and so did every unrelated reader of that row. The ops log is the
|
|
14892
|
+
* CHEAPEST thing in the recorder and it was paying for the most expensive.
|
|
14893
|
+
*
|
|
14894
|
+
* Writes stay BEST-EFFORT: `createRecordingOpsLogSink().append` never throws —
|
|
14895
|
+
* a failed audit write must not fail the operation it records.
|
|
14896
|
+
*/
|
|
14897
|
+
/**
|
|
14898
|
+
* Obtain the `recorder` / `ops-log` record kind handle.
|
|
14899
|
+
* `ctx.records.declare(recorderRecordKinds)` MUST have run before any read or
|
|
14900
|
+
* write (called at boot in `onInitialize`).
|
|
14901
|
+
*/
|
|
14902
|
+
function recordingOpsLogState(records) {
|
|
14903
|
+
return records.kind(recorderRecordKinds["ops-log"]);
|
|
15061
14904
|
}
|
|
15062
14905
|
/**
|
|
15063
14906
|
* Trim the ring to the newest `maxEntries` rows. Returns how many went.
|
|
15064
14907
|
*
|
|
15065
14908
|
* The cursor is the OLDEST row that must SURVIVE — offset `maxEntries - 1`
|
|
15066
|
-
* newest-first — and the delete
|
|
14909
|
+
* newest-first — and the delete removes everything strictly BEFORE its `at`
|
|
14910
|
+
* (`atBefore: cursor.at` deletes rows where `at < cursor.at`).
|
|
15067
14911
|
*
|
|
15068
|
-
* Strictly,
|
|
15069
|
-
*
|
|
15070
|
-
* and
|
|
15071
|
-
* Erring this way keeps a few EXTRA audit rows on a tie and never fewer than
|
|
15072
|
-
* the cap.
|
|
14912
|
+
* Strictly, to avoid dropping the cursor row itself and any rows tied on its
|
|
14913
|
+
* millisecond. The ring may hold a few MORE than the cap on a millisecond tie
|
|
14914
|
+
* and never FEWER.
|
|
15073
14915
|
*/
|
|
15074
14916
|
async function trimOpsLog(state, maxEntries = 500) {
|
|
15075
|
-
if (await state.count() <= maxEntries) return 0;
|
|
15076
|
-
const
|
|
15077
|
-
|
|
15078
|
-
|
|
15079
|
-
|
|
15080
|
-
|
|
15081
|
-
|
|
15082
|
-
|
|
15083
|
-
}))[0];
|
|
15084
|
-
if (oldestKept === void 0) return 0;
|
|
15085
|
-
return state.deleteWhere({ whereBetween: { at: [0, oldestKept.value.at - 1] } });
|
|
14917
|
+
if (await state.count({}) <= maxEntries) return 0;
|
|
14918
|
+
const page = await state.query({
|
|
14919
|
+
desc: true,
|
|
14920
|
+
limit: maxEntries
|
|
14921
|
+
});
|
|
14922
|
+
const oldest = page.records[page.records.length - 1];
|
|
14923
|
+
if (oldest === void 0) return 0;
|
|
14924
|
+
return state.deleteWhere({ atBefore: oldest.at ?? 0 });
|
|
15086
14925
|
}
|
|
15087
14926
|
/**
|
|
15088
14927
|
* Append one row and persist, then trim the ring. Validates at the boundary.
|
|
@@ -15090,7 +14929,11 @@ async function trimOpsLog(state, maxEntries = 500) {
|
|
|
15090
14929
|
*/
|
|
15091
14930
|
async function appendOpsLog(state, entry, maxEntries = 500) {
|
|
15092
14931
|
const validated = require_dist.OpsLogEntrySchema.parse(entry);
|
|
15093
|
-
await state.
|
|
14932
|
+
await state.put({
|
|
14933
|
+
id: validated.id,
|
|
14934
|
+
at: validated.at,
|
|
14935
|
+
data: validated
|
|
14936
|
+
});
|
|
15094
14937
|
await trimOpsLog(state, maxEntries);
|
|
15095
14938
|
return validated;
|
|
15096
14939
|
}
|
|
@@ -15098,18 +14941,15 @@ async function appendOpsLog(state, entry, maxEntries = 500) {
|
|
|
15098
14941
|
* List ops-log rows newest-first, optionally scoped to one device, capped at
|
|
15099
14942
|
* `limit` (default {@link OPS_LOG_DEFAULT_LIMIT}).
|
|
15100
14943
|
*
|
|
15101
|
-
* Scoped to a device this is an INDEXED query
|
|
15102
|
-
* followed by a filter and a sort in JS.
|
|
14944
|
+
* Scoped to a device this is an INDEXED query on the `device` index — not a
|
|
14945
|
+
* read of the whole ring followed by a filter and a sort in JS.
|
|
15103
14946
|
*/
|
|
15104
14947
|
async function listOpsLog(state, query) {
|
|
15105
|
-
return (await state.
|
|
14948
|
+
return (await state.query({
|
|
15106
14949
|
...query.deviceId === void 0 ? {} : { where: { deviceId: query.deviceId } },
|
|
15107
|
-
|
|
15108
|
-
field: "at",
|
|
15109
|
-
direction: "desc"
|
|
15110
|
-
},
|
|
14950
|
+
desc: true,
|
|
15111
14951
|
limit: query.limit ?? 200
|
|
15112
|
-
})).map((
|
|
14952
|
+
})).records.map((r) => r.data);
|
|
15113
14953
|
}
|
|
15114
14954
|
/**
|
|
15115
14955
|
* Build the recorder's ops-log sink. `append` stamps `domain:'recording'`,
|
|
@@ -23567,7 +23407,6 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
23567
23407
|
readCensus = new StoreReadCensus();
|
|
23568
23408
|
/** Memoised so every `configStore()` / `exportsStore()` / `opsLogStore()`
|
|
23569
23409
|
* call shares ONE census instead of each wrapping the raw client afresh. */
|
|
23570
|
-
censusedSettingsStoreClient = null;
|
|
23571
23410
|
constructor() {
|
|
23572
23411
|
super({ ...DEFAULT_CONFIG });
|
|
23573
23412
|
}
|
|
@@ -23604,6 +23443,9 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
23604
23443
|
} });
|
|
23605
23444
|
return { providers: [] };
|
|
23606
23445
|
}
|
|
23446
|
+
const records = this.ctx.records;
|
|
23447
|
+
if (records === void 0) throw new Error("recorder (recording node) needs ctx.records — recording node must be the hub");
|
|
23448
|
+
await records.declare(recorderRecordKinds);
|
|
23607
23449
|
await this.declareRowCollections();
|
|
23608
23450
|
this.segmentHours = new SegmentHourLedger({
|
|
23609
23451
|
store: this.ctx.api.settingsStore,
|
|
@@ -25424,26 +25266,16 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
25424
25266
|
} });
|
|
25425
25267
|
}
|
|
25426
25268
|
/**
|
|
25427
|
-
* Declare the
|
|
25428
|
-
* `
|
|
25429
|
-
*
|
|
25269
|
+
* Declare the remaining RowMapStore collection that replaced the
|
|
25270
|
+
* `recordingConfigs` blob key. Idempotent — a re-declaration of the same
|
|
25271
|
+
* shape is a no-op in the engine.
|
|
25272
|
+
*
|
|
25273
|
+
* `recorder:exports` and `recorder:ops-log` are now record kinds declared
|
|
25274
|
+
* via `ctx.records.declare(recorderRecordKinds)` at the top of
|
|
25275
|
+
* `onInitialize`.
|
|
25430
25276
|
*/
|
|
25431
25277
|
async declareRowCollections() {
|
|
25432
25278
|
await this.configStore().declare();
|
|
25433
|
-
await this.exportsStore().declare();
|
|
25434
|
-
await this.opsLogStore().declare();
|
|
25435
|
-
}
|
|
25436
|
-
/**
|
|
25437
|
-
* `ctx.api.settingsStore`, wrapped ONCE through {@link censusStoreClient}.
|
|
25438
|
-
*
|
|
25439
|
-
* LAZY (see `calendar` above): `ctx` is not assigned until `initialize()`
|
|
25440
|
-
* runs, so this cannot be a field initialiser. The wrap is permanent and
|
|
25441
|
-
* cheap when disarmed (one `get` trap and a comparison) — what is armed and
|
|
25442
|
-
* disarmed is `this.readCensus`, through `debug.storeCensus`, not this seam.
|
|
25443
|
-
*/
|
|
25444
|
-
censusedSettingsStore() {
|
|
25445
|
-
this.censusedSettingsStoreClient ??= censusStoreClient(this.ctx.api.settingsStore, this.readCensus);
|
|
25446
|
-
return this.censusedSettingsStoreClient;
|
|
25447
25279
|
}
|
|
25448
25280
|
configsState = null;
|
|
25449
25281
|
/** A camera's recording config — its per-device settings (D865), migrated on read. */
|
|
@@ -25456,14 +25288,23 @@ var RecorderV2Addon = class extends require_dist.BaseAddon {
|
|
|
25456
25288
|
});
|
|
25457
25289
|
return this.configsState;
|
|
25458
25290
|
}
|
|
25459
|
-
/**
|
|
25460
|
-
* wrapper as the config store). */
|
|
25291
|
+
/** Record kind handle over the `recorder` / `exports` kind (hub recording node only). */
|
|
25461
25292
|
exportsStore() {
|
|
25462
|
-
return recordingExportsState(this.
|
|
25293
|
+
return recordingExportsState(this.requireRecords());
|
|
25463
25294
|
}
|
|
25464
|
-
/**
|
|
25295
|
+
/** Record kind handle over the `recorder` / `ops-log` kind (hub recording node only). */
|
|
25465
25296
|
opsLogStore() {
|
|
25466
|
-
return recordingOpsLogState(this.
|
|
25297
|
+
return recordingOpsLogState(this.requireRecords());
|
|
25298
|
+
}
|
|
25299
|
+
/**
|
|
25300
|
+
* Returns `ctx.records`, throwing clearly when it is absent.
|
|
25301
|
+
* Only called on the designated recording node (hub by default); an agent
|
|
25302
|
+
* exits before `onInitialize` reaches any store call.
|
|
25303
|
+
*/
|
|
25304
|
+
requireRecords() {
|
|
25305
|
+
const records = this.ctx.records;
|
|
25306
|
+
if (records === void 0) throw new Error("recorder (recording node) needs ctx.records — recording node must be the hub");
|
|
25307
|
+
return records;
|
|
25467
25308
|
}
|
|
25468
25309
|
/** Durable handle over the recorder-owned placement blob (assignments + volume ids). */
|
|
25469
25310
|
placementStore() {
|