@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.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { $n as
|
|
2
|
-
import {
|
|
1
|
+
import { $n as sleep$1, $t as recordingArchiveCapability, An as errMsg$1, B as RECORDING_SOURCE_CAMSTACK_ADDON, Bn as defineCustomActions, Bt as memoizePinnedFfmpeg, C as EVENT_PAD_MS, Ct as evictionPolicyForMode, Ft as loadContributionCapability, G as VIDEOCLIPS_MAX_OFFER_BYTES, H as RecordingConfigSchema, Ht as modeMayWrite, K as VISIT_MERGE_GAP_MS, Mn as BaseAddon, Mt as isFirstLevelMacroClass, N as OpsLogEntrySchema, On as DeviceFeature, Ot as gbToBytes, Qn as selectAssignedProfileSlots, R as RECORDING_EXPORT_MAX_READ_BYTES, Rt as mayReadLocation, S as EVENT_OWNER_TYPES, T as ExportRecordSchema, Tn as evaluateSensorEdge, Tt as exportSubjectOf, U as RecordingSignalStatusSchema, Vn as defineRecordKinds, Vt as migrateLegacyDeviceSignalConfig, Wn as hydrateSchema, Xn as nodePin, Z as armRemovalReconcile, _n as stripRetiredBandPreBufferSec, at as clipExportFailure, cr as object, dr as string, en as recordingCapability, er as userError, hn as storageOccupancyCapability, ht as deriveRecordingMode, in as resolveCreateExportSubject, kn as DeviceType, ln as resolveRecordingProfiles, lr as preprocess, mn as storageEvictableCapability, nn as recordingSignalCapability, nr as array, o as BatteryStatusSchema, on as resolveLocationMode, ot as clipExportFailureOf, pr as EventCategory, pt as deriveBatteryPresence, rr as boolean, s as CLIP_BROKER_PACED_RATES, sr as number, tn as recordingExportCapability, tr as _enum, tt as batteryCapability, ur as record, v as DEFAULT_EVENTS_BAND_BUFFER_SEC, vt as deviceRemovalHookCapability, wt as evictionPolicyOfLocation, yn as videoclipsCapability, z as RECORDING_SOURCE_CAMSTACK, zn as customAction, zt as mayWriteToLocation } from "../dist-BaHsDOWa.mjs";
|
|
2
|
+
import { i as NO_COST_CLAIM, o as physicalRootOf, t as ChildCostRegistry } from "../node-B0a0dXhg.mjs";
|
|
3
3
|
import { t as resolveHubHostname } from "../hub-hostname-cCknRYKj.mjs";
|
|
4
4
|
import { d as PREBUFFER_MAX_MS, l as withRecordingIntent, r as RECORDING_PRE_ROLL_REQUEST_MS } from "../restream-intent-DyGmLAU9.mjs";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
@@ -5196,29 +5196,6 @@ function activeModeForConfig(config) {
|
|
|
5196
5196
|
* pays for ten minutes, not until the next restart.
|
|
5197
5197
|
*/
|
|
5198
5198
|
var CENSUS_MAX_WINDOW_MS = 10 * 6e4;
|
|
5199
|
-
/** Store procedures worth attributing. Reads only: a write is an operator
|
|
5200
|
-
* gesture or a pipeline product, and neither is a mystery. */
|
|
5201
|
-
var CENSUSED_PROCEDURES = new Set([
|
|
5202
|
-
"query",
|
|
5203
|
-
"count",
|
|
5204
|
-
"aggregate",
|
|
5205
|
-
"get",
|
|
5206
|
-
"histogram",
|
|
5207
|
-
"getAll"
|
|
5208
|
-
]);
|
|
5209
|
-
/**
|
|
5210
|
-
* Can `new Proxy` wrap this, and is it worth wrapping?
|
|
5211
|
-
*
|
|
5212
|
-
* Functions count. The generated store client is a **tRPC proxy** and every
|
|
5213
|
-
* intermediate node of one is CALLABLE — `typeof client.query` is `'function'`,
|
|
5214
|
-
* not `'object'`. A wrapper that tested only for `'object'` handed the whole
|
|
5215
|
-
* client back unwrapped, deployed, and reported zero `query` and zero `count`
|
|
5216
|
-
* call sites while the engine was recording thousands a minute against the same
|
|
5217
|
-
* tables. Silence read as "nobody in this addon does it".
|
|
5218
|
-
*/
|
|
5219
|
-
function isProxyable(value) {
|
|
5220
|
-
return typeof value === "object" && value !== null || typeof value === "function";
|
|
5221
|
-
}
|
|
5222
5199
|
function rowsOf(value) {
|
|
5223
5200
|
if (Array.isArray(value)) return value.length;
|
|
5224
5201
|
if (typeof value === "number") return value;
|
|
@@ -5342,34 +5319,6 @@ var StoreReadCensus = class {
|
|
|
5342
5319
|
})).sort((a, b) => b.calls - a.calls);
|
|
5343
5320
|
}
|
|
5344
5321
|
};
|
|
5345
|
-
/**
|
|
5346
|
-
* Wrap a settings-store client so every read is counted.
|
|
5347
|
-
*
|
|
5348
|
-
* A `Proxy` over the generated tRPC client rather than a hand-written facade:
|
|
5349
|
-
* the client has a procedure per cap method and a facade would need updating
|
|
5350
|
-
* every time the cap gains one - which is how a diagnostic quietly stops seeing
|
|
5351
|
-
* half the traffic. Unrecognised procedures pass straight through.
|
|
5352
|
-
*/
|
|
5353
|
-
function censusStoreClient(client, census) {
|
|
5354
|
-
return new Proxy(client, { get(target, prop, receiver) {
|
|
5355
|
-
const procedure = Reflect.get(target, prop, receiver);
|
|
5356
|
-
if (typeof prop !== "string" || !CENSUSED_PROCEDURES.has(prop)) return procedure;
|
|
5357
|
-
if (!isProxyable(procedure)) return procedure;
|
|
5358
|
-
const op = prop;
|
|
5359
|
-
return new Proxy(procedure, { get(procTarget, procProp, procReceiver) {
|
|
5360
|
-
const call = Reflect.get(procTarget, procProp, procReceiver);
|
|
5361
|
-
if (procProp !== "query" && procProp !== "mutate") return call;
|
|
5362
|
-
if (typeof call !== "function") return call;
|
|
5363
|
-
const wrapped = async (...args) => {
|
|
5364
|
-
const site = census.siteOf(wrapped);
|
|
5365
|
-
const result = await Reflect.apply(call, procTarget, args);
|
|
5366
|
-
census.record(op, site, result);
|
|
5367
|
-
return result;
|
|
5368
|
-
};
|
|
5369
|
-
return wrapped;
|
|
5370
|
-
} });
|
|
5371
|
-
} });
|
|
5372
|
-
}
|
|
5373
5322
|
/** Log the census as one line, including when it collected nothing - silence
|
|
5374
5323
|
* after an arm is indistinguishable from a census that never armed. */
|
|
5375
5324
|
function logCensus(census, logger, topN) {
|
|
@@ -6143,7 +6092,9 @@ Object.freeze({
|
|
|
6143
6092
|
},
|
|
6144
6093
|
"setDeviceConfig": { "config": { "__nested__": { "bands": [] } } }
|
|
6145
6094
|
},
|
|
6095
|
+
"records": { "query": { "limit": 100 } },
|
|
6146
6096
|
"storage": { "upsertLocation": { "isSystem": false } },
|
|
6097
|
+
"storage-admin": { "readRecords": { "limit": 100 } },
|
|
6147
6098
|
"storage-provider": {
|
|
6148
6099
|
"beginDownload": { "location": { "__nested__": { "isSystem": false } } },
|
|
6149
6100
|
"beginUpload": { "location": { "__nested__": { "isSystem": false } } },
|
|
@@ -6578,6 +6529,18 @@ Object.freeze({
|
|
|
6578
6529
|
{
|
|
6579
6530
|
"path": "swingHorizontal",
|
|
6580
6531
|
"valueType": "boolean"
|
|
6532
|
+
},
|
|
6533
|
+
{
|
|
6534
|
+
"path": "minTemp",
|
|
6535
|
+
"valueType": "number"
|
|
6536
|
+
},
|
|
6537
|
+
{
|
|
6538
|
+
"path": "maxTemp",
|
|
6539
|
+
"valueType": "number"
|
|
6540
|
+
},
|
|
6541
|
+
{
|
|
6542
|
+
"path": "tempStep",
|
|
6543
|
+
"valueType": "number"
|
|
6581
6544
|
}
|
|
6582
6545
|
],
|
|
6583
6546
|
"color": [
|
|
@@ -8347,259 +8310,76 @@ function buildExportArgs(input) {
|
|
|
8347
8310
|
outPath
|
|
8348
8311
|
];
|
|
8349
8312
|
}
|
|
8350
|
-
var
|
|
8351
|
-
var RowMapStore = class RowMapStore {
|
|
8352
|
-
spec;
|
|
8353
|
-
store;
|
|
8354
|
-
logger;
|
|
8355
|
-
constructor(deps) {
|
|
8356
|
-
this.spec = deps.spec;
|
|
8357
|
-
this.store = deps.store;
|
|
8358
|
-
this.logger = deps.logger;
|
|
8359
|
-
}
|
|
8360
|
-
/** The collection this store owns — for the caller's own log lines. */
|
|
8361
|
-
get collection() {
|
|
8362
|
-
return this.spec.collection;
|
|
8363
|
-
}
|
|
8313
|
+
var recorderRecordKinds = defineRecordKinds({
|
|
8364
8314
|
/**
|
|
8365
|
-
*
|
|
8366
|
-
*
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
return store.declareCollection.mutate({
|
|
8370
|
-
collection: spec.collection,
|
|
8371
|
-
columns: [
|
|
8372
|
-
{
|
|
8373
|
-
name: "id",
|
|
8374
|
-
type: "TEXT",
|
|
8375
|
-
primaryKey: true,
|
|
8376
|
-
notNull: true
|
|
8377
|
-
},
|
|
8378
|
-
...spec.columns,
|
|
8379
|
-
{
|
|
8380
|
-
name: ROW_MAP_VALUE_COLUMN,
|
|
8381
|
-
type: "JSON",
|
|
8382
|
-
notNull: true
|
|
8383
|
-
}
|
|
8384
|
-
],
|
|
8385
|
-
...spec.indexes !== void 0 ? { indexes: [...spec.indexes] } : {}
|
|
8386
|
-
});
|
|
8387
|
-
}
|
|
8388
|
-
declare() {
|
|
8389
|
-
return RowMapStore.declare(this.store, this.spec);
|
|
8390
|
-
}
|
|
8391
|
-
/** One entry, by key. `null` when absent OR unparseable (which is said aloud). */
|
|
8392
|
-
async read(key) {
|
|
8393
|
-
const raw = await this.store.get.query({
|
|
8394
|
-
collection: this.spec.collection,
|
|
8395
|
-
key
|
|
8396
|
-
});
|
|
8397
|
-
if (!isRecord(raw)) return null;
|
|
8398
|
-
const row = raw;
|
|
8399
|
-
const parsed = this.spec.schema.safeParse(row[ROW_MAP_VALUE_COLUMN]);
|
|
8400
|
-
if (!parsed.success) {
|
|
8401
|
-
this.logger.warn("row-map row skipped as unparseable", {
|
|
8402
|
-
...this.deviceTags(row),
|
|
8403
|
-
meta: {
|
|
8404
|
-
collection: this.spec.collection,
|
|
8405
|
-
key,
|
|
8406
|
-
error: parsed.error.message
|
|
8407
|
-
}
|
|
8408
|
-
});
|
|
8409
|
-
return null;
|
|
8410
|
-
}
|
|
8411
|
-
return parsed.data;
|
|
8412
|
-
}
|
|
8413
|
-
/**
|
|
8414
|
-
* Every entry, keyed. Unparseable rows are skipped — one bad entry costs that
|
|
8415
|
-
* entry, never the map.
|
|
8416
|
-
*/
|
|
8417
|
-
async readAll() {
|
|
8418
|
-
const records = await this.store.query.query({
|
|
8419
|
-
collection: this.spec.collection,
|
|
8420
|
-
filter: { limit: this.spec.loadLimit }
|
|
8421
|
-
});
|
|
8422
|
-
const out = /* @__PURE__ */ new Map();
|
|
8423
|
-
let skipped = 0;
|
|
8424
|
-
for (const record of records) {
|
|
8425
|
-
const parsed = this.spec.schema.safeParse(record.data[ROW_MAP_VALUE_COLUMN]);
|
|
8426
|
-
if (!parsed.success) {
|
|
8427
|
-
skipped += 1;
|
|
8428
|
-
continue;
|
|
8429
|
-
}
|
|
8430
|
-
out.set(record.id, parsed.data);
|
|
8431
|
-
}
|
|
8432
|
-
if (skipped > 0) this.logger.warn("row-map rows skipped as unparseable", { meta: {
|
|
8433
|
-
collection: this.spec.collection,
|
|
8434
|
-
skipped,
|
|
8435
|
-
kept: out.size
|
|
8436
|
-
} });
|
|
8437
|
-
if (records.length >= this.spec.loadLimit) this.logger.warn("row-map read hit its own bound — the map may be truncated", { meta: {
|
|
8438
|
-
collection: this.spec.collection,
|
|
8439
|
-
loadLimit: this.spec.loadLimit
|
|
8440
|
-
} });
|
|
8441
|
-
return out;
|
|
8442
|
-
}
|
|
8443
|
-
/**
|
|
8444
|
-
* Every entry as it was WRITTEN, unvalidated and unparsed — the read a
|
|
8445
|
-
* MIGRATION needs.
|
|
8446
|
-
*
|
|
8447
|
-
* {@link readAll} answers through the CURRENT schema, which is exactly what a
|
|
8448
|
-
* migration must not do: a retired key is stripped by a non-strict `z.object`
|
|
8449
|
-
* before the migration could see it, so the row it is there to rescue looks
|
|
8450
|
-
* like a row that never needed rescuing. Only a migration should call this;
|
|
8451
|
-
* everything else wants the validated read.
|
|
8452
|
-
*/
|
|
8453
|
-
async readAllRaw() {
|
|
8454
|
-
const records = await this.store.query.query({
|
|
8455
|
-
collection: this.spec.collection,
|
|
8456
|
-
filter: { limit: this.spec.loadLimit }
|
|
8457
|
-
});
|
|
8458
|
-
const out = /* @__PURE__ */ new Map();
|
|
8459
|
-
for (const record of records) out.set(record.id, record.data[ROW_MAP_VALUE_COLUMN]);
|
|
8460
|
-
return out;
|
|
8461
|
-
}
|
|
8462
|
-
/** Insert or replace ONE entry. Validates at the boundary. */
|
|
8463
|
-
async write(key, value) {
|
|
8464
|
-
const validated = this.spec.schema.parse(value);
|
|
8465
|
-
const columns = this.spec.project(key, validated);
|
|
8466
|
-
await this.store.set.mutate({
|
|
8467
|
-
collection: this.spec.collection,
|
|
8468
|
-
key,
|
|
8469
|
-
value: {
|
|
8470
|
-
...columns,
|
|
8471
|
-
[ROW_MAP_VALUE_COLUMN]: validated
|
|
8472
|
-
}
|
|
8473
|
-
});
|
|
8474
|
-
}
|
|
8475
|
-
/** Drop ONE entry. Idempotent. */
|
|
8476
|
-
async remove(key) {
|
|
8477
|
-
await this.store.delete.mutate({
|
|
8478
|
-
collection: this.spec.collection,
|
|
8479
|
-
key
|
|
8480
|
-
});
|
|
8481
|
-
}
|
|
8482
|
-
/**
|
|
8483
|
-
* Make the collection hold exactly `next` — the write path for an owner whose
|
|
8484
|
-
* in-memory map IS the authority (a persister handed the complete set).
|
|
8315
|
+
* One row per export job, inserted by `createExport` and updated as the
|
|
8316
|
+
* render progresses. The system NEVER deletes a row — it is the export AUDIT.
|
|
8317
|
+
* `deleteExport` and the janitor flip `state` and clear the file, keeping
|
|
8318
|
+
* the row. The record's `at` field maps to `createdAt`.
|
|
8485
8319
|
*
|
|
8486
|
-
*
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
|
|
8490
|
-
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8497
|
-
|
|
8498
|
-
|
|
8499
|
-
|
|
8500
|
-
|
|
8501
|
-
dropped: doomed.length,
|
|
8502
|
-
keys: doomed.slice(0, 20).map((r) => r.id),
|
|
8503
|
-
remaining: next.size
|
|
8504
|
-
} });
|
|
8505
|
-
return doomed.length;
|
|
8506
|
-
}
|
|
8320
|
+
* Indexed by `deviceId` to serve the per-device listing efficiently.
|
|
8321
|
+
*/
|
|
8322
|
+
exports: {
|
|
8323
|
+
schema: ExportRecordSchema,
|
|
8324
|
+
class: "ledger",
|
|
8325
|
+
writeTrigger: "one row per export job, inserted by createExport; rewritten by the render engine (progress / state patches) and the download hook",
|
|
8326
|
+
device: "none",
|
|
8327
|
+
at: "required",
|
|
8328
|
+
indexes: [{
|
|
8329
|
+
name: "device",
|
|
8330
|
+
fields: ["deviceId"]
|
|
8331
|
+
}],
|
|
8332
|
+
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" },
|
|
8333
|
+
adoptsFrom: "recorder:exports"
|
|
8334
|
+
},
|
|
8507
8335
|
/**
|
|
8508
|
-
*
|
|
8336
|
+
* A bounded ring of per-operation audit rows. One row per recordings
|
|
8337
|
+
* operation (prune, evict, relocate, export, …), appended best-effort by
|
|
8338
|
+
* `createRecordingOpsLogSink().append`. The ring is trimmed to the newest
|
|
8339
|
+
* `OPS_LOG_RING_DEFAULT_MAX` (500) rows after every append.
|
|
8509
8340
|
*
|
|
8510
|
-
*
|
|
8511
|
-
*
|
|
8512
|
-
* parse of the whole map followed by a filter in JS.
|
|
8513
|
-
*
|
|
8514
|
-
* A `limit` is REQUIRED — `settings-store.query` silently caps an unbounded
|
|
8515
|
-
* read at 2 000 rows and answers as if that were everything.
|
|
8516
|
-
*/
|
|
8517
|
-
async readWhere(filter) {
|
|
8518
|
-
const records = await this.store.query.query({
|
|
8519
|
-
collection: this.spec.collection,
|
|
8520
|
-
filter: {
|
|
8521
|
-
...filter.where !== void 0 ? { where: filter.where } : {},
|
|
8522
|
-
...filter.whereIn !== void 0 ? { whereIn: filter.whereIn } : {},
|
|
8523
|
-
...filter.whereBetween !== void 0 ? { whereBetween: filter.whereBetween } : {},
|
|
8524
|
-
...filter.orderBy !== void 0 ? { orderBy: filter.orderBy } : {},
|
|
8525
|
-
...filter.offset !== void 0 ? { offset: filter.offset } : {},
|
|
8526
|
-
limit: filter.limit ?? this.spec.loadLimit
|
|
8527
|
-
}
|
|
8528
|
-
});
|
|
8529
|
-
const out = [];
|
|
8530
|
-
let skipped = 0;
|
|
8531
|
-
for (const record of records) {
|
|
8532
|
-
const parsed = this.spec.schema.safeParse(record.data[ROW_MAP_VALUE_COLUMN]);
|
|
8533
|
-
if (!parsed.success) {
|
|
8534
|
-
skipped += 1;
|
|
8535
|
-
continue;
|
|
8536
|
-
}
|
|
8537
|
-
out.push({
|
|
8538
|
-
key: record.id,
|
|
8539
|
-
value: parsed.data
|
|
8540
|
-
});
|
|
8541
|
-
}
|
|
8542
|
-
if (skipped > 0) this.logger.warn("row-map rows skipped as unparseable", { meta: {
|
|
8543
|
-
collection: this.spec.collection,
|
|
8544
|
-
skipped,
|
|
8545
|
-
kept: out.length
|
|
8546
|
-
} });
|
|
8547
|
-
return out;
|
|
8548
|
-
}
|
|
8549
|
-
/** How many rows match, without materialising one. */
|
|
8550
|
-
async count(filter) {
|
|
8551
|
-
return this.store.count.query({
|
|
8552
|
-
collection: this.spec.collection,
|
|
8553
|
-
...filter !== void 0 ? { filter: {
|
|
8554
|
-
...filter.where !== void 0 ? { where: filter.where } : {},
|
|
8555
|
-
...filter.whereIn !== void 0 ? { whereIn: filter.whereIn } : {},
|
|
8556
|
-
...filter.whereBetween !== void 0 ? { whereBetween: filter.whereBetween } : {}
|
|
8557
|
-
} } : {}
|
|
8558
|
-
});
|
|
8559
|
-
}
|
|
8560
|
-
/**
|
|
8561
|
-
* Delete every row matching a predicate, in ONE statement. Returns how many
|
|
8562
|
-
* went, and says so — a retention trim that silently drops rows is
|
|
8563
|
-
* indistinguishable from one that had nothing to do.
|
|
8341
|
+
* Indexed by `deviceId` so a per-device listing is an indexed query, not a
|
|
8342
|
+
* full scan of the ring.
|
|
8564
8343
|
*
|
|
8565
|
-
* The
|
|
8566
|
-
|
|
8567
|
-
|
|
8568
|
-
|
|
8569
|
-
|
|
8570
|
-
|
|
8571
|
-
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
|
|
8576
|
-
|
|
8577
|
-
|
|
8578
|
-
|
|
8579
|
-
return deleted;
|
|
8580
|
-
}
|
|
8581
|
-
/** `tags: { deviceId }` off the PROJECTED column, when the spec names one. */
|
|
8582
|
-
deviceTags(row) {
|
|
8583
|
-
const column = this.spec.deviceIdColumn;
|
|
8584
|
-
if (column === void 0) return {};
|
|
8585
|
-
const raw = row[column];
|
|
8586
|
-
return typeof raw === "number" && Number.isInteger(raw) ? { tags: { deviceId: raw } } : {};
|
|
8344
|
+
* The record's `at` field maps to `OpsLogEntry.at`.
|
|
8345
|
+
*/
|
|
8346
|
+
"ops-log": {
|
|
8347
|
+
schema: OpsLogEntrySchema,
|
|
8348
|
+
class: "ledger",
|
|
8349
|
+
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",
|
|
8350
|
+
device: "none",
|
|
8351
|
+
at: "required",
|
|
8352
|
+
indexes: [{
|
|
8353
|
+
name: "device",
|
|
8354
|
+
fields: ["deviceId"]
|
|
8355
|
+
}],
|
|
8356
|
+
retention: { none: "a bounded ring of the newest 500 rows; older rows are removed in application code (trimOpsLog) after each append — owner-pruned" },
|
|
8357
|
+
adoptsFrom: "recorder:ops-log"
|
|
8587
8358
|
}
|
|
8588
|
-
};
|
|
8589
|
-
/** A store payload that is a plain object, or `null`. A type guard, not a cast. */
|
|
8590
|
-
function isRecord(value) {
|
|
8591
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8592
|
-
}
|
|
8359
|
+
});
|
|
8593
8360
|
//#endregion
|
|
8594
8361
|
//#region src/recorder/addon/export-store.ts
|
|
8595
8362
|
/**
|
|
8596
8363
|
* Recording-export persistence for the recorder addon.
|
|
8597
8364
|
*
|
|
8598
|
-
* ONE ROW PER EXPORT in `recorder
|
|
8599
|
-
* rows are never removed by the system
|
|
8600
|
-
* flip `state` and clear the file,
|
|
8365
|
+
* ONE ROW PER EXPORT in the `recorder` / `exports` record kind, through a
|
|
8366
|
+
* {@link RecordKindHandle}. History rows are never removed by the system
|
|
8367
|
+
* (audit) — `deleteExport` / the janitor flip `state` and clear the file,
|
|
8368
|
+
* keeping the row.
|
|
8369
|
+
*
|
|
8370
|
+
* ## Records migration (phase 2, step 25)
|
|
8371
|
+
*
|
|
8372
|
+
* The store was previously backed by a {@link RowMapStore} over a SettingsStore
|
|
8373
|
+
* collection named `recorder:exports`. It is now backed by a
|
|
8374
|
+
* `RecordKindHandle<ExportRecord>` obtained from `ctx.records`. The record's
|
|
8375
|
+
* `at` field maps to `ExportRecord.createdAt`.
|
|
8601
8376
|
*
|
|
8602
|
-
*
|
|
8377
|
+
* The per-ID serialiser ({@link serialize}) is preserved: two overlapping
|
|
8378
|
+
* patches on the SAME export row are still read-modify-write, so they must
|
|
8379
|
+
* be ordered. The `RecordKindHandle` (an object) is used as the WeakMap key,
|
|
8380
|
+
* keeping the same compaction semantics.
|
|
8381
|
+
*
|
|
8382
|
+
* ## What the RowMapStore replaced, and why it mattered
|
|
8603
8383
|
*
|
|
8604
8384
|
* Every job used to live in one key of the recorder's `addon-settings` / `root`
|
|
8605
8385
|
* blob — 201 833 bytes on the reference hub, of which the export history was
|
|
@@ -8614,11 +8394,6 @@ function isRecord(value) {
|
|
|
8614
8394
|
* erased, and the poll answered `unknown export "547ea182-…"`; the scheduler
|
|
8615
8395
|
* read the vanished row as a failed render and re-queued forever. A per-row
|
|
8616
8396
|
* upsert makes that class of loss unrepresentable — a write names ONE row.
|
|
8617
|
-
*
|
|
8618
|
-
* What is still needed is a per-ID serialiser: two patches to the SAME export
|
|
8619
|
-
* still read-modify-write the same row. That is what {@link serialize} bounds,
|
|
8620
|
-
* and it is now per (store, id) rather than per store, so two exports never
|
|
8621
|
-
* queue behind each other.
|
|
8622
8397
|
*/
|
|
8623
8398
|
/**
|
|
8624
8399
|
* The storage class export MP4s live in: `artifacts`, SHARED with the
|
|
@@ -8663,76 +8438,26 @@ function exportOutPathFor(root, deviceId, exportId) {
|
|
|
8663
8438
|
return path.join(exportTenantDirFor(root, deviceId), `${exportId}.mp4`);
|
|
8664
8439
|
}
|
|
8665
8440
|
/**
|
|
8666
|
-
*
|
|
8667
|
-
*
|
|
8668
|
-
*
|
|
8669
|
-
* retention="the SYSTEM never deletes a row — it is the export AUDIT. `deleteExport`
|
|
8670
|
-
* and the janitor flip `state` to 'deleted'/'expired' and remove the FILE, keeping
|
|
8671
|
-
* the row. `removeExport` exists for an operator-driven purge and nothing calls it
|
|
8672
|
-
* on a schedule."
|
|
8441
|
+
* Bound for the whole-history read. Explicit because a records query without
|
|
8442
|
+
* an explicit limit uses the default cap (100); an export list that silently
|
|
8443
|
+
* loses its oldest entries looks exactly like a purge.
|
|
8673
8444
|
*/
|
|
8674
|
-
var
|
|
8445
|
+
var RECORDING_EXPORTS_LIMIT = 2e4;
|
|
8675
8446
|
/**
|
|
8676
|
-
*
|
|
8677
|
-
*
|
|
8678
|
-
*
|
|
8447
|
+
* Obtain the `recorder` / `exports` record kind handle.
|
|
8448
|
+
* `ctx.records.declare(recorderRecordKinds)` MUST have run before any read or
|
|
8449
|
+
* write (called at boot in `onInitialize`).
|
|
8679
8450
|
*/
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
collection: RECORDING_EXPORTS_COLLECTION,
|
|
8683
|
-
schema: ExportRecordSchema,
|
|
8684
|
-
columns: [
|
|
8685
|
-
{
|
|
8686
|
-
name: "deviceId",
|
|
8687
|
-
type: "INTEGER",
|
|
8688
|
-
notNull: true
|
|
8689
|
-
},
|
|
8690
|
-
{
|
|
8691
|
-
name: "createdAt",
|
|
8692
|
-
type: "INTEGER",
|
|
8693
|
-
notNull: true
|
|
8694
|
-
},
|
|
8695
|
-
{
|
|
8696
|
-
name: "state",
|
|
8697
|
-
type: "TEXT",
|
|
8698
|
-
notNull: true
|
|
8699
|
-
}
|
|
8700
|
-
],
|
|
8701
|
-
indexes: [{
|
|
8702
|
-
name: "idx_recorder_exports_device",
|
|
8703
|
-
columns: ["deviceId"]
|
|
8704
|
-
}, {
|
|
8705
|
-
name: "idx_recorder_exports_created",
|
|
8706
|
-
columns: ["createdAt"]
|
|
8707
|
-
}],
|
|
8708
|
-
project: (_key, value) => ({
|
|
8709
|
-
deviceId: value.deviceId,
|
|
8710
|
-
createdAt: value.createdAt,
|
|
8711
|
-
state: value.state
|
|
8712
|
-
}),
|
|
8713
|
-
deviceIdColumn: "deviceId",
|
|
8714
|
-
loadLimit: RECORDING_EXPORTS_LIMIT
|
|
8715
|
-
};
|
|
8716
|
-
/** Build the handle. `declare()` MUST run once at boot before any read/write. */
|
|
8717
|
-
function recordingExportsState(store, logger) {
|
|
8718
|
-
return new RowMapStore({
|
|
8719
|
-
spec: RECORDING_EXPORTS_SPEC,
|
|
8720
|
-
store,
|
|
8721
|
-
logger
|
|
8722
|
-
});
|
|
8451
|
+
function recordingExportsState(records) {
|
|
8452
|
+
return records.kind(recorderRecordKinds["exports"]);
|
|
8723
8453
|
}
|
|
8724
8454
|
/**
|
|
8725
8455
|
* One in-flight mutation chain per (handle, export id).
|
|
8726
8456
|
*
|
|
8727
|
-
*
|
|
8728
|
-
*
|
|
8729
|
-
*
|
|
8730
|
-
*
|
|
8731
|
-
* unrelated exports fully concurrent (the old chain was per HANDLE, so a slow
|
|
8732
|
-
* patch queued every other export behind it).
|
|
8733
|
-
*
|
|
8734
|
-
* A `WeakMap` keyed by the handle keeps a disposed store collectable; the inner
|
|
8735
|
-
* map is keyed by export id.
|
|
8457
|
+
* Two overlapping patches on the SAME export still read-modify-write one row.
|
|
8458
|
+
* Serialising per id keeps them atomic while leaving unrelated exports fully
|
|
8459
|
+
* concurrent. A `WeakMap` keyed by the handle keeps a disposed handle
|
|
8460
|
+
* collectable; the inner map is keyed by export id.
|
|
8736
8461
|
*/
|
|
8737
8462
|
var mutationChains = /* @__PURE__ */ new WeakMap();
|
|
8738
8463
|
/** Run `work` after every mutation already queued for `(state, id)`, never before. */
|
|
@@ -8752,7 +8477,11 @@ async function serialize(state, id, work) {
|
|
|
8752
8477
|
async function upsertExport(state, record) {
|
|
8753
8478
|
const validated = ExportRecordSchema.parse(record);
|
|
8754
8479
|
return serialize(state, validated.id, async () => {
|
|
8755
|
-
await state.
|
|
8480
|
+
await state.put({
|
|
8481
|
+
id: validated.id,
|
|
8482
|
+
at: validated.createdAt,
|
|
8483
|
+
data: validated
|
|
8484
|
+
});
|
|
8756
8485
|
return validated;
|
|
8757
8486
|
});
|
|
8758
8487
|
}
|
|
@@ -8762,13 +8491,17 @@ async function upsertExport(state, record) {
|
|
|
8762
8491
|
*/
|
|
8763
8492
|
async function patchExport(state, id, patch) {
|
|
8764
8493
|
return serialize(state, id, async () => {
|
|
8765
|
-
const
|
|
8766
|
-
if (
|
|
8494
|
+
const row = await state.get(id);
|
|
8495
|
+
if (row === null) return null;
|
|
8767
8496
|
const merged = ExportRecordSchema.parse({
|
|
8768
|
-
...
|
|
8497
|
+
...row.data,
|
|
8769
8498
|
...patch
|
|
8770
8499
|
});
|
|
8771
|
-
await state.
|
|
8500
|
+
await state.put({
|
|
8501
|
+
id,
|
|
8502
|
+
at: merged.createdAt,
|
|
8503
|
+
data: merged
|
|
8504
|
+
});
|
|
8772
8505
|
return merged;
|
|
8773
8506
|
});
|
|
8774
8507
|
}
|
|
@@ -8779,17 +8512,21 @@ async function patchExport(state, id, patch) {
|
|
|
8779
8512
|
*/
|
|
8780
8513
|
async function patchExportUnless(state, id, patch, refuse) {
|
|
8781
8514
|
return serialize(state, id, async () => {
|
|
8782
|
-
const
|
|
8783
|
-
if (
|
|
8784
|
-
if (refuse(
|
|
8515
|
+
const row = await state.get(id);
|
|
8516
|
+
if (row === null) return { kind: "missing" };
|
|
8517
|
+
if (refuse(row.data)) return {
|
|
8785
8518
|
kind: "refused",
|
|
8786
|
-
record:
|
|
8519
|
+
record: row.data
|
|
8787
8520
|
};
|
|
8788
8521
|
const merged = ExportRecordSchema.parse({
|
|
8789
|
-
...
|
|
8522
|
+
...row.data,
|
|
8790
8523
|
...patch
|
|
8791
8524
|
});
|
|
8792
|
-
await state.
|
|
8525
|
+
await state.put({
|
|
8526
|
+
id,
|
|
8527
|
+
at: merged.createdAt,
|
|
8528
|
+
data: merged
|
|
8529
|
+
});
|
|
8793
8530
|
return {
|
|
8794
8531
|
kind: "patched",
|
|
8795
8532
|
record: merged
|
|
@@ -8798,24 +8535,22 @@ async function patchExportUnless(state, id, patch, refuse) {
|
|
|
8798
8535
|
}
|
|
8799
8536
|
/** Read one export row by id — ONE keyed row read. */
|
|
8800
8537
|
async function getExportRecord(state, id) {
|
|
8801
|
-
|
|
8538
|
+
const row = await state.get(id);
|
|
8539
|
+
return row === null ? null : row.data;
|
|
8802
8540
|
}
|
|
8803
8541
|
/**
|
|
8804
|
-
* List export rows, newest-first
|
|
8542
|
+
* List export rows, newest-first (by `createdAt` / record `at`), optionally
|
|
8543
|
+
* scoped to one device.
|
|
8805
8544
|
*
|
|
8806
|
-
* Scoped to a device this is an INDEXED query on the
|
|
8807
|
-
*
|
|
8808
|
-
* in JS.
|
|
8545
|
+
* Scoped to a device this is an INDEXED query on the `device` index — not a
|
|
8546
|
+
* parse of every export in the installation followed by a filter in JS.
|
|
8809
8547
|
*/
|
|
8810
8548
|
async function listExportsFor(state, deviceId) {
|
|
8811
|
-
return (await state.
|
|
8549
|
+
return (await state.query({
|
|
8812
8550
|
...deviceId === void 0 ? {} : { where: { deviceId } },
|
|
8813
|
-
|
|
8814
|
-
field: "createdAt",
|
|
8815
|
-
direction: "desc"
|
|
8816
|
-
},
|
|
8551
|
+
desc: true,
|
|
8817
8552
|
limit: RECORDING_EXPORTS_LIMIT
|
|
8818
|
-
})).map((
|
|
8553
|
+
})).records.map((r) => r.data);
|
|
8819
8554
|
}
|
|
8820
8555
|
//#endregion
|
|
8821
8556
|
//#region src/recorder/addon/export-timelapse-chunks.ts
|
|
@@ -11773,36 +11508,125 @@ function runFfmpeg(renderDeps, ffmpegPath, args, ext) {
|
|
|
11773
11508
|
});
|
|
11774
11509
|
}
|
|
11775
11510
|
//#endregion
|
|
11511
|
+
//#region src/durable/durable-legacy-kind-handle.ts
|
|
11512
|
+
function notImpl(method) {
|
|
11513
|
+
throw new Error(`DurableLegacyKindHandle.${method}() is not available on the legacy (agent) path`);
|
|
11514
|
+
}
|
|
11515
|
+
function isRecord(value) {
|
|
11516
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11517
|
+
}
|
|
11518
|
+
function toRecordRow(id, data) {
|
|
11519
|
+
return {
|
|
11520
|
+
id,
|
|
11521
|
+
data,
|
|
11522
|
+
deviceId: null,
|
|
11523
|
+
at: null,
|
|
11524
|
+
expiresAt: null,
|
|
11525
|
+
rev: 0,
|
|
11526
|
+
updatedAt: 0
|
|
11527
|
+
};
|
|
11528
|
+
}
|
|
11529
|
+
var DurableLegacyKindHandle = class {
|
|
11530
|
+
name;
|
|
11531
|
+
spec;
|
|
11532
|
+
store;
|
|
11533
|
+
constructor(opts) {
|
|
11534
|
+
this.name = opts.spec.collection;
|
|
11535
|
+
this.spec = opts.spec;
|
|
11536
|
+
this.store = opts.store;
|
|
11537
|
+
}
|
|
11538
|
+
async get(id) {
|
|
11539
|
+
const row = (await this.store.query.query({
|
|
11540
|
+
collection: this.spec.collection,
|
|
11541
|
+
filter: {
|
|
11542
|
+
where: { [this.pkColumn()]: id },
|
|
11543
|
+
limit: 1
|
|
11544
|
+
}
|
|
11545
|
+
}))[0];
|
|
11546
|
+
if (row === void 0 || !isRecord(row.data)) return null;
|
|
11547
|
+
const parsed = this.spec.fromRecord(row.id, row.data);
|
|
11548
|
+
if (parsed === null) return null;
|
|
11549
|
+
return toRecordRow(row.id, this.spec.toValue(parsed));
|
|
11550
|
+
}
|
|
11551
|
+
async query(q) {
|
|
11552
|
+
const rows = await this.store.query.query({
|
|
11553
|
+
collection: this.spec.collection,
|
|
11554
|
+
filter: { limit: q.limit ?? 1e4 }
|
|
11555
|
+
});
|
|
11556
|
+
const records = [];
|
|
11557
|
+
for (const row of rows) {
|
|
11558
|
+
if (!isRecord(row.data)) continue;
|
|
11559
|
+
const parsed = this.spec.fromRecord(row.id, row.data);
|
|
11560
|
+
if (parsed === null) continue;
|
|
11561
|
+
records.push(toRecordRow(row.id, this.spec.toValue(parsed)));
|
|
11562
|
+
}
|
|
11563
|
+
return {
|
|
11564
|
+
records,
|
|
11565
|
+
nextCursor: null
|
|
11566
|
+
};
|
|
11567
|
+
}
|
|
11568
|
+
async put(input) {
|
|
11569
|
+
await this.store.set.mutate({
|
|
11570
|
+
collection: this.spec.collection,
|
|
11571
|
+
key: input.id,
|
|
11572
|
+
value: input.data
|
|
11573
|
+
});
|
|
11574
|
+
return { rev: 0 };
|
|
11575
|
+
}
|
|
11576
|
+
async delete(id) {
|
|
11577
|
+
await this.store.delete.mutate({
|
|
11578
|
+
collection: this.spec.collection,
|
|
11579
|
+
key: id
|
|
11580
|
+
});
|
|
11581
|
+
return true;
|
|
11582
|
+
}
|
|
11583
|
+
getMany = () => notImpl("getMany");
|
|
11584
|
+
getResolved = () => notImpl("getResolved");
|
|
11585
|
+
putMany = () => notImpl("putMany");
|
|
11586
|
+
patch = () => notImpl("patch");
|
|
11587
|
+
deleteMany = () => notImpl("deleteMany");
|
|
11588
|
+
count = () => notImpl("count");
|
|
11589
|
+
histogram = () => notImpl("histogram");
|
|
11590
|
+
aggregate = () => notImpl("aggregate");
|
|
11591
|
+
deleteWhere = () => notImpl("deleteWhere");
|
|
11592
|
+
allocateId = () => notImpl("allocateId");
|
|
11593
|
+
pkColumn() {
|
|
11594
|
+
return this.spec.columns.find((c) => c.primaryKey === true)?.name ?? "id";
|
|
11595
|
+
}
|
|
11596
|
+
};
|
|
11597
|
+
/** Legacy settings-store handle for one {@link DurableLedgerSpec}. */
|
|
11598
|
+
function durableLegacyKind(spec, store) {
|
|
11599
|
+
return new DurableLegacyKindHandle({
|
|
11600
|
+
spec,
|
|
11601
|
+
store
|
|
11602
|
+
});
|
|
11603
|
+
}
|
|
11604
|
+
/** Agent boot: declare the SQLite collection behind a durable ledger spec. */
|
|
11605
|
+
function declareDurableLegacyCollection(store, spec) {
|
|
11606
|
+
return store.declareCollection.mutate({
|
|
11607
|
+
collection: spec.collection,
|
|
11608
|
+
columns: [...spec.columns],
|
|
11609
|
+
...spec.indexes !== void 0 ? { indexes: [...spec.indexes] } : {}
|
|
11610
|
+
});
|
|
11611
|
+
}
|
|
11612
|
+
//#endregion
|
|
11776
11613
|
//#region src/durable/durable-ledger.ts
|
|
11777
11614
|
/** Default reseed cap — every current consumer's row set is installation-bounded. */
|
|
11778
11615
|
var DEFAULT_LOAD_LIMIT = 1e5;
|
|
11779
|
-
|
|
11616
|
+
/** Page size for records reseed queries. */
|
|
11617
|
+
var QUERY_PAGE = 1e3;
|
|
11618
|
+
var DurableLedger = class {
|
|
11780
11619
|
mirror = /* @__PURE__ */ new Map();
|
|
11781
11620
|
/** Whether ANY load has read the store successfully — see {@link hasAnswered}. */
|
|
11782
11621
|
answered = false;
|
|
11783
11622
|
spec;
|
|
11784
|
-
|
|
11623
|
+
kind;
|
|
11785
11624
|
logger;
|
|
11786
11625
|
constructor(deps) {
|
|
11787
11626
|
this.spec = deps.spec;
|
|
11788
|
-
this.
|
|
11627
|
+
this.kind = deps.kind;
|
|
11789
11628
|
this.logger = deps.logger;
|
|
11790
11629
|
}
|
|
11791
|
-
/**
|
|
11792
|
-
* Register the collection. MUST run at boot, before any read or write: the
|
|
11793
|
-
* SQLite backend answers 412 for an undeclared collection and takes the whole
|
|
11794
|
-
* runner down with it (the addon-ai boot-crash lesson).
|
|
11795
|
-
*/
|
|
11796
|
-
static declare(store, spec) {
|
|
11797
|
-
return store.declareCollection.mutate({
|
|
11798
|
-
collection: spec.collection,
|
|
11799
|
-
columns: [...spec.columns],
|
|
11800
|
-
...spec.indexes !== void 0 ? { indexes: [...spec.indexes] } : {}
|
|
11801
|
-
});
|
|
11802
|
-
}
|
|
11803
|
-
declare() {
|
|
11804
|
-
return DurableLedger.declare(this.store, this.spec);
|
|
11805
|
-
}
|
|
11806
11630
|
/** The collection this ledger owns — for the caller's own log lines. */
|
|
11807
11631
|
get collection() {
|
|
11808
11632
|
return this.spec.collection;
|
|
@@ -11819,31 +11643,43 @@ var DurableLedger = class DurableLedger {
|
|
|
11819
11643
|
*/
|
|
11820
11644
|
async load() {
|
|
11821
11645
|
try {
|
|
11822
|
-
const records = await this.store.query.query({
|
|
11823
|
-
collection: this.spec.collection,
|
|
11824
|
-
filter: { limit: this.spec.loadLimit ?? DEFAULT_LOAD_LIMIT }
|
|
11825
|
-
});
|
|
11826
11646
|
const next = /* @__PURE__ */ new Map();
|
|
11827
11647
|
let skipped = 0;
|
|
11828
|
-
|
|
11829
|
-
|
|
11830
|
-
|
|
11831
|
-
|
|
11832
|
-
|
|
11648
|
+
let seen = 0;
|
|
11649
|
+
const cap = this.spec.loadLimit ?? DEFAULT_LOAD_LIMIT;
|
|
11650
|
+
let cursor;
|
|
11651
|
+
for (;;) {
|
|
11652
|
+
const limit = Math.min(QUERY_PAGE, cap - seen);
|
|
11653
|
+
if (limit <= 0) break;
|
|
11654
|
+
const page = await this.kind.query({
|
|
11655
|
+
limit,
|
|
11656
|
+
...cursor === void 0 ? {} : { cursor }
|
|
11657
|
+
});
|
|
11658
|
+
seen += page.records.length;
|
|
11659
|
+
for (const record of page.records) {
|
|
11660
|
+
const row = this.spec.fromRecord(record.id, record.data);
|
|
11661
|
+
if (row === null) {
|
|
11662
|
+
skipped += 1;
|
|
11663
|
+
continue;
|
|
11664
|
+
}
|
|
11665
|
+
next.set(this.spec.keyOf(row), row);
|
|
11833
11666
|
}
|
|
11834
|
-
|
|
11667
|
+
if (page.nextCursor === null) break;
|
|
11668
|
+
cursor = page.nextCursor;
|
|
11835
11669
|
}
|
|
11836
11670
|
this.mirror.clear();
|
|
11837
11671
|
for (const [key, row] of next) this.mirror.set(key, row);
|
|
11838
11672
|
this.answered = true;
|
|
11839
11673
|
if (skipped > 0) this.logger.warn("durable rows skipped as malformed — they gate NOTHING", { meta: {
|
|
11840
11674
|
collection: this.spec.collection,
|
|
11675
|
+
kind: this.kind.name,
|
|
11841
11676
|
skipped
|
|
11842
11677
|
} });
|
|
11843
11678
|
return [...this.mirror.values()];
|
|
11844
11679
|
} catch (err) {
|
|
11845
11680
|
this.logger.warn("durable load failed — keeping the state already in memory", { meta: {
|
|
11846
11681
|
collection: this.spec.collection,
|
|
11682
|
+
kind: this.kind.name,
|
|
11847
11683
|
error: String(err),
|
|
11848
11684
|
held: this.mirror.size
|
|
11849
11685
|
} });
|
|
@@ -11908,15 +11744,15 @@ var DurableLedger = class DurableLedger {
|
|
|
11908
11744
|
*/
|
|
11909
11745
|
async put(row) {
|
|
11910
11746
|
const key = this.spec.keyOf(row);
|
|
11747
|
+
const body = this.spec.toValue(row);
|
|
11911
11748
|
if (this.spec.writeMode === "write-behind") {
|
|
11912
11749
|
this.mirror.set(key, row);
|
|
11913
|
-
await this.
|
|
11750
|
+
await this.persistRow(key, body);
|
|
11914
11751
|
return;
|
|
11915
11752
|
}
|
|
11916
|
-
await this.
|
|
11917
|
-
|
|
11918
|
-
|
|
11919
|
-
value: this.spec.toValue(row)
|
|
11753
|
+
await this.kind.put({
|
|
11754
|
+
id: key,
|
|
11755
|
+
data: body
|
|
11920
11756
|
});
|
|
11921
11757
|
this.mirror.set(key, row);
|
|
11922
11758
|
}
|
|
@@ -11957,13 +11793,11 @@ var DurableLedger = class DurableLedger {
|
|
|
11957
11793
|
async forget(key) {
|
|
11958
11794
|
this.mirror.delete(key);
|
|
11959
11795
|
try {
|
|
11960
|
-
await this.
|
|
11961
|
-
collection: this.spec.collection,
|
|
11962
|
-
key
|
|
11963
|
-
});
|
|
11796
|
+
await this.kind.delete(key);
|
|
11964
11797
|
} catch (err) {
|
|
11965
11798
|
this.logger.debug("durable delete failed", { meta: {
|
|
11966
11799
|
collection: this.spec.collection,
|
|
11800
|
+
kind: this.kind.name,
|
|
11967
11801
|
key,
|
|
11968
11802
|
error: String(err)
|
|
11969
11803
|
} });
|
|
@@ -11999,15 +11833,13 @@ var DurableLedger = class DurableLedger {
|
|
|
11999
11833
|
for (const key of [...this.mirror.keys()]) {
|
|
12000
11834
|
if (activeKeys.has(key)) continue;
|
|
12001
11835
|
try {
|
|
12002
|
-
await this.
|
|
12003
|
-
collection: this.spec.collection,
|
|
12004
|
-
key
|
|
12005
|
-
});
|
|
11836
|
+
await this.kind.delete(key);
|
|
12006
11837
|
this.mirror.delete(key);
|
|
12007
11838
|
pruned += 1;
|
|
12008
11839
|
} catch (err) {
|
|
12009
11840
|
this.logger.debug("durable prune delete failed", { meta: {
|
|
12010
11841
|
collection: this.spec.collection,
|
|
11842
|
+
kind: this.kind.name,
|
|
12011
11843
|
key,
|
|
12012
11844
|
error: String(err)
|
|
12013
11845
|
} });
|
|
@@ -12020,25 +11852,34 @@ var DurableLedger = class DurableLedger {
|
|
|
12020
11852
|
* decision path: the mirror already holds the truth for this process, and the
|
|
12021
11853
|
* worst a lost write can do is one silent re-seed after the next restart.
|
|
12022
11854
|
*/
|
|
12023
|
-
|
|
12024
|
-
const
|
|
11855
|
+
persist(row) {
|
|
11856
|
+
const key = this.spec.keyOf(row);
|
|
11857
|
+
this.persistRow(key, this.spec.toValue(row));
|
|
11858
|
+
}
|
|
11859
|
+
async persistRow(key, body) {
|
|
11860
|
+
const deviceId = this.deviceIdFromBody(body);
|
|
12025
11861
|
try {
|
|
12026
|
-
await this.
|
|
12027
|
-
|
|
12028
|
-
|
|
12029
|
-
value: this.spec.toValue(row)
|
|
11862
|
+
await this.kind.put({
|
|
11863
|
+
id: key,
|
|
11864
|
+
data: body
|
|
12030
11865
|
});
|
|
12031
11866
|
} catch (err) {
|
|
12032
11867
|
this.logger.warn("durable persist failed — this key may re-seed on boot", {
|
|
12033
11868
|
...deviceId !== void 0 ? { tags: { deviceId } } : {},
|
|
12034
11869
|
meta: {
|
|
12035
11870
|
collection: this.spec.collection,
|
|
12036
|
-
|
|
11871
|
+
kind: this.kind.name,
|
|
11872
|
+
key,
|
|
12037
11873
|
error: String(err)
|
|
12038
11874
|
}
|
|
12039
11875
|
});
|
|
12040
11876
|
}
|
|
12041
11877
|
}
|
|
11878
|
+
deviceIdFromBody(body) {
|
|
11879
|
+
const raw = body["deviceId"];
|
|
11880
|
+
const n = typeof raw === "number" ? raw : Number(raw);
|
|
11881
|
+
return Number.isFinite(n) ? n : void 0;
|
|
11882
|
+
}
|
|
12042
11883
|
};
|
|
12043
11884
|
//#endregion
|
|
12044
11885
|
//#region src/recorder/addon/segment-hour-ledger.ts
|
|
@@ -12599,15 +12440,15 @@ var SegmentHourLedger = class SegmentHourLedger {
|
|
|
12599
12440
|
this.rowCap = deps.loadPacing?.rowCap ?? 25e4;
|
|
12600
12441
|
this.ledger = new DurableLedger({
|
|
12601
12442
|
spec: SPEC,
|
|
12602
|
-
|
|
12443
|
+
kind: durableLegacyKind(SPEC, deps.store),
|
|
12603
12444
|
logger: deps.logger
|
|
12604
12445
|
});
|
|
12605
12446
|
}
|
|
12606
12447
|
static declare(store) {
|
|
12607
|
-
return
|
|
12448
|
+
return declareDurableLegacyCollection(store, SPEC);
|
|
12608
12449
|
}
|
|
12609
12450
|
declare() {
|
|
12610
|
-
return this.
|
|
12451
|
+
return declareDurableLegacyCollection(this.store, SPEC);
|
|
12611
12452
|
}
|
|
12612
12453
|
/**
|
|
12613
12454
|
* Boot load — every hour row, in one query.
|
|
@@ -15018,63 +14859,61 @@ var LocationRefresher = class {
|
|
|
15018
14859
|
}
|
|
15019
14860
|
}
|
|
15020
14861
|
};
|
|
15021
|
-
|
|
15022
|
-
|
|
15023
|
-
|
|
15024
|
-
|
|
15025
|
-
|
|
15026
|
-
|
|
15027
|
-
|
|
15028
|
-
|
|
15029
|
-
|
|
15030
|
-
|
|
15031
|
-
|
|
15032
|
-
|
|
15033
|
-
|
|
15034
|
-
|
|
15035
|
-
|
|
15036
|
-
|
|
15037
|
-
|
|
15038
|
-
|
|
15039
|
-
|
|
15040
|
-
|
|
15041
|
-
|
|
15042
|
-
|
|
15043
|
-
|
|
15044
|
-
|
|
15045
|
-
|
|
15046
|
-
|
|
15047
|
-
|
|
15048
|
-
|
|
15049
|
-
|
|
15050
|
-
|
|
15051
|
-
|
|
15052
|
-
|
|
14862
|
+
//#endregion
|
|
14863
|
+
//#region src/recorder/addon/ops-log-store.ts
|
|
14864
|
+
/**
|
|
14865
|
+
* Recordings ops-log persistence for the recorder addon.
|
|
14866
|
+
*
|
|
14867
|
+
* ONE ROW PER OPERATION in the `recorder` / `ops-log` record kind, through a
|
|
14868
|
+
* {@link RecordKindHandle}. The ring is bounded — `maxEntries` newest rows
|
|
14869
|
+
* survive — trimmed with one `deleteWhere({ atBefore })` per append.
|
|
14870
|
+
*
|
|
14871
|
+
* ## Records migration (phase 2, step 26)
|
|
14872
|
+
*
|
|
14873
|
+
* The store was previously backed by a {@link RowMapStore} over a SettingsStore
|
|
14874
|
+
* collection named `recorder:ops-log`. It is now backed by a
|
|
14875
|
+
* `RecordKindHandle<OpsLogEntry>` obtained from `ctx.records`. The record's
|
|
14876
|
+
* `at` field maps to `OpsLogEntry.at`.
|
|
14877
|
+
*
|
|
14878
|
+
* ## What the RowMapStore replaced
|
|
14879
|
+
*
|
|
14880
|
+
* The ring lived in ONE key of the recorder's `addon-settings` / `root` blob,
|
|
14881
|
+
* as a flat JSON array. On the reference hub it was 147 437 bytes of that row's
|
|
14882
|
+
* 201 833 — so appending one audit line read, parsed, re-serialised and rewrote
|
|
14883
|
+
* ~200 KB, and so did every unrelated reader of that row. The ops log is the
|
|
14884
|
+
* CHEAPEST thing in the recorder and it was paying for the most expensive.
|
|
14885
|
+
*
|
|
14886
|
+
* Writes stay BEST-EFFORT: `createRecordingOpsLogSink().append` never throws —
|
|
14887
|
+
* a failed audit write must not fail the operation it records.
|
|
14888
|
+
*/
|
|
14889
|
+
/**
|
|
14890
|
+
* Obtain the `recorder` / `ops-log` record kind handle.
|
|
14891
|
+
* `ctx.records.declare(recorderRecordKinds)` MUST have run before any read or
|
|
14892
|
+
* write (called at boot in `onInitialize`).
|
|
14893
|
+
*/
|
|
14894
|
+
function recordingOpsLogState(records) {
|
|
14895
|
+
return records.kind(recorderRecordKinds["ops-log"]);
|
|
15053
14896
|
}
|
|
15054
14897
|
/**
|
|
15055
14898
|
* Trim the ring to the newest `maxEntries` rows. Returns how many went.
|
|
15056
14899
|
*
|
|
15057
14900
|
* The cursor is the OLDEST row that must SURVIVE — offset `maxEntries - 1`
|
|
15058
|
-
* newest-first — and the delete
|
|
14901
|
+
* newest-first — and the delete removes everything strictly BEFORE its `at`
|
|
14902
|
+
* (`atBefore: cursor.at` deletes rows where `at < cursor.at`).
|
|
15059
14903
|
*
|
|
15060
|
-
* Strictly,
|
|
15061
|
-
*
|
|
15062
|
-
* and
|
|
15063
|
-
* Erring this way keeps a few EXTRA audit rows on a tie and never fewer than
|
|
15064
|
-
* the cap.
|
|
14904
|
+
* Strictly, to avoid dropping the cursor row itself and any rows tied on its
|
|
14905
|
+
* millisecond. The ring may hold a few MORE than the cap on a millisecond tie
|
|
14906
|
+
* and never FEWER.
|
|
15065
14907
|
*/
|
|
15066
14908
|
async function trimOpsLog(state, maxEntries = 500) {
|
|
15067
|
-
if (await state.count() <= maxEntries) return 0;
|
|
15068
|
-
const
|
|
15069
|
-
|
|
15070
|
-
|
|
15071
|
-
|
|
15072
|
-
|
|
15073
|
-
|
|
15074
|
-
|
|
15075
|
-
}))[0];
|
|
15076
|
-
if (oldestKept === void 0) return 0;
|
|
15077
|
-
return state.deleteWhere({ whereBetween: { at: [0, oldestKept.value.at - 1] } });
|
|
14909
|
+
if (await state.count({}) <= maxEntries) return 0;
|
|
14910
|
+
const page = await state.query({
|
|
14911
|
+
desc: true,
|
|
14912
|
+
limit: maxEntries
|
|
14913
|
+
});
|
|
14914
|
+
const oldest = page.records[page.records.length - 1];
|
|
14915
|
+
if (oldest === void 0) return 0;
|
|
14916
|
+
return state.deleteWhere({ atBefore: oldest.at ?? 0 });
|
|
15078
14917
|
}
|
|
15079
14918
|
/**
|
|
15080
14919
|
* Append one row and persist, then trim the ring. Validates at the boundary.
|
|
@@ -15082,7 +14921,11 @@ async function trimOpsLog(state, maxEntries = 500) {
|
|
|
15082
14921
|
*/
|
|
15083
14922
|
async function appendOpsLog(state, entry, maxEntries = 500) {
|
|
15084
14923
|
const validated = OpsLogEntrySchema.parse(entry);
|
|
15085
|
-
await state.
|
|
14924
|
+
await state.put({
|
|
14925
|
+
id: validated.id,
|
|
14926
|
+
at: validated.at,
|
|
14927
|
+
data: validated
|
|
14928
|
+
});
|
|
15086
14929
|
await trimOpsLog(state, maxEntries);
|
|
15087
14930
|
return validated;
|
|
15088
14931
|
}
|
|
@@ -15090,18 +14933,15 @@ async function appendOpsLog(state, entry, maxEntries = 500) {
|
|
|
15090
14933
|
* List ops-log rows newest-first, optionally scoped to one device, capped at
|
|
15091
14934
|
* `limit` (default {@link OPS_LOG_DEFAULT_LIMIT}).
|
|
15092
14935
|
*
|
|
15093
|
-
* Scoped to a device this is an INDEXED query
|
|
15094
|
-
* followed by a filter and a sort in JS.
|
|
14936
|
+
* Scoped to a device this is an INDEXED query on the `device` index — not a
|
|
14937
|
+
* read of the whole ring followed by a filter and a sort in JS.
|
|
15095
14938
|
*/
|
|
15096
14939
|
async function listOpsLog(state, query) {
|
|
15097
|
-
return (await state.
|
|
14940
|
+
return (await state.query({
|
|
15098
14941
|
...query.deviceId === void 0 ? {} : { where: { deviceId: query.deviceId } },
|
|
15099
|
-
|
|
15100
|
-
field: "at",
|
|
15101
|
-
direction: "desc"
|
|
15102
|
-
},
|
|
14942
|
+
desc: true,
|
|
15103
14943
|
limit: query.limit ?? 200
|
|
15104
|
-
})).map((
|
|
14944
|
+
})).records.map((r) => r.data);
|
|
15105
14945
|
}
|
|
15106
14946
|
/**
|
|
15107
14947
|
* Build the recorder's ops-log sink. `append` stamps `domain:'recording'`,
|
|
@@ -23559,7 +23399,6 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
23559
23399
|
readCensus = new StoreReadCensus();
|
|
23560
23400
|
/** Memoised so every `configStore()` / `exportsStore()` / `opsLogStore()`
|
|
23561
23401
|
* call shares ONE census instead of each wrapping the raw client afresh. */
|
|
23562
|
-
censusedSettingsStoreClient = null;
|
|
23563
23402
|
constructor() {
|
|
23564
23403
|
super({ ...DEFAULT_CONFIG });
|
|
23565
23404
|
}
|
|
@@ -23596,6 +23435,9 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
23596
23435
|
} });
|
|
23597
23436
|
return { providers: [] };
|
|
23598
23437
|
}
|
|
23438
|
+
const records = this.ctx.records;
|
|
23439
|
+
if (records === void 0) throw new Error("recorder (recording node) needs ctx.records — recording node must be the hub");
|
|
23440
|
+
await records.declare(recorderRecordKinds);
|
|
23599
23441
|
await this.declareRowCollections();
|
|
23600
23442
|
this.segmentHours = new SegmentHourLedger({
|
|
23601
23443
|
store: this.ctx.api.settingsStore,
|
|
@@ -25416,26 +25258,16 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
25416
25258
|
} });
|
|
25417
25259
|
}
|
|
25418
25260
|
/**
|
|
25419
|
-
* Declare the
|
|
25420
|
-
* `
|
|
25421
|
-
*
|
|
25261
|
+
* Declare the remaining RowMapStore collection that replaced the
|
|
25262
|
+
* `recordingConfigs` blob key. Idempotent — a re-declaration of the same
|
|
25263
|
+
* shape is a no-op in the engine.
|
|
25264
|
+
*
|
|
25265
|
+
* `recorder:exports` and `recorder:ops-log` are now record kinds declared
|
|
25266
|
+
* via `ctx.records.declare(recorderRecordKinds)` at the top of
|
|
25267
|
+
* `onInitialize`.
|
|
25422
25268
|
*/
|
|
25423
25269
|
async declareRowCollections() {
|
|
25424
25270
|
await this.configStore().declare();
|
|
25425
|
-
await this.exportsStore().declare();
|
|
25426
|
-
await this.opsLogStore().declare();
|
|
25427
|
-
}
|
|
25428
|
-
/**
|
|
25429
|
-
* `ctx.api.settingsStore`, wrapped ONCE through {@link censusStoreClient}.
|
|
25430
|
-
*
|
|
25431
|
-
* LAZY (see `calendar` above): `ctx` is not assigned until `initialize()`
|
|
25432
|
-
* runs, so this cannot be a field initialiser. The wrap is permanent and
|
|
25433
|
-
* cheap when disarmed (one `get` trap and a comparison) — what is armed and
|
|
25434
|
-
* disarmed is `this.readCensus`, through `debug.storeCensus`, not this seam.
|
|
25435
|
-
*/
|
|
25436
|
-
censusedSettingsStore() {
|
|
25437
|
-
this.censusedSettingsStoreClient ??= censusStoreClient(this.ctx.api.settingsStore, this.readCensus);
|
|
25438
|
-
return this.censusedSettingsStoreClient;
|
|
25439
25271
|
}
|
|
25440
25272
|
configsState = null;
|
|
25441
25273
|
/** A camera's recording config — its per-device settings (D865), migrated on read. */
|
|
@@ -25448,14 +25280,23 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
25448
25280
|
});
|
|
25449
25281
|
return this.configsState;
|
|
25450
25282
|
}
|
|
25451
|
-
/**
|
|
25452
|
-
* wrapper as the config store). */
|
|
25283
|
+
/** Record kind handle over the `recorder` / `exports` kind (hub recording node only). */
|
|
25453
25284
|
exportsStore() {
|
|
25454
|
-
return recordingExportsState(this.
|
|
25285
|
+
return recordingExportsState(this.requireRecords());
|
|
25455
25286
|
}
|
|
25456
|
-
/**
|
|
25287
|
+
/** Record kind handle over the `recorder` / `ops-log` kind (hub recording node only). */
|
|
25457
25288
|
opsLogStore() {
|
|
25458
|
-
return recordingOpsLogState(this.
|
|
25289
|
+
return recordingOpsLogState(this.requireRecords());
|
|
25290
|
+
}
|
|
25291
|
+
/**
|
|
25292
|
+
* Returns `ctx.records`, throwing clearly when it is absent.
|
|
25293
|
+
* Only called on the designated recording node (hub by default); an agent
|
|
25294
|
+
* exits before `onInitialize` reaches any store call.
|
|
25295
|
+
*/
|
|
25296
|
+
requireRecords() {
|
|
25297
|
+
const records = this.ctx.records;
|
|
25298
|
+
if (records === void 0) throw new Error("recorder (recording node) needs ctx.records — recording node must be the hub");
|
|
25299
|
+
return records;
|
|
25459
25300
|
}
|
|
25460
25301
|
/** Durable handle over the recorder-owned placement blob (assignments + volume ids). */
|
|
25461
25302
|
placementStore() {
|