@camstack/addon-pipeline 1.2.375 → 1.2.602
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio-analyzer/index.js +3 -3
- package/dist/audio-analyzer/index.mjs +3 -3
- package/dist/{custom-model-catalog-DF5qs8u4.mjs → custom-model-catalog--ms2wSDI.mjs} +2 -2
- package/dist/{custom-model-catalog-gbLoUFki.js → custom-model-catalog-CVvdKnH2.js} +2 -2
- package/dist/detection-pipeline/index.js +4 -4
- package/dist/detection-pipeline/index.mjs +4 -4
- package/dist/{dist-C0Mg8e3I.js → dist-B4HyxJe1.js} +703 -28
- package/dist/{dist-QyeZXkAO.mjs → dist-BmSyTuy4.mjs} +698 -29
- package/dist/{model-catalogs-BTuX-P0k.js → model-catalogs-Bj0f2Obs.js} +1 -1
- package/dist/{model-catalogs-CIE4Dz4x.mjs → model-catalogs-OnXVUgQY.mjs} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/{node-ndjp9DQO.mjs → node--4HtlkFo.mjs} +38 -3
- package/dist/{node-CifGKjsd.js → node-DfuKhWUZ.js} +42 -1
- package/dist/pipeline-runner/index.js +4 -4
- package/dist/pipeline-runner/index.mjs +4 -4
- package/dist/{process-memory-BMfSP5-i.js → process-memory-BZ1_q3g3.js} +1 -1
- package/dist/{process-memory-CaMI-Ry2.mjs → process-memory-uf7ZfkSz.mjs} +1 -1
- package/dist/recorder/index.js +446 -518
- package/dist/recorder/index.mjs +446 -518
- package/dist/{segment-demux-js-LhwVw18z.js → segment-demux-js-C7fMqBGZ.js} +1 -1
- package/dist/{segment-demux-js-CLXBgUTo.mjs → segment-demux-js-CURD3CEv.mjs} +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-B4MD_B4k.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-Cd6rjANN.mjs} +3 -3
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DUPQbxcH.mjs +26 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-CGcD3ujP.mjs +26 -0
- package/dist/stream-broker/demux-worker-child.js +1 -1
- package/dist/stream-broker/demux-worker-child.mjs +1 -1
- package/dist/stream-broker/{hostInit-DBDp5YHD.mjs → hostInit-CV-FMXnr.mjs} +3 -3
- package/dist/stream-broker/index.js +10 -9
- package/dist/stream-broker/index.mjs +10 -9
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BvMrdOCD.mjs +0 -26
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-JVFs9LgI.mjs +0 -26
package/dist/recorder/index.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-BmSyTuy4.mjs";
|
|
2
|
+
import { i as NO_COST_CLAIM, o as physicalRootOf, t as ChildCostRegistry } from "../node--4HtlkFo.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) {
|
|
@@ -6779,6 +6728,94 @@ Object.freeze({
|
|
|
6779
6728
|
"path": "online",
|
|
6780
6729
|
"valueType": "boolean"
|
|
6781
6730
|
}],
|
|
6731
|
+
"dishwasher": [
|
|
6732
|
+
{
|
|
6733
|
+
"path": "state",
|
|
6734
|
+
"valueType": "enum",
|
|
6735
|
+
"enumValues": [
|
|
6736
|
+
"idle",
|
|
6737
|
+
"running",
|
|
6738
|
+
"paused",
|
|
6739
|
+
"finished",
|
|
6740
|
+
"error",
|
|
6741
|
+
"unavailable"
|
|
6742
|
+
]
|
|
6743
|
+
},
|
|
6744
|
+
{
|
|
6745
|
+
"path": "program",
|
|
6746
|
+
"valueType": "string"
|
|
6747
|
+
},
|
|
6748
|
+
{
|
|
6749
|
+
"path": "remainingMinutes",
|
|
6750
|
+
"valueType": "number"
|
|
6751
|
+
},
|
|
6752
|
+
{
|
|
6753
|
+
"path": "progressPercent",
|
|
6754
|
+
"valueType": "number"
|
|
6755
|
+
},
|
|
6756
|
+
{
|
|
6757
|
+
"path": "doorOpen",
|
|
6758
|
+
"valueType": "boolean"
|
|
6759
|
+
},
|
|
6760
|
+
{
|
|
6761
|
+
"path": "remoteStartAllowed",
|
|
6762
|
+
"valueType": "boolean"
|
|
6763
|
+
},
|
|
6764
|
+
{
|
|
6765
|
+
"path": "powerOn",
|
|
6766
|
+
"valueType": "boolean"
|
|
6767
|
+
},
|
|
6768
|
+
{
|
|
6769
|
+
"path": "saltLevel",
|
|
6770
|
+
"valueType": "enum",
|
|
6771
|
+
"enumValues": [
|
|
6772
|
+
"ok",
|
|
6773
|
+
"low",
|
|
6774
|
+
"empty"
|
|
6775
|
+
]
|
|
6776
|
+
},
|
|
6777
|
+
{
|
|
6778
|
+
"path": "rinseAidLevel",
|
|
6779
|
+
"valueType": "enum",
|
|
6780
|
+
"enumValues": [
|
|
6781
|
+
"ok",
|
|
6782
|
+
"low",
|
|
6783
|
+
"empty"
|
|
6784
|
+
]
|
|
6785
|
+
},
|
|
6786
|
+
{
|
|
6787
|
+
"path": "halfLoad",
|
|
6788
|
+
"valueType": "boolean"
|
|
6789
|
+
},
|
|
6790
|
+
{
|
|
6791
|
+
"path": "hygienePlus",
|
|
6792
|
+
"valueType": "boolean"
|
|
6793
|
+
},
|
|
6794
|
+
{
|
|
6795
|
+
"path": "extraDry",
|
|
6796
|
+
"valueType": "boolean"
|
|
6797
|
+
},
|
|
6798
|
+
{
|
|
6799
|
+
"path": "intensiveZone",
|
|
6800
|
+
"valueType": "boolean"
|
|
6801
|
+
},
|
|
6802
|
+
{
|
|
6803
|
+
"path": "varioSpeedPlus",
|
|
6804
|
+
"valueType": "boolean"
|
|
6805
|
+
},
|
|
6806
|
+
{
|
|
6807
|
+
"path": "childLock",
|
|
6808
|
+
"valueType": "boolean"
|
|
6809
|
+
},
|
|
6810
|
+
{
|
|
6811
|
+
"path": "errorCode",
|
|
6812
|
+
"valueType": "number"
|
|
6813
|
+
},
|
|
6814
|
+
{
|
|
6815
|
+
"path": "errorLabel",
|
|
6816
|
+
"valueType": "string"
|
|
6817
|
+
}
|
|
6818
|
+
],
|
|
6782
6819
|
"doorbell": [{
|
|
6783
6820
|
"path": "lastPressedAt",
|
|
6784
6821
|
"valueType": "number"
|
|
@@ -6869,6 +6906,10 @@ Object.freeze({
|
|
|
6869
6906
|
"path": "detected",
|
|
6870
6907
|
"valueType": "boolean"
|
|
6871
6908
|
}],
|
|
6909
|
+
"heating-valve": [{
|
|
6910
|
+
"path": "position",
|
|
6911
|
+
"valueType": "number"
|
|
6912
|
+
}],
|
|
6872
6913
|
"humidifier": [
|
|
6873
6914
|
{
|
|
6874
6915
|
"path": "on",
|
|
@@ -8361,259 +8402,76 @@ function buildExportArgs(input) {
|
|
|
8361
8402
|
outPath
|
|
8362
8403
|
];
|
|
8363
8404
|
}
|
|
8364
|
-
var
|
|
8365
|
-
var RowMapStore = class RowMapStore {
|
|
8366
|
-
spec;
|
|
8367
|
-
store;
|
|
8368
|
-
logger;
|
|
8369
|
-
constructor(deps) {
|
|
8370
|
-
this.spec = deps.spec;
|
|
8371
|
-
this.store = deps.store;
|
|
8372
|
-
this.logger = deps.logger;
|
|
8373
|
-
}
|
|
8374
|
-
/** The collection this store owns — for the caller's own log lines. */
|
|
8375
|
-
get collection() {
|
|
8376
|
-
return this.spec.collection;
|
|
8377
|
-
}
|
|
8405
|
+
var recorderRecordKinds = defineRecordKinds({
|
|
8378
8406
|
/**
|
|
8379
|
-
*
|
|
8380
|
-
*
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
return store.declareCollection.mutate({
|
|
8384
|
-
collection: spec.collection,
|
|
8385
|
-
columns: [
|
|
8386
|
-
{
|
|
8387
|
-
name: "id",
|
|
8388
|
-
type: "TEXT",
|
|
8389
|
-
primaryKey: true,
|
|
8390
|
-
notNull: true
|
|
8391
|
-
},
|
|
8392
|
-
...spec.columns,
|
|
8393
|
-
{
|
|
8394
|
-
name: ROW_MAP_VALUE_COLUMN,
|
|
8395
|
-
type: "JSON",
|
|
8396
|
-
notNull: true
|
|
8397
|
-
}
|
|
8398
|
-
],
|
|
8399
|
-
...spec.indexes !== void 0 ? { indexes: [...spec.indexes] } : {}
|
|
8400
|
-
});
|
|
8401
|
-
}
|
|
8402
|
-
declare() {
|
|
8403
|
-
return RowMapStore.declare(this.store, this.spec);
|
|
8404
|
-
}
|
|
8405
|
-
/** One entry, by key. `null` when absent OR unparseable (which is said aloud). */
|
|
8406
|
-
async read(key) {
|
|
8407
|
-
const raw = await this.store.get.query({
|
|
8408
|
-
collection: this.spec.collection,
|
|
8409
|
-
key
|
|
8410
|
-
});
|
|
8411
|
-
if (!isRecord(raw)) return null;
|
|
8412
|
-
const row = raw;
|
|
8413
|
-
const parsed = this.spec.schema.safeParse(row[ROW_MAP_VALUE_COLUMN]);
|
|
8414
|
-
if (!parsed.success) {
|
|
8415
|
-
this.logger.warn("row-map row skipped as unparseable", {
|
|
8416
|
-
...this.deviceTags(row),
|
|
8417
|
-
meta: {
|
|
8418
|
-
collection: this.spec.collection,
|
|
8419
|
-
key,
|
|
8420
|
-
error: parsed.error.message
|
|
8421
|
-
}
|
|
8422
|
-
});
|
|
8423
|
-
return null;
|
|
8424
|
-
}
|
|
8425
|
-
return parsed.data;
|
|
8426
|
-
}
|
|
8427
|
-
/**
|
|
8428
|
-
* Every entry, keyed. Unparseable rows are skipped — one bad entry costs that
|
|
8429
|
-
* entry, never the map.
|
|
8430
|
-
*/
|
|
8431
|
-
async readAll() {
|
|
8432
|
-
const records = await this.store.query.query({
|
|
8433
|
-
collection: this.spec.collection,
|
|
8434
|
-
filter: { limit: this.spec.loadLimit }
|
|
8435
|
-
});
|
|
8436
|
-
const out = /* @__PURE__ */ new Map();
|
|
8437
|
-
let skipped = 0;
|
|
8438
|
-
for (const record of records) {
|
|
8439
|
-
const parsed = this.spec.schema.safeParse(record.data[ROW_MAP_VALUE_COLUMN]);
|
|
8440
|
-
if (!parsed.success) {
|
|
8441
|
-
skipped += 1;
|
|
8442
|
-
continue;
|
|
8443
|
-
}
|
|
8444
|
-
out.set(record.id, parsed.data);
|
|
8445
|
-
}
|
|
8446
|
-
if (skipped > 0) this.logger.warn("row-map rows skipped as unparseable", { meta: {
|
|
8447
|
-
collection: this.spec.collection,
|
|
8448
|
-
skipped,
|
|
8449
|
-
kept: out.size
|
|
8450
|
-
} });
|
|
8451
|
-
if (records.length >= this.spec.loadLimit) this.logger.warn("row-map read hit its own bound — the map may be truncated", { meta: {
|
|
8452
|
-
collection: this.spec.collection,
|
|
8453
|
-
loadLimit: this.spec.loadLimit
|
|
8454
|
-
} });
|
|
8455
|
-
return out;
|
|
8456
|
-
}
|
|
8457
|
-
/**
|
|
8458
|
-
* Every entry as it was WRITTEN, unvalidated and unparsed — the read a
|
|
8459
|
-
* MIGRATION needs.
|
|
8460
|
-
*
|
|
8461
|
-
* {@link readAll} answers through the CURRENT schema, which is exactly what a
|
|
8462
|
-
* migration must not do: a retired key is stripped by a non-strict `z.object`
|
|
8463
|
-
* before the migration could see it, so the row it is there to rescue looks
|
|
8464
|
-
* like a row that never needed rescuing. Only a migration should call this;
|
|
8465
|
-
* everything else wants the validated read.
|
|
8466
|
-
*/
|
|
8467
|
-
async readAllRaw() {
|
|
8468
|
-
const records = await this.store.query.query({
|
|
8469
|
-
collection: this.spec.collection,
|
|
8470
|
-
filter: { limit: this.spec.loadLimit }
|
|
8471
|
-
});
|
|
8472
|
-
const out = /* @__PURE__ */ new Map();
|
|
8473
|
-
for (const record of records) out.set(record.id, record.data[ROW_MAP_VALUE_COLUMN]);
|
|
8474
|
-
return out;
|
|
8475
|
-
}
|
|
8476
|
-
/** Insert or replace ONE entry. Validates at the boundary. */
|
|
8477
|
-
async write(key, value) {
|
|
8478
|
-
const validated = this.spec.schema.parse(value);
|
|
8479
|
-
const columns = this.spec.project(key, validated);
|
|
8480
|
-
await this.store.set.mutate({
|
|
8481
|
-
collection: this.spec.collection,
|
|
8482
|
-
key,
|
|
8483
|
-
value: {
|
|
8484
|
-
...columns,
|
|
8485
|
-
[ROW_MAP_VALUE_COLUMN]: validated
|
|
8486
|
-
}
|
|
8487
|
-
});
|
|
8488
|
-
}
|
|
8489
|
-
/** Drop ONE entry. Idempotent. */
|
|
8490
|
-
async remove(key) {
|
|
8491
|
-
await this.store.delete.mutate({
|
|
8492
|
-
collection: this.spec.collection,
|
|
8493
|
-
key
|
|
8494
|
-
});
|
|
8495
|
-
}
|
|
8496
|
-
/**
|
|
8497
|
-
* Make the collection hold exactly `next` — the write path for an owner whose
|
|
8498
|
-
* in-memory map IS the authority (a persister handed the complete set).
|
|
8407
|
+
* One row per export job, inserted by `createExport` and updated as the
|
|
8408
|
+
* render progresses. The system NEVER deletes a row — it is the export AUDIT.
|
|
8409
|
+
* `deleteExport` and the janitor flip `state` and clear the file, keeping
|
|
8410
|
+
* the row. The record's `at` field maps to `createdAt`.
|
|
8499
8411
|
*
|
|
8500
|
-
*
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
dropped: doomed.length,
|
|
8516
|
-
keys: doomed.slice(0, 20).map((r) => r.id),
|
|
8517
|
-
remaining: next.size
|
|
8518
|
-
} });
|
|
8519
|
-
return doomed.length;
|
|
8520
|
-
}
|
|
8412
|
+
* Indexed by `deviceId` to serve the per-device listing efficiently.
|
|
8413
|
+
*/
|
|
8414
|
+
exports: {
|
|
8415
|
+
schema: ExportRecordSchema,
|
|
8416
|
+
class: "ledger",
|
|
8417
|
+
writeTrigger: "one row per export job, inserted by createExport; rewritten by the render engine (progress / state patches) and the download hook",
|
|
8418
|
+
device: "none",
|
|
8419
|
+
at: "required",
|
|
8420
|
+
indexes: [{
|
|
8421
|
+
name: "device",
|
|
8422
|
+
fields: ["deviceId"]
|
|
8423
|
+
}],
|
|
8424
|
+
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" },
|
|
8425
|
+
adoptsFrom: "recorder:exports"
|
|
8426
|
+
},
|
|
8521
8427
|
/**
|
|
8522
|
-
*
|
|
8523
|
-
*
|
|
8524
|
-
*
|
|
8525
|
-
*
|
|
8526
|
-
* parse of the whole map followed by a filter in JS.
|
|
8428
|
+
* A bounded ring of per-operation audit rows. One row per recordings
|
|
8429
|
+
* operation (prune, evict, relocate, export, …), appended best-effort by
|
|
8430
|
+
* `createRecordingOpsLogSink().append`. The ring is trimmed to the newest
|
|
8431
|
+
* `OPS_LOG_RING_DEFAULT_MAX` (500) rows after every append.
|
|
8527
8432
|
*
|
|
8528
|
-
*
|
|
8529
|
-
*
|
|
8530
|
-
*/
|
|
8531
|
-
async readWhere(filter) {
|
|
8532
|
-
const records = await this.store.query.query({
|
|
8533
|
-
collection: this.spec.collection,
|
|
8534
|
-
filter: {
|
|
8535
|
-
...filter.where !== void 0 ? { where: filter.where } : {},
|
|
8536
|
-
...filter.whereIn !== void 0 ? { whereIn: filter.whereIn } : {},
|
|
8537
|
-
...filter.whereBetween !== void 0 ? { whereBetween: filter.whereBetween } : {},
|
|
8538
|
-
...filter.orderBy !== void 0 ? { orderBy: filter.orderBy } : {},
|
|
8539
|
-
...filter.offset !== void 0 ? { offset: filter.offset } : {},
|
|
8540
|
-
limit: filter.limit ?? this.spec.loadLimit
|
|
8541
|
-
}
|
|
8542
|
-
});
|
|
8543
|
-
const out = [];
|
|
8544
|
-
let skipped = 0;
|
|
8545
|
-
for (const record of records) {
|
|
8546
|
-
const parsed = this.spec.schema.safeParse(record.data[ROW_MAP_VALUE_COLUMN]);
|
|
8547
|
-
if (!parsed.success) {
|
|
8548
|
-
skipped += 1;
|
|
8549
|
-
continue;
|
|
8550
|
-
}
|
|
8551
|
-
out.push({
|
|
8552
|
-
key: record.id,
|
|
8553
|
-
value: parsed.data
|
|
8554
|
-
});
|
|
8555
|
-
}
|
|
8556
|
-
if (skipped > 0) this.logger.warn("row-map rows skipped as unparseable", { meta: {
|
|
8557
|
-
collection: this.spec.collection,
|
|
8558
|
-
skipped,
|
|
8559
|
-
kept: out.length
|
|
8560
|
-
} });
|
|
8561
|
-
return out;
|
|
8562
|
-
}
|
|
8563
|
-
/** How many rows match, without materialising one. */
|
|
8564
|
-
async count(filter) {
|
|
8565
|
-
return this.store.count.query({
|
|
8566
|
-
collection: this.spec.collection,
|
|
8567
|
-
...filter !== void 0 ? { filter: {
|
|
8568
|
-
...filter.where !== void 0 ? { where: filter.where } : {},
|
|
8569
|
-
...filter.whereIn !== void 0 ? { whereIn: filter.whereIn } : {},
|
|
8570
|
-
...filter.whereBetween !== void 0 ? { whereBetween: filter.whereBetween } : {}
|
|
8571
|
-
} } : {}
|
|
8572
|
-
});
|
|
8573
|
-
}
|
|
8574
|
-
/**
|
|
8575
|
-
* Delete every row matching a predicate, in ONE statement. Returns how many
|
|
8576
|
-
* went, and says so — a retention trim that silently drops rows is
|
|
8577
|
-
* indistinguishable from one that had nothing to do.
|
|
8433
|
+
* Indexed by `deviceId` so a per-device listing is an indexed query, not a
|
|
8434
|
+
* full scan of the ring.
|
|
8578
8435
|
*
|
|
8579
|
-
* The
|
|
8580
|
-
|
|
8581
|
-
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
|
|
8585
|
-
|
|
8586
|
-
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
return deleted;
|
|
8594
|
-
}
|
|
8595
|
-
/** `tags: { deviceId }` off the PROJECTED column, when the spec names one. */
|
|
8596
|
-
deviceTags(row) {
|
|
8597
|
-
const column = this.spec.deviceIdColumn;
|
|
8598
|
-
if (column === void 0) return {};
|
|
8599
|
-
const raw = row[column];
|
|
8600
|
-
return typeof raw === "number" && Number.isInteger(raw) ? { tags: { deviceId: raw } } : {};
|
|
8436
|
+
* The record's `at` field maps to `OpsLogEntry.at`.
|
|
8437
|
+
*/
|
|
8438
|
+
"ops-log": {
|
|
8439
|
+
schema: OpsLogEntrySchema,
|
|
8440
|
+
class: "ledger",
|
|
8441
|
+
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",
|
|
8442
|
+
device: "none",
|
|
8443
|
+
at: "required",
|
|
8444
|
+
indexes: [{
|
|
8445
|
+
name: "device",
|
|
8446
|
+
fields: ["deviceId"]
|
|
8447
|
+
}],
|
|
8448
|
+
retention: { none: "a bounded ring of the newest 500 rows; older rows are removed in application code (trimOpsLog) after each append — owner-pruned" },
|
|
8449
|
+
adoptsFrom: "recorder:ops-log"
|
|
8601
8450
|
}
|
|
8602
|
-
};
|
|
8603
|
-
/** A store payload that is a plain object, or `null`. A type guard, not a cast. */
|
|
8604
|
-
function isRecord(value) {
|
|
8605
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
8606
|
-
}
|
|
8451
|
+
});
|
|
8607
8452
|
//#endregion
|
|
8608
8453
|
//#region src/recorder/addon/export-store.ts
|
|
8609
8454
|
/**
|
|
8610
8455
|
* Recording-export persistence for the recorder addon.
|
|
8611
8456
|
*
|
|
8612
|
-
* ONE ROW PER EXPORT in `recorder
|
|
8613
|
-
* rows are never removed by the system
|
|
8614
|
-
* flip `state` and clear the file,
|
|
8457
|
+
* ONE ROW PER EXPORT in the `recorder` / `exports` record kind, through a
|
|
8458
|
+
* {@link RecordKindHandle}. History rows are never removed by the system
|
|
8459
|
+
* (audit) — `deleteExport` / the janitor flip `state` and clear the file,
|
|
8460
|
+
* keeping the row.
|
|
8461
|
+
*
|
|
8462
|
+
* ## Records migration (phase 2, step 25)
|
|
8615
8463
|
*
|
|
8616
|
-
*
|
|
8464
|
+
* The store was previously backed by a {@link RowMapStore} over a SettingsStore
|
|
8465
|
+
* collection named `recorder:exports`. It is now backed by a
|
|
8466
|
+
* `RecordKindHandle<ExportRecord>` obtained from `ctx.records`. The record's
|
|
8467
|
+
* `at` field maps to `ExportRecord.createdAt`.
|
|
8468
|
+
*
|
|
8469
|
+
* The per-ID serialiser ({@link serialize}) is preserved: two overlapping
|
|
8470
|
+
* patches on the SAME export row are still read-modify-write, so they must
|
|
8471
|
+
* be ordered. The `RecordKindHandle` (an object) is used as the WeakMap key,
|
|
8472
|
+
* keeping the same compaction semantics.
|
|
8473
|
+
*
|
|
8474
|
+
* ## What the RowMapStore replaced, and why it mattered
|
|
8617
8475
|
*
|
|
8618
8476
|
* Every job used to live in one key of the recorder's `addon-settings` / `root`
|
|
8619
8477
|
* blob — 201 833 bytes on the reference hub, of which the export history was
|
|
@@ -8628,11 +8486,6 @@ function isRecord(value) {
|
|
|
8628
8486
|
* erased, and the poll answered `unknown export "547ea182-…"`; the scheduler
|
|
8629
8487
|
* read the vanished row as a failed render and re-queued forever. A per-row
|
|
8630
8488
|
* upsert makes that class of loss unrepresentable — a write names ONE row.
|
|
8631
|
-
*
|
|
8632
|
-
* What is still needed is a per-ID serialiser: two patches to the SAME export
|
|
8633
|
-
* still read-modify-write the same row. That is what {@link serialize} bounds,
|
|
8634
|
-
* and it is now per (store, id) rather than per store, so two exports never
|
|
8635
|
-
* queue behind each other.
|
|
8636
8489
|
*/
|
|
8637
8490
|
/**
|
|
8638
8491
|
* The storage class export MP4s live in: `artifacts`, SHARED with the
|
|
@@ -8677,76 +8530,26 @@ function exportOutPathFor(root, deviceId, exportId) {
|
|
|
8677
8530
|
return path.join(exportTenantDirFor(root, deviceId), `${exportId}.mp4`);
|
|
8678
8531
|
}
|
|
8679
8532
|
/**
|
|
8680
|
-
*
|
|
8681
|
-
*
|
|
8682
|
-
*
|
|
8683
|
-
* retention="the SYSTEM never deletes a row — it is the export AUDIT. `deleteExport`
|
|
8684
|
-
* and the janitor flip `state` to 'deleted'/'expired' and remove the FILE, keeping
|
|
8685
|
-
* the row. `removeExport` exists for an operator-driven purge and nothing calls it
|
|
8686
|
-
* on a schedule."
|
|
8533
|
+
* Bound for the whole-history read. Explicit because a records query without
|
|
8534
|
+
* an explicit limit uses the default cap (100); an export list that silently
|
|
8535
|
+
* loses its oldest entries looks exactly like a purge.
|
|
8687
8536
|
*/
|
|
8688
|
-
var
|
|
8537
|
+
var RECORDING_EXPORTS_LIMIT = 2e4;
|
|
8689
8538
|
/**
|
|
8690
|
-
*
|
|
8691
|
-
*
|
|
8692
|
-
*
|
|
8539
|
+
* Obtain the `recorder` / `exports` record kind handle.
|
|
8540
|
+
* `ctx.records.declare(recorderRecordKinds)` MUST have run before any read or
|
|
8541
|
+
* write (called at boot in `onInitialize`).
|
|
8693
8542
|
*/
|
|
8694
|
-
|
|
8695
|
-
|
|
8696
|
-
collection: RECORDING_EXPORTS_COLLECTION,
|
|
8697
|
-
schema: ExportRecordSchema,
|
|
8698
|
-
columns: [
|
|
8699
|
-
{
|
|
8700
|
-
name: "deviceId",
|
|
8701
|
-
type: "INTEGER",
|
|
8702
|
-
notNull: true
|
|
8703
|
-
},
|
|
8704
|
-
{
|
|
8705
|
-
name: "createdAt",
|
|
8706
|
-
type: "INTEGER",
|
|
8707
|
-
notNull: true
|
|
8708
|
-
},
|
|
8709
|
-
{
|
|
8710
|
-
name: "state",
|
|
8711
|
-
type: "TEXT",
|
|
8712
|
-
notNull: true
|
|
8713
|
-
}
|
|
8714
|
-
],
|
|
8715
|
-
indexes: [{
|
|
8716
|
-
name: "idx_recorder_exports_device",
|
|
8717
|
-
columns: ["deviceId"]
|
|
8718
|
-
}, {
|
|
8719
|
-
name: "idx_recorder_exports_created",
|
|
8720
|
-
columns: ["createdAt"]
|
|
8721
|
-
}],
|
|
8722
|
-
project: (_key, value) => ({
|
|
8723
|
-
deviceId: value.deviceId,
|
|
8724
|
-
createdAt: value.createdAt,
|
|
8725
|
-
state: value.state
|
|
8726
|
-
}),
|
|
8727
|
-
deviceIdColumn: "deviceId",
|
|
8728
|
-
loadLimit: RECORDING_EXPORTS_LIMIT
|
|
8729
|
-
};
|
|
8730
|
-
/** Build the handle. `declare()` MUST run once at boot before any read/write. */
|
|
8731
|
-
function recordingExportsState(store, logger) {
|
|
8732
|
-
return new RowMapStore({
|
|
8733
|
-
spec: RECORDING_EXPORTS_SPEC,
|
|
8734
|
-
store,
|
|
8735
|
-
logger
|
|
8736
|
-
});
|
|
8543
|
+
function recordingExportsState(records) {
|
|
8544
|
+
return records.kind(recorderRecordKinds["exports"]);
|
|
8737
8545
|
}
|
|
8738
8546
|
/**
|
|
8739
8547
|
* One in-flight mutation chain per (handle, export id).
|
|
8740
8548
|
*
|
|
8741
|
-
*
|
|
8742
|
-
*
|
|
8743
|
-
*
|
|
8744
|
-
*
|
|
8745
|
-
* unrelated exports fully concurrent (the old chain was per HANDLE, so a slow
|
|
8746
|
-
* patch queued every other export behind it).
|
|
8747
|
-
*
|
|
8748
|
-
* A `WeakMap` keyed by the handle keeps a disposed store collectable; the inner
|
|
8749
|
-
* map is keyed by export id.
|
|
8549
|
+
* Two overlapping patches on the SAME export still read-modify-write one row.
|
|
8550
|
+
* Serialising per id keeps them atomic while leaving unrelated exports fully
|
|
8551
|
+
* concurrent. A `WeakMap` keyed by the handle keeps a disposed handle
|
|
8552
|
+
* collectable; the inner map is keyed by export id.
|
|
8750
8553
|
*/
|
|
8751
8554
|
var mutationChains = /* @__PURE__ */ new WeakMap();
|
|
8752
8555
|
/** Run `work` after every mutation already queued for `(state, id)`, never before. */
|
|
@@ -8766,7 +8569,11 @@ async function serialize(state, id, work) {
|
|
|
8766
8569
|
async function upsertExport(state, record) {
|
|
8767
8570
|
const validated = ExportRecordSchema.parse(record);
|
|
8768
8571
|
return serialize(state, validated.id, async () => {
|
|
8769
|
-
await state.
|
|
8572
|
+
await state.put({
|
|
8573
|
+
id: validated.id,
|
|
8574
|
+
at: validated.createdAt,
|
|
8575
|
+
data: validated
|
|
8576
|
+
});
|
|
8770
8577
|
return validated;
|
|
8771
8578
|
});
|
|
8772
8579
|
}
|
|
@@ -8776,13 +8583,17 @@ async function upsertExport(state, record) {
|
|
|
8776
8583
|
*/
|
|
8777
8584
|
async function patchExport(state, id, patch) {
|
|
8778
8585
|
return serialize(state, id, async () => {
|
|
8779
|
-
const
|
|
8780
|
-
if (
|
|
8586
|
+
const row = await state.get(id);
|
|
8587
|
+
if (row === null) return null;
|
|
8781
8588
|
const merged = ExportRecordSchema.parse({
|
|
8782
|
-
...
|
|
8589
|
+
...row.data,
|
|
8783
8590
|
...patch
|
|
8784
8591
|
});
|
|
8785
|
-
await state.
|
|
8592
|
+
await state.put({
|
|
8593
|
+
id,
|
|
8594
|
+
at: merged.createdAt,
|
|
8595
|
+
data: merged
|
|
8596
|
+
});
|
|
8786
8597
|
return merged;
|
|
8787
8598
|
});
|
|
8788
8599
|
}
|
|
@@ -8793,17 +8604,21 @@ async function patchExport(state, id, patch) {
|
|
|
8793
8604
|
*/
|
|
8794
8605
|
async function patchExportUnless(state, id, patch, refuse) {
|
|
8795
8606
|
return serialize(state, id, async () => {
|
|
8796
|
-
const
|
|
8797
|
-
if (
|
|
8798
|
-
if (refuse(
|
|
8607
|
+
const row = await state.get(id);
|
|
8608
|
+
if (row === null) return { kind: "missing" };
|
|
8609
|
+
if (refuse(row.data)) return {
|
|
8799
8610
|
kind: "refused",
|
|
8800
|
-
record:
|
|
8611
|
+
record: row.data
|
|
8801
8612
|
};
|
|
8802
8613
|
const merged = ExportRecordSchema.parse({
|
|
8803
|
-
...
|
|
8614
|
+
...row.data,
|
|
8804
8615
|
...patch
|
|
8805
8616
|
});
|
|
8806
|
-
await state.
|
|
8617
|
+
await state.put({
|
|
8618
|
+
id,
|
|
8619
|
+
at: merged.createdAt,
|
|
8620
|
+
data: merged
|
|
8621
|
+
});
|
|
8807
8622
|
return {
|
|
8808
8623
|
kind: "patched",
|
|
8809
8624
|
record: merged
|
|
@@ -8812,24 +8627,22 @@ async function patchExportUnless(state, id, patch, refuse) {
|
|
|
8812
8627
|
}
|
|
8813
8628
|
/** Read one export row by id — ONE keyed row read. */
|
|
8814
8629
|
async function getExportRecord(state, id) {
|
|
8815
|
-
|
|
8630
|
+
const row = await state.get(id);
|
|
8631
|
+
return row === null ? null : row.data;
|
|
8816
8632
|
}
|
|
8817
8633
|
/**
|
|
8818
|
-
* List export rows, newest-first
|
|
8634
|
+
* List export rows, newest-first (by `createdAt` / record `at`), optionally
|
|
8635
|
+
* scoped to one device.
|
|
8819
8636
|
*
|
|
8820
|
-
* Scoped to a device this is an INDEXED query on the
|
|
8821
|
-
*
|
|
8822
|
-
* in JS.
|
|
8637
|
+
* Scoped to a device this is an INDEXED query on the `device` index — not a
|
|
8638
|
+
* parse of every export in the installation followed by a filter in JS.
|
|
8823
8639
|
*/
|
|
8824
8640
|
async function listExportsFor(state, deviceId) {
|
|
8825
|
-
return (await state.
|
|
8641
|
+
return (await state.query({
|
|
8826
8642
|
...deviceId === void 0 ? {} : { where: { deviceId } },
|
|
8827
|
-
|
|
8828
|
-
field: "createdAt",
|
|
8829
|
-
direction: "desc"
|
|
8830
|
-
},
|
|
8643
|
+
desc: true,
|
|
8831
8644
|
limit: RECORDING_EXPORTS_LIMIT
|
|
8832
|
-
})).map((
|
|
8645
|
+
})).records.map((r) => r.data);
|
|
8833
8646
|
}
|
|
8834
8647
|
//#endregion
|
|
8835
8648
|
//#region src/recorder/addon/export-timelapse-chunks.ts
|
|
@@ -11787,36 +11600,125 @@ function runFfmpeg(renderDeps, ffmpegPath, args, ext) {
|
|
|
11787
11600
|
});
|
|
11788
11601
|
}
|
|
11789
11602
|
//#endregion
|
|
11603
|
+
//#region src/durable/durable-legacy-kind-handle.ts
|
|
11604
|
+
function notImpl(method) {
|
|
11605
|
+
throw new Error(`DurableLegacyKindHandle.${method}() is not available on the legacy (agent) path`);
|
|
11606
|
+
}
|
|
11607
|
+
function isRecord(value) {
|
|
11608
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11609
|
+
}
|
|
11610
|
+
function toRecordRow(id, data) {
|
|
11611
|
+
return {
|
|
11612
|
+
id,
|
|
11613
|
+
data,
|
|
11614
|
+
deviceId: null,
|
|
11615
|
+
at: null,
|
|
11616
|
+
expiresAt: null,
|
|
11617
|
+
rev: 0,
|
|
11618
|
+
updatedAt: 0
|
|
11619
|
+
};
|
|
11620
|
+
}
|
|
11621
|
+
var DurableLegacyKindHandle = class {
|
|
11622
|
+
name;
|
|
11623
|
+
spec;
|
|
11624
|
+
store;
|
|
11625
|
+
constructor(opts) {
|
|
11626
|
+
this.name = opts.spec.collection;
|
|
11627
|
+
this.spec = opts.spec;
|
|
11628
|
+
this.store = opts.store;
|
|
11629
|
+
}
|
|
11630
|
+
async get(id) {
|
|
11631
|
+
const row = (await this.store.query.query({
|
|
11632
|
+
collection: this.spec.collection,
|
|
11633
|
+
filter: {
|
|
11634
|
+
where: { [this.pkColumn()]: id },
|
|
11635
|
+
limit: 1
|
|
11636
|
+
}
|
|
11637
|
+
}))[0];
|
|
11638
|
+
if (row === void 0 || !isRecord(row.data)) return null;
|
|
11639
|
+
const parsed = this.spec.fromRecord(row.id, row.data);
|
|
11640
|
+
if (parsed === null) return null;
|
|
11641
|
+
return toRecordRow(row.id, this.spec.toValue(parsed));
|
|
11642
|
+
}
|
|
11643
|
+
async query(q) {
|
|
11644
|
+
const rows = await this.store.query.query({
|
|
11645
|
+
collection: this.spec.collection,
|
|
11646
|
+
filter: { limit: q.limit ?? 1e4 }
|
|
11647
|
+
});
|
|
11648
|
+
const records = [];
|
|
11649
|
+
for (const row of rows) {
|
|
11650
|
+
if (!isRecord(row.data)) continue;
|
|
11651
|
+
const parsed = this.spec.fromRecord(row.id, row.data);
|
|
11652
|
+
if (parsed === null) continue;
|
|
11653
|
+
records.push(toRecordRow(row.id, this.spec.toValue(parsed)));
|
|
11654
|
+
}
|
|
11655
|
+
return {
|
|
11656
|
+
records,
|
|
11657
|
+
nextCursor: null
|
|
11658
|
+
};
|
|
11659
|
+
}
|
|
11660
|
+
async put(input) {
|
|
11661
|
+
await this.store.set.mutate({
|
|
11662
|
+
collection: this.spec.collection,
|
|
11663
|
+
key: input.id,
|
|
11664
|
+
value: input.data
|
|
11665
|
+
});
|
|
11666
|
+
return { rev: 0 };
|
|
11667
|
+
}
|
|
11668
|
+
async delete(id) {
|
|
11669
|
+
await this.store.delete.mutate({
|
|
11670
|
+
collection: this.spec.collection,
|
|
11671
|
+
key: id
|
|
11672
|
+
});
|
|
11673
|
+
return true;
|
|
11674
|
+
}
|
|
11675
|
+
getMany = () => notImpl("getMany");
|
|
11676
|
+
getResolved = () => notImpl("getResolved");
|
|
11677
|
+
putMany = () => notImpl("putMany");
|
|
11678
|
+
patch = () => notImpl("patch");
|
|
11679
|
+
deleteMany = () => notImpl("deleteMany");
|
|
11680
|
+
count = () => notImpl("count");
|
|
11681
|
+
histogram = () => notImpl("histogram");
|
|
11682
|
+
aggregate = () => notImpl("aggregate");
|
|
11683
|
+
deleteWhere = () => notImpl("deleteWhere");
|
|
11684
|
+
allocateId = () => notImpl("allocateId");
|
|
11685
|
+
pkColumn() {
|
|
11686
|
+
return this.spec.columns.find((c) => c.primaryKey === true)?.name ?? "id";
|
|
11687
|
+
}
|
|
11688
|
+
};
|
|
11689
|
+
/** Legacy settings-store handle for one {@link DurableLedgerSpec}. */
|
|
11690
|
+
function durableLegacyKind(spec, store) {
|
|
11691
|
+
return new DurableLegacyKindHandle({
|
|
11692
|
+
spec,
|
|
11693
|
+
store
|
|
11694
|
+
});
|
|
11695
|
+
}
|
|
11696
|
+
/** Agent boot: declare the SQLite collection behind a durable ledger spec. */
|
|
11697
|
+
function declareDurableLegacyCollection(store, spec) {
|
|
11698
|
+
return store.declareCollection.mutate({
|
|
11699
|
+
collection: spec.collection,
|
|
11700
|
+
columns: [...spec.columns],
|
|
11701
|
+
...spec.indexes !== void 0 ? { indexes: [...spec.indexes] } : {}
|
|
11702
|
+
});
|
|
11703
|
+
}
|
|
11704
|
+
//#endregion
|
|
11790
11705
|
//#region src/durable/durable-ledger.ts
|
|
11791
11706
|
/** Default reseed cap — every current consumer's row set is installation-bounded. */
|
|
11792
11707
|
var DEFAULT_LOAD_LIMIT = 1e5;
|
|
11793
|
-
|
|
11708
|
+
/** Page size for records reseed queries. */
|
|
11709
|
+
var QUERY_PAGE = 1e3;
|
|
11710
|
+
var DurableLedger = class {
|
|
11794
11711
|
mirror = /* @__PURE__ */ new Map();
|
|
11795
11712
|
/** Whether ANY load has read the store successfully — see {@link hasAnswered}. */
|
|
11796
11713
|
answered = false;
|
|
11797
11714
|
spec;
|
|
11798
|
-
|
|
11715
|
+
kind;
|
|
11799
11716
|
logger;
|
|
11800
11717
|
constructor(deps) {
|
|
11801
11718
|
this.spec = deps.spec;
|
|
11802
|
-
this.
|
|
11719
|
+
this.kind = deps.kind;
|
|
11803
11720
|
this.logger = deps.logger;
|
|
11804
11721
|
}
|
|
11805
|
-
/**
|
|
11806
|
-
* Register the collection. MUST run at boot, before any read or write: the
|
|
11807
|
-
* SQLite backend answers 412 for an undeclared collection and takes the whole
|
|
11808
|
-
* runner down with it (the addon-ai boot-crash lesson).
|
|
11809
|
-
*/
|
|
11810
|
-
static declare(store, spec) {
|
|
11811
|
-
return store.declareCollection.mutate({
|
|
11812
|
-
collection: spec.collection,
|
|
11813
|
-
columns: [...spec.columns],
|
|
11814
|
-
...spec.indexes !== void 0 ? { indexes: [...spec.indexes] } : {}
|
|
11815
|
-
});
|
|
11816
|
-
}
|
|
11817
|
-
declare() {
|
|
11818
|
-
return DurableLedger.declare(this.store, this.spec);
|
|
11819
|
-
}
|
|
11820
11722
|
/** The collection this ledger owns — for the caller's own log lines. */
|
|
11821
11723
|
get collection() {
|
|
11822
11724
|
return this.spec.collection;
|
|
@@ -11833,31 +11735,43 @@ var DurableLedger = class DurableLedger {
|
|
|
11833
11735
|
*/
|
|
11834
11736
|
async load() {
|
|
11835
11737
|
try {
|
|
11836
|
-
const records = await this.store.query.query({
|
|
11837
|
-
collection: this.spec.collection,
|
|
11838
|
-
filter: { limit: this.spec.loadLimit ?? DEFAULT_LOAD_LIMIT }
|
|
11839
|
-
});
|
|
11840
11738
|
const next = /* @__PURE__ */ new Map();
|
|
11841
11739
|
let skipped = 0;
|
|
11842
|
-
|
|
11843
|
-
|
|
11844
|
-
|
|
11845
|
-
|
|
11846
|
-
|
|
11740
|
+
let seen = 0;
|
|
11741
|
+
const cap = this.spec.loadLimit ?? DEFAULT_LOAD_LIMIT;
|
|
11742
|
+
let cursor;
|
|
11743
|
+
for (;;) {
|
|
11744
|
+
const limit = Math.min(QUERY_PAGE, cap - seen);
|
|
11745
|
+
if (limit <= 0) break;
|
|
11746
|
+
const page = await this.kind.query({
|
|
11747
|
+
limit,
|
|
11748
|
+
...cursor === void 0 ? {} : { cursor }
|
|
11749
|
+
});
|
|
11750
|
+
seen += page.records.length;
|
|
11751
|
+
for (const record of page.records) {
|
|
11752
|
+
const row = this.spec.fromRecord(record.id, record.data);
|
|
11753
|
+
if (row === null) {
|
|
11754
|
+
skipped += 1;
|
|
11755
|
+
continue;
|
|
11756
|
+
}
|
|
11757
|
+
next.set(this.spec.keyOf(row), row);
|
|
11847
11758
|
}
|
|
11848
|
-
|
|
11759
|
+
if (page.nextCursor === null) break;
|
|
11760
|
+
cursor = page.nextCursor;
|
|
11849
11761
|
}
|
|
11850
11762
|
this.mirror.clear();
|
|
11851
11763
|
for (const [key, row] of next) this.mirror.set(key, row);
|
|
11852
11764
|
this.answered = true;
|
|
11853
11765
|
if (skipped > 0) this.logger.warn("durable rows skipped as malformed — they gate NOTHING", { meta: {
|
|
11854
11766
|
collection: this.spec.collection,
|
|
11767
|
+
kind: this.kind.name,
|
|
11855
11768
|
skipped
|
|
11856
11769
|
} });
|
|
11857
11770
|
return [...this.mirror.values()];
|
|
11858
11771
|
} catch (err) {
|
|
11859
11772
|
this.logger.warn("durable load failed — keeping the state already in memory", { meta: {
|
|
11860
11773
|
collection: this.spec.collection,
|
|
11774
|
+
kind: this.kind.name,
|
|
11861
11775
|
error: String(err),
|
|
11862
11776
|
held: this.mirror.size
|
|
11863
11777
|
} });
|
|
@@ -11922,16 +11836,13 @@ var DurableLedger = class DurableLedger {
|
|
|
11922
11836
|
*/
|
|
11923
11837
|
async put(row) {
|
|
11924
11838
|
const key = this.spec.keyOf(row);
|
|
11839
|
+
const body = this.spec.toValue(row);
|
|
11925
11840
|
if (this.spec.writeMode === "write-behind") {
|
|
11926
11841
|
this.mirror.set(key, row);
|
|
11927
|
-
await this.
|
|
11842
|
+
await this.persistRow(key, body, row);
|
|
11928
11843
|
return;
|
|
11929
11844
|
}
|
|
11930
|
-
await this.
|
|
11931
|
-
collection: this.spec.collection,
|
|
11932
|
-
key,
|
|
11933
|
-
value: this.spec.toValue(row)
|
|
11934
|
-
});
|
|
11845
|
+
await this.kind.put(this.buildPut(key, body, row));
|
|
11935
11846
|
this.mirror.set(key, row);
|
|
11936
11847
|
}
|
|
11937
11848
|
/**
|
|
@@ -11971,13 +11882,11 @@ var DurableLedger = class DurableLedger {
|
|
|
11971
11882
|
async forget(key) {
|
|
11972
11883
|
this.mirror.delete(key);
|
|
11973
11884
|
try {
|
|
11974
|
-
await this.
|
|
11975
|
-
collection: this.spec.collection,
|
|
11976
|
-
key
|
|
11977
|
-
});
|
|
11885
|
+
await this.kind.delete(key);
|
|
11978
11886
|
} catch (err) {
|
|
11979
11887
|
this.logger.debug("durable delete failed", { meta: {
|
|
11980
11888
|
collection: this.spec.collection,
|
|
11889
|
+
kind: this.kind.name,
|
|
11981
11890
|
key,
|
|
11982
11891
|
error: String(err)
|
|
11983
11892
|
} });
|
|
@@ -12013,15 +11922,13 @@ var DurableLedger = class DurableLedger {
|
|
|
12013
11922
|
for (const key of [...this.mirror.keys()]) {
|
|
12014
11923
|
if (activeKeys.has(key)) continue;
|
|
12015
11924
|
try {
|
|
12016
|
-
await this.
|
|
12017
|
-
collection: this.spec.collection,
|
|
12018
|
-
key
|
|
12019
|
-
});
|
|
11925
|
+
await this.kind.delete(key);
|
|
12020
11926
|
this.mirror.delete(key);
|
|
12021
11927
|
pruned += 1;
|
|
12022
11928
|
} catch (err) {
|
|
12023
11929
|
this.logger.debug("durable prune delete failed", { meta: {
|
|
12024
11930
|
collection: this.spec.collection,
|
|
11931
|
+
kind: this.kind.name,
|
|
12025
11932
|
key,
|
|
12026
11933
|
error: String(err)
|
|
12027
11934
|
} });
|
|
@@ -12034,25 +11941,46 @@ var DurableLedger = class DurableLedger {
|
|
|
12034
11941
|
* decision path: the mirror already holds the truth for this process, and the
|
|
12035
11942
|
* worst a lost write can do is one silent re-seed after the next restart.
|
|
12036
11943
|
*/
|
|
12037
|
-
|
|
12038
|
-
const
|
|
11944
|
+
persist(row) {
|
|
11945
|
+
const key = this.spec.keyOf(row);
|
|
11946
|
+
this.persistRow(key, this.spec.toValue(row), row);
|
|
11947
|
+
}
|
|
11948
|
+
buildPut(key, body, row) {
|
|
11949
|
+
const deviceId = this.resolveRecordDeviceId(row, body);
|
|
11950
|
+
return {
|
|
11951
|
+
id: key,
|
|
11952
|
+
data: body,
|
|
11953
|
+
...deviceId !== void 0 ? { deviceId } : {}
|
|
11954
|
+
};
|
|
11955
|
+
}
|
|
11956
|
+
resolveRecordDeviceId(row, body) {
|
|
11957
|
+
if (row !== void 0) {
|
|
11958
|
+
const fromSpec = this.spec.deviceIdOf?.(row);
|
|
11959
|
+
if (fromSpec !== void 0 && Number.isFinite(fromSpec)) return fromSpec;
|
|
11960
|
+
}
|
|
11961
|
+
return this.deviceIdFromBody(body);
|
|
11962
|
+
}
|
|
11963
|
+
async persistRow(key, body, row) {
|
|
11964
|
+
const deviceId = this.resolveRecordDeviceId(row, body);
|
|
12039
11965
|
try {
|
|
12040
|
-
await this.
|
|
12041
|
-
collection: this.spec.collection,
|
|
12042
|
-
key: this.spec.keyOf(row),
|
|
12043
|
-
value: this.spec.toValue(row)
|
|
12044
|
-
});
|
|
11966
|
+
await this.kind.put(this.buildPut(key, body, row));
|
|
12045
11967
|
} catch (err) {
|
|
12046
11968
|
this.logger.warn("durable persist failed — this key may re-seed on boot", {
|
|
12047
11969
|
...deviceId !== void 0 ? { tags: { deviceId } } : {},
|
|
12048
11970
|
meta: {
|
|
12049
11971
|
collection: this.spec.collection,
|
|
12050
|
-
|
|
11972
|
+
kind: this.kind.name,
|
|
11973
|
+
key,
|
|
12051
11974
|
error: String(err)
|
|
12052
11975
|
}
|
|
12053
11976
|
});
|
|
12054
11977
|
}
|
|
12055
11978
|
}
|
|
11979
|
+
deviceIdFromBody(body) {
|
|
11980
|
+
const raw = body["deviceId"];
|
|
11981
|
+
const n = typeof raw === "number" ? raw : Number(raw);
|
|
11982
|
+
return Number.isFinite(n) ? n : void 0;
|
|
11983
|
+
}
|
|
12056
11984
|
};
|
|
12057
11985
|
//#endregion
|
|
12058
11986
|
//#region src/recorder/addon/segment-hour-ledger.ts
|
|
@@ -12613,15 +12541,15 @@ var SegmentHourLedger = class SegmentHourLedger {
|
|
|
12613
12541
|
this.rowCap = deps.loadPacing?.rowCap ?? 25e4;
|
|
12614
12542
|
this.ledger = new DurableLedger({
|
|
12615
12543
|
spec: SPEC,
|
|
12616
|
-
|
|
12544
|
+
kind: durableLegacyKind(SPEC, deps.store),
|
|
12617
12545
|
logger: deps.logger
|
|
12618
12546
|
});
|
|
12619
12547
|
}
|
|
12620
12548
|
static declare(store) {
|
|
12621
|
-
return
|
|
12549
|
+
return declareDurableLegacyCollection(store, SPEC);
|
|
12622
12550
|
}
|
|
12623
12551
|
declare() {
|
|
12624
|
-
return this.
|
|
12552
|
+
return declareDurableLegacyCollection(this.store, SPEC);
|
|
12625
12553
|
}
|
|
12626
12554
|
/**
|
|
12627
12555
|
* Boot load — every hour row, in one query.
|
|
@@ -15032,63 +14960,61 @@ var LocationRefresher = class {
|
|
|
15032
14960
|
}
|
|
15033
14961
|
}
|
|
15034
14962
|
};
|
|
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
|
-
|
|
15061
|
-
|
|
15062
|
-
|
|
15063
|
-
|
|
15064
|
-
|
|
15065
|
-
|
|
15066
|
-
|
|
14963
|
+
//#endregion
|
|
14964
|
+
//#region src/recorder/addon/ops-log-store.ts
|
|
14965
|
+
/**
|
|
14966
|
+
* Recordings ops-log persistence for the recorder addon.
|
|
14967
|
+
*
|
|
14968
|
+
* ONE ROW PER OPERATION in the `recorder` / `ops-log` record kind, through a
|
|
14969
|
+
* {@link RecordKindHandle}. The ring is bounded — `maxEntries` newest rows
|
|
14970
|
+
* survive — trimmed with one `deleteWhere({ atBefore })` per append.
|
|
14971
|
+
*
|
|
14972
|
+
* ## Records migration (phase 2, step 26)
|
|
14973
|
+
*
|
|
14974
|
+
* The store was previously backed by a {@link RowMapStore} over a SettingsStore
|
|
14975
|
+
* collection named `recorder:ops-log`. It is now backed by a
|
|
14976
|
+
* `RecordKindHandle<OpsLogEntry>` obtained from `ctx.records`. The record's
|
|
14977
|
+
* `at` field maps to `OpsLogEntry.at`.
|
|
14978
|
+
*
|
|
14979
|
+
* ## What the RowMapStore replaced
|
|
14980
|
+
*
|
|
14981
|
+
* The ring lived in ONE key of the recorder's `addon-settings` / `root` blob,
|
|
14982
|
+
* as a flat JSON array. On the reference hub it was 147 437 bytes of that row's
|
|
14983
|
+
* 201 833 — so appending one audit line read, parsed, re-serialised and rewrote
|
|
14984
|
+
* ~200 KB, and so did every unrelated reader of that row. The ops log is the
|
|
14985
|
+
* CHEAPEST thing in the recorder and it was paying for the most expensive.
|
|
14986
|
+
*
|
|
14987
|
+
* Writes stay BEST-EFFORT: `createRecordingOpsLogSink().append` never throws —
|
|
14988
|
+
* a failed audit write must not fail the operation it records.
|
|
14989
|
+
*/
|
|
14990
|
+
/**
|
|
14991
|
+
* Obtain the `recorder` / `ops-log` record kind handle.
|
|
14992
|
+
* `ctx.records.declare(recorderRecordKinds)` MUST have run before any read or
|
|
14993
|
+
* write (called at boot in `onInitialize`).
|
|
14994
|
+
*/
|
|
14995
|
+
function recordingOpsLogState(records) {
|
|
14996
|
+
return records.kind(recorderRecordKinds["ops-log"]);
|
|
15067
14997
|
}
|
|
15068
14998
|
/**
|
|
15069
14999
|
* Trim the ring to the newest `maxEntries` rows. Returns how many went.
|
|
15070
15000
|
*
|
|
15071
15001
|
* The cursor is the OLDEST row that must SURVIVE — offset `maxEntries - 1`
|
|
15072
|
-
* newest-first — and the delete
|
|
15002
|
+
* newest-first — and the delete removes everything strictly BEFORE its `at`
|
|
15003
|
+
* (`atBefore: cursor.at` deletes rows where `at < cursor.at`).
|
|
15073
15004
|
*
|
|
15074
|
-
* Strictly,
|
|
15075
|
-
*
|
|
15076
|
-
* and
|
|
15077
|
-
* Erring this way keeps a few EXTRA audit rows on a tie and never fewer than
|
|
15078
|
-
* the cap.
|
|
15005
|
+
* Strictly, to avoid dropping the cursor row itself and any rows tied on its
|
|
15006
|
+
* millisecond. The ring may hold a few MORE than the cap on a millisecond tie
|
|
15007
|
+
* and never FEWER.
|
|
15079
15008
|
*/
|
|
15080
15009
|
async function trimOpsLog(state, maxEntries = 500) {
|
|
15081
|
-
if (await state.count() <= maxEntries) return 0;
|
|
15082
|
-
const
|
|
15083
|
-
|
|
15084
|
-
|
|
15085
|
-
|
|
15086
|
-
|
|
15087
|
-
|
|
15088
|
-
|
|
15089
|
-
}))[0];
|
|
15090
|
-
if (oldestKept === void 0) return 0;
|
|
15091
|
-
return state.deleteWhere({ whereBetween: { at: [0, oldestKept.value.at - 1] } });
|
|
15010
|
+
if (await state.count({}) <= maxEntries) return 0;
|
|
15011
|
+
const page = await state.query({
|
|
15012
|
+
desc: true,
|
|
15013
|
+
limit: maxEntries
|
|
15014
|
+
});
|
|
15015
|
+
const oldest = page.records[page.records.length - 1];
|
|
15016
|
+
if (oldest === void 0) return 0;
|
|
15017
|
+
return state.deleteWhere({ atBefore: oldest.at ?? 0 });
|
|
15092
15018
|
}
|
|
15093
15019
|
/**
|
|
15094
15020
|
* Append one row and persist, then trim the ring. Validates at the boundary.
|
|
@@ -15096,7 +15022,11 @@ async function trimOpsLog(state, maxEntries = 500) {
|
|
|
15096
15022
|
*/
|
|
15097
15023
|
async function appendOpsLog(state, entry, maxEntries = 500) {
|
|
15098
15024
|
const validated = OpsLogEntrySchema.parse(entry);
|
|
15099
|
-
await state.
|
|
15025
|
+
await state.put({
|
|
15026
|
+
id: validated.id,
|
|
15027
|
+
at: validated.at,
|
|
15028
|
+
data: validated
|
|
15029
|
+
});
|
|
15100
15030
|
await trimOpsLog(state, maxEntries);
|
|
15101
15031
|
return validated;
|
|
15102
15032
|
}
|
|
@@ -15104,18 +15034,15 @@ async function appendOpsLog(state, entry, maxEntries = 500) {
|
|
|
15104
15034
|
* List ops-log rows newest-first, optionally scoped to one device, capped at
|
|
15105
15035
|
* `limit` (default {@link OPS_LOG_DEFAULT_LIMIT}).
|
|
15106
15036
|
*
|
|
15107
|
-
* Scoped to a device this is an INDEXED query
|
|
15108
|
-
* followed by a filter and a sort in JS.
|
|
15037
|
+
* Scoped to a device this is an INDEXED query on the `device` index — not a
|
|
15038
|
+
* read of the whole ring followed by a filter and a sort in JS.
|
|
15109
15039
|
*/
|
|
15110
15040
|
async function listOpsLog(state, query) {
|
|
15111
|
-
return (await state.
|
|
15041
|
+
return (await state.query({
|
|
15112
15042
|
...query.deviceId === void 0 ? {} : { where: { deviceId: query.deviceId } },
|
|
15113
|
-
|
|
15114
|
-
field: "at",
|
|
15115
|
-
direction: "desc"
|
|
15116
|
-
},
|
|
15043
|
+
desc: true,
|
|
15117
15044
|
limit: query.limit ?? 200
|
|
15118
|
-
})).map((
|
|
15045
|
+
})).records.map((r) => r.data);
|
|
15119
15046
|
}
|
|
15120
15047
|
/**
|
|
15121
15048
|
* Build the recorder's ops-log sink. `append` stamps `domain:'recording'`,
|
|
@@ -23573,7 +23500,6 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
23573
23500
|
readCensus = new StoreReadCensus();
|
|
23574
23501
|
/** Memoised so every `configStore()` / `exportsStore()` / `opsLogStore()`
|
|
23575
23502
|
* call shares ONE census instead of each wrapping the raw client afresh. */
|
|
23576
|
-
censusedSettingsStoreClient = null;
|
|
23577
23503
|
constructor() {
|
|
23578
23504
|
super({ ...DEFAULT_CONFIG });
|
|
23579
23505
|
}
|
|
@@ -23610,6 +23536,9 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
23610
23536
|
} });
|
|
23611
23537
|
return { providers: [] };
|
|
23612
23538
|
}
|
|
23539
|
+
const records = this.ctx.records;
|
|
23540
|
+
if (records === void 0) throw new Error("recorder (recording node) needs ctx.records — recording node must be the hub");
|
|
23541
|
+
await records.declare(recorderRecordKinds);
|
|
23613
23542
|
await this.declareRowCollections();
|
|
23614
23543
|
this.segmentHours = new SegmentHourLedger({
|
|
23615
23544
|
store: this.ctx.api.settingsStore,
|
|
@@ -25430,26 +25359,16 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
25430
25359
|
} });
|
|
25431
25360
|
}
|
|
25432
25361
|
/**
|
|
25433
|
-
* Declare the
|
|
25434
|
-
* `
|
|
25435
|
-
*
|
|
25362
|
+
* Declare the remaining RowMapStore collection that replaced the
|
|
25363
|
+
* `recordingConfigs` blob key. Idempotent — a re-declaration of the same
|
|
25364
|
+
* shape is a no-op in the engine.
|
|
25365
|
+
*
|
|
25366
|
+
* `recorder:exports` and `recorder:ops-log` are now record kinds declared
|
|
25367
|
+
* via `ctx.records.declare(recorderRecordKinds)` at the top of
|
|
25368
|
+
* `onInitialize`.
|
|
25436
25369
|
*/
|
|
25437
25370
|
async declareRowCollections() {
|
|
25438
25371
|
await this.configStore().declare();
|
|
25439
|
-
await this.exportsStore().declare();
|
|
25440
|
-
await this.opsLogStore().declare();
|
|
25441
|
-
}
|
|
25442
|
-
/**
|
|
25443
|
-
* `ctx.api.settingsStore`, wrapped ONCE through {@link censusStoreClient}.
|
|
25444
|
-
*
|
|
25445
|
-
* LAZY (see `calendar` above): `ctx` is not assigned until `initialize()`
|
|
25446
|
-
* runs, so this cannot be a field initialiser. The wrap is permanent and
|
|
25447
|
-
* cheap when disarmed (one `get` trap and a comparison) — what is armed and
|
|
25448
|
-
* disarmed is `this.readCensus`, through `debug.storeCensus`, not this seam.
|
|
25449
|
-
*/
|
|
25450
|
-
censusedSettingsStore() {
|
|
25451
|
-
this.censusedSettingsStoreClient ??= censusStoreClient(this.ctx.api.settingsStore, this.readCensus);
|
|
25452
|
-
return this.censusedSettingsStoreClient;
|
|
25453
25372
|
}
|
|
25454
25373
|
configsState = null;
|
|
25455
25374
|
/** A camera's recording config — its per-device settings (D865), migrated on read. */
|
|
@@ -25462,14 +25381,23 @@ var RecorderV2Addon = class extends BaseAddon {
|
|
|
25462
25381
|
});
|
|
25463
25382
|
return this.configsState;
|
|
25464
25383
|
}
|
|
25465
|
-
/**
|
|
25466
|
-
* wrapper as the config store). */
|
|
25384
|
+
/** Record kind handle over the `recorder` / `exports` kind (hub recording node only). */
|
|
25467
25385
|
exportsStore() {
|
|
25468
|
-
return recordingExportsState(this.
|
|
25386
|
+
return recordingExportsState(this.requireRecords());
|
|
25469
25387
|
}
|
|
25470
|
-
/**
|
|
25388
|
+
/** Record kind handle over the `recorder` / `ops-log` kind (hub recording node only). */
|
|
25471
25389
|
opsLogStore() {
|
|
25472
|
-
return recordingOpsLogState(this.
|
|
25390
|
+
return recordingOpsLogState(this.requireRecords());
|
|
25391
|
+
}
|
|
25392
|
+
/**
|
|
25393
|
+
* Returns `ctx.records`, throwing clearly when it is absent.
|
|
25394
|
+
* Only called on the designated recording node (hub by default); an agent
|
|
25395
|
+
* exits before `onInitialize` reaches any store call.
|
|
25396
|
+
*/
|
|
25397
|
+
requireRecords() {
|
|
25398
|
+
const records = this.ctx.records;
|
|
25399
|
+
if (records === void 0) throw new Error("recorder (recording node) needs ctx.records — recording node must be the hub");
|
|
25400
|
+
return records;
|
|
25473
25401
|
}
|
|
25474
25402
|
/** Durable handle over the recorder-owned placement blob (assignments + volume ids). */
|
|
25475
25403
|
placementStore() {
|