@camstack/system 1.2.456 → 1.2.458
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/addon-runner.js +259 -37
- package/dist/addon-runner.mjs +289 -67
- package/dist/{agent-https-fm4AR8XL.mjs → agent-https-CQA3W-3e.mjs} +2 -2
- package/dist/{agent-https-yjxdfJDA.js → agent-https-DOELEug8.js} +2 -2
- 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 +2 -2
- package/dist/builtins/alerts/alerts.addon.mjs +2 -2
- 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.addon.js +2 -2
- package/dist/builtins/composer/composer.addon.mjs +2 -2
- package/dist/builtins/console-logging/console-destination.d.ts +1 -2
- package/dist/builtins/console-logging/index.js +1 -4
- package/dist/builtins/console-logging/index.mjs +1 -4
- 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/capture/capture-engine.d.ts +6 -0
- package/dist/builtins/device-history/device-history.addon.d.ts +13 -0
- package/dist/builtins/device-history/device-history.addon.js +736 -315
- package/dist/builtins/device-history/device-history.addon.mjs +736 -315
- package/dist/builtins/device-history/ingest/producer-ingest.d.ts +11 -1
- package/dist/builtins/device-history/query/history-query.d.ts +35 -9
- package/dist/builtins/device-history/query/point-shape.d.ts +33 -0
- package/dist/builtins/device-history/retention/retention.d.ts +1 -1
- package/dist/builtins/device-history/rollup/rollup-engine.d.ts +2 -0
- package/dist/builtins/device-history/rollup/rollup-math.d.ts +6 -0
- package/dist/builtins/device-history/store/history-store.d.ts +7 -0
- package/dist/builtins/device-history/store/series-registry.d.ts +26 -1
- 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/hub-forwarder/hub-forwarder-destination.d.ts +1 -2
- package/dist/builtins/hub-forwarder/index.js +1 -4
- package/dist/builtins/hub-forwarder/index.mjs +1 -4
- package/dist/builtins/keychain/index.js +4 -4
- package/dist/builtins/keychain/index.mjs +4 -4
- 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 +3 -3
- package/dist/builtins/local-auth/local-auth.addon.mjs +3 -3
- 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 +323 -7
- package/dist/builtins/loki-logging/index.mjs +323 -7
- package/dist/builtins/loki-logging/loki-destination.d.ts +20 -3
- package/dist/builtins/loki-logging/loki-read.d.ts +51 -0
- package/dist/builtins/native-metrics/native-metrics.addon.js +290 -112
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +290 -112
- package/dist/builtins/native-metrics/node-metrics-history-recorder.d.ts +106 -35
- package/dist/builtins/platform-probe/coral-accelerators.d.ts +26 -9
- package/dist/builtins/platform-probe/index.js +11 -68
- package/dist/builtins/platform-probe/index.mjs +11 -68
- 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/location-history-recorder.d.ts +76 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.d.ts +9 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +144 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +144 -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 +165 -3
- package/dist/builtins/winston-logging/index.mjs +165 -3
- package/dist/builtins/winston-logging/log-file-cursor.d.ts +12 -0
- package/dist/builtins/winston-logging/log-file-index-store.d.ts +11 -0
- package/dist/builtins/winston-logging/log-file-index.d.ts +77 -0
- package/dist/builtins/winston-logging/log-file-read-client.d.ts +44 -0
- package/dist/builtins/winston-logging/log-file-read-protocol.d.ts +41 -0
- package/dist/builtins/winston-logging/log-file-read-worker.d.ts +1 -0
- package/dist/builtins/winston-logging/log-file-reader.d.ts +51 -0
- package/dist/builtins/winston-logging/winston-destination.d.ts +20 -2
- package/dist/{composition-sources-Ds0Vr6eD.mjs → composition-sources-BVguN7VV.mjs} +1 -1
- package/dist/{composition-sources-gB4coDJR.js → composition-sources-DURQNDJ4.js} +1 -1
- package/dist/{constants-i5oD_gLB.mjs → constants-FGjnsE2x.mjs} +1 -1
- package/dist/{constants-CavyKfHZ.js → constants-Njka8Cv6.js} +1 -1
- package/dist/coral-accelerators-Bi_c3Rft.js +175 -0
- package/dist/coral-accelerators-DcnZvctM.mjs +162 -0
- package/dist/{device-ca-bpoEr0zG.js → device-ca-BrVK_5s3.js} +1 -1
- package/dist/{device-ca-qz1Pmeu1.mjs → device-ca-lEaAQ9Xi.mjs} +1 -1
- package/dist/{dist-BpqA2CT0.js → dist-BZQjLPiz.js} +526 -57
- package/dist/{dist-BSF_C8W2.mjs → dist-B_GRPhnY.mjs} +509 -58
- package/dist/file-host-worker.js +1 -1
- package/dist/file-host-worker.mjs +1 -1
- package/dist/{hub-mdns-ciao-CHaGDzyz.mjs → hub-mdns-ciao-Bg2HfTwP.mjs} +2 -2
- package/dist/{hub-mdns-ciao-CZlZRxxA.js → hub-mdns-ciao-nce3TXs_.js} +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +140 -48
- package/dist/index.mjs +134 -49
- package/dist/kernel/config-schema.d.ts +5 -0
- package/dist/kernel/hwaccel/hwaccel-resolver.d.ts +7 -3
- package/dist/kernel/moleculer/accessory-prune-gate.d.ts +55 -0
- package/dist/log-file-read-protocol-B7M3X3bT.mjs +785 -0
- package/dist/log-file-read-protocol-D0WcLbIV.js +817 -0
- package/dist/log-file-read-worker.js +64 -0
- package/dist/log-file-read-worker.mjs +64 -0
- package/dist/log-read-filter-Bi066jwU.js +134 -0
- package/dist/log-read-filter-D7Ev3zm-.mjs +93 -0
- package/dist/logging/log-manager.d.ts +10 -6
- package/dist/logging/log-read-filter.d.ts +43 -0
- package/dist/{phase6-declare-gate-n-IZRklX.js → phase6-declare-gate-Dag1_WR2.js} +1 -1
- package/dist/{phase6-declare-gate-B4yfsf2n.mjs → phase6-declare-gate-Lb-mrVvz.mjs} +1 -1
- package/dist/{restore-archive-BGUM4FKf.mjs → restore-archive-C7kLDI_0.mjs} +1 -1
- package/dist/{restore-archive-B4QG7XmX.js → restore-archive-FyfZ8YMC.js} +1 -1
- package/dist/{wal-maintenance-CNEs_I94.js → wal-maintenance-DfSIOj24.js} +0 -0
- package/dist/{wal-maintenance-AerF4XE6.mjs → wal-maintenance-Duejh7Lx.mjs} +0 -0
- package/package.json +1 -1
|
@@ -3,7 +3,239 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-BZQjLPiz.js");
|
|
7
|
+
//#region src/builtins/device-history/time/bucket-time.ts
|
|
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
|
+
/** Tier by tier in slices, then the series row (and whatever the slices missed). */
|
|
194
|
+
async purgeSeries(series, now) {
|
|
195
|
+
for (const tier of TIERS) {
|
|
196
|
+
const lo = tier === "raw" || tier === "5m" ? Math.max(series.firstTs - BUCKET_LEAD_MS, now - SHORT_TIER_LOOKBACK_MS) : series.firstTs - BUCKET_LEAD_MS;
|
|
197
|
+
await this.deleteSliced(tier, series.id, Math.max(0, lo), now + DAY_MS);
|
|
198
|
+
}
|
|
199
|
+
await this.deps.store.deleteSeries(series.id);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Delete every series of a DELETED device now (D865). Driven by a tombstone
|
|
203
|
+
* through the `device-removal-hook`, never by a listing: the week-long
|
|
204
|
+
* absence clock above remains the backstop for a device removed before
|
|
205
|
+
* tombstones existed.
|
|
206
|
+
*/
|
|
207
|
+
async purgeDevice(deviceId, now) {
|
|
208
|
+
const { registry, counters } = this.deps;
|
|
209
|
+
let purged = 0;
|
|
210
|
+
for (const series of registry.all()) {
|
|
211
|
+
if (series.deviceId !== deviceId) continue;
|
|
212
|
+
await this.purgeSeries(series, now);
|
|
213
|
+
registry.remove(series.id);
|
|
214
|
+
counters.purgedSeries += 1;
|
|
215
|
+
purged += 1;
|
|
216
|
+
}
|
|
217
|
+
return purged;
|
|
218
|
+
}
|
|
219
|
+
async purgeDeparted(now) {
|
|
220
|
+
const { registry, departures, counters, logger } = this.deps;
|
|
221
|
+
let purged = 0;
|
|
222
|
+
for (const series of registry.all()) {
|
|
223
|
+
if (purged >= MAX_PURGES_PER_RUN) break;
|
|
224
|
+
if (series.deviceId === null) continue;
|
|
225
|
+
const since = departures.missingSince(series.deviceId);
|
|
226
|
+
if (since === void 0 || now - since < DEPARTED_ABSENCE_MS) continue;
|
|
227
|
+
await this.purgeSeries(series, now);
|
|
228
|
+
registry.remove(series.id);
|
|
229
|
+
counters.purgedSeries += 1;
|
|
230
|
+
purged += 1;
|
|
231
|
+
logger.info("device-history: purged the series of an unregistered device", {
|
|
232
|
+
tags: { deviceId: series.deviceId },
|
|
233
|
+
meta: { key: series.key }
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
//#endregion
|
|
7
239
|
//#region src/builtins/device-history/store/series-registry.ts
|
|
8
240
|
function subjectIdOf(subject) {
|
|
9
241
|
return subject.kind === "device" ? `d:${subject.deviceId}` : `n:${subject.nodeId}`;
|
|
@@ -11,16 +243,29 @@ function subjectIdOf(subject) {
|
|
|
11
243
|
function deviceIdOf(subject) {
|
|
12
244
|
return subject.kind === "device" ? subject.deviceId : null;
|
|
13
245
|
}
|
|
246
|
+
/** Every metric key is a cap field (`cap:`), an addon's series (`addon:`) or derived. */
|
|
247
|
+
function isMetricKey(key) {
|
|
248
|
+
return key.startsWith("cap:") || key.startsWith("addon:");
|
|
249
|
+
}
|
|
14
250
|
function identityOf(key, subjectId, scope) {
|
|
15
251
|
return `${key}\u0000${subjectId}\u0000${scope}`;
|
|
16
252
|
}
|
|
17
253
|
var LOAD_PAGE = 5e3;
|
|
254
|
+
/** Append what `next` names that `stored` lacks, in `next`'s order: an ordinal never moves. */
|
|
255
|
+
function mergeEnumValues(stored, next) {
|
|
256
|
+
if (stored === null) return next;
|
|
257
|
+
if (next === null) return stored;
|
|
258
|
+
const missing = next.filter((v) => !stored.includes(v));
|
|
259
|
+
return missing.length === 0 ? stored : [...stored, ...missing];
|
|
260
|
+
}
|
|
18
261
|
var SeriesRegistry = class {
|
|
19
262
|
store;
|
|
20
263
|
metrics = /* @__PURE__ */ new Map();
|
|
21
264
|
byIdentity = /* @__PURE__ */ new Map();
|
|
22
265
|
byId = /* @__PURE__ */ new Map();
|
|
23
266
|
inflight = /* @__PURE__ */ new Map();
|
|
267
|
+
/** Enum metrics whose value list grew in memory and is not stored yet. */
|
|
268
|
+
dirtyMetrics = /* @__PURE__ */ new Set();
|
|
24
269
|
removeListeners = [];
|
|
25
270
|
constructor(store) {
|
|
26
271
|
this.store = store;
|
|
@@ -29,7 +274,10 @@ var SeriesRegistry = class {
|
|
|
29
274
|
this.metrics.clear();
|
|
30
275
|
this.byIdentity.clear();
|
|
31
276
|
this.byId.clear();
|
|
32
|
-
|
|
277
|
+
const stray = [];
|
|
278
|
+
for (const m of await this.store.loadMetrics()) if (isMetricKey(m.key)) this.metrics.set(m.key, m);
|
|
279
|
+
else stray.push(m.key);
|
|
280
|
+
if (stray.length > 0) await this.store.deleteMetrics(stray);
|
|
33
281
|
let afterId = 0;
|
|
34
282
|
for (;;) {
|
|
35
283
|
const page = await this.store.loadSeriesPage(afterId, LOAD_PAGE);
|
|
@@ -49,12 +297,48 @@ var SeriesRegistry = class {
|
|
|
49
297
|
allMetrics() {
|
|
50
298
|
return [...this.metrics.values()];
|
|
51
299
|
}
|
|
52
|
-
/**
|
|
300
|
+
/**
|
|
301
|
+
* Persist and remember a metric; idempotent. An `enum` value list is MERGED
|
|
302
|
+
* append-only with the stored one, so reordering a `z.enum` never changes
|
|
303
|
+
* what a stored ordinal means.
|
|
304
|
+
*/
|
|
53
305
|
async declareMetric(metric) {
|
|
54
306
|
const existing = this.metrics.get(metric.key);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
307
|
+
const merged = {
|
|
308
|
+
...metric,
|
|
309
|
+
values: mergeEnumValues(existing?.values ?? null, metric.values)
|
|
310
|
+
};
|
|
311
|
+
if (existing !== void 0 && sameMetric(existing, merged)) return;
|
|
312
|
+
await this.store.upsertMetric(merged);
|
|
313
|
+
this.metrics.set(metric.key, merged);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* The ordinal of `value` in an `enum` metric's list, learning (appending) a
|
|
317
|
+
* value seen for the first time. `null` when the key is not an enum metric or
|
|
318
|
+
* its list is full ({@link MAX_ENUM_VALUES}). A learned value is stored by
|
|
319
|
+
* {@link persistDirtyMetrics} BEFORE any point that uses it.
|
|
320
|
+
*/
|
|
321
|
+
ordinalOf(key, value) {
|
|
322
|
+
const metric = this.metrics.get(key);
|
|
323
|
+
if (metric === void 0 || metric.kind !== "enum") return null;
|
|
324
|
+
const values = metric.values ?? [];
|
|
325
|
+
const known = values.indexOf(value);
|
|
326
|
+
if (known >= 0) return known;
|
|
327
|
+
if (values.length >= 1024) return null;
|
|
328
|
+
this.metrics.set(key, {
|
|
329
|
+
...metric,
|
|
330
|
+
values: [...values, value]
|
|
331
|
+
});
|
|
332
|
+
this.dirtyMetrics.add(key);
|
|
333
|
+
return values.length;
|
|
334
|
+
}
|
|
335
|
+
/** Store every value list that grew in memory. Throws (and keeps them dirty) on a failed write. */
|
|
336
|
+
async persistDirtyMetrics() {
|
|
337
|
+
for (const key of this.dirtyMetrics) {
|
|
338
|
+
const metric = this.metrics.get(key);
|
|
339
|
+
if (metric !== void 0) await this.store.upsertMetric(metric);
|
|
340
|
+
this.dirtyMetrics.delete(key);
|
|
341
|
+
}
|
|
58
342
|
}
|
|
59
343
|
find(key, subjectId, scope) {
|
|
60
344
|
return this.byIdentity.get(identityOf(key, subjectId, scope));
|
|
@@ -119,7 +403,7 @@ var SeriesRegistry = class {
|
|
|
119
403
|
}
|
|
120
404
|
};
|
|
121
405
|
function sameMetric(a, b) {
|
|
122
|
-
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;
|
|
406
|
+
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);
|
|
123
407
|
}
|
|
124
408
|
//#endregion
|
|
125
409
|
//#region src/builtins/device-history/capture/capture-filter.ts
|
|
@@ -143,10 +427,10 @@ var DEADBAND_EPSILON = 1e-9;
|
|
|
143
427
|
/** The capture floor of a field: its own `minIntervalMs`, else the kind's default. */
|
|
144
428
|
function minIntervalOf(spec) {
|
|
145
429
|
if (spec.minIntervalMs !== void 0) return spec.minIntervalMs;
|
|
146
|
-
return spec.kind === "boolean" ? 0 : require_dist.HISTORY_DEFAULT_MIN_INTERVAL_MS;
|
|
430
|
+
return spec.kind === "boolean" || spec.kind === "enum" ? 0 : require_dist.HISTORY_DEFAULT_MIN_INTERVAL_MS;
|
|
147
431
|
}
|
|
148
432
|
function clearsDeadband(spec, last, value) {
|
|
149
|
-
if (spec.kind === "boolean") return true;
|
|
433
|
+
if (spec.kind === "boolean" || spec.kind === "enum") return true;
|
|
150
434
|
const delta = Math.abs(value - last);
|
|
151
435
|
if (spec.deadband !== void 0 && delta < spec.deadband - DEADBAND_EPSILON) return false;
|
|
152
436
|
if (spec.deadbandPct !== void 0) {
|
|
@@ -331,7 +615,8 @@ function catalogMetrics(catalog, now) {
|
|
|
331
615
|
unit: spec.unit ?? null,
|
|
332
616
|
displayName: spec.displayName ?? humanize(capName, path),
|
|
333
617
|
ingest: "points",
|
|
334
|
-
declaredAt: now
|
|
618
|
+
declaredAt: now,
|
|
619
|
+
values: spec.kind === "enum" ? spec.values ?? [] : null
|
|
335
620
|
})));
|
|
336
621
|
}
|
|
337
622
|
/** Devices waiting on a type lookup; beyond this the oldest is dropped (and counted). */
|
|
@@ -380,11 +665,28 @@ var CaptureEngine = class {
|
|
|
380
665
|
counters.skip("excluded-device-type");
|
|
381
666
|
continue;
|
|
382
667
|
}
|
|
383
|
-
const value =
|
|
668
|
+
const value = this.readValue(slice, capName, path, spec);
|
|
384
669
|
if (value === null) continue;
|
|
385
670
|
this.consider(deviceId, capName, path, spec, value, at, false);
|
|
386
671
|
}
|
|
387
672
|
}
|
|
673
|
+
/**
|
|
674
|
+
* The number a field contributes, or `null` (not a reading — never 0). An
|
|
675
|
+
* `enum` state becomes the ordinal of its value in the metric's list; a value
|
|
676
|
+
* the full list cannot take is refused and counted (`enum-overflow`).
|
|
677
|
+
*/
|
|
678
|
+
readValue(slice, capName, path, spec) {
|
|
679
|
+
if (spec.skipWhenSet !== void 0) {
|
|
680
|
+
const gate = readPath(slice, spec.skipWhenSet);
|
|
681
|
+
if (gate !== void 0 && gate !== null) return null;
|
|
682
|
+
}
|
|
683
|
+
if (spec.kind !== "enum") return readNumeric(slice, path, spec.kind);
|
|
684
|
+
const raw = readPath(slice, path);
|
|
685
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
686
|
+
const ordinal = this.deps.registry.ordinalOf(capSeriesKey(capName, path), raw);
|
|
687
|
+
if (ordinal === null) this.deps.counters.skip("enum-overflow");
|
|
688
|
+
return ordinal;
|
|
689
|
+
}
|
|
388
690
|
consider(deviceId, capName, path, spec, value, at, reconciling) {
|
|
389
691
|
const key = capSeriesKey(capName, path);
|
|
390
692
|
const subject = {
|
|
@@ -490,7 +792,7 @@ var CaptureEngine = class {
|
|
|
490
792
|
if (slice === void 0) continue;
|
|
491
793
|
for (const [path, spec] of Object.entries(fields)) {
|
|
492
794
|
if (spec.excludeDeviceTypes?.includes(type) === true) continue;
|
|
493
|
-
const value =
|
|
795
|
+
const value = this.readValue(slice, capName, path, spec);
|
|
494
796
|
if (value === null) continue;
|
|
495
797
|
this.consider(deviceId, capName, path, spec, value, now, true);
|
|
496
798
|
}
|
|
@@ -573,6 +875,7 @@ var PointWriter = class {
|
|
|
573
875
|
this.queue = [];
|
|
574
876
|
const { registry, store, counters, logger, clock } = this.deps;
|
|
575
877
|
try {
|
|
878
|
+
await registry.persistDirtyMetrics();
|
|
576
879
|
const bySeries = /* @__PURE__ */ new Map();
|
|
577
880
|
for (const point of batch) {
|
|
578
881
|
const series = await registry.ensure(point.key, point.subject, point.scope, point.ts);
|
|
@@ -664,52 +967,27 @@ var PointWriter = class {
|
|
|
664
967
|
}
|
|
665
968
|
};
|
|
666
969
|
//#endregion
|
|
667
|
-
//#region src/builtins/device-history/
|
|
970
|
+
//#region src/builtins/device-history/ingest/producer-ingest.ts
|
|
971
|
+
/** One point per series per this many ms, sustained. */
|
|
972
|
+
var RECORD_MIN_INTERVAL_MS = 1e4;
|
|
668
973
|
/**
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
*
|
|
673
|
-
*
|
|
674
|
-
* the environment here.
|
|
974
|
+
* The slot after accepting a point at `ts`. A point older than the newest
|
|
975
|
+
* accepted one (a late arrival) moves nothing. Otherwise the consumed slot is
|
|
976
|
+
* the later of the one that was due and `ts` itself: an early point uses the
|
|
977
|
+
* due slot (the grid holds, so slack never compounds into a higher rate), a
|
|
978
|
+
* late one restarts the grid at `ts` (no credit is banked for a gap).
|
|
675
979
|
*/
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
return d.getTime();
|
|
687
|
-
}
|
|
688
|
-
/** The local midnight following the local midnight `dayStart`. */
|
|
689
|
-
function nextLocalDayStart(dayStart) {
|
|
690
|
-
const d = new Date(dayStart);
|
|
691
|
-
d.setDate(d.getDate() + 1);
|
|
692
|
-
d.setHours(0, 0, 0, 0);
|
|
693
|
-
return d.getTime();
|
|
694
|
-
}
|
|
695
|
-
/** Start of the `tier` bucket containing `ts`. */
|
|
696
|
-
function bucketStartOf(tier, ts) {
|
|
697
|
-
if (tier === "5m") return floorTo(ts, FIVE_MINUTES_MS);
|
|
698
|
-
if (tier === "1h") return floorTo(ts, HOUR_MS);
|
|
699
|
-
return startOfLocalDay(ts);
|
|
700
|
-
}
|
|
701
|
-
/** Exclusive end of the `tier` bucket that starts at `start`. */
|
|
702
|
-
function bucketEndOf(tier, start) {
|
|
703
|
-
if (tier === "5m") return start + FIVE_MINUTES_MS;
|
|
704
|
-
if (tier === "1h") return start + HOUR_MS;
|
|
705
|
-
return nextLocalDayStart(start);
|
|
980
|
+
function nextSlot(slot, ts) {
|
|
981
|
+
if (slot === void 0) return {
|
|
982
|
+
lastTs: ts,
|
|
983
|
+
nextDue: ts + RECORD_MIN_INTERVAL_MS
|
|
984
|
+
};
|
|
985
|
+
if (ts < slot.lastTs) return slot;
|
|
986
|
+
return {
|
|
987
|
+
lastTs: ts,
|
|
988
|
+
nextDue: Math.max(slot.nextDue, ts) + RECORD_MIN_INTERVAL_MS
|
|
989
|
+
};
|
|
706
990
|
}
|
|
707
|
-
/** Quiet time after a bucket closes before it is rolled up (late writers). */
|
|
708
|
-
var ROLLUP_GRACE_MS = {
|
|
709
|
-
"5m": 3e4,
|
|
710
|
-
"1h": 5 * 6e4,
|
|
711
|
-
"1d": 15 * 6e4
|
|
712
|
-
};
|
|
713
991
|
function addRefusal(refusals, key, reason) {
|
|
714
992
|
const byReason = refusals.get(key) ?? /* @__PURE__ */ new Map();
|
|
715
993
|
byReason.set(reason, (byReason.get(reason) ?? 0) + 1);
|
|
@@ -727,7 +1005,7 @@ function toResult(accepted, refusals) {
|
|
|
727
1005
|
}
|
|
728
1006
|
var ProducerIngest = class {
|
|
729
1007
|
deps;
|
|
730
|
-
/**
|
|
1008
|
+
/** Rate state per series identity, for the sustained-rate bound. */
|
|
731
1009
|
lastAccepted = /* @__PURE__ */ new Map();
|
|
732
1010
|
constructor(deps) {
|
|
733
1011
|
this.deps = deps;
|
|
@@ -748,7 +1026,8 @@ var ProducerIngest = class {
|
|
|
748
1026
|
unit: input.unit ?? null,
|
|
749
1027
|
displayName: input.displayName,
|
|
750
1028
|
ingest: input.ingest,
|
|
751
|
-
declaredAt: this.deps.clock()
|
|
1029
|
+
declaredAt: this.deps.clock(),
|
|
1030
|
+
values: null
|
|
752
1031
|
};
|
|
753
1032
|
await this.deps.registry.declareMetric(metric);
|
|
754
1033
|
return { key };
|
|
@@ -785,13 +1064,13 @@ var ProducerIngest = class {
|
|
|
785
1064
|
continue;
|
|
786
1065
|
}
|
|
787
1066
|
const identity = identityOf(point.key, subjectIdOf(point.subject), point.scope);
|
|
788
|
-
const
|
|
789
|
-
if (
|
|
1067
|
+
const slot = this.lastAccepted.get(identity);
|
|
1068
|
+
if (slot !== void 0 && point.ts >= slot.lastTs && point.ts < slot.nextDue - 1e3) {
|
|
790
1069
|
addRefusal(refusals, point.key, "rate");
|
|
791
1070
|
counters.refuse("rate");
|
|
792
1071
|
continue;
|
|
793
1072
|
}
|
|
794
|
-
this.lastAccepted.set(identity,
|
|
1073
|
+
this.lastAccepted.set(identity, nextSlot(slot, point.ts));
|
|
795
1074
|
this.deps.writer.enqueue({
|
|
796
1075
|
key: point.key,
|
|
797
1076
|
subject: point.subject,
|
|
@@ -835,7 +1114,8 @@ var ProducerIngest = class {
|
|
|
835
1114
|
last: bucket.last,
|
|
836
1115
|
lastTs: bucket.lastTs,
|
|
837
1116
|
twSum: null,
|
|
838
|
-
twMs: null
|
|
1117
|
+
twMs: null,
|
|
1118
|
+
states: null
|
|
839
1119
|
});
|
|
840
1120
|
}
|
|
841
1121
|
await store.upsertRollups("5m", rows);
|
|
@@ -874,7 +1154,109 @@ var ProducerIngest = class {
|
|
|
874
1154
|
}
|
|
875
1155
|
};
|
|
876
1156
|
//#endregion
|
|
1157
|
+
//#region src/builtins/device-history/query/point-shape.ts
|
|
1158
|
+
/** kWh in one W·ms: 1 kWh = 1 000 W × 3 600 000 ms. */
|
|
1159
|
+
var KWH_PER_WATT_MS = 1 / 36e8;
|
|
1160
|
+
/** Growth from `prev` to `cur`; a drop is a reset counted from zero. `fallback` when `prev` is unknown. */
|
|
1161
|
+
function counterDelta(prev, cur, fallback) {
|
|
1162
|
+
if (cur === null) return null;
|
|
1163
|
+
if (prev === null) return fallback;
|
|
1164
|
+
return cur >= prev ? cur - prev : cur;
|
|
1165
|
+
}
|
|
1166
|
+
/** The name of an ordinal, or `null` when the series is not an enum or the ordinal is unknown. */
|
|
1167
|
+
function stateName(meaning, ordinal) {
|
|
1168
|
+
if (meaning.kind !== "enum" || meaning.values === null || ordinal === null) return null;
|
|
1169
|
+
return meaning.values[ordinal] ?? null;
|
|
1170
|
+
}
|
|
1171
|
+
function namedStates(meaning, states) {
|
|
1172
|
+
if (meaning.kind !== "enum" || states === null) return null;
|
|
1173
|
+
const out = {};
|
|
1174
|
+
for (const [ordinal, ms] of Object.entries(states)) {
|
|
1175
|
+
const name = stateName(meaning, Number(ordinal));
|
|
1176
|
+
if (name !== null) out[name] = (out[name] ?? 0) + ms;
|
|
1177
|
+
}
|
|
1178
|
+
return out;
|
|
1179
|
+
}
|
|
1180
|
+
/** Raw points, oldest first; `prev` is the value of the point before the first one. */
|
|
1181
|
+
function rawPoints(rows, meaning, prev) {
|
|
1182
|
+
const isEnum = meaning.kind === "enum";
|
|
1183
|
+
let held = prev;
|
|
1184
|
+
return rows.map((r) => {
|
|
1185
|
+
const delta = meaning.kind === "counter" ? counterDelta(held, r.value, null) : null;
|
|
1186
|
+
held = r.value;
|
|
1187
|
+
return {
|
|
1188
|
+
t: r.ts,
|
|
1189
|
+
samples: 1,
|
|
1190
|
+
mean: isEnum ? null : r.value,
|
|
1191
|
+
twMean: null,
|
|
1192
|
+
min: r.value,
|
|
1193
|
+
max: r.value,
|
|
1194
|
+
last: r.value,
|
|
1195
|
+
delta,
|
|
1196
|
+
state: stateName(meaning, r.value),
|
|
1197
|
+
states: null
|
|
1198
|
+
};
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
/** Rollup rows, oldest first; `prevLast` is the `last` of the bucket before the first one. */
|
|
1202
|
+
function rollupPoints(rows, meaning, prevLast) {
|
|
1203
|
+
const isEnum = meaning.kind === "enum";
|
|
1204
|
+
let held = prevLast;
|
|
1205
|
+
return rows.map((row) => {
|
|
1206
|
+
const delta = meaning.kind === "counter" ? counterDelta(held, row.last, row.last === null ? null : row.last - row.min) : null;
|
|
1207
|
+
if (row.last !== null) held = row.last;
|
|
1208
|
+
return {
|
|
1209
|
+
t: row.bucketStart,
|
|
1210
|
+
samples: row.samples,
|
|
1211
|
+
mean: isEnum || row.samples <= 0 ? null : row.sum / row.samples,
|
|
1212
|
+
twMean: !isEnum && row.twSum !== null && row.twMs !== null && row.twMs > 0 ? row.twSum / row.twMs : null,
|
|
1213
|
+
min: row.min,
|
|
1214
|
+
max: row.max,
|
|
1215
|
+
last: row.last,
|
|
1216
|
+
delta,
|
|
1217
|
+
state: stateName(meaning, row.last),
|
|
1218
|
+
states: namedStates(meaning, row.states)
|
|
1219
|
+
};
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
/** Energy used in each bucket of a `watts` gauge: its time-weighted integral, in kWh. */
|
|
1223
|
+
function integratedEnergyPoints(rows) {
|
|
1224
|
+
return rows.map((row) => ({
|
|
1225
|
+
t: row.bucketStart,
|
|
1226
|
+
samples: row.samples,
|
|
1227
|
+
mean: null,
|
|
1228
|
+
twMean: null,
|
|
1229
|
+
min: row.min,
|
|
1230
|
+
max: row.max,
|
|
1231
|
+
last: null,
|
|
1232
|
+
delta: row.twSum === null ? null : row.twSum * KWH_PER_WATT_MS,
|
|
1233
|
+
state: null,
|
|
1234
|
+
states: null
|
|
1235
|
+
}));
|
|
1236
|
+
}
|
|
1237
|
+
//#endregion
|
|
877
1238
|
//#region src/builtins/device-history/query/history-query.ts
|
|
1239
|
+
/**
|
|
1240
|
+
* Reads: `listSeries`, `query` and `queryMany` (plan §5.1).
|
|
1241
|
+
*
|
|
1242
|
+
* `auto` resolution picks the finest tier whose retention covers `from` and
|
|
1243
|
+
* whose point count fits `maxPoints`: a rollup tier fits when
|
|
1244
|
+
* `ceil(range / bucket) <= maxPoints`, raw fits when a read of `maxPoints + 1`
|
|
1245
|
+
* rows comes back short. The coarsest covering tier is the last resort and is
|
|
1246
|
+
* truncated to its newest `maxPoints` points (`truncated: true`) rather than
|
|
1247
|
+
* answering nothing. A series fed by closed buckets has NO raw rows, so raw is
|
|
1248
|
+
* never a candidate for it: an empty raw read "fits" every window.
|
|
1249
|
+
*
|
|
1250
|
+
* `before` is the last point strictly before the window in the chosen tier, so a
|
|
1251
|
+
* step chart can start at the value in force when the window opens — a quiet
|
|
1252
|
+
* bucket has no row, and the held value is otherwise invisible.
|
|
1253
|
+
*
|
|
1254
|
+
* {@link DEVICE_HISTORY_ENERGY_KEY} is computed here at read time, never stored:
|
|
1255
|
+
* the device's `kwhTotal` meter when it has one (per-bucket growth), else the
|
|
1256
|
+
* time-weighted integral of its `watts` (`tw_sum`, already in every rollup).
|
|
1257
|
+
* One place makes that choice, so two charts of one device cannot disagree, and
|
|
1258
|
+
* it covers every hour of history recorded before the key existed.
|
|
1259
|
+
*/
|
|
878
1260
|
var TIER_ORDER = [
|
|
879
1261
|
"raw",
|
|
880
1262
|
"5m",
|
|
@@ -888,46 +1270,101 @@ var BUCKET_MS = {
|
|
|
888
1270
|
};
|
|
889
1271
|
/** A key with more scopes than this is cut (a zone x class matrix, not a chart). */
|
|
890
1272
|
var MAX_SCOPES = 50;
|
|
1273
|
+
/** The power-meter series the energy key is derived from, meter first. */
|
|
1274
|
+
var ENERGY_METER_KEY = capSeriesKey("power-meter", "kwhTotal");
|
|
1275
|
+
var ENERGY_POWER_KEY = capSeriesKey("power-meter", "watts");
|
|
891
1276
|
function retentionMsOf(tier, policy) {
|
|
892
1277
|
if (tier === "raw") return policy.rawDays * DAY_MS;
|
|
893
1278
|
if (tier === "5m") return policy.fiveMinuteDays * DAY_MS;
|
|
894
1279
|
if (tier === "1h") return policy.hourDays * DAY_MS;
|
|
895
1280
|
return policy.dayDays === null || policy.dayDays <= 0 ? null : policy.dayDays * DAY_MS;
|
|
896
1281
|
}
|
|
897
|
-
|
|
1282
|
+
/** Rows arrive newest first and one over the bound; keep the newest `maxPoints`, oldest first. */
|
|
1283
|
+
function bounded(newestFirst, maxPoints) {
|
|
1284
|
+
const overflow = newestFirst.length > maxPoints;
|
|
898
1285
|
return {
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
mean: value,
|
|
902
|
-
twMean: null,
|
|
903
|
-
min: value,
|
|
904
|
-
max: value,
|
|
905
|
-
last: value
|
|
1286
|
+
rows: (overflow ? newestFirst.slice(0, maxPoints) : newestFirst).toReversed(),
|
|
1287
|
+
overflow
|
|
906
1288
|
};
|
|
907
1289
|
}
|
|
908
|
-
function
|
|
909
|
-
return
|
|
910
|
-
t: row.bucketStart,
|
|
911
|
-
samples: row.samples,
|
|
912
|
-
mean: row.samples > 0 ? row.sum / row.samples : null,
|
|
913
|
-
twMean: row.twSum !== null && row.twMs !== null && row.twMs > 0 ? row.twSum / row.twMs : null,
|
|
914
|
-
min: row.min,
|
|
915
|
-
max: row.max,
|
|
916
|
-
last: row.last
|
|
917
|
-
};
|
|
1290
|
+
function errorText(err) {
|
|
1291
|
+
return err instanceof Error ? err.message : String(err);
|
|
918
1292
|
}
|
|
919
1293
|
var HistoryQuery = class {
|
|
920
1294
|
deps;
|
|
921
1295
|
constructor(deps) {
|
|
922
1296
|
this.deps = deps;
|
|
923
1297
|
}
|
|
924
|
-
async query(input) {
|
|
925
|
-
|
|
926
|
-
const
|
|
927
|
-
const wanted =
|
|
928
|
-
const
|
|
1298
|
+
async query(input) {
|
|
1299
|
+
if (input.key === "derived:energy.kwh") return this.queryEnergy(input);
|
|
1300
|
+
const { registry } = this.deps;
|
|
1301
|
+
const wanted = this.seriesOf(input);
|
|
1302
|
+
const metric = registry.metric(input.key);
|
|
1303
|
+
const meaning = {
|
|
1304
|
+
kind: metric?.kind ?? "gauge",
|
|
1305
|
+
values: metric?.values ?? null
|
|
1306
|
+
};
|
|
1307
|
+
const { chosen, reads } = await this.choose(input, wanted, metric?.ingest === "buckets");
|
|
1308
|
+
const series = [];
|
|
1309
|
+
for (const [s, read] of reads) {
|
|
1310
|
+
const before = await this.before(chosen, s, input.from, meaning);
|
|
1311
|
+
const points = read.tier === "raw" ? rawPoints(read.rows, meaning, before?.value ?? null) : rollupPoints(read.rows, meaning, before?.value ?? null);
|
|
1312
|
+
series.push({
|
|
1313
|
+
scope: s.scope,
|
|
1314
|
+
before,
|
|
1315
|
+
points,
|
|
1316
|
+
derivedFrom: null
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
return {
|
|
1320
|
+
resolution: chosen,
|
|
1321
|
+
series,
|
|
1322
|
+
truncated: reads.some(([, r]) => r.overflow)
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
/** Several reads over one window; one failing item answers its error, not the batch's. */
|
|
1326
|
+
async queryMany(input) {
|
|
1327
|
+
const out = [];
|
|
1328
|
+
for (const item of input.items) {
|
|
1329
|
+
const subject = "deviceId" in item ? {
|
|
1330
|
+
kind: "device",
|
|
1331
|
+
deviceId: item.deviceId
|
|
1332
|
+
} : {
|
|
1333
|
+
kind: "node",
|
|
1334
|
+
nodeId: item.forNodeId
|
|
1335
|
+
};
|
|
1336
|
+
try {
|
|
1337
|
+
const result = await this.query({
|
|
1338
|
+
key: item.key,
|
|
1339
|
+
subject,
|
|
1340
|
+
...item.scope !== void 0 ? { scope: item.scope } : {},
|
|
1341
|
+
from: input.from,
|
|
1342
|
+
to: input.to,
|
|
1343
|
+
resolution: input.resolution,
|
|
1344
|
+
maxPoints: input.maxPoints
|
|
1345
|
+
});
|
|
1346
|
+
out.push({
|
|
1347
|
+
ok: true,
|
|
1348
|
+
result
|
|
1349
|
+
});
|
|
1350
|
+
} catch (err) {
|
|
1351
|
+
out.push({
|
|
1352
|
+
ok: false,
|
|
1353
|
+
error: errorText(err)
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
return out;
|
|
1358
|
+
}
|
|
1359
|
+
seriesOf(input) {
|
|
1360
|
+
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);
|
|
1361
|
+
}
|
|
1362
|
+
/** The tier to answer from and its reads. `noRaw`: the series has no raw rows to read. */
|
|
1363
|
+
async choose(input, wanted, noRaw) {
|
|
1364
|
+
const covering = this.coveringTiers(input.from);
|
|
1365
|
+
const candidates = noRaw ? withoutRaw(covering) : covering;
|
|
929
1366
|
const forced = input.resolution === "auto" ? null : input.resolution;
|
|
930
|
-
const order = forced === null ? candidates : [forced];
|
|
1367
|
+
const order = forced === null ? candidates : [noRaw && forced === "raw" ? "5m" : forced];
|
|
931
1368
|
let chosen = order[order.length - 1] ?? "1d";
|
|
932
1369
|
let reads = [];
|
|
933
1370
|
for (const [index, tier] of order.entries()) {
|
|
@@ -938,16 +1375,9 @@ var HistoryQuery = class {
|
|
|
938
1375
|
if (reads.some(([, r]) => r.overflow) && !last) continue;
|
|
939
1376
|
break;
|
|
940
1377
|
}
|
|
941
|
-
const series = [];
|
|
942
|
-
for (const [s, read] of reads) series.push({
|
|
943
|
-
scope: s.scope,
|
|
944
|
-
before: await this.before(chosen, s, input.from),
|
|
945
|
-
points: read.points
|
|
946
|
-
});
|
|
947
1378
|
return {
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
truncated: reads.some(([, r]) => r.overflow)
|
|
1379
|
+
chosen,
|
|
1380
|
+
reads
|
|
951
1381
|
};
|
|
952
1382
|
}
|
|
953
1383
|
/** Tiers whose retention covers `from`, finest first; the coarsest is always present. */
|
|
@@ -969,35 +1399,79 @@ var HistoryQuery = class {
|
|
|
969
1399
|
async readTier(tier, series, input) {
|
|
970
1400
|
const { store } = this.deps;
|
|
971
1401
|
const limit = input.maxPoints + 1;
|
|
972
|
-
if (tier === "raw") {
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
}
|
|
1402
|
+
if (tier === "raw") return {
|
|
1403
|
+
tier,
|
|
1404
|
+
...bounded(await store.readRaw(series.id, input.from, input.to, limit, "desc"), input.maxPoints)
|
|
1405
|
+
};
|
|
976
1406
|
const lower = bucketStartOf(tier, input.from);
|
|
977
|
-
const rows = await store.readRollups(tier, series.id, lower, input.to, limit, "desc");
|
|
978
|
-
return this.shape(rows.map(rollupPoint), input.maxPoints);
|
|
979
|
-
}
|
|
980
|
-
/** Rows arrive newest first and one over the bound; keep the newest `maxPoints`, oldest first. */
|
|
981
|
-
shape(newestFirst, maxPoints) {
|
|
982
|
-
const overflow = newestFirst.length > maxPoints;
|
|
983
1407
|
return {
|
|
984
|
-
|
|
985
|
-
|
|
1408
|
+
tier,
|
|
1409
|
+
...bounded(await store.readRollups(tier, series.id, lower, input.to, limit, "desc"), input.maxPoints)
|
|
986
1410
|
};
|
|
987
1411
|
}
|
|
988
|
-
async before(tier, series, from) {
|
|
1412
|
+
async before(tier, series, from, meaning) {
|
|
989
1413
|
const { store } = this.deps;
|
|
990
1414
|
if (tier === "raw") {
|
|
991
1415
|
const p = await store.rawBefore(series.id, from);
|
|
992
1416
|
return p === null ? null : {
|
|
993
1417
|
t: p.ts,
|
|
994
|
-
value: p.value
|
|
1418
|
+
value: p.value,
|
|
1419
|
+
state: stateName(meaning, p.value)
|
|
995
1420
|
};
|
|
996
1421
|
}
|
|
997
1422
|
const row = await store.rollupBefore(tier, series.id, bucketStartOf(tier, from));
|
|
998
1423
|
return row === null ? null : {
|
|
999
1424
|
t: row.bucketStart,
|
|
1000
|
-
value: row.last
|
|
1425
|
+
value: row.last,
|
|
1426
|
+
state: stateName(meaning, row.last)
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
/** The series energy is derived from for a device: the meter when it has one, else its power. */
|
|
1430
|
+
energySource(subject) {
|
|
1431
|
+
if (subject.kind !== "device") return null;
|
|
1432
|
+
const subjectId = subjectIdOf(subject);
|
|
1433
|
+
const meter = this.deps.registry.find(ENERGY_METER_KEY, subjectId, "");
|
|
1434
|
+
if (meter !== void 0) return {
|
|
1435
|
+
series: meter,
|
|
1436
|
+
from: "meter"
|
|
1437
|
+
};
|
|
1438
|
+
const power = this.deps.registry.find(ENERGY_POWER_KEY, subjectId, "");
|
|
1439
|
+
return power === void 0 ? null : {
|
|
1440
|
+
series: power,
|
|
1441
|
+
from: "power"
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
async queryEnergy(input) {
|
|
1445
|
+
const source = this.energySource(input.subject);
|
|
1446
|
+
if (source === null || input.scope !== void 0 && input.scope !== "") return {
|
|
1447
|
+
resolution: "5m",
|
|
1448
|
+
series: [],
|
|
1449
|
+
truncated: false
|
|
1450
|
+
};
|
|
1451
|
+
const { chosen, reads } = await this.choose(input, [source.series], true);
|
|
1452
|
+
const read = reads[0]?.[1];
|
|
1453
|
+
if (read === void 0 || read.tier === "raw") return {
|
|
1454
|
+
resolution: chosen,
|
|
1455
|
+
series: [],
|
|
1456
|
+
truncated: false
|
|
1457
|
+
};
|
|
1458
|
+
let points;
|
|
1459
|
+
if (source.from === "meter") {
|
|
1460
|
+
const prev = await this.deps.store.rollupBefore(read.tier, source.series.id, bucketStartOf(read.tier, input.from));
|
|
1461
|
+
points = rollupPoints(read.rows, {
|
|
1462
|
+
kind: "counter",
|
|
1463
|
+
values: null
|
|
1464
|
+
}, prev?.last ?? null);
|
|
1465
|
+
} else points = integratedEnergyPoints(read.rows);
|
|
1466
|
+
return {
|
|
1467
|
+
resolution: chosen,
|
|
1468
|
+
series: [{
|
|
1469
|
+
scope: "",
|
|
1470
|
+
before: null,
|
|
1471
|
+
points,
|
|
1472
|
+
derivedFrom: source.series.key
|
|
1473
|
+
}],
|
|
1474
|
+
truncated: read.overflow
|
|
1001
1475
|
};
|
|
1002
1476
|
}
|
|
1003
1477
|
async listSeries(subject) {
|
|
@@ -1014,10 +1488,23 @@ var HistoryQuery = class {
|
|
|
1014
1488
|
unit: await this.unitOf(subject, s, metric.unit, slices),
|
|
1015
1489
|
displayName: metric.displayName,
|
|
1016
1490
|
source: metric.source,
|
|
1491
|
+
values: metric.kind === "enum" ? metric.values ?? [] : null,
|
|
1017
1492
|
firstTs: s.firstTs,
|
|
1018
1493
|
lastTs: s.lastTs
|
|
1019
1494
|
});
|
|
1020
1495
|
}
|
|
1496
|
+
const energy = this.energySource(subject);
|
|
1497
|
+
if (energy !== null) infos.push({
|
|
1498
|
+
key: require_dist.DEVICE_HISTORY_ENERGY_KEY,
|
|
1499
|
+
scope: "",
|
|
1500
|
+
kind: "counter",
|
|
1501
|
+
unit: "kWh",
|
|
1502
|
+
displayName: "Energy",
|
|
1503
|
+
source: "derived",
|
|
1504
|
+
values: null,
|
|
1505
|
+
firstTs: energy.series.firstTs,
|
|
1506
|
+
lastTs: energy.series.lastTs
|
|
1507
|
+
});
|
|
1021
1508
|
return infos.toSorted((a, b) => a.key.localeCompare(b.key) || a.scope.localeCompare(b.scope));
|
|
1022
1509
|
}
|
|
1023
1510
|
/** The metric's fixed unit, else the device's own (a `unitFrom` field), else unknown. */
|
|
@@ -1038,191 +1525,11 @@ var HistoryQuery = class {
|
|
|
1038
1525
|
return this.deps.catalog[body.slice(0, dot)]?.[body.slice(dot + 1)];
|
|
1039
1526
|
}
|
|
1040
1527
|
};
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
store;
|
|
1046
|
-
registry;
|
|
1047
|
-
logger;
|
|
1048
|
-
missing = /* @__PURE__ */ new Map();
|
|
1049
|
-
constructor(store, registry, logger) {
|
|
1050
|
-
this.store = store;
|
|
1051
|
-
this.registry = registry;
|
|
1052
|
-
this.logger = logger;
|
|
1053
|
-
}
|
|
1054
|
-
async load() {
|
|
1055
|
-
this.missing = await this.store.loadDepartures();
|
|
1056
|
-
}
|
|
1057
|
-
missingSince(deviceId) {
|
|
1058
|
-
return this.missing.get(deviceId);
|
|
1059
|
-
}
|
|
1060
|
-
/** Forget a device's clock — its series were removed because it was DELETED (D865). */
|
|
1061
|
-
async forget(deviceId) {
|
|
1062
|
-
await this.store.clearMissing([deviceId]);
|
|
1063
|
-
const next = new Map(this.missing);
|
|
1064
|
-
next.delete(deviceId);
|
|
1065
|
-
this.missing = next;
|
|
1066
|
-
}
|
|
1067
|
-
/** Feed one SUCCESSFUL listing. A failed read never reaches here. */
|
|
1068
|
-
async observe(listed, now) {
|
|
1069
|
-
const withSeries = new Set(this.registry.all().flatMap((s) => s.deviceId === null ? [] : [s.deviceId]));
|
|
1070
|
-
const back = [...this.missing.keys()].filter((id) => listed.has(id));
|
|
1071
|
-
const gone = [...withSeries].filter((id) => !listed.has(id) && !this.missing.has(id));
|
|
1072
|
-
try {
|
|
1073
|
-
await this.store.clearMissing(back);
|
|
1074
|
-
await this.store.markMissing(gone, now);
|
|
1075
|
-
} catch (err) {
|
|
1076
|
-
this.logger.warn("device-history: departure clock not written", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
1077
|
-
return;
|
|
1078
|
-
}
|
|
1079
|
-
const next = new Map(this.missing);
|
|
1080
|
-
for (const id of back) next.delete(id);
|
|
1081
|
-
for (const id of gone) next.set(id, now);
|
|
1082
|
-
this.missing = next;
|
|
1083
|
-
}
|
|
1084
|
-
};
|
|
1085
|
-
//#endregion
|
|
1086
|
-
//#region src/builtins/device-history/retention/retention.ts
|
|
1087
|
-
var DEFAULT_RETENTION = {
|
|
1088
|
-
rawDays: 7,
|
|
1089
|
-
fiveMinuteDays: 14,
|
|
1090
|
-
hourDays: 365,
|
|
1091
|
-
dayDays: null
|
|
1092
|
-
};
|
|
1093
|
-
/** Cutoff per tier: rows strictly older are deleted. `null` = keep forever. */
|
|
1094
|
-
function retentionCutoffs(policy, now) {
|
|
1095
|
-
return {
|
|
1096
|
-
raw: now - policy.rawDays * DAY_MS,
|
|
1097
|
-
"5m": now - policy.fiveMinuteDays * DAY_MS,
|
|
1098
|
-
"1h": now - policy.hourDays * DAY_MS,
|
|
1099
|
-
"1d": policy.dayDays === null || policy.dayDays <= 0 ? null : now - policy.dayDays * DAY_MS
|
|
1100
|
-
};
|
|
1528
|
+
/** Every covering tier but raw; the coarsest stays when it is the only one. */
|
|
1529
|
+
function withoutRaw(tiers) {
|
|
1530
|
+
const rollups = tiers.filter((t) => t !== "raw");
|
|
1531
|
+
return rollups.length > 0 ? rollups : ["5m"];
|
|
1101
1532
|
}
|
|
1102
|
-
var TIERS = [
|
|
1103
|
-
"raw",
|
|
1104
|
-
"5m",
|
|
1105
|
-
"1h",
|
|
1106
|
-
"1d"
|
|
1107
|
-
];
|
|
1108
|
-
var RETENTION_STEP_MS = {
|
|
1109
|
-
raw: HOUR_MS,
|
|
1110
|
-
"5m": HOUR_MS,
|
|
1111
|
-
"1h": DAY_MS,
|
|
1112
|
-
"1d": DAY_MS
|
|
1113
|
-
};
|
|
1114
|
-
var MAX_PURGES_PER_RUN = 20;
|
|
1115
|
-
/** Width of one delete statement's time range, per tier. */
|
|
1116
|
-
var SLICE_MS = {
|
|
1117
|
-
raw: DAY_MS,
|
|
1118
|
-
"5m": DAY_MS,
|
|
1119
|
-
"1h": 30 * DAY_MS,
|
|
1120
|
-
"1d": 30 * DAY_MS
|
|
1121
|
-
};
|
|
1122
|
-
/** A bucket's start may precede the series' first point by up to a day. */
|
|
1123
|
-
var BUCKET_LEAD_MS = 2 * DAY_MS;
|
|
1124
|
-
/** The short tiers hold nothing older than this unless the operator configured more. */
|
|
1125
|
-
var SHORT_TIER_LOOKBACK_MS = 400 * DAY_MS;
|
|
1126
|
-
var yieldToEventLoop = () => new Promise((resolve) => setImmediate(resolve));
|
|
1127
|
-
var RetentionJob = class {
|
|
1128
|
-
deps;
|
|
1129
|
-
appliedUpTo = /* @__PURE__ */ new Map();
|
|
1130
|
-
running = null;
|
|
1131
|
-
constructor(deps) {
|
|
1132
|
-
this.deps = deps;
|
|
1133
|
-
}
|
|
1134
|
-
run(policy, now) {
|
|
1135
|
-
if (this.running !== null) return this.running;
|
|
1136
|
-
const run = this.doRun(policy, now).finally(() => {
|
|
1137
|
-
this.running = null;
|
|
1138
|
-
});
|
|
1139
|
-
this.running = run;
|
|
1140
|
-
return run;
|
|
1141
|
-
}
|
|
1142
|
-
async doRun(policy, now) {
|
|
1143
|
-
const { registry, counters, logger } = this.deps;
|
|
1144
|
-
const cutoffs = retentionCutoffs(policy, now);
|
|
1145
|
-
try {
|
|
1146
|
-
for (const tier of TIERS) {
|
|
1147
|
-
const cutoff = cutoffs[tier];
|
|
1148
|
-
if (cutoff === null) continue;
|
|
1149
|
-
for (const series of registry.all()) {
|
|
1150
|
-
if (series.firstTs - 864e5 >= cutoff) continue;
|
|
1151
|
-
if (tier === "raw" && registry.metric(series.key)?.ingest === "buckets") continue;
|
|
1152
|
-
const marker = `${tier}:${series.id}`;
|
|
1153
|
-
const applied = this.appliedUpTo.get(marker);
|
|
1154
|
-
if (applied !== void 0 && cutoff - applied < RETENTION_STEP_MS[tier]) continue;
|
|
1155
|
-
const lo = Math.max(0, applied === void 0 ? series.firstTs - BUCKET_LEAD_MS : applied - RETENTION_STEP_MS[tier]);
|
|
1156
|
-
counters.addDeleted(tier, await this.deleteSliced(tier, series.id, lo, cutoff - 1));
|
|
1157
|
-
this.appliedUpTo.set(marker, cutoff);
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
await this.purgeDeparted(now);
|
|
1161
|
-
counters.retentionLastRunAt = now;
|
|
1162
|
-
} catch (err) {
|
|
1163
|
-
logger.warn("device-history: retention pass failed — retried at the next run", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
/** Delete `[lo, hiInclusive]` of one tier in time slices, yielding between them. */
|
|
1167
|
-
/** Drop the visit markers of a purged series. */
|
|
1168
|
-
forgetSeries(seriesId) {
|
|
1169
|
-
for (const tier of TIERS) this.appliedUpTo.delete(`${tier}:${seriesId}`);
|
|
1170
|
-
}
|
|
1171
|
-
async deleteSliced(tier, seriesId, lo, hiInclusive) {
|
|
1172
|
-
const yieldNow = this.deps.yieldNow ?? yieldToEventLoop;
|
|
1173
|
-
let deleted = 0;
|
|
1174
|
-
for (let from = lo; from <= hiInclusive; from += SLICE_MS[tier]) {
|
|
1175
|
-
const to = Math.min(from + SLICE_MS[tier] - 1, hiInclusive);
|
|
1176
|
-
deleted += await this.deps.store.deleteRange(tier, seriesId, from, to);
|
|
1177
|
-
await yieldNow();
|
|
1178
|
-
}
|
|
1179
|
-
return deleted;
|
|
1180
|
-
}
|
|
1181
|
-
/** Tier by tier in slices, then the series row (and whatever the slices missed). */
|
|
1182
|
-
async purgeSeries(series, now) {
|
|
1183
|
-
for (const tier of TIERS) {
|
|
1184
|
-
const lo = tier === "raw" || tier === "5m" ? Math.max(series.firstTs - BUCKET_LEAD_MS, now - SHORT_TIER_LOOKBACK_MS) : series.firstTs - BUCKET_LEAD_MS;
|
|
1185
|
-
await this.deleteSliced(tier, series.id, Math.max(0, lo), now + DAY_MS);
|
|
1186
|
-
}
|
|
1187
|
-
await this.deps.store.deleteSeries(series.id);
|
|
1188
|
-
}
|
|
1189
|
-
/**
|
|
1190
|
-
* Delete every series of a DELETED device now (D865). Driven by a tombstone
|
|
1191
|
-
* through the `device-removal-hook`, never by a listing: the week-long
|
|
1192
|
-
* absence clock above remains the backstop for a device removed before
|
|
1193
|
-
* tombstones existed.
|
|
1194
|
-
*/
|
|
1195
|
-
async purgeDevice(deviceId, now) {
|
|
1196
|
-
const { registry, counters } = this.deps;
|
|
1197
|
-
let purged = 0;
|
|
1198
|
-
for (const series of registry.all()) {
|
|
1199
|
-
if (series.deviceId !== deviceId) continue;
|
|
1200
|
-
await this.purgeSeries(series, now);
|
|
1201
|
-
registry.remove(series.id);
|
|
1202
|
-
counters.purgedSeries += 1;
|
|
1203
|
-
purged += 1;
|
|
1204
|
-
}
|
|
1205
|
-
return purged;
|
|
1206
|
-
}
|
|
1207
|
-
async purgeDeparted(now) {
|
|
1208
|
-
const { registry, departures, counters, logger } = this.deps;
|
|
1209
|
-
let purged = 0;
|
|
1210
|
-
for (const series of registry.all()) {
|
|
1211
|
-
if (purged >= MAX_PURGES_PER_RUN) break;
|
|
1212
|
-
if (series.deviceId === null) continue;
|
|
1213
|
-
const since = departures.missingSince(series.deviceId);
|
|
1214
|
-
if (since === void 0 || now - since < DEPARTED_ABSENCE_MS) continue;
|
|
1215
|
-
await this.purgeSeries(series, now);
|
|
1216
|
-
registry.remove(series.id);
|
|
1217
|
-
counters.purgedSeries += 1;
|
|
1218
|
-
purged += 1;
|
|
1219
|
-
logger.info("device-history: purged the series of an unregistered device", {
|
|
1220
|
-
tags: { deviceId: series.deviceId },
|
|
1221
|
-
meta: { key: series.key }
|
|
1222
|
-
});
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
};
|
|
1226
1533
|
//#endregion
|
|
1227
1534
|
//#region src/builtins/device-history/store/history-schema.ts
|
|
1228
1535
|
var HISTORY_NAMESPACE = "device-history";
|
|
@@ -1292,6 +1599,10 @@ function rollupDeclaration(collection) {
|
|
|
1292
1599
|
{
|
|
1293
1600
|
name: "tw_ms",
|
|
1294
1601
|
type: "INTEGER"
|
|
1602
|
+
},
|
|
1603
|
+
{
|
|
1604
|
+
name: "state_ms",
|
|
1605
|
+
type: "TEXT"
|
|
1295
1606
|
}
|
|
1296
1607
|
]
|
|
1297
1608
|
};
|
|
@@ -1346,6 +1657,10 @@ var HISTORY_DECLARATIONS = [
|
|
|
1346
1657
|
name: "declared_at",
|
|
1347
1658
|
type: "INTEGER",
|
|
1348
1659
|
notNull: true
|
|
1660
|
+
},
|
|
1661
|
+
{
|
|
1662
|
+
name: "enum_values",
|
|
1663
|
+
type: "TEXT"
|
|
1349
1664
|
}
|
|
1350
1665
|
]
|
|
1351
1666
|
},
|
|
@@ -1519,9 +1834,34 @@ function chunks(items, size) {
|
|
|
1519
1834
|
return out;
|
|
1520
1835
|
}
|
|
1521
1836
|
function parseKind(value) {
|
|
1522
|
-
if (value === "gauge" || value === "counter" || value === "boolean") return value;
|
|
1837
|
+
if (value === "gauge" || value === "counter" || value === "boolean" || value === "enum") return value;
|
|
1523
1838
|
throw new Error(`history row: unknown kind "${value}"`);
|
|
1524
1839
|
}
|
|
1840
|
+
/** A JSON TEXT column: the engine hands back the decoded value, or the text itself. */
|
|
1841
|
+
function jsonColumn(data, column) {
|
|
1842
|
+
const raw = data[column];
|
|
1843
|
+
if (raw === null || raw === void 0) return null;
|
|
1844
|
+
return typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
1845
|
+
}
|
|
1846
|
+
/** A JSON column that must hold a string array, or nothing. */
|
|
1847
|
+
function parseValues(data) {
|
|
1848
|
+
const parsed = jsonColumn(data, "enum_values");
|
|
1849
|
+
if (parsed === null) return null;
|
|
1850
|
+
if (Array.isArray(parsed) && parsed.every((v) => typeof v === "string")) return parsed;
|
|
1851
|
+
throw new Error("history row: column \"enum_values\" is not a JSON string array");
|
|
1852
|
+
}
|
|
1853
|
+
/** A JSON column that must hold `{ "<ordinal>": ms }`, or nothing. */
|
|
1854
|
+
function parseStates(data) {
|
|
1855
|
+
const parsed = jsonColumn(data, "state_ms");
|
|
1856
|
+
if (parsed === null) return null;
|
|
1857
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("history row: column \"state_ms\" is not a JSON object");
|
|
1858
|
+
const out = {};
|
|
1859
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
1860
|
+
if (typeof v !== "number" || !Number.isFinite(v)) throw new Error("history row: column \"state_ms\" holds a non-number");
|
|
1861
|
+
out[k] = v;
|
|
1862
|
+
}
|
|
1863
|
+
return out;
|
|
1864
|
+
}
|
|
1525
1865
|
function parseMetric(data) {
|
|
1526
1866
|
const source = readText(data, "source");
|
|
1527
1867
|
const subjectKind = readText(data, "subject_kind");
|
|
@@ -1538,7 +1878,8 @@ function parseMetric(data) {
|
|
|
1538
1878
|
unit: readTextOrNull(data, "unit"),
|
|
1539
1879
|
displayName: readText(data, "display_name"),
|
|
1540
1880
|
ingest,
|
|
1541
|
-
declaredAt: readNumber(data, "declared_at")
|
|
1881
|
+
declaredAt: readNumber(data, "declared_at"),
|
|
1882
|
+
values: parseValues(data)
|
|
1542
1883
|
};
|
|
1543
1884
|
}
|
|
1544
1885
|
function parseSeries(data) {
|
|
@@ -1563,7 +1904,8 @@ function parseRollup(data) {
|
|
|
1563
1904
|
last: readNumberOrNull(data, "last"),
|
|
1564
1905
|
lastTs: readNumberOrNull(data, "last_ts"),
|
|
1565
1906
|
twSum: readNumberOrNull(data, "tw_sum"),
|
|
1566
|
-
twMs: readNumberOrNull(data, "tw_ms")
|
|
1907
|
+
twMs: readNumberOrNull(data, "tw_ms"),
|
|
1908
|
+
states: parseStates(data)
|
|
1567
1909
|
};
|
|
1568
1910
|
}
|
|
1569
1911
|
function rollupData(row) {
|
|
@@ -1577,7 +1919,8 @@ function rollupData(row) {
|
|
|
1577
1919
|
last: row.last,
|
|
1578
1920
|
last_ts: row.lastTs,
|
|
1579
1921
|
tw_sum: row.twSum,
|
|
1580
|
-
tw_ms: row.twMs
|
|
1922
|
+
tw_ms: row.twMs,
|
|
1923
|
+
state_ms: row.states === null ? null : JSON.stringify(row.states)
|
|
1581
1924
|
};
|
|
1582
1925
|
}
|
|
1583
1926
|
function collectionOf(tier) {
|
|
@@ -1601,22 +1944,37 @@ var HistoryStore = class {
|
|
|
1601
1944
|
filter: { limit: 2e4 }
|
|
1602
1945
|
})).map((r) => parseMetric(r.data));
|
|
1603
1946
|
}
|
|
1947
|
+
/**
|
|
1948
|
+
* Delete metric rows no lookup can reach: rows an earlier build wrote under an
|
|
1949
|
+
* engine-minted random key (every metric key is `cap:` / `addon:` prefixed).
|
|
1950
|
+
*/
|
|
1951
|
+
async deleteMetrics(keys) {
|
|
1952
|
+
for (const batch of chunks(keys, WRITE_CHUNK)) await this.door.deleteWhere.mutate({
|
|
1953
|
+
namespace: HISTORY_NAMESPACE,
|
|
1954
|
+
collection: COLLECTIONS.metrics,
|
|
1955
|
+
filter: { whereIn: { key: [...batch] } }
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1604
1958
|
async upsertMetric(metric) {
|
|
1605
1959
|
await this.door.upsertMany.mutate({
|
|
1606
1960
|
namespace: HISTORY_NAMESPACE,
|
|
1607
1961
|
collection: COLLECTIONS.metrics,
|
|
1608
1962
|
conflict: ["key"],
|
|
1609
|
-
records: [{
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1963
|
+
records: [{
|
|
1964
|
+
id: metric.key,
|
|
1965
|
+
data: {
|
|
1966
|
+
key: metric.key,
|
|
1967
|
+
source: metric.source,
|
|
1968
|
+
owner_addon: metric.ownerAddon,
|
|
1969
|
+
subject_kind: metric.subjectKind,
|
|
1970
|
+
kind: metric.kind,
|
|
1971
|
+
unit: metric.unit,
|
|
1972
|
+
display_name: metric.displayName,
|
|
1973
|
+
ingest: metric.ingest,
|
|
1974
|
+
declared_at: metric.declaredAt,
|
|
1975
|
+
enum_values: metric.values === null ? null : JSON.stringify(metric.values)
|
|
1976
|
+
}
|
|
1977
|
+
}]
|
|
1620
1978
|
});
|
|
1621
1979
|
}
|
|
1622
1980
|
/** Series with `id > afterId`, ascending — the boot load pages with this. */
|
|
@@ -1863,6 +2221,11 @@ var HistoryStore = class {
|
|
|
1863
2221
|
* The held value also counts toward `min`/`max` when it was in force at the
|
|
1864
2222
|
* start of a bucket, because it really was the series' value then.
|
|
1865
2223
|
*/
|
|
2224
|
+
function credit(states, value, ms) {
|
|
2225
|
+
if (states === null || ms <= 0) return;
|
|
2226
|
+
const key = String(value);
|
|
2227
|
+
states[key] = (states[key] ?? 0) + ms;
|
|
2228
|
+
}
|
|
1866
2229
|
/** One row per 5-minute bucket that has at least one point. */
|
|
1867
2230
|
function rawToFiveMinuteRows(input) {
|
|
1868
2231
|
const rows = [];
|
|
@@ -1881,6 +2244,7 @@ function rawToFiveMinuteRows(input) {
|
|
|
1881
2244
|
let max = Number.NEGATIVE_INFINITY;
|
|
1882
2245
|
let twSum = 0;
|
|
1883
2246
|
let twMs = 0;
|
|
2247
|
+
const states = input.trackStates === true ? {} : null;
|
|
1884
2248
|
let cursor = bucketStart;
|
|
1885
2249
|
let value = held;
|
|
1886
2250
|
if (value !== null) {
|
|
@@ -1895,6 +2259,7 @@ function rawToFiveMinuteRows(input) {
|
|
|
1895
2259
|
const dt = p.ts - cursor;
|
|
1896
2260
|
twSum += value * dt;
|
|
1897
2261
|
twMs += dt;
|
|
2262
|
+
credit(states, value, dt);
|
|
1898
2263
|
}
|
|
1899
2264
|
cursor = p.ts;
|
|
1900
2265
|
value = p.value;
|
|
@@ -1908,6 +2273,7 @@ function rawToFiveMinuteRows(input) {
|
|
|
1908
2273
|
if (value !== null && coverageEnd > cursor) {
|
|
1909
2274
|
twSum += value * (coverageEnd - cursor);
|
|
1910
2275
|
twMs += coverageEnd - cursor;
|
|
2276
|
+
credit(states, value, coverageEnd - cursor);
|
|
1911
2277
|
}
|
|
1912
2278
|
held = last.value;
|
|
1913
2279
|
rows.push({
|
|
@@ -1920,7 +2286,8 @@ function rawToFiveMinuteRows(input) {
|
|
|
1920
2286
|
last: last.value,
|
|
1921
2287
|
lastTs: last.ts,
|
|
1922
2288
|
twSum,
|
|
1923
|
-
twMs
|
|
2289
|
+
twMs,
|
|
2290
|
+
states
|
|
1924
2291
|
});
|
|
1925
2292
|
}
|
|
1926
2293
|
return rows;
|
|
@@ -1937,6 +2304,7 @@ function combineRows(input) {
|
|
|
1937
2304
|
let max = Number.NEGATIVE_INFINITY;
|
|
1938
2305
|
let twSum = 0;
|
|
1939
2306
|
let twMs = 0;
|
|
2307
|
+
const states = input.trackStates === true ? {} : null;
|
|
1940
2308
|
let held = input.carryIn;
|
|
1941
2309
|
let cursor = input.windowStart;
|
|
1942
2310
|
for (const row of rows) {
|
|
@@ -1948,6 +2316,7 @@ function combineRows(input) {
|
|
|
1948
2316
|
if (hasTw && gap > 0 && held !== null) {
|
|
1949
2317
|
twSum += held * gap;
|
|
1950
2318
|
twMs += gap;
|
|
2319
|
+
credit(states, held, gap);
|
|
1951
2320
|
if (held < min) min = held;
|
|
1952
2321
|
if (held > max) max = held;
|
|
1953
2322
|
}
|
|
@@ -1955,6 +2324,7 @@ function combineRows(input) {
|
|
|
1955
2324
|
twSum += row.twSum;
|
|
1956
2325
|
twMs += row.twMs;
|
|
1957
2326
|
}
|
|
2327
|
+
if (states !== null && row.states !== null) for (const [state, ms] of Object.entries(row.states)) states[state] = (states[state] ?? 0) + ms;
|
|
1958
2328
|
cursor = row.bucketStart + input.childWidthMs;
|
|
1959
2329
|
held = row.last;
|
|
1960
2330
|
}
|
|
@@ -1962,6 +2332,7 @@ function combineRows(input) {
|
|
|
1962
2332
|
if (hasTw && held !== null && coverageEnd > cursor) {
|
|
1963
2333
|
twSum += held * (coverageEnd - cursor);
|
|
1964
2334
|
twMs += coverageEnd - cursor;
|
|
2335
|
+
credit(states, held, coverageEnd - cursor);
|
|
1965
2336
|
}
|
|
1966
2337
|
return {
|
|
1967
2338
|
seriesId: input.seriesId,
|
|
@@ -1973,7 +2344,8 @@ function combineRows(input) {
|
|
|
1973
2344
|
last: lastRow.last,
|
|
1974
2345
|
lastTs: lastRow.lastTs,
|
|
1975
2346
|
twSum: hasTw ? twSum : null,
|
|
1976
|
-
twMs: hasTw ? twMs : null
|
|
2347
|
+
twMs: hasTw ? twMs : null,
|
|
2348
|
+
states
|
|
1977
2349
|
};
|
|
1978
2350
|
}
|
|
1979
2351
|
/** How far back a boot rebuilds from raw and re-marks the upper tiers. */
|
|
@@ -2114,6 +2486,11 @@ var RollupEngine = class {
|
|
|
2114
2486
|
}
|
|
2115
2487
|
if (handled > 0) this.deps.counters.last1dAt = now;
|
|
2116
2488
|
}
|
|
2489
|
+
/** An `enum` series keeps the time spent in each state in its rollups. */
|
|
2490
|
+
isEnum(seriesId) {
|
|
2491
|
+
const series = this.deps.registry.byIdOf(seriesId);
|
|
2492
|
+
return series !== void 0 && this.deps.registry.metric(series.key)?.kind === "enum";
|
|
2493
|
+
}
|
|
2117
2494
|
/** Raw → 5m rows for buckets in `[from, toExclusive)`; writes them and marks their hours. */
|
|
2118
2495
|
async recomputeFiveMinute(seriesId, from, toExclusive, horizon) {
|
|
2119
2496
|
const { store } = this.deps;
|
|
@@ -2130,7 +2507,8 @@ var RollupEngine = class {
|
|
|
2130
2507
|
seriesId,
|
|
2131
2508
|
points,
|
|
2132
2509
|
carryIn: await store.rawBefore(seriesId, from),
|
|
2133
|
-
horizon
|
|
2510
|
+
horizon,
|
|
2511
|
+
trackStates: this.isEnum(seriesId)
|
|
2134
2512
|
});
|
|
2135
2513
|
if (rows.length === 0) return 0;
|
|
2136
2514
|
await store.upsertRollups("5m", rows);
|
|
@@ -2154,7 +2532,8 @@ var RollupEngine = class {
|
|
|
2154
2532
|
childWidthMs,
|
|
2155
2533
|
rows: await store.readRollups(child, seriesId, windowStart, windowEnd - 1, READ_PAGE),
|
|
2156
2534
|
carryIn: (await store.rollupBefore(child, seriesId, windowStart))?.last ?? null,
|
|
2157
|
-
horizon: now
|
|
2535
|
+
horizon: now,
|
|
2536
|
+
trackStates: this.isEnum(seriesId)
|
|
2158
2537
|
});
|
|
2159
2538
|
if (combined === null) return;
|
|
2160
2539
|
await store.upsertRollups(parent, [combined]);
|
|
@@ -2614,6 +2993,13 @@ var HistoryRuntime = class {
|
|
|
2614
2993
|
* are hub capabilities, and a forked runner cannot call them from
|
|
2615
2994
|
* `onInitialize`. Until then the provider answers "starting" rather than an
|
|
2616
2995
|
* empty history (an unanswered query is not an empty one, D315).
|
|
2996
|
+
*
|
|
2997
|
+
* Readiness follows the STORE, not the registration: the provider registers at
|
|
2998
|
+
* `onInitialize`, but `ready` is emitted only once the runtime is open in
|
|
2999
|
+
* `onHubReachable` (`autoEmitReadiness` is off). Producers declare their
|
|
3000
|
+
* series on `ready`; the automatic emit at registration sent every one of
|
|
3001
|
+
* them into "device-history is starting" on each boot (`.50`, 1.2.607:
|
|
3002
|
+
* metrics-native and pipeline-analytics).
|
|
2617
3003
|
*/
|
|
2618
3004
|
var DEFAULT_CONFIG = {
|
|
2619
3005
|
rawDays: DEFAULT_RETENTION.rawDays,
|
|
@@ -2637,9 +3023,40 @@ var DeviceHistoryAddon = class extends require_dist.BaseAddon {
|
|
|
2637
3023
|
runtime = null;
|
|
2638
3024
|
unsubscribe = null;
|
|
2639
3025
|
removalHook = null;
|
|
3026
|
+
/** This instance's readiness generation (the base class keeps its own private). */
|
|
3027
|
+
readinessGeneration = `device-history-${Date.now().toString(36)}`;
|
|
2640
3028
|
constructor() {
|
|
2641
3029
|
super({ ...DEFAULT_CONFIG });
|
|
2642
3030
|
}
|
|
3031
|
+
/** `ready` is the store being open, emitted by {@link emitStoreReadiness}. */
|
|
3032
|
+
get autoEmitReadiness() {
|
|
3033
|
+
return false;
|
|
3034
|
+
}
|
|
3035
|
+
/** Readiness of both provided caps, on the bare node this runner belongs to. */
|
|
3036
|
+
emitStoreReadiness(state) {
|
|
3037
|
+
const ctx = this.ctxIfReady;
|
|
3038
|
+
if (ctx === null) return;
|
|
3039
|
+
const raw = ctx.kernel?.localNodeId ?? "hub";
|
|
3040
|
+
const nodeId = raw.includes("/") ? raw.split("/")[0] ?? raw : raw;
|
|
3041
|
+
for (const capName of [require_dist.deviceHistoryCapability.name, require_dist.deviceRemovalHookCapability.name]) try {
|
|
3042
|
+
require_dist.emitReadiness(ctx.eventBus, {
|
|
3043
|
+
capName,
|
|
3044
|
+
scope: {
|
|
3045
|
+
type: "node",
|
|
3046
|
+
nodeId
|
|
3047
|
+
},
|
|
3048
|
+
state,
|
|
3049
|
+
generation: this.readinessGeneration,
|
|
3050
|
+
sourceNodeId: nodeId
|
|
3051
|
+
});
|
|
3052
|
+
} catch (err) {
|
|
3053
|
+
ctx.logger.warn("device-history: readiness not emitted", { meta: {
|
|
3054
|
+
capName,
|
|
3055
|
+
state,
|
|
3056
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3057
|
+
} });
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
2643
3060
|
retention() {
|
|
2644
3061
|
const c = this.config;
|
|
2645
3062
|
return {
|
|
@@ -2661,7 +3078,7 @@ var DeviceHistoryAddon = class extends require_dist.BaseAddon {
|
|
|
2661
3078
|
}),
|
|
2662
3079
|
listNodeSeries: async (input) => this.requireRuntime().query.listSeries({
|
|
2663
3080
|
kind: "node",
|
|
2664
|
-
nodeId: input.
|
|
3081
|
+
nodeId: input.forNodeId
|
|
2665
3082
|
}),
|
|
2666
3083
|
query: async ({ deviceId, ...window }) => this.requireRuntime().query.query({
|
|
2667
3084
|
...window,
|
|
@@ -2670,13 +3087,14 @@ var DeviceHistoryAddon = class extends require_dist.BaseAddon {
|
|
|
2670
3087
|
deviceId
|
|
2671
3088
|
}
|
|
2672
3089
|
}),
|
|
2673
|
-
queryNode: async ({
|
|
3090
|
+
queryNode: async ({ forNodeId, ...window }) => this.requireRuntime().query.query({
|
|
2674
3091
|
...window,
|
|
2675
3092
|
subject: {
|
|
2676
3093
|
kind: "node",
|
|
2677
|
-
nodeId
|
|
3094
|
+
nodeId: forNodeId
|
|
2678
3095
|
}
|
|
2679
3096
|
}),
|
|
3097
|
+
queryMany: async (input) => ({ results: await this.requireRuntime().query.queryMany(input) }),
|
|
2680
3098
|
declareSeries: async (input) => this.requireRuntime().ingest.declareSeries(callerOf(input), input),
|
|
2681
3099
|
record: async (input) => this.requireRuntime().ingest.record(callerOf(input), input.points),
|
|
2682
3100
|
recordBuckets: async (input) => this.requireRuntime().ingest.recordBuckets(callerOf(input), input.buckets),
|
|
@@ -2684,6 +3102,7 @@ var DeviceHistoryAddon = class extends require_dist.BaseAddon {
|
|
|
2684
3102
|
setDeviceEnabled: async (input) => ({ enabled: await this.requireRuntime().setEnabled(input.deviceId, input.enabled) }),
|
|
2685
3103
|
getStatus: async () => this.requireRuntime().status()
|
|
2686
3104
|
};
|
|
3105
|
+
this.emitStoreReadiness("starting");
|
|
2687
3106
|
this.ctx.logger.info("Initialized — device-history provider registered (store opens on hub link)");
|
|
2688
3107
|
const removalHook = {
|
|
2689
3108
|
onDeviceRemoved: async (input) => {
|
|
@@ -2760,6 +3179,7 @@ var DeviceHistoryAddon = class extends require_dist.BaseAddon {
|
|
|
2760
3179
|
logger: this.ctx.logger,
|
|
2761
3180
|
remove: (input) => removalHook.onDeviceRemoved(input)
|
|
2762
3181
|
}));
|
|
3182
|
+
this.emitStoreReadiness("ready");
|
|
2763
3183
|
this.ctx.logger.info("device-history: capturing", { meta: {
|
|
2764
3184
|
series: runtime.status().series,
|
|
2765
3185
|
metrics: runtime.status().metrics
|
|
@@ -2771,6 +3191,7 @@ var DeviceHistoryAddon = class extends require_dist.BaseAddon {
|
|
|
2771
3191
|
await this.runtime?.stop();
|
|
2772
3192
|
}
|
|
2773
3193
|
async onShutdown() {
|
|
3194
|
+
this.emitStoreReadiness("down");
|
|
2774
3195
|
await this.stopRuntime();
|
|
2775
3196
|
this.runtime = null;
|
|
2776
3197
|
}
|