@camstack/system 1.2.476 → 1.2.478
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/assets/display-palette/icon.svg +7 -0
- package/dist/{agent-https-vpf_D97z.mjs → agent-https-CXMsY05a.mjs} +1 -1
- package/dist/{agent-https-DkXBXEvz.js → agent-https-D6a1IRPK.js} +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
- package/dist/builtins/calendars/calendars.addon.js +1 -1
- package/dist/builtins/calendars/calendars.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/composer/composer-runtime-factory.d.ts +2 -1
- package/dist/builtins/composer/composer.addon.js +453 -6
- package/dist/builtins/composer/composer.addon.mjs +453 -6
- package/dist/builtins/composer/composer.d.ts +6 -0
- package/dist/builtins/composer/composition-runtime.d.ts +3 -0
- package/dist/builtins/composer/field-record.d.ts +7 -0
- package/dist/builtins/composer/pulse-seed-resolve.d.ts +59 -0
- package/dist/builtins/composer/pulse-seed.d.ts +75 -0
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
- package/dist/builtins/device-history/device-history.addon.js +63 -953
- package/dist/builtins/device-history/device-history.addon.mjs +2 -892
- package/dist/builtins/device-manager/device-manager.addon.js +1 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
- package/dist/builtins/display-palette/display-palette.addon.d.ts +12 -0
- package/dist/builtins/display-palette/display-palette.addon.js +85 -0
- package/dist/builtins/display-palette/display-palette.addon.mjs +79 -0
- package/dist/builtins/display-palette/index.d.ts +1 -0
- package/dist/builtins/display-palette/index.js +8 -0
- package/dist/builtins/display-palette/index.mjs +2 -0
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/keychain/index.js +3 -3
- package/dist/builtins/keychain/index.mjs +3 -3
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +2 -2
- package/dist/builtins/local-auth/local-auth.addon.mjs +2 -2
- package/dist/builtins/local-network/local-network.addon.js +2 -2
- package/dist/builtins/local-network/local-network.addon.mjs +2 -2
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/record-entries/index.js +1 -1
- package/dist/builtins/record-entries/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/index.js +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +3 -3
- package/dist/builtins/system-config/system-config.addon.mjs +3 -3
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{composition-sources-B5nK7pHt.mjs → composition-sources-CQcYYZxL.mjs} +1 -1
- package/dist/{composition-sources-BNOP9mFp.js → composition-sources-yfxjp3qV.js} +1 -1
- package/dist/{constants-D4VigrMM.js → constants-D--JWd55.js} +1 -1
- package/dist/{constants-jbhoFV7w.mjs → constants-lWkcQxVE.mjs} +1 -1
- package/dist/{device-ca-DhyQeXsa.js → device-ca-D0KnLHMw.js} +1 -1
- package/dist/{device-ca-CvBZeyVX.mjs → device-ca-J9ZsyoFw.mjs} +1 -1
- package/dist/{dist-B_V9r61Z.mjs → dist-OC-L50JY.mjs} +751 -70
- package/dist/{dist-CNU7cdFf.js → dist-iVUkxljP.js} +792 -69
- package/dist/file-host-worker.js +1 -1
- package/dist/file-host-worker.mjs +1 -1
- package/dist/{hub-mdns-ciao-DFidf52M.js → hub-mdns-ciao-BDKiKV75.js} +2 -2
- package/dist/{hub-mdns-ciao-BuUg0gpI.mjs → hub-mdns-ciao-CvXWbJub.mjs} +2 -2
- package/dist/index.js +7 -7
- package/dist/index.mjs +7 -7
- package/dist/{restore-archive-CDo4Zlmx.mjs → restore-archive-D1Uzojhc.mjs} +1 -1
- package/dist/{restore-archive-BtCheeWu.js → restore-archive-DxkdJpWd.js} +1 -1
- package/dist/retention-B3z_lBP1.js +1024 -0
- package/dist/retention-DdVnmXXS.mjs +893 -0
- package/dist/{wal-maintenance-DItlr0Yu.mjs → wal-maintenance-DuO0D2H3.mjs} +0 -0
- package/dist/{wal-maintenance-B7xBeIFi.js → wal-maintenance-o9TyZeYp.js} +0 -0
- package/package.json +19 -1
|
@@ -3,898 +3,8 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Bucket arithmetic for the three rollup tiers (D850, plan §4/§5.5).
|
|
10
|
-
*
|
|
11
|
-
* 5m and 1h buckets are aligned to UTC; a 1d bucket starts at HUB-LOCAL
|
|
12
|
-
* midnight, so a day is 23, 24 or 25 hours long across a DST change (operator
|
|
13
|
-
* answer Q4). Pure: the zone is the process zone (`TZ`), nothing is read from
|
|
14
|
-
* the environment here.
|
|
15
|
-
*/
|
|
16
|
-
var FIVE_MINUTES_MS = 5 * 6e4;
|
|
17
|
-
var HOUR_MS = 60 * 6e4;
|
|
18
|
-
var DAY_MS = 24 * HOUR_MS;
|
|
19
|
-
function floorTo(ts, widthMs) {
|
|
20
|
-
return Math.floor(ts / widthMs) * widthMs;
|
|
21
|
-
}
|
|
22
|
-
/** Hub-local midnight at or before `ts`. */
|
|
23
|
-
function startOfLocalDay(ts) {
|
|
24
|
-
const d = new Date(ts);
|
|
25
|
-
d.setHours(0, 0, 0, 0);
|
|
26
|
-
return d.getTime();
|
|
27
|
-
}
|
|
28
|
-
/** The local midnight following the local midnight `dayStart`. */
|
|
29
|
-
function nextLocalDayStart(dayStart) {
|
|
30
|
-
const d = new Date(dayStart);
|
|
31
|
-
d.setDate(d.getDate() + 1);
|
|
32
|
-
d.setHours(0, 0, 0, 0);
|
|
33
|
-
return d.getTime();
|
|
34
|
-
}
|
|
35
|
-
/** Start of the `tier` bucket containing `ts`. */
|
|
36
|
-
function bucketStartOf(tier, ts) {
|
|
37
|
-
if (tier === "5m") return floorTo(ts, FIVE_MINUTES_MS);
|
|
38
|
-
if (tier === "1h") return floorTo(ts, HOUR_MS);
|
|
39
|
-
return startOfLocalDay(ts);
|
|
40
|
-
}
|
|
41
|
-
/** Exclusive end of the `tier` bucket that starts at `start`. */
|
|
42
|
-
function bucketEndOf(tier, start) {
|
|
43
|
-
if (tier === "5m") return start + FIVE_MINUTES_MS;
|
|
44
|
-
if (tier === "1h") return start + HOUR_MS;
|
|
45
|
-
return nextLocalDayStart(start);
|
|
46
|
-
}
|
|
47
|
-
/** Quiet time after a bucket closes before it is rolled up (late writers). */
|
|
48
|
-
var ROLLUP_GRACE_MS = {
|
|
49
|
-
"5m": 3e4,
|
|
50
|
-
"1h": 5 * 6e4,
|
|
51
|
-
"1d": 15 * 6e4
|
|
52
|
-
};
|
|
53
|
-
//#endregion
|
|
54
|
-
//#region src/builtins/device-history/retention/departure-tracker.ts
|
|
55
|
-
var DEPARTED_ABSENCE_MS = 7 * DAY_MS;
|
|
56
|
-
var DepartureTracker = class {
|
|
57
|
-
store;
|
|
58
|
-
registry;
|
|
59
|
-
logger;
|
|
60
|
-
missing = /* @__PURE__ */ new Map();
|
|
61
|
-
constructor(store, registry, logger) {
|
|
62
|
-
this.store = store;
|
|
63
|
-
this.registry = registry;
|
|
64
|
-
this.logger = logger;
|
|
65
|
-
}
|
|
66
|
-
async load() {
|
|
67
|
-
this.missing = await this.store.loadDepartures();
|
|
68
|
-
}
|
|
69
|
-
missingSince(deviceId) {
|
|
70
|
-
return this.missing.get(deviceId);
|
|
71
|
-
}
|
|
72
|
-
/** Forget a device's clock — its series were removed because it was DELETED (D865). */
|
|
73
|
-
async forget(deviceId) {
|
|
74
|
-
await this.store.clearMissing([deviceId]);
|
|
75
|
-
const next = new Map(this.missing);
|
|
76
|
-
next.delete(deviceId);
|
|
77
|
-
this.missing = next;
|
|
78
|
-
}
|
|
79
|
-
/** Feed one SUCCESSFUL listing. A failed read never reaches here. */
|
|
80
|
-
async observe(listed, now) {
|
|
81
|
-
const withSeries = new Set(this.registry.all().flatMap((s) => s.deviceId === null ? [] : [s.deviceId]));
|
|
82
|
-
const back = [...this.missing.keys()].filter((id) => listed.has(id));
|
|
83
|
-
const gone = [...withSeries].filter((id) => !listed.has(id) && !this.missing.has(id));
|
|
84
|
-
try {
|
|
85
|
-
await this.store.clearMissing(back);
|
|
86
|
-
await this.store.markMissing(gone, now);
|
|
87
|
-
} catch (err) {
|
|
88
|
-
this.logger.warn("device-history: departure clock not written", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
const next = new Map(this.missing);
|
|
92
|
-
for (const id of back) next.delete(id);
|
|
93
|
-
for (const id of gone) next.set(id, now);
|
|
94
|
-
this.missing = next;
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
//#endregion
|
|
98
|
-
//#region src/builtins/device-history/retention/retention.ts
|
|
99
|
-
var DEFAULT_RETENTION = {
|
|
100
|
-
rawDays: 7,
|
|
101
|
-
fiveMinuteDays: 30,
|
|
102
|
-
hourDays: 365,
|
|
103
|
-
dayDays: null
|
|
104
|
-
};
|
|
105
|
-
/** Cutoff per tier: rows strictly older are deleted. `null` = keep forever. */
|
|
106
|
-
function retentionCutoffs(policy, now) {
|
|
107
|
-
return {
|
|
108
|
-
raw: now - policy.rawDays * DAY_MS,
|
|
109
|
-
"5m": now - policy.fiveMinuteDays * DAY_MS,
|
|
110
|
-
"1h": now - policy.hourDays * DAY_MS,
|
|
111
|
-
"1d": policy.dayDays === null || policy.dayDays <= 0 ? null : now - policy.dayDays * DAY_MS
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
var TIERS = [
|
|
115
|
-
"raw",
|
|
116
|
-
"5m",
|
|
117
|
-
"1h",
|
|
118
|
-
"1d"
|
|
119
|
-
];
|
|
120
|
-
var RETENTION_STEP_MS = {
|
|
121
|
-
raw: HOUR_MS,
|
|
122
|
-
"5m": HOUR_MS,
|
|
123
|
-
"1h": DAY_MS,
|
|
124
|
-
"1d": DAY_MS
|
|
125
|
-
};
|
|
126
|
-
var MAX_PURGES_PER_RUN = 20;
|
|
127
|
-
/** Width of one delete statement's time range, per tier. */
|
|
128
|
-
var SLICE_MS = {
|
|
129
|
-
raw: DAY_MS,
|
|
130
|
-
"5m": DAY_MS,
|
|
131
|
-
"1h": 30 * DAY_MS,
|
|
132
|
-
"1d": 30 * DAY_MS
|
|
133
|
-
};
|
|
134
|
-
/** A bucket's start may precede the series' first point by up to a day. */
|
|
135
|
-
var BUCKET_LEAD_MS = 2 * DAY_MS;
|
|
136
|
-
/** The short tiers hold nothing older than this unless the operator configured more. */
|
|
137
|
-
var SHORT_TIER_LOOKBACK_MS = 400 * DAY_MS;
|
|
138
|
-
var yieldToEventLoop = () => new Promise((resolve) => setImmediate(resolve));
|
|
139
|
-
var RetentionJob = class {
|
|
140
|
-
deps;
|
|
141
|
-
appliedUpTo = /* @__PURE__ */ new Map();
|
|
142
|
-
running = null;
|
|
143
|
-
constructor(deps) {
|
|
144
|
-
this.deps = deps;
|
|
145
|
-
}
|
|
146
|
-
run(policy, now) {
|
|
147
|
-
if (this.running !== null) return this.running;
|
|
148
|
-
const run = this.doRun(policy, now).finally(() => {
|
|
149
|
-
this.running = null;
|
|
150
|
-
});
|
|
151
|
-
this.running = run;
|
|
152
|
-
return run;
|
|
153
|
-
}
|
|
154
|
-
async doRun(policy, now) {
|
|
155
|
-
const { registry, counters, logger } = this.deps;
|
|
156
|
-
const cutoffs = retentionCutoffs(policy, now);
|
|
157
|
-
try {
|
|
158
|
-
for (const tier of TIERS) {
|
|
159
|
-
const cutoff = cutoffs[tier];
|
|
160
|
-
if (cutoff === null) continue;
|
|
161
|
-
for (const series of registry.all()) {
|
|
162
|
-
if (series.firstTs - 864e5 >= cutoff) continue;
|
|
163
|
-
if (tier === "raw" && registry.metric(series.key)?.ingest === "buckets") continue;
|
|
164
|
-
const marker = `${tier}:${series.id}`;
|
|
165
|
-
const applied = this.appliedUpTo.get(marker);
|
|
166
|
-
if (applied !== void 0 && cutoff - applied < RETENTION_STEP_MS[tier]) continue;
|
|
167
|
-
const lo = Math.max(0, applied === void 0 ? series.firstTs - BUCKET_LEAD_MS : applied - RETENTION_STEP_MS[tier]);
|
|
168
|
-
counters.addDeleted(tier, await this.deleteSliced(tier, series.id, lo, cutoff - 1));
|
|
169
|
-
this.appliedUpTo.set(marker, cutoff);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
await this.purgeDeparted(now);
|
|
173
|
-
counters.retentionLastRunAt = now;
|
|
174
|
-
} catch (err) {
|
|
175
|
-
logger.warn("device-history: retention pass failed — retried at the next run", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
/** Delete `[lo, hiInclusive]` of one tier in time slices, yielding between them. */
|
|
179
|
-
/** Drop the visit markers of a purged series. */
|
|
180
|
-
forgetSeries(seriesId) {
|
|
181
|
-
for (const tier of TIERS) this.appliedUpTo.delete(`${tier}:${seriesId}`);
|
|
182
|
-
}
|
|
183
|
-
async deleteSliced(tier, seriesId, lo, hiInclusive) {
|
|
184
|
-
const yieldNow = this.deps.yieldNow ?? yieldToEventLoop;
|
|
185
|
-
let deleted = 0;
|
|
186
|
-
for (let from = lo; from <= hiInclusive; from += SLICE_MS[tier]) {
|
|
187
|
-
const to = Math.min(from + SLICE_MS[tier] - 1, hiInclusive);
|
|
188
|
-
deleted += await this.deps.store.deleteRange(tier, seriesId, from, to);
|
|
189
|
-
await yieldNow();
|
|
190
|
-
}
|
|
191
|
-
return deleted;
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Tier by tier in slices, then the series row (and whatever the slices missed).
|
|
195
|
-
* Answers the rows the slices deleted per tier. The caller removes the series
|
|
196
|
-
* from the registry, which releases its per-series state.
|
|
197
|
-
*/
|
|
198
|
-
async purgeSeries(series, now) {
|
|
199
|
-
const deleted = {
|
|
200
|
-
raw: 0,
|
|
201
|
-
"5m": 0,
|
|
202
|
-
"1h": 0,
|
|
203
|
-
"1d": 0
|
|
204
|
-
};
|
|
205
|
-
for (const tier of TIERS) {
|
|
206
|
-
const lo = tier === "raw" || tier === "5m" ? Math.max(series.firstTs - BUCKET_LEAD_MS, now - SHORT_TIER_LOOKBACK_MS) : series.firstTs - BUCKET_LEAD_MS;
|
|
207
|
-
deleted[tier] = await this.deleteSliced(tier, series.id, Math.max(0, lo), now + DAY_MS);
|
|
208
|
-
this.deps.counters.addDeleted(tier, deleted[tier]);
|
|
209
|
-
}
|
|
210
|
-
await this.deps.store.deleteSeries(series.id);
|
|
211
|
-
return deleted;
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* Delete every series of a DELETED device now (D865). Driven by a tombstone
|
|
215
|
-
* through the `device-removal-hook`, never by a listing: the week-long
|
|
216
|
-
* absence clock above remains the backstop for a device removed before
|
|
217
|
-
* tombstones existed.
|
|
218
|
-
*/
|
|
219
|
-
async purgeDevice(deviceId, now) {
|
|
220
|
-
const { registry, counters } = this.deps;
|
|
221
|
-
let purged = 0;
|
|
222
|
-
for (const series of registry.all()) {
|
|
223
|
-
if (series.deviceId !== deviceId) continue;
|
|
224
|
-
await this.purgeSeries(series, now);
|
|
225
|
-
registry.remove(series.id);
|
|
226
|
-
counters.purgedSeries += 1;
|
|
227
|
-
purged += 1;
|
|
228
|
-
}
|
|
229
|
-
return purged;
|
|
230
|
-
}
|
|
231
|
-
async purgeDeparted(now) {
|
|
232
|
-
const { registry, departures, counters, logger } = this.deps;
|
|
233
|
-
let purged = 0;
|
|
234
|
-
for (const series of registry.all()) {
|
|
235
|
-
if (purged >= MAX_PURGES_PER_RUN) break;
|
|
236
|
-
if (series.deviceId === null) continue;
|
|
237
|
-
const since = departures.missingSince(series.deviceId);
|
|
238
|
-
if (since === void 0 || now - since < DEPARTED_ABSENCE_MS) continue;
|
|
239
|
-
await this.purgeSeries(series, now);
|
|
240
|
-
registry.remove(series.id);
|
|
241
|
-
counters.purgedSeries += 1;
|
|
242
|
-
purged += 1;
|
|
243
|
-
logger.info("device-history: purged the series of an unregistered device", {
|
|
244
|
-
tags: { deviceId: series.deviceId },
|
|
245
|
-
meta: { key: series.key }
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
//#endregion
|
|
251
|
-
//#region src/builtins/device-history/store/series-registry.ts
|
|
252
|
-
function subjectIdOf(subject) {
|
|
253
|
-
return subject.kind === "device" ? `d:${subject.deviceId}` : `n:${subject.nodeId}`;
|
|
254
|
-
}
|
|
255
|
-
function deviceIdOf(subject) {
|
|
256
|
-
return subject.kind === "device" ? subject.deviceId : null;
|
|
257
|
-
}
|
|
258
|
-
/** Every metric key is a cap field (`cap:`), an addon's series (`addon:`) or derived. */
|
|
259
|
-
function isMetricKey(key) {
|
|
260
|
-
return key.startsWith("cap:") || key.startsWith("addon:");
|
|
261
|
-
}
|
|
262
|
-
function identityOf(key, subjectId, scope) {
|
|
263
|
-
return `${key}\u0000${subjectId}\u0000${scope}`;
|
|
264
|
-
}
|
|
265
|
-
var LOAD_PAGE = 5e3;
|
|
266
|
-
/** Append what `next` names that `stored` lacks, in `next`'s order: an ordinal never moves. */
|
|
267
|
-
function mergeEnumValues(stored, next) {
|
|
268
|
-
if (stored === null) return next;
|
|
269
|
-
if (next === null) return stored;
|
|
270
|
-
const missing = next.filter((v) => !stored.includes(v));
|
|
271
|
-
return missing.length === 0 ? stored : [...stored, ...missing];
|
|
272
|
-
}
|
|
273
|
-
var SeriesRegistry = class {
|
|
274
|
-
store;
|
|
275
|
-
metrics = /* @__PURE__ */ new Map();
|
|
276
|
-
byIdentity = /* @__PURE__ */ new Map();
|
|
277
|
-
byId = /* @__PURE__ */ new Map();
|
|
278
|
-
inflight = /* @__PURE__ */ new Map();
|
|
279
|
-
/** Enum metrics whose value list grew in memory and is not stored yet. */
|
|
280
|
-
dirtyMetrics = /* @__PURE__ */ new Set();
|
|
281
|
-
removeListeners = [];
|
|
282
|
-
constructor(store) {
|
|
283
|
-
this.store = store;
|
|
284
|
-
}
|
|
285
|
-
async load() {
|
|
286
|
-
this.metrics.clear();
|
|
287
|
-
this.byIdentity.clear();
|
|
288
|
-
this.byId.clear();
|
|
289
|
-
const stray = [];
|
|
290
|
-
for (const m of await this.store.loadMetrics()) if (isMetricKey(m.key)) this.metrics.set(m.key, m);
|
|
291
|
-
else stray.push(m.key);
|
|
292
|
-
if (stray.length > 0) await this.store.deleteMetrics(stray);
|
|
293
|
-
let afterId = 0;
|
|
294
|
-
for (;;) {
|
|
295
|
-
const page = await this.store.loadSeriesPage(afterId, LOAD_PAGE);
|
|
296
|
-
for (const s of page) this.index(s);
|
|
297
|
-
const last = page[page.length - 1];
|
|
298
|
-
if (last === void 0 || page.length < LOAD_PAGE) break;
|
|
299
|
-
afterId = last.id;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
index(series) {
|
|
303
|
-
this.byId.set(series.id, series);
|
|
304
|
-
this.byIdentity.set(identityOf(series.key, series.subject, series.scope), series);
|
|
305
|
-
}
|
|
306
|
-
metric(key) {
|
|
307
|
-
return this.metrics.get(key);
|
|
308
|
-
}
|
|
309
|
-
allMetrics() {
|
|
310
|
-
return [...this.metrics.values()];
|
|
311
|
-
}
|
|
312
|
-
/**
|
|
313
|
-
* Persist and remember a metric; idempotent. An `enum` value list is MERGED
|
|
314
|
-
* append-only with the stored one, so reordering a `z.enum` never changes
|
|
315
|
-
* what a stored ordinal means.
|
|
316
|
-
*/
|
|
317
|
-
async declareMetric(metric) {
|
|
318
|
-
const existing = this.metrics.get(metric.key);
|
|
319
|
-
const merged = {
|
|
320
|
-
...metric,
|
|
321
|
-
values: mergeEnumValues(existing?.values ?? null, metric.values)
|
|
322
|
-
};
|
|
323
|
-
if (existing !== void 0 && sameMetric(existing, merged)) return;
|
|
324
|
-
await this.store.upsertMetric(merged);
|
|
325
|
-
this.metrics.set(metric.key, merged);
|
|
326
|
-
}
|
|
327
|
-
/**
|
|
328
|
-
* The ordinal of `value` in an `enum` metric's list, learning (appending) a
|
|
329
|
-
* value seen for the first time. `null` when the key is not an enum metric or
|
|
330
|
-
* its list is full ({@link MAX_ENUM_VALUES}). A learned value is stored by
|
|
331
|
-
* {@link persistDirtyMetrics} BEFORE any point that uses it.
|
|
332
|
-
*/
|
|
333
|
-
ordinalOf(key, value) {
|
|
334
|
-
const metric = this.metrics.get(key);
|
|
335
|
-
if (metric === void 0 || metric.kind !== "enum") return null;
|
|
336
|
-
const values = metric.values ?? [];
|
|
337
|
-
const known = values.indexOf(value);
|
|
338
|
-
if (known >= 0) return known;
|
|
339
|
-
if (values.length >= 1024) return null;
|
|
340
|
-
this.metrics.set(key, {
|
|
341
|
-
...metric,
|
|
342
|
-
values: [...values, value]
|
|
343
|
-
});
|
|
344
|
-
this.dirtyMetrics.add(key);
|
|
345
|
-
return values.length;
|
|
346
|
-
}
|
|
347
|
-
/** Store every value list that grew in memory. Throws (and keeps them dirty) on a failed write. */
|
|
348
|
-
async persistDirtyMetrics() {
|
|
349
|
-
for (const key of this.dirtyMetrics) {
|
|
350
|
-
const metric = this.metrics.get(key);
|
|
351
|
-
if (metric !== void 0) await this.store.upsertMetric(metric);
|
|
352
|
-
this.dirtyMetrics.delete(key);
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
find(key, subjectId, scope) {
|
|
356
|
-
return this.byIdentity.get(identityOf(key, subjectId, scope));
|
|
357
|
-
}
|
|
358
|
-
byIdOf(id) {
|
|
359
|
-
return this.byId.get(id);
|
|
360
|
-
}
|
|
361
|
-
all() {
|
|
362
|
-
return [...this.byId.values()];
|
|
363
|
-
}
|
|
364
|
-
forSubject(subjectId) {
|
|
365
|
-
return this.all().filter((s) => s.subject === subjectId);
|
|
366
|
-
}
|
|
367
|
-
forKeyAndSubject(key, subjectId) {
|
|
368
|
-
return this.all().filter((s) => s.key === key && s.subject === subjectId);
|
|
369
|
-
}
|
|
370
|
-
/** The series, created on first sight. `ts` stamps `first_ts`/`last_ts` of a new one. */
|
|
371
|
-
async ensure(key, subject, scope, ts) {
|
|
372
|
-
const subjectId = subjectIdOf(subject);
|
|
373
|
-
const identity = identityOf(key, subjectId, scope);
|
|
374
|
-
const known = this.byIdentity.get(identity);
|
|
375
|
-
if (known !== void 0) return known;
|
|
376
|
-
const pending = this.inflight.get(identity);
|
|
377
|
-
if (pending !== void 0) return pending;
|
|
378
|
-
const created = this.create(key, subjectId, deviceIdOf(subject), scope, ts).finally(() => {
|
|
379
|
-
this.inflight.delete(identity);
|
|
380
|
-
});
|
|
381
|
-
this.inflight.set(identity, created);
|
|
382
|
-
return created;
|
|
383
|
-
}
|
|
384
|
-
async create(key, subjectId, deviceId, scope, ts) {
|
|
385
|
-
const stored = await this.store.findSeries(key, subjectId, scope) ?? await this.store.insertSeries({
|
|
386
|
-
key,
|
|
387
|
-
subject: subjectId,
|
|
388
|
-
deviceId,
|
|
389
|
-
scope,
|
|
390
|
-
ts
|
|
391
|
-
});
|
|
392
|
-
this.index(stored);
|
|
393
|
-
return stored;
|
|
394
|
-
}
|
|
395
|
-
/** Remember a widened span (the store write is the caller's). */
|
|
396
|
-
noteSpan(id, firstTs, lastTs) {
|
|
397
|
-
const current = this.byId.get(id);
|
|
398
|
-
if (current === void 0) return;
|
|
399
|
-
this.index({
|
|
400
|
-
...current,
|
|
401
|
-
firstTs: Math.min(current.firstTs, firstTs),
|
|
402
|
-
lastTs: Math.max(current.lastTs, lastTs)
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
remove(id) {
|
|
406
|
-
const current = this.byId.get(id);
|
|
407
|
-
if (current === void 0) return;
|
|
408
|
-
this.byId.delete(id);
|
|
409
|
-
this.byIdentity.delete(identityOf(current.key, current.subject, current.scope));
|
|
410
|
-
for (const listener of this.removeListeners) listener(current);
|
|
411
|
-
}
|
|
412
|
-
/** Per-series state elsewhere (last recorded value, dirty marks, rate limits) is dropped through this. */
|
|
413
|
-
onRemove(listener) {
|
|
414
|
-
this.removeListeners.push(listener);
|
|
415
|
-
}
|
|
416
|
-
};
|
|
417
|
-
function sameMetric(a, b) {
|
|
418
|
-
return a.source === b.source && a.ownerAddon === b.ownerAddon && a.subjectKind === b.subjectKind && a.kind === b.kind && a.unit === b.unit && a.displayName === b.displayName && a.ingest === b.ingest && JSON.stringify(a.values) === JSON.stringify(b.values);
|
|
419
|
-
}
|
|
420
|
-
//#endregion
|
|
421
|
-
//#region src/builtins/device-history/capture/capture-filter.ts
|
|
422
|
-
/**
|
|
423
|
-
* The per-point capture decision for a cap field (D850, plan §5.3). PURE.
|
|
424
|
-
*
|
|
425
|
-
* A point is written only when the value CHANGED from the last one recorded,
|
|
426
|
-
* by more than the field's deadband, and no sooner than its capture floor:
|
|
427
|
-
*
|
|
428
|
-
* - `unchanged` — the same value again; a change-only series stores nothing;
|
|
429
|
-
* - `deadband` — a change smaller than `deadband` (absolute) or
|
|
430
|
-
* `deadbandPct` (relative to the last recorded value); a slow
|
|
431
|
-
* drift is still caught, because the comparison is against
|
|
432
|
-
* the last RECORDED value, not the last seen one;
|
|
433
|
-
* - `defer` — changed, but inside `minIntervalMs` of the last recorded
|
|
434
|
-
* point. NOT dropped: the caller keeps it pending and emits it
|
|
435
|
-
* once the floor has passed, so a burst never loses its end.
|
|
436
|
-
*/
|
|
437
|
-
/** Tolerance for floating-point steps such as 21.3 - 21.2 against a 0.1 deadband. */
|
|
438
|
-
var DEADBAND_EPSILON = 1e-9;
|
|
439
|
-
/** The capture floor of a field: its own `minIntervalMs`, else the kind's default. */
|
|
440
|
-
function minIntervalOf(spec) {
|
|
441
|
-
if (spec.minIntervalMs !== void 0) return spec.minIntervalMs;
|
|
442
|
-
return spec.kind === "boolean" || spec.kind === "enum" ? 0 : require_dist.HISTORY_DEFAULT_MIN_INTERVAL_MS;
|
|
443
|
-
}
|
|
444
|
-
function clearsDeadband(spec, last, value) {
|
|
445
|
-
if (spec.kind === "boolean" || spec.kind === "enum") return true;
|
|
446
|
-
const delta = Math.abs(value - last);
|
|
447
|
-
if (spec.deadband !== void 0 && delta < spec.deadband - DEADBAND_EPSILON) return false;
|
|
448
|
-
if (spec.deadbandPct !== void 0) {
|
|
449
|
-
if (delta < Math.abs(last) * spec.deadbandPct / 100 - DEADBAND_EPSILON) return false;
|
|
450
|
-
}
|
|
451
|
-
return true;
|
|
452
|
-
}
|
|
453
|
-
function decideCapture(spec, recorded, value, now, options = {}) {
|
|
454
|
-
if (!Number.isFinite(value)) return {
|
|
455
|
-
action: "skip",
|
|
456
|
-
reason: "non-finite"
|
|
457
|
-
};
|
|
458
|
-
if (recorded === null) return { action: "emit" };
|
|
459
|
-
if (value === recorded.value) return {
|
|
460
|
-
action: "skip",
|
|
461
|
-
reason: "unchanged"
|
|
462
|
-
};
|
|
463
|
-
if (!clearsDeadband(spec, recorded.value, value)) return {
|
|
464
|
-
action: "skip",
|
|
465
|
-
reason: "deadband"
|
|
466
|
-
};
|
|
467
|
-
if (options.ignoreFloor !== true) {
|
|
468
|
-
const notBefore = recorded.ts + minIntervalOf(spec);
|
|
469
|
-
if (now < notBefore) return {
|
|
470
|
-
action: "defer",
|
|
471
|
-
notBefore
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
return { action: "emit" };
|
|
475
|
-
}
|
|
476
|
-
//#endregion
|
|
477
|
-
//#region src/builtins/device-history/capture/device-directory.ts
|
|
478
|
-
/**
|
|
479
|
-
* Which devices exist and what type they are — the "real devices only" gate
|
|
480
|
-
* (`hub` and `container` are not devices anyone charts) and the
|
|
481
|
-
* `excludeDeviceTypes` gate of a history field.
|
|
482
|
-
*
|
|
483
|
-
* Built from `deviceManager.listAll({ projection: 'slim' })` at boot and every
|
|
484
|
-
* reconcile, and per device (`getDevice`) the first time a state event names one
|
|
485
|
-
* the map does not know. A FAILED read changes nothing (D49): the previous map
|
|
486
|
-
* stays, and the failure is counted and logged, never read as "no devices".
|
|
487
|
-
*/
|
|
488
|
-
function isRealDeviceType(type) {
|
|
489
|
-
return type !== require_dist.DeviceType.Hub && type !== require_dist.DeviceType.Container;
|
|
490
|
-
}
|
|
491
|
-
var DeviceDirectory = class {
|
|
492
|
-
source;
|
|
493
|
-
counters;
|
|
494
|
-
logger;
|
|
495
|
-
clock;
|
|
496
|
-
types = /* @__PURE__ */ new Map();
|
|
497
|
-
inflight = /* @__PURE__ */ new Map();
|
|
498
|
-
refreshInflight = null;
|
|
499
|
-
lastFullRefreshAt = null;
|
|
500
|
-
constructor(source, counters, logger, clock) {
|
|
501
|
-
this.source = source;
|
|
502
|
-
this.counters = counters;
|
|
503
|
-
this.logger = logger;
|
|
504
|
-
this.clock = clock;
|
|
505
|
-
}
|
|
506
|
-
typeOf(deviceId) {
|
|
507
|
-
return this.types.get(deviceId);
|
|
508
|
-
}
|
|
509
|
-
size() {
|
|
510
|
-
return this.types.size;
|
|
511
|
-
}
|
|
512
|
-
/** When the full listing last ANSWERED; `null` before the first answer. */
|
|
513
|
-
lastRefreshAt() {
|
|
514
|
-
return this.lastFullRefreshAt;
|
|
515
|
-
}
|
|
516
|
-
/** Ids in the directory now (the last full listing plus per-device lookups). */
|
|
517
|
-
listedIds() {
|
|
518
|
-
return new Set(this.types.keys());
|
|
519
|
-
}
|
|
520
|
-
knows(deviceId) {
|
|
521
|
-
return this.types.has(deviceId);
|
|
522
|
-
}
|
|
523
|
-
/** Replace the map from the full listing. Returns false (map untouched) when the read failed. */
|
|
524
|
-
refresh() {
|
|
525
|
-
if (this.refreshInflight !== null) return this.refreshInflight;
|
|
526
|
-
const run = this.doRefresh().finally(() => {
|
|
527
|
-
this.refreshInflight = null;
|
|
528
|
-
});
|
|
529
|
-
this.refreshInflight = run;
|
|
530
|
-
return run;
|
|
531
|
-
}
|
|
532
|
-
async doRefresh() {
|
|
533
|
-
try {
|
|
534
|
-
const all = await this.source.listAll();
|
|
535
|
-
this.types = new Map(all.map((d) => [d.id, d.type]));
|
|
536
|
-
this.lastFullRefreshAt = this.clock();
|
|
537
|
-
this.counters.directoryRefreshAt = this.lastFullRefreshAt;
|
|
538
|
-
return true;
|
|
539
|
-
} catch (err) {
|
|
540
|
-
this.logger.warn("device directory: listing failed — keeping the previous map", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
541
|
-
return false;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
/** The device's type, asking the hub once when the map does not know it. */
|
|
545
|
-
ensure(deviceId) {
|
|
546
|
-
const known = this.types.get(deviceId);
|
|
547
|
-
if (known !== void 0) return Promise.resolve(known);
|
|
548
|
-
const pending = this.inflight.get(deviceId);
|
|
549
|
-
if (pending !== void 0) return pending;
|
|
550
|
-
const run = this.lookup(deviceId).finally(() => {
|
|
551
|
-
this.inflight.delete(deviceId);
|
|
552
|
-
});
|
|
553
|
-
this.inflight.set(deviceId, run);
|
|
554
|
-
return run;
|
|
555
|
-
}
|
|
556
|
-
async lookup(deviceId) {
|
|
557
|
-
try {
|
|
558
|
-
const device = await this.source.getDevice(deviceId);
|
|
559
|
-
if (device === null) return void 0;
|
|
560
|
-
this.types.set(device.id, device.type);
|
|
561
|
-
return device.type;
|
|
562
|
-
} catch (err) {
|
|
563
|
-
this.logger.warn("device directory: lookup failed", {
|
|
564
|
-
tags: { deviceId },
|
|
565
|
-
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
566
|
-
});
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
};
|
|
571
|
-
//#endregion
|
|
572
|
-
//#region src/builtins/device-history/capture/field-path.ts
|
|
573
|
-
/**
|
|
574
|
-
* Reading a history field out of a cap slice. A catalog path is a top-level
|
|
575
|
-
* `runtimeState` key or a path into a FIXED object (`dustBin.level`); the
|
|
576
|
-
* catalog guard keeps it out of arrays and records, so a plain walk is enough.
|
|
577
|
-
*/
|
|
578
|
-
function isRecord$2(value) {
|
|
579
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
580
|
-
}
|
|
581
|
-
/** The value at `path`, or `undefined` when any step is missing or not an object. */
|
|
582
|
-
function readPath(slice, path) {
|
|
583
|
-
let current = slice;
|
|
584
|
-
for (const step of path.split(".")) {
|
|
585
|
-
if (!isRecord$2(current)) return void 0;
|
|
586
|
-
current = current[step];
|
|
587
|
-
}
|
|
588
|
-
return current;
|
|
589
|
-
}
|
|
590
|
-
/**
|
|
591
|
-
* The number a field contributes: a finite number for a gauge/counter, 0/1 for
|
|
592
|
-
* a boolean field. Anything else (null, undefined, a string, NaN) is NOT a
|
|
593
|
-
* reading — `null` means unknown and must never become 0.
|
|
594
|
-
*/
|
|
595
|
-
function readNumeric(slice, path, kind) {
|
|
596
|
-
const raw = readPath(slice, path);
|
|
597
|
-
if (kind === "boolean") {
|
|
598
|
-
if (typeof raw === "boolean") return raw ? 1 : 0;
|
|
599
|
-
return null;
|
|
600
|
-
}
|
|
601
|
-
if (typeof raw === "number" && Number.isFinite(raw)) return raw;
|
|
602
|
-
return null;
|
|
603
|
-
}
|
|
604
|
-
/** A unit string a `unitFrom` sibling field states, or `null` when it states none. */
|
|
605
|
-
function readUnit(slice, unitFrom) {
|
|
606
|
-
const raw = readPath(slice, unitFrom);
|
|
607
|
-
return typeof raw === "string" && raw.length > 0 ? raw : null;
|
|
608
|
-
}
|
|
609
|
-
//#endregion
|
|
610
|
-
//#region src/builtins/device-history/capture/capture-engine.ts
|
|
611
|
-
/**
|
|
612
|
-
* Capture of capability fields (D850, plan §5.3), driven by the GENERATED
|
|
613
|
-
* history catalog — a field is recorded because its `*.cap.ts` says so, never
|
|
614
|
-
* because someone listed it here.
|
|
615
|
-
*
|
|
616
|
-
* For a `DeviceStateChanged { deviceId, capName, slice }` the engine:
|
|
617
|
-
* 1. ignores a cap with no catalog entry (the common case — cheap, uncounted);
|
|
618
|
-
* 2. skips an opted-out device (counted), a device whose type is not yet
|
|
619
|
-
* known (parked, then replayed once the hub answers), and a `hub` /
|
|
620
|
-
* `container` (counted: not a real device);
|
|
621
|
-
* 3. per field: skips an excluded device type, an absent or non-numeric value
|
|
622
|
-
* (null is UNKNOWN, never 0), then runs {@link decideCapture} against the
|
|
623
|
-
* last RECORDED value — change-only, deadband, capture floor;
|
|
624
|
-
* 4. buffers the point for the batched write.
|
|
625
|
-
*
|
|
626
|
-
* A change inside the capture floor is kept PENDING and emitted when the floor
|
|
627
|
-
* has passed ({@link CaptureEngine.tickPending}), so the end of a burst is never
|
|
628
|
-
* lost. Reconcile ({@link CaptureEngine.reconcile}) restores what a dropped
|
|
629
|
-
* event missed from the state mirror's snapshot.
|
|
630
|
-
*/
|
|
631
|
-
var CAP_KEY_PREFIX = "cap:";
|
|
632
|
-
function capSeriesKey(capName, fieldPath) {
|
|
633
|
-
return `${CAP_KEY_PREFIX}${capName}.${fieldPath}`;
|
|
634
|
-
}
|
|
635
|
-
/** The series key of an item field: `cap:<capName>.<path>[].<field>`. */
|
|
636
|
-
function capItemSeriesKey(capName, items, field) {
|
|
637
|
-
return capSeriesKey(capName, require_dist.historyItemFieldPath(items.path, field));
|
|
638
|
-
}
|
|
639
|
-
function humanize(capName, fieldPath) {
|
|
640
|
-
const field = fieldPath.replace(/([A-Z])/g, " $1").replace(/\./g, " ").toLowerCase();
|
|
641
|
-
return `${capName.replace(/-/g, " ")} ${field}`;
|
|
642
|
-
}
|
|
643
|
-
function metricOf(key, capName, path, spec, now) {
|
|
644
|
-
return {
|
|
645
|
-
key,
|
|
646
|
-
source: "cap",
|
|
647
|
-
ownerAddon: "device-history",
|
|
648
|
-
subjectKind: "device",
|
|
649
|
-
kind: spec.kind,
|
|
650
|
-
unit: spec.unit ?? null,
|
|
651
|
-
displayName: spec.displayName ?? humanize(capName, path),
|
|
652
|
-
ingest: "points",
|
|
653
|
-
declaredAt: now,
|
|
654
|
-
values: spec.kind === "enum" ? spec.values ?? [] : null
|
|
655
|
-
};
|
|
656
|
-
}
|
|
657
|
-
/** The metric rows of every catalog field and keyed-item field (owner `device-history`). */
|
|
658
|
-
function catalogMetrics(catalog, itemCatalog, now) {
|
|
659
|
-
const fields = Object.entries(catalog).flatMap(([capName, specs]) => Object.entries(specs).map(([path, spec]) => metricOf(capSeriesKey(capName, path), capName, path, spec, now)));
|
|
660
|
-
const items = Object.entries(itemCatalog).flatMap(([capName, block]) => Object.entries(block.fields).map(([field, spec]) => metricOf(capItemSeriesKey(capName, block, field), capName, field, spec, now)));
|
|
661
|
-
return [...fields, ...items];
|
|
662
|
-
}
|
|
663
|
-
function isRecord$1(value) {
|
|
664
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
665
|
-
}
|
|
666
|
-
/** Devices waiting on a type lookup; beyond this the oldest is dropped (and counted). */
|
|
667
|
-
var MAX_PARKED_DEVICES = 5e3;
|
|
668
|
-
/** Seeding reads the newest raw row of each cap series, this many at once. */
|
|
669
|
-
var SEED_CONCURRENCY = 8;
|
|
670
|
-
var CaptureEngine = class {
|
|
671
|
-
deps;
|
|
672
|
-
recorded = /* @__PURE__ */ new Map();
|
|
673
|
-
pending = /* @__PURE__ */ new Map();
|
|
674
|
-
parked = /* @__PURE__ */ new Map();
|
|
675
|
-
constructor(deps) {
|
|
676
|
-
this.deps = deps;
|
|
677
|
-
}
|
|
678
|
-
/** Learn the last recorded value of every cap series from the store, so a restart repeats nothing. */
|
|
679
|
-
async seed() {
|
|
680
|
-
const { registry, store } = this.deps;
|
|
681
|
-
const capSeries = registry.all().filter((s) => s.key.startsWith(CAP_KEY_PREFIX));
|
|
682
|
-
for (let i = 0; i < capSeries.length; i += SEED_CONCURRENCY) await Promise.all(capSeries.slice(i, i + SEED_CONCURRENCY).map(async (series) => {
|
|
683
|
-
const newest = await store.rawBefore(series.id, Number.MAX_SAFE_INTEGER);
|
|
684
|
-
if (newest === null) return;
|
|
685
|
-
this.recorded.set(identityOf(series.key, series.subject, series.scope), newest);
|
|
686
|
-
}));
|
|
687
|
-
}
|
|
688
|
-
/** One `DeviceStateChanged`. Synchronous: the bus must never wait on us. */
|
|
689
|
-
handleState(deviceId, capName, slice, at) {
|
|
690
|
-
const fields = this.deps.catalog[capName];
|
|
691
|
-
const items = this.deps.itemCatalog[capName];
|
|
692
|
-
if (fields === void 0 && items === void 0) return;
|
|
693
|
-
const { counters, directory } = this.deps;
|
|
694
|
-
counters.eventsSeen += 1;
|
|
695
|
-
if (this.deps.optedOut().has(deviceId)) {
|
|
696
|
-
counters.skip("opted-out");
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
const type = directory.typeOf(deviceId);
|
|
700
|
-
if (type === void 0) {
|
|
701
|
-
this.park(deviceId, capName, slice, at);
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
704
|
-
if (!isRealDeviceType(type)) {
|
|
705
|
-
counters.skip("not-a-real-device");
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
this.captureSlice(deviceId, type, capName, slice, at, false);
|
|
709
|
-
}
|
|
710
|
-
/** Every catalogued field and keyed item of one cap slice, for a real device. */
|
|
711
|
-
captureSlice(deviceId, type, capName, slice, at, reconciling) {
|
|
712
|
-
for (const [path, spec] of Object.entries(this.deps.catalog[capName] ?? {})) {
|
|
713
|
-
if (spec.excludeDeviceTypes?.includes(type) === true) {
|
|
714
|
-
if (!reconciling) this.deps.counters.skip("excluded-device-type");
|
|
715
|
-
continue;
|
|
716
|
-
}
|
|
717
|
-
const key = capSeriesKey(capName, path);
|
|
718
|
-
const value = this.readValue(slice, key, path, spec);
|
|
719
|
-
if (value === null) continue;
|
|
720
|
-
this.consider(deviceId, key, "", spec, value, at, reconciling);
|
|
721
|
-
}
|
|
722
|
-
const items = this.deps.itemCatalog[capName];
|
|
723
|
-
if (items !== void 0) this.captureItems(deviceId, type, capName, items, slice, at, reconciling);
|
|
724
|
-
}
|
|
725
|
-
/**
|
|
726
|
-
* A keyed array (D917): each item with a usable key contributes its fields
|
|
727
|
-
* under that key as the series scope. A value that is not an array is no
|
|
728
|
-
* reading; an item without a string key cannot be followed and is counted.
|
|
729
|
-
*/
|
|
730
|
-
captureItems(deviceId, type, capName, items, slice, at, reconciling) {
|
|
731
|
-
const list = readPath(slice, items.path);
|
|
732
|
-
if (!Array.isArray(list)) return;
|
|
733
|
-
const { counters } = this.deps;
|
|
734
|
-
if (list.length > 64) counters.skip("item-overflow", list.length - 64);
|
|
735
|
-
for (const entry of list.slice(0, 64)) {
|
|
736
|
-
const scope = isRecord$1(entry) ? entry[items.key] : void 0;
|
|
737
|
-
if (!isRecord$1(entry) || typeof scope !== "string" || scope.length === 0 || scope.length > 128) {
|
|
738
|
-
counters.skip("item-key-invalid");
|
|
739
|
-
continue;
|
|
740
|
-
}
|
|
741
|
-
for (const [field, spec] of Object.entries(items.fields)) {
|
|
742
|
-
if (spec.excludeDeviceTypes?.includes(type) === true) continue;
|
|
743
|
-
const key = capItemSeriesKey(capName, items, field);
|
|
744
|
-
const value = this.readValue(entry, key, field, spec);
|
|
745
|
-
if (value === null) continue;
|
|
746
|
-
this.consider(deviceId, key, scope, spec, value, at, reconciling);
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
/**
|
|
751
|
-
* The number a field contributes, or `null` (not a reading — never 0). An
|
|
752
|
-
* `enum` state becomes the ordinal of its value in the metric's list; a value
|
|
753
|
-
* the full list cannot take is refused and counted (`enum-overflow`).
|
|
754
|
-
*/
|
|
755
|
-
readValue(slice, key, path, spec) {
|
|
756
|
-
if (spec.skipWhenSet !== void 0) {
|
|
757
|
-
const gate = readPath(slice, spec.skipWhenSet);
|
|
758
|
-
if (gate !== void 0 && gate !== null) return null;
|
|
759
|
-
}
|
|
760
|
-
if (spec.kind !== "enum") return readNumeric(slice, path, spec.kind);
|
|
761
|
-
const raw = readPath(slice, path);
|
|
762
|
-
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
763
|
-
const ordinal = this.deps.registry.ordinalOf(key, raw);
|
|
764
|
-
if (ordinal === null) this.deps.counters.skip("enum-overflow");
|
|
765
|
-
return ordinal;
|
|
766
|
-
}
|
|
767
|
-
consider(deviceId, key, scope, spec, value, at, reconciling) {
|
|
768
|
-
const subject = {
|
|
769
|
-
kind: "device",
|
|
770
|
-
deviceId
|
|
771
|
-
};
|
|
772
|
-
const identity = identityOf(key, `d:${deviceId}`, scope);
|
|
773
|
-
const decision = decideCapture(spec, this.recorded.get(identity) ?? null, value, at, { ignoreFloor: reconciling });
|
|
774
|
-
if (decision.action === "emit") {
|
|
775
|
-
this.emit(identity, key, subject, scope, value, at);
|
|
776
|
-
if (reconciling) this.deps.counters.reconciledPoints += 1;
|
|
777
|
-
return;
|
|
778
|
-
}
|
|
779
|
-
if (decision.action === "defer") {
|
|
780
|
-
this.pending.set(identity, {
|
|
781
|
-
key,
|
|
782
|
-
subject,
|
|
783
|
-
scope,
|
|
784
|
-
spec,
|
|
785
|
-
value
|
|
786
|
-
});
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
this.pending.delete(identity);
|
|
790
|
-
this.deps.counters.skip(decision.reason);
|
|
791
|
-
}
|
|
792
|
-
emit(identity, key, subject, scope, value, ts) {
|
|
793
|
-
this.recorded.set(identity, {
|
|
794
|
-
value,
|
|
795
|
-
ts
|
|
796
|
-
});
|
|
797
|
-
this.pending.delete(identity);
|
|
798
|
-
this.deps.counters.pointsEmitted += 1;
|
|
799
|
-
this.deps.writer.enqueue({
|
|
800
|
-
key,
|
|
801
|
-
subject,
|
|
802
|
-
scope,
|
|
803
|
-
ts,
|
|
804
|
-
value
|
|
805
|
-
});
|
|
806
|
-
}
|
|
807
|
-
/** Emit every pending change whose capture floor has passed, stamped where the floor ended. */
|
|
808
|
-
tickPending(now) {
|
|
809
|
-
for (const [identity, p] of this.pending) {
|
|
810
|
-
const recorded = this.recorded.get(identity);
|
|
811
|
-
const deviceId = p.subject.kind === "device" ? p.subject.deviceId : -1;
|
|
812
|
-
if (this.deps.optedOut().has(deviceId)) {
|
|
813
|
-
this.pending.delete(identity);
|
|
814
|
-
this.deps.counters.skip("opted-out");
|
|
815
|
-
continue;
|
|
816
|
-
}
|
|
817
|
-
const notBefore = (recorded?.ts ?? now) + minIntervalOf(p.spec);
|
|
818
|
-
if (now < notBefore) continue;
|
|
819
|
-
this.emit(identity, p.key, p.subject, p.scope, p.value, notBefore);
|
|
820
|
-
this.deps.counters.trailingEmitted += 1;
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
park(deviceId, capName, slice, at) {
|
|
824
|
-
const existing = this.parked.get(deviceId);
|
|
825
|
-
if (existing !== void 0) {
|
|
826
|
-
existing.set(capName, {
|
|
827
|
-
capName,
|
|
828
|
-
slice,
|
|
829
|
-
at
|
|
830
|
-
});
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
if (this.parked.size >= MAX_PARKED_DEVICES) {
|
|
834
|
-
const oldest = this.parked.keys().next();
|
|
835
|
-
if (oldest.done !== true) this.parked.delete(oldest.value);
|
|
836
|
-
this.deps.counters.skip("parked-overflow");
|
|
837
|
-
}
|
|
838
|
-
this.parked.set(deviceId, new Map([[capName, {
|
|
839
|
-
capName,
|
|
840
|
-
slice,
|
|
841
|
-
at
|
|
842
|
-
}]]));
|
|
843
|
-
this.deps.directory.ensure(deviceId).then((type) => {
|
|
844
|
-
const waiting = this.parked.get(deviceId);
|
|
845
|
-
this.parked.delete(deviceId);
|
|
846
|
-
if (waiting === void 0) return;
|
|
847
|
-
if (type === void 0) {
|
|
848
|
-
this.deps.counters.skip("unknown-device", waiting.size);
|
|
849
|
-
return;
|
|
850
|
-
}
|
|
851
|
-
for (const p of waiting.values()) this.handleState(deviceId, p.capName, p.slice, p.at);
|
|
852
|
-
});
|
|
853
|
-
}
|
|
854
|
-
/**
|
|
855
|
-
* Restore a missed change from the state mirror's whole snapshot: a field whose
|
|
856
|
-
* value differs from the last recorded one gets a point stamped `now`.
|
|
857
|
-
* Additive only — it never removes anything, and a snapshot that could not be
|
|
858
|
-
* read is the CALLER's failure to count (D49), not an empty snapshot.
|
|
859
|
-
*/
|
|
860
|
-
reconcile(snapshots, now) {
|
|
861
|
-
const optedOut = this.deps.optedOut();
|
|
862
|
-
for (const [deviceKey, caps] of Object.entries(snapshots)) {
|
|
863
|
-
const deviceId = Number(deviceKey);
|
|
864
|
-
if (!Number.isInteger(deviceId) || optedOut.has(deviceId)) continue;
|
|
865
|
-
const type = this.deps.directory.typeOf(deviceId);
|
|
866
|
-
if (type === void 0 || !isRealDeviceType(type)) continue;
|
|
867
|
-
for (const [capName, slice] of Object.entries(caps)) this.captureSlice(deviceId, type, capName, slice, now, true);
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
/**
|
|
871
|
-
* Forget what was "recorded" for points that never reached the store (dropped),
|
|
872
|
-
* so the next change-only comparison — and the next reconcile — starts from what
|
|
873
|
-
* is really there instead of leaving a hole until the value moves.
|
|
874
|
-
*/
|
|
875
|
-
forget(points) {
|
|
876
|
-
for (const p of points) {
|
|
877
|
-
const identity = identityOf(p.key, subjectIdOf(p.subject), p.scope);
|
|
878
|
-
this.recorded.delete(identity);
|
|
879
|
-
this.pending.delete(identity);
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
/** A purged series leaves no state behind. */
|
|
883
|
-
forgetSeries(series) {
|
|
884
|
-
const identity = identityOf(series.key, series.subject, series.scope);
|
|
885
|
-
this.recorded.delete(identity);
|
|
886
|
-
this.pending.delete(identity);
|
|
887
|
-
}
|
|
888
|
-
/** For status and specs. */
|
|
889
|
-
pendingCount() {
|
|
890
|
-
return this.pending.size;
|
|
891
|
-
}
|
|
892
|
-
/** The last recorded state of a series, for specs. */
|
|
893
|
-
lastRecorded(key, subjectId, scope = "") {
|
|
894
|
-
return this.recorded.get(identityOf(key, subjectId, scope));
|
|
895
|
-
}
|
|
896
|
-
};
|
|
897
|
-
//#endregion
|
|
6
|
+
const require_dist = require("../../dist-iVUkxljP.js");
|
|
7
|
+
const require_retention = require("../../retention-B3z_lBP1.js");
|
|
898
8
|
//#region src/builtins/device-history/capture/point-writer.ts
|
|
899
9
|
var FLUSH_INTERVAL_MS = 1e4;
|
|
900
10
|
var MAX_BUFFER_AGE_MS = 6e4;
|
|
@@ -1080,7 +190,7 @@ var ProducerIngest = class {
|
|
|
1080
190
|
}
|
|
1081
191
|
/** A purged series leaves no rate-limit state behind. */
|
|
1082
192
|
forgetSeries(series) {
|
|
1083
|
-
this.lastAccepted.delete(identityOf(series.key, series.subject, series.scope));
|
|
193
|
+
this.lastAccepted.delete(require_retention.identityOf(series.key, series.subject, series.scope));
|
|
1084
194
|
}
|
|
1085
195
|
async declareSeries(callerAddonId, input) {
|
|
1086
196
|
if (callerAddonId === null) throw new Error("declareSeries: the caller addon could not be identified");
|
|
@@ -1109,7 +219,7 @@ var ProducerIngest = class {
|
|
|
1109
219
|
if (subject.kind === "device") {
|
|
1110
220
|
if (this.deps.optedOut().has(subject.deviceId)) return "opted-out";
|
|
1111
221
|
const type = await this.deps.directory.ensure(subject.deviceId);
|
|
1112
|
-
if (type === void 0 || !isRealDeviceType(type)) return "not-a-real-device";
|
|
222
|
+
if (type === void 0 || !require_retention.isRealDeviceType(type)) return "not-a-real-device";
|
|
1113
223
|
}
|
|
1114
224
|
return null;
|
|
1115
225
|
}
|
|
@@ -1131,7 +241,7 @@ var ProducerIngest = class {
|
|
|
1131
241
|
counters.refuse("out-of-range");
|
|
1132
242
|
continue;
|
|
1133
243
|
}
|
|
1134
|
-
const identity = identityOf(point.key, subjectIdOf(point.subject), point.scope);
|
|
244
|
+
const identity = require_retention.identityOf(point.key, require_retention.subjectIdOf(point.subject), point.scope);
|
|
1135
245
|
const slot = this.lastAccepted.get(identity);
|
|
1136
246
|
if (slot !== void 0 && point.ts >= slot.lastTs && point.ts < slot.nextDue - 1e3) {
|
|
1137
247
|
addRefusal(refusals, point.key, "rate");
|
|
@@ -1164,7 +274,7 @@ var ProducerIngest = class {
|
|
|
1164
274
|
counters.refuse(reason);
|
|
1165
275
|
continue;
|
|
1166
276
|
}
|
|
1167
|
-
const aligned = bucket.bucketStart % FIVE_MINUTES_MS === 0;
|
|
277
|
+
const aligned = bucket.bucketStart % require_retention.FIVE_MINUTES_MS === 0;
|
|
1168
278
|
const consistent = Number.isFinite(bucket.sum) && Number.isFinite(bucket.min) && Number.isFinite(bucket.max) && bucket.min <= bucket.max;
|
|
1169
279
|
if (!aligned || !consistent || bucket.bucketStart > now + 3e5 || bucket.bucketStart < now - fiveMinuteMs) {
|
|
1170
280
|
addRefusal(refusals, bucket.key, "out-of-range");
|
|
@@ -1332,20 +442,20 @@ var TIER_ORDER = [
|
|
|
1332
442
|
"1d"
|
|
1333
443
|
];
|
|
1334
444
|
var BUCKET_MS = {
|
|
1335
|
-
"5m": FIVE_MINUTES_MS,
|
|
1336
|
-
"1h": HOUR_MS,
|
|
1337
|
-
"1d": DAY_MS
|
|
445
|
+
"5m": require_retention.FIVE_MINUTES_MS,
|
|
446
|
+
"1h": require_retention.HOUR_MS,
|
|
447
|
+
"1d": require_retention.DAY_MS
|
|
1338
448
|
};
|
|
1339
449
|
/** A key with more scopes than this is cut (a zone x class matrix, not a chart). */
|
|
1340
450
|
var MAX_SCOPES = 50;
|
|
1341
451
|
/** The power-meter series the energy key is derived from, meter first. */
|
|
1342
|
-
var ENERGY_METER_KEY = capSeriesKey("power-meter", "kwhTotal");
|
|
1343
|
-
var ENERGY_POWER_KEY = capSeriesKey("power-meter", "watts");
|
|
452
|
+
var ENERGY_METER_KEY = require_retention.capSeriesKey("power-meter", "kwhTotal");
|
|
453
|
+
var ENERGY_POWER_KEY = require_retention.capSeriesKey("power-meter", "watts");
|
|
1344
454
|
function retentionMsOf(tier, policy) {
|
|
1345
|
-
if (tier === "raw") return policy.rawDays * DAY_MS;
|
|
1346
|
-
if (tier === "5m") return policy.fiveMinuteDays * DAY_MS;
|
|
1347
|
-
if (tier === "1h") return policy.hourDays * DAY_MS;
|
|
1348
|
-
return policy.dayDays === null || policy.dayDays <= 0 ? null : policy.dayDays * DAY_MS;
|
|
455
|
+
if (tier === "raw") return policy.rawDays * require_retention.DAY_MS;
|
|
456
|
+
if (tier === "5m") return policy.fiveMinuteDays * require_retention.DAY_MS;
|
|
457
|
+
if (tier === "1h") return policy.hourDays * require_retention.DAY_MS;
|
|
458
|
+
return policy.dayDays === null || policy.dayDays <= 0 ? null : policy.dayDays * require_retention.DAY_MS;
|
|
1349
459
|
}
|
|
1350
460
|
/** Rows arrive newest first and one over the bound; keep the newest `maxPoints`, oldest first. */
|
|
1351
461
|
function bounded(newestFirst, maxPoints) {
|
|
@@ -1427,7 +537,7 @@ var HistoryQuery = class {
|
|
|
1427
537
|
return out;
|
|
1428
538
|
}
|
|
1429
539
|
seriesOf(input) {
|
|
1430
|
-
return this.deps.registry.forKeyAndSubject(input.key, subjectIdOf(input.subject)).filter((s) => input.scope === void 0 || s.scope === input.scope).toSorted((a, b) => a.scope.localeCompare(b.scope)).slice(0, MAX_SCOPES);
|
|
540
|
+
return this.deps.registry.forKeyAndSubject(input.key, require_retention.subjectIdOf(input.subject)).filter((s) => input.scope === void 0 || s.scope === input.scope).toSorted((a, b) => a.scope.localeCompare(b.scope)).slice(0, MAX_SCOPES);
|
|
1431
541
|
}
|
|
1432
542
|
/** The tier to answer from and its reads. `noRaw`: the series has no raw rows to read. */
|
|
1433
543
|
async choose(input, wanted, noRaw) {
|
|
@@ -1473,7 +583,7 @@ var HistoryQuery = class {
|
|
|
1473
583
|
tier,
|
|
1474
584
|
...bounded(await store.readRaw(series.id, input.from, input.to, limit, "desc"), input.maxPoints)
|
|
1475
585
|
};
|
|
1476
|
-
const lower = bucketStartOf(tier, input.from);
|
|
586
|
+
const lower = require_retention.bucketStartOf(tier, input.from);
|
|
1477
587
|
return {
|
|
1478
588
|
tier,
|
|
1479
589
|
...bounded(await store.readRollups(tier, series.id, lower, input.to, limit, "desc"), input.maxPoints)
|
|
@@ -1489,7 +599,7 @@ var HistoryQuery = class {
|
|
|
1489
599
|
state: stateName(meaning, p.value)
|
|
1490
600
|
};
|
|
1491
601
|
}
|
|
1492
|
-
const row = await store.rollupBefore(tier, series.id, bucketStartOf(tier, from));
|
|
602
|
+
const row = await store.rollupBefore(tier, series.id, require_retention.bucketStartOf(tier, from));
|
|
1493
603
|
return row === null ? null : {
|
|
1494
604
|
t: row.bucketStart,
|
|
1495
605
|
value: row.last,
|
|
@@ -1499,7 +609,7 @@ var HistoryQuery = class {
|
|
|
1499
609
|
/** The series energy is derived from for a device: the meter when it has one, else its power. */
|
|
1500
610
|
energySource(subject) {
|
|
1501
611
|
if (subject.kind !== "device") return null;
|
|
1502
|
-
const subjectId = subjectIdOf(subject);
|
|
612
|
+
const subjectId = require_retention.subjectIdOf(subject);
|
|
1503
613
|
const meter = this.deps.registry.find(ENERGY_METER_KEY, subjectId, "");
|
|
1504
614
|
if (meter !== void 0) return {
|
|
1505
615
|
series: meter,
|
|
@@ -1527,7 +637,7 @@ var HistoryQuery = class {
|
|
|
1527
637
|
};
|
|
1528
638
|
let points;
|
|
1529
639
|
if (source.from === "meter") {
|
|
1530
|
-
const prev = await this.deps.store.rollupBefore(read.tier, source.series.id, bucketStartOf(read.tier, input.from));
|
|
640
|
+
const prev = await this.deps.store.rollupBefore(read.tier, source.series.id, require_retention.bucketStartOf(read.tier, input.from));
|
|
1531
641
|
points = rollupPoints(read.rows, {
|
|
1532
642
|
kind: "counter",
|
|
1533
643
|
values: null
|
|
@@ -1548,7 +658,7 @@ var HistoryQuery = class {
|
|
|
1548
658
|
const { registry } = this.deps;
|
|
1549
659
|
const slices = /* @__PURE__ */ new Map();
|
|
1550
660
|
const infos = [];
|
|
1551
|
-
for (const s of registry.forSubject(subjectIdOf(subject))) {
|
|
661
|
+
for (const s of registry.forSubject(require_retention.subjectIdOf(subject))) {
|
|
1552
662
|
const metric = registry.metric(s.key);
|
|
1553
663
|
if (metric === void 0) continue;
|
|
1554
664
|
const item = this.itemFields.get(s.key);
|
|
@@ -1586,7 +696,7 @@ var HistoryQuery = class {
|
|
|
1586
696
|
*/
|
|
1587
697
|
async itemNaming(deviceId, item, scope, fixed, slices) {
|
|
1588
698
|
const slice = await this.sliceOf(deviceId, item.capName, slices);
|
|
1589
|
-
const list = slice === null ? void 0 : readPath(slice, item.items.path);
|
|
699
|
+
const list = slice === null ? void 0 : require_retention.readPath(slice, item.items.path);
|
|
1590
700
|
const entry = Array.isArray(list) ? list.find((e) => isObject(e) && e[item.items.key] === scope) : void 0;
|
|
1591
701
|
const rawLabel = entry !== void 0 && item.items.label !== void 0 ? entry[item.items.label] : void 0;
|
|
1592
702
|
const label = typeof rawLabel === "string" && rawLabel.length > 0 ? rawLabel : scope;
|
|
@@ -1596,7 +706,7 @@ var HistoryQuery = class {
|
|
|
1596
706
|
};
|
|
1597
707
|
return {
|
|
1598
708
|
label,
|
|
1599
|
-
unit: entry === void 0 ? null : readUnit(entry, item.spec.unitFrom)
|
|
709
|
+
unit: entry === void 0 ? null : require_retention.readUnit(entry, item.spec.unitFrom)
|
|
1600
710
|
};
|
|
1601
711
|
}
|
|
1602
712
|
sliceOf(deviceId, capName, slices) {
|
|
@@ -1609,12 +719,12 @@ var HistoryQuery = class {
|
|
|
1609
719
|
if (fixed !== null || subject.kind !== "device" || !series.key.startsWith("cap:")) return fixed;
|
|
1610
720
|
const spec = this.specOf(series.key);
|
|
1611
721
|
if (spec?.unitFrom === void 0) return null;
|
|
1612
|
-
const capName = series.key.slice(CAP_KEY_PREFIX.length, series.key.indexOf("."));
|
|
722
|
+
const capName = series.key.slice(require_retention.CAP_KEY_PREFIX.length, series.key.indexOf("."));
|
|
1613
723
|
const slice = await this.sliceOf(subject.deviceId, capName, slices);
|
|
1614
|
-
return slice === null ? null : readUnit(slice, spec.unitFrom);
|
|
724
|
+
return slice === null ? null : require_retention.readUnit(slice, spec.unitFrom);
|
|
1615
725
|
}
|
|
1616
726
|
specOf(key) {
|
|
1617
|
-
const body = key.slice(CAP_KEY_PREFIX.length);
|
|
727
|
+
const body = key.slice(require_retention.CAP_KEY_PREFIX.length);
|
|
1618
728
|
const dot = body.indexOf(".");
|
|
1619
729
|
if (dot < 0) return void 0;
|
|
1620
730
|
return this.deps.catalog[body.slice(0, dot)]?.[body.slice(dot + 1)];
|
|
@@ -1625,7 +735,7 @@ function isObject(value) {
|
|
|
1625
735
|
}
|
|
1626
736
|
/** Series key → keyed-item field, for every item field of the catalog. */
|
|
1627
737
|
function itemFieldsByKey(itemCatalog) {
|
|
1628
|
-
return new Map(Object.entries(itemCatalog).flatMap(([capName, items]) => Object.entries(items.fields).map(([field, spec]) => [capItemSeriesKey(capName, items, field), {
|
|
738
|
+
return new Map(Object.entries(itemCatalog).flatMap(([capName, items]) => Object.entries(items.fields).map(([field, spec]) => [require_retention.capItemSeriesKey(capName, items, field), {
|
|
1629
739
|
capName,
|
|
1630
740
|
items,
|
|
1631
741
|
spec
|
|
@@ -2487,8 +1597,8 @@ function rawToFiveMinuteRows(input) {
|
|
|
2487
1597
|
while (i < points.length) {
|
|
2488
1598
|
const first = points[i];
|
|
2489
1599
|
if (first === void 0) break;
|
|
2490
|
-
const bucketStart = floorTo(first.ts, FIVE_MINUTES_MS);
|
|
2491
|
-
const bucketEnd = bucketStart + FIVE_MINUTES_MS;
|
|
1600
|
+
const bucketStart = require_retention.floorTo(first.ts, require_retention.FIVE_MINUTES_MS);
|
|
1601
|
+
const bucketEnd = bucketStart + require_retention.FIVE_MINUTES_MS;
|
|
2492
1602
|
const coverageEnd = Math.min(bucketEnd, Math.max(horizon, bucketStart));
|
|
2493
1603
|
let samples = 0;
|
|
2494
1604
|
let sum = 0;
|
|
@@ -2601,7 +1711,7 @@ function combineRows(input) {
|
|
|
2601
1711
|
};
|
|
2602
1712
|
}
|
|
2603
1713
|
/** How far back a boot rebuilds from raw and re-marks the upper tiers. */
|
|
2604
|
-
var BOOT_WINDOW_MS = 3 * HOUR_MS;
|
|
1714
|
+
var BOOT_WINDOW_MS = 3 * require_retention.HOUR_MS;
|
|
2605
1715
|
var markVersion = 0;
|
|
2606
1716
|
function mark(sets, seriesId, bucketStart) {
|
|
2607
1717
|
const map = sets.get(seriesId) ?? /* @__PURE__ */ new Map();
|
|
@@ -2621,14 +1731,14 @@ function settle(sets, seriesId, handled) {
|
|
|
2621
1731
|
if (map.size === 0) sets.delete(seriesId);
|
|
2622
1732
|
}
|
|
2623
1733
|
/** Buckets further apart than this are recomputed as separate ranges, not one long read. */
|
|
2624
|
-
var MAX_RANGE_GAP_MS = HOUR_MS;
|
|
1734
|
+
var MAX_RANGE_GAP_MS = require_retention.HOUR_MS;
|
|
2625
1735
|
/** `[from, toExclusive)` ranges covering sorted 5-minute bucket starts, split at long gaps. */
|
|
2626
1736
|
function clusterRanges(sorted) {
|
|
2627
1737
|
const ranges = [];
|
|
2628
1738
|
for (const start of sorted) {
|
|
2629
1739
|
const current = ranges[ranges.length - 1];
|
|
2630
|
-
if (current !== void 0 && start - current[1] <= MAX_RANGE_GAP_MS) current[1] = start + FIVE_MINUTES_MS;
|
|
2631
|
-
else ranges.push([start, start + FIVE_MINUTES_MS]);
|
|
1740
|
+
if (current !== void 0 && start - current[1] <= MAX_RANGE_GAP_MS) current[1] = start + require_retention.FIVE_MINUTES_MS;
|
|
1741
|
+
else ranges.push([start, start + require_retention.FIVE_MINUTES_MS]);
|
|
2632
1742
|
}
|
|
2633
1743
|
return ranges;
|
|
2634
1744
|
}
|
|
@@ -2653,14 +1763,14 @@ var RollupEngine = class {
|
|
|
2653
1763
|
markPoints(written) {
|
|
2654
1764
|
const now = this.deps.clock();
|
|
2655
1765
|
for (const { seriesId, timestamps } of written) for (const ts of timestamps) {
|
|
2656
|
-
const bucket = floorTo(ts, FIVE_MINUTES_MS);
|
|
1766
|
+
const bucket = require_retention.floorTo(ts, require_retention.FIVE_MINUTES_MS);
|
|
2657
1767
|
mark(this.dirty5m, seriesId, bucket);
|
|
2658
|
-
if (bucket + 3e5 + ROLLUP_GRACE_MS["5m"] <= now) mark(this.dirty5m, seriesId, bucket + FIVE_MINUTES_MS);
|
|
1768
|
+
if (bucket + 3e5 + require_retention.ROLLUP_GRACE_MS["5m"] <= now) mark(this.dirty5m, seriesId, bucket + require_retention.FIVE_MINUTES_MS);
|
|
2659
1769
|
}
|
|
2660
1770
|
}
|
|
2661
1771
|
/** A 5m row landed (computed, or a producer bucket): its hour is dirty. */
|
|
2662
1772
|
markFiveMinuteRow(seriesId, bucketStart) {
|
|
2663
|
-
mark(this.dirty1h, seriesId, bucketStartOf("1h", bucketStart));
|
|
1773
|
+
mark(this.dirty1h, seriesId, require_retention.bucketStartOf("1h", bucketStart));
|
|
2664
1774
|
}
|
|
2665
1775
|
/** One pass over the three tiers. Single-flight; errors are counted and the work stays dirty. */
|
|
2666
1776
|
tick() {
|
|
@@ -2686,7 +1796,7 @@ var RollupEngine = class {
|
|
|
2686
1796
|
}
|
|
2687
1797
|
}
|
|
2688
1798
|
closedBuckets(map, tier, now) {
|
|
2689
|
-
return [...map].filter(([start]) => bucketEndOf(tier, start) + ROLLUP_GRACE_MS[tier] <= now).map(([bucket, version]) => ({
|
|
1799
|
+
return [...map].filter(([start]) => require_retention.bucketEndOf(tier, start) + require_retention.ROLLUP_GRACE_MS[tier] <= now).map(([bucket, version]) => ({
|
|
2690
1800
|
bucket,
|
|
2691
1801
|
version
|
|
2692
1802
|
})).toSorted((a, b) => a.bucket - b.bucket);
|
|
@@ -2769,14 +1879,14 @@ var RollupEngine = class {
|
|
|
2769
1879
|
return rows.length;
|
|
2770
1880
|
}
|
|
2771
1881
|
async recomputeHour(seriesId, hour, now) {
|
|
2772
|
-
await this.recomputeFrom("5m", "1h", FIVE_MINUTES_MS, seriesId, hour, now);
|
|
1882
|
+
await this.recomputeFrom("5m", "1h", require_retention.FIVE_MINUTES_MS, seriesId, hour, now);
|
|
2773
1883
|
}
|
|
2774
1884
|
async recomputeDay(seriesId, day, now) {
|
|
2775
|
-
await this.recomputeFrom("1h", "1d", HOUR_MS, seriesId, day, now);
|
|
1885
|
+
await this.recomputeFrom("1h", "1d", require_retention.HOUR_MS, seriesId, day, now);
|
|
2776
1886
|
}
|
|
2777
1887
|
async recomputeFrom(child, parent, childWidthMs, seriesId, windowStart, now) {
|
|
2778
1888
|
const { store } = this.deps;
|
|
2779
|
-
const windowEnd = bucketEndOf(parent, windowStart);
|
|
1889
|
+
const windowEnd = require_retention.bucketEndOf(parent, windowStart);
|
|
2780
1890
|
const combined = combineRows({
|
|
2781
1891
|
seriesId,
|
|
2782
1892
|
windowStart,
|
|
@@ -2790,7 +1900,7 @@ var RollupEngine = class {
|
|
|
2790
1900
|
if (combined === null) return;
|
|
2791
1901
|
await store.upsertRollups(parent, [combined]);
|
|
2792
1902
|
this.deps.counters.noteRollupRows(parent, seriesId, [combined.bucketStart]);
|
|
2793
|
-
if (parent === "1h") mark(this.dirty1d, seriesId, bucketStartOf("1d", windowStart));
|
|
1903
|
+
if (parent === "1h") mark(this.dirty1d, seriesId, require_retention.bucketStartOf("1d", windowStart));
|
|
2794
1904
|
}
|
|
2795
1905
|
/**
|
|
2796
1906
|
* After a restart the dirty sets are gone. Rebuild the recent window from raw
|
|
@@ -2798,21 +1908,21 @@ var RollupEngine = class {
|
|
|
2798
1908
|
* 1h / 1d buckets of that window dirty so the next ticks recompute them.
|
|
2799
1909
|
*/
|
|
2800
1910
|
async bootRecompute(now) {
|
|
2801
|
-
const windowStart = floorTo(now - BOOT_WINDOW_MS, FIVE_MINUTES_MS);
|
|
2802
|
-
const openBucket = floorTo(now, FIVE_MINUTES_MS);
|
|
1911
|
+
const windowStart = require_retention.floorTo(now - BOOT_WINDOW_MS, require_retention.FIVE_MINUTES_MS);
|
|
1912
|
+
const openBucket = require_retention.floorTo(now, require_retention.FIVE_MINUTES_MS);
|
|
2803
1913
|
let series = 0;
|
|
2804
1914
|
for (const s of this.deps.registry.all()) {
|
|
2805
1915
|
if (s.lastTs < windowStart) continue;
|
|
2806
1916
|
const metric = this.deps.registry.metric(s.key);
|
|
2807
1917
|
series += 1;
|
|
2808
1918
|
if (metric?.ingest === "points") {
|
|
2809
|
-
await this.recomputeFiveMinute(s.id, windowStart, openBucket + FIVE_MINUTES_MS, now);
|
|
1919
|
+
await this.recomputeFiveMinute(s.id, windowStart, openBucket + require_retention.FIVE_MINUTES_MS, now);
|
|
2810
1920
|
mark(this.dirty5m, s.id, openBucket);
|
|
2811
|
-
mark(this.dirty5m, s.id, openBucket - FIVE_MINUTES_MS);
|
|
1921
|
+
mark(this.dirty5m, s.id, openBucket - require_retention.FIVE_MINUTES_MS);
|
|
2812
1922
|
}
|
|
2813
|
-
for (let hour = floorTo(windowStart, HOUR_MS); hour <= now; hour += HOUR_MS) mark(this.dirty1h, s.id, hour);
|
|
2814
|
-
mark(this.dirty1d, s.id, bucketStartOf("1d", windowStart));
|
|
2815
|
-
mark(this.dirty1d, s.id, bucketStartOf("1d", now));
|
|
1923
|
+
for (let hour = require_retention.floorTo(windowStart, require_retention.HOUR_MS); hour <= now; hour += require_retention.HOUR_MS) mark(this.dirty1h, s.id, hour);
|
|
1924
|
+
mark(this.dirty1d, s.id, require_retention.bucketStartOf("1d", windowStart));
|
|
1925
|
+
mark(this.dirty1d, s.id, require_retention.bucketStartOf("1d", now));
|
|
2816
1926
|
}
|
|
2817
1927
|
return series;
|
|
2818
1928
|
}
|
|
@@ -2965,8 +2075,8 @@ var HistoryRuntime = class {
|
|
|
2965
2075
|
const { logger, clock, catalog, itemCatalog } = deps;
|
|
2966
2076
|
this.startedAt = clock();
|
|
2967
2077
|
this.store = new HistoryStore(deps.door);
|
|
2968
|
-
this.registry = new SeriesRegistry(this.store);
|
|
2969
|
-
this.directory = new DeviceDirectory(deps.deviceSource, this.counters, logger, clock);
|
|
2078
|
+
this.registry = new require_retention.SeriesRegistry(this.store);
|
|
2079
|
+
this.directory = new require_retention.DeviceDirectory(deps.deviceSource, this.counters, logger, clock);
|
|
2970
2080
|
this.rollup = new RollupEngine({
|
|
2971
2081
|
store: this.store,
|
|
2972
2082
|
registry: this.registry,
|
|
@@ -2984,7 +2094,7 @@ var HistoryRuntime = class {
|
|
|
2984
2094
|
onDropped: (dropped) => this.capture.forget(dropped)
|
|
2985
2095
|
});
|
|
2986
2096
|
const optedOut = () => this.optedOutDevices;
|
|
2987
|
-
this.capture = new CaptureEngine({
|
|
2097
|
+
this.capture = new require_retention.CaptureEngine({
|
|
2988
2098
|
catalog,
|
|
2989
2099
|
itemCatalog,
|
|
2990
2100
|
registry: this.registry,
|
|
@@ -3008,8 +2118,8 @@ var HistoryRuntime = class {
|
|
|
3008
2118
|
retention: () => {
|
|
3009
2119
|
const policy = deps.retention();
|
|
3010
2120
|
return {
|
|
3011
|
-
rawMs: policy.rawDays * DAY_MS,
|
|
3012
|
-
fiveMinuteMs: policy.fiveMinuteDays * DAY_MS
|
|
2121
|
+
rawMs: policy.rawDays * require_retention.DAY_MS,
|
|
2122
|
+
fiveMinuteMs: policy.fiveMinuteDays * require_retention.DAY_MS
|
|
3013
2123
|
};
|
|
3014
2124
|
},
|
|
3015
2125
|
onBucketWritten: (seriesId, bucketStart) => this.rollup.markFiveMinuteRow(seriesId, bucketStart)
|
|
@@ -3023,14 +2133,14 @@ var HistoryRuntime = class {
|
|
|
3023
2133
|
itemCatalog,
|
|
3024
2134
|
readCapSlice: deps.readCapSlice
|
|
3025
2135
|
});
|
|
3026
|
-
this.departures = new DepartureTracker(this.store, this.registry, logger);
|
|
2136
|
+
this.departures = new require_retention.DepartureTracker(this.store, this.registry, logger);
|
|
3027
2137
|
this.registry.onRemove((series) => {
|
|
3028
2138
|
this.capture.forgetSeries(series);
|
|
3029
2139
|
this.ingest.forgetSeries(series);
|
|
3030
2140
|
this.rollup.forgetSeries(series.id);
|
|
3031
2141
|
this.retentionJob.forgetSeries(series.id);
|
|
3032
2142
|
});
|
|
3033
|
-
this.retentionJob = new RetentionJob({
|
|
2143
|
+
this.retentionJob = new require_retention.RetentionJob({
|
|
3034
2144
|
store: this.store,
|
|
3035
2145
|
registry: this.registry,
|
|
3036
2146
|
departures: this.departures,
|
|
@@ -3042,7 +2152,7 @@ var HistoryRuntime = class {
|
|
|
3042
2152
|
registry: this.registry,
|
|
3043
2153
|
departures: this.departures,
|
|
3044
2154
|
retention: this.retentionJob,
|
|
3045
|
-
declaredCapKeys: new Set(catalogMetrics(catalog, itemCatalog, 0).map((m) => m.key)),
|
|
2155
|
+
declaredCapKeys: new Set(require_retention.catalogMetrics(catalog, itemCatalog, 0).map((m) => m.key)),
|
|
3046
2156
|
logger,
|
|
3047
2157
|
clock
|
|
3048
2158
|
});
|
|
@@ -3050,7 +2160,7 @@ var HistoryRuntime = class {
|
|
|
3050
2160
|
async prepare() {
|
|
3051
2161
|
await this.store.declare();
|
|
3052
2162
|
await this.registry.load();
|
|
3053
|
-
for (const metric of catalogMetrics(this.deps.catalog, this.deps.itemCatalog, this.deps.clock())) await this.registry.declareMetric(metric);
|
|
2163
|
+
for (const metric of require_retention.catalogMetrics(this.deps.catalog, this.deps.itemCatalog, this.deps.clock())) await this.registry.declareMetric(metric);
|
|
3054
2164
|
this.optedOutDevices = await this.store.loadOptOuts();
|
|
3055
2165
|
await this.departures.load();
|
|
3056
2166
|
await this.directory.refresh();
|
|
@@ -3274,9 +2384,9 @@ var HistoryRuntime = class {
|
|
|
3274
2384
|
* metrics-native and pipeline-analytics).
|
|
3275
2385
|
*/
|
|
3276
2386
|
var DEFAULT_CONFIG = {
|
|
3277
|
-
rawDays: DEFAULT_RETENTION.rawDays,
|
|
3278
|
-
fiveMinuteDays: DEFAULT_RETENTION.fiveMinuteDays,
|
|
3279
|
-
hourDays: DEFAULT_RETENTION.hourDays,
|
|
2387
|
+
rawDays: require_retention.DEFAULT_RETENTION.rawDays,
|
|
2388
|
+
fiveMinuteDays: require_retention.DEFAULT_RETENTION.fiveMinuteDays,
|
|
2389
|
+
hourDays: require_retention.DEFAULT_RETENTION.hourDays,
|
|
3280
2390
|
dayDays: 0
|
|
3281
2391
|
};
|
|
3282
2392
|
function isRecord(value) {
|
|
@@ -3332,9 +2442,9 @@ var DeviceHistoryAddon = class extends require_dist.BaseAddon {
|
|
|
3332
2442
|
retention() {
|
|
3333
2443
|
const c = this.config;
|
|
3334
2444
|
return {
|
|
3335
|
-
rawDays: positiveOr(c.rawDays, DEFAULT_RETENTION.rawDays),
|
|
3336
|
-
fiveMinuteDays: positiveOr(c.fiveMinuteDays, DEFAULT_RETENTION.fiveMinuteDays),
|
|
3337
|
-
hourDays: positiveOr(c.hourDays, DEFAULT_RETENTION.hourDays),
|
|
2445
|
+
rawDays: positiveOr(c.rawDays, require_retention.DEFAULT_RETENTION.rawDays),
|
|
2446
|
+
fiveMinuteDays: positiveOr(c.fiveMinuteDays, require_retention.DEFAULT_RETENTION.fiveMinuteDays),
|
|
2447
|
+
hourDays: positiveOr(c.hourDays, require_retention.DEFAULT_RETENTION.hourDays),
|
|
3338
2448
|
dayDays: positiveOr(c.dayDays, 0) > 0 ? c.dayDays : null
|
|
3339
2449
|
};
|
|
3340
2450
|
}
|