@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
|
@@ -1,4 +1,236 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { B as HISTORY_CATALOG, Ji as emitReadiness, O as DEVICE_HISTORY_ENERGY_KEY, Oa as EventCategory, Tt as armRemovalReconcile, di as HISTORY_DEFAULT_MIN_INTERVAL_MS, ei as DeviceType, in as deviceRemovalHookCapability, ni as BaseAddon, nn as deviceHistoryCapability } from "../../dist-B_GRPhnY.mjs";
|
|
2
|
+
//#region src/builtins/device-history/time/bucket-time.ts
|
|
3
|
+
/**
|
|
4
|
+
* Bucket arithmetic for the three rollup tiers (D850, plan §4/§5.5).
|
|
5
|
+
*
|
|
6
|
+
* 5m and 1h buckets are aligned to UTC; a 1d bucket starts at HUB-LOCAL
|
|
7
|
+
* midnight, so a day is 23, 24 or 25 hours long across a DST change (operator
|
|
8
|
+
* answer Q4). Pure: the zone is the process zone (`TZ`), nothing is read from
|
|
9
|
+
* the environment here.
|
|
10
|
+
*/
|
|
11
|
+
var FIVE_MINUTES_MS = 5 * 6e4;
|
|
12
|
+
var HOUR_MS = 60 * 6e4;
|
|
13
|
+
var DAY_MS = 24 * HOUR_MS;
|
|
14
|
+
function floorTo(ts, widthMs) {
|
|
15
|
+
return Math.floor(ts / widthMs) * widthMs;
|
|
16
|
+
}
|
|
17
|
+
/** Hub-local midnight at or before `ts`. */
|
|
18
|
+
function startOfLocalDay(ts) {
|
|
19
|
+
const d = new Date(ts);
|
|
20
|
+
d.setHours(0, 0, 0, 0);
|
|
21
|
+
return d.getTime();
|
|
22
|
+
}
|
|
23
|
+
/** The local midnight following the local midnight `dayStart`. */
|
|
24
|
+
function nextLocalDayStart(dayStart) {
|
|
25
|
+
const d = new Date(dayStart);
|
|
26
|
+
d.setDate(d.getDate() + 1);
|
|
27
|
+
d.setHours(0, 0, 0, 0);
|
|
28
|
+
return d.getTime();
|
|
29
|
+
}
|
|
30
|
+
/** Start of the `tier` bucket containing `ts`. */
|
|
31
|
+
function bucketStartOf(tier, ts) {
|
|
32
|
+
if (tier === "5m") return floorTo(ts, FIVE_MINUTES_MS);
|
|
33
|
+
if (tier === "1h") return floorTo(ts, HOUR_MS);
|
|
34
|
+
return startOfLocalDay(ts);
|
|
35
|
+
}
|
|
36
|
+
/** Exclusive end of the `tier` bucket that starts at `start`. */
|
|
37
|
+
function bucketEndOf(tier, start) {
|
|
38
|
+
if (tier === "5m") return start + FIVE_MINUTES_MS;
|
|
39
|
+
if (tier === "1h") return start + HOUR_MS;
|
|
40
|
+
return nextLocalDayStart(start);
|
|
41
|
+
}
|
|
42
|
+
/** Quiet time after a bucket closes before it is rolled up (late writers). */
|
|
43
|
+
var ROLLUP_GRACE_MS = {
|
|
44
|
+
"5m": 3e4,
|
|
45
|
+
"1h": 5 * 6e4,
|
|
46
|
+
"1d": 15 * 6e4
|
|
47
|
+
};
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/builtins/device-history/retention/departure-tracker.ts
|
|
50
|
+
var DEPARTED_ABSENCE_MS = 7 * DAY_MS;
|
|
51
|
+
var DepartureTracker = class {
|
|
52
|
+
store;
|
|
53
|
+
registry;
|
|
54
|
+
logger;
|
|
55
|
+
missing = /* @__PURE__ */ new Map();
|
|
56
|
+
constructor(store, registry, logger) {
|
|
57
|
+
this.store = store;
|
|
58
|
+
this.registry = registry;
|
|
59
|
+
this.logger = logger;
|
|
60
|
+
}
|
|
61
|
+
async load() {
|
|
62
|
+
this.missing = await this.store.loadDepartures();
|
|
63
|
+
}
|
|
64
|
+
missingSince(deviceId) {
|
|
65
|
+
return this.missing.get(deviceId);
|
|
66
|
+
}
|
|
67
|
+
/** Forget a device's clock — its series were removed because it was DELETED (D865). */
|
|
68
|
+
async forget(deviceId) {
|
|
69
|
+
await this.store.clearMissing([deviceId]);
|
|
70
|
+
const next = new Map(this.missing);
|
|
71
|
+
next.delete(deviceId);
|
|
72
|
+
this.missing = next;
|
|
73
|
+
}
|
|
74
|
+
/** Feed one SUCCESSFUL listing. A failed read never reaches here. */
|
|
75
|
+
async observe(listed, now) {
|
|
76
|
+
const withSeries = new Set(this.registry.all().flatMap((s) => s.deviceId === null ? [] : [s.deviceId]));
|
|
77
|
+
const back = [...this.missing.keys()].filter((id) => listed.has(id));
|
|
78
|
+
const gone = [...withSeries].filter((id) => !listed.has(id) && !this.missing.has(id));
|
|
79
|
+
try {
|
|
80
|
+
await this.store.clearMissing(back);
|
|
81
|
+
await this.store.markMissing(gone, now);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
this.logger.warn("device-history: departure clock not written", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const next = new Map(this.missing);
|
|
87
|
+
for (const id of back) next.delete(id);
|
|
88
|
+
for (const id of gone) next.set(id, now);
|
|
89
|
+
this.missing = next;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/builtins/device-history/retention/retention.ts
|
|
94
|
+
var DEFAULT_RETENTION = {
|
|
95
|
+
rawDays: 7,
|
|
96
|
+
fiveMinuteDays: 30,
|
|
97
|
+
hourDays: 365,
|
|
98
|
+
dayDays: null
|
|
99
|
+
};
|
|
100
|
+
/** Cutoff per tier: rows strictly older are deleted. `null` = keep forever. */
|
|
101
|
+
function retentionCutoffs(policy, now) {
|
|
102
|
+
return {
|
|
103
|
+
raw: now - policy.rawDays * DAY_MS,
|
|
104
|
+
"5m": now - policy.fiveMinuteDays * DAY_MS,
|
|
105
|
+
"1h": now - policy.hourDays * DAY_MS,
|
|
106
|
+
"1d": policy.dayDays === null || policy.dayDays <= 0 ? null : now - policy.dayDays * DAY_MS
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
var TIERS = [
|
|
110
|
+
"raw",
|
|
111
|
+
"5m",
|
|
112
|
+
"1h",
|
|
113
|
+
"1d"
|
|
114
|
+
];
|
|
115
|
+
var RETENTION_STEP_MS = {
|
|
116
|
+
raw: HOUR_MS,
|
|
117
|
+
"5m": HOUR_MS,
|
|
118
|
+
"1h": DAY_MS,
|
|
119
|
+
"1d": DAY_MS
|
|
120
|
+
};
|
|
121
|
+
var MAX_PURGES_PER_RUN = 20;
|
|
122
|
+
/** Width of one delete statement's time range, per tier. */
|
|
123
|
+
var SLICE_MS = {
|
|
124
|
+
raw: DAY_MS,
|
|
125
|
+
"5m": DAY_MS,
|
|
126
|
+
"1h": 30 * DAY_MS,
|
|
127
|
+
"1d": 30 * DAY_MS
|
|
128
|
+
};
|
|
129
|
+
/** A bucket's start may precede the series' first point by up to a day. */
|
|
130
|
+
var BUCKET_LEAD_MS = 2 * DAY_MS;
|
|
131
|
+
/** The short tiers hold nothing older than this unless the operator configured more. */
|
|
132
|
+
var SHORT_TIER_LOOKBACK_MS = 400 * DAY_MS;
|
|
133
|
+
var yieldToEventLoop = () => new Promise((resolve) => setImmediate(resolve));
|
|
134
|
+
var RetentionJob = class {
|
|
135
|
+
deps;
|
|
136
|
+
appliedUpTo = /* @__PURE__ */ new Map();
|
|
137
|
+
running = null;
|
|
138
|
+
constructor(deps) {
|
|
139
|
+
this.deps = deps;
|
|
140
|
+
}
|
|
141
|
+
run(policy, now) {
|
|
142
|
+
if (this.running !== null) return this.running;
|
|
143
|
+
const run = this.doRun(policy, now).finally(() => {
|
|
144
|
+
this.running = null;
|
|
145
|
+
});
|
|
146
|
+
this.running = run;
|
|
147
|
+
return run;
|
|
148
|
+
}
|
|
149
|
+
async doRun(policy, now) {
|
|
150
|
+
const { registry, counters, logger } = this.deps;
|
|
151
|
+
const cutoffs = retentionCutoffs(policy, now);
|
|
152
|
+
try {
|
|
153
|
+
for (const tier of TIERS) {
|
|
154
|
+
const cutoff = cutoffs[tier];
|
|
155
|
+
if (cutoff === null) continue;
|
|
156
|
+
for (const series of registry.all()) {
|
|
157
|
+
if (series.firstTs - 864e5 >= cutoff) continue;
|
|
158
|
+
if (tier === "raw" && registry.metric(series.key)?.ingest === "buckets") continue;
|
|
159
|
+
const marker = `${tier}:${series.id}`;
|
|
160
|
+
const applied = this.appliedUpTo.get(marker);
|
|
161
|
+
if (applied !== void 0 && cutoff - applied < RETENTION_STEP_MS[tier]) continue;
|
|
162
|
+
const lo = Math.max(0, applied === void 0 ? series.firstTs - BUCKET_LEAD_MS : applied - RETENTION_STEP_MS[tier]);
|
|
163
|
+
counters.addDeleted(tier, await this.deleteSliced(tier, series.id, lo, cutoff - 1));
|
|
164
|
+
this.appliedUpTo.set(marker, cutoff);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
await this.purgeDeparted(now);
|
|
168
|
+
counters.retentionLastRunAt = now;
|
|
169
|
+
} catch (err) {
|
|
170
|
+
logger.warn("device-history: retention pass failed — retried at the next run", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** Delete `[lo, hiInclusive]` of one tier in time slices, yielding between them. */
|
|
174
|
+
/** Drop the visit markers of a purged series. */
|
|
175
|
+
forgetSeries(seriesId) {
|
|
176
|
+
for (const tier of TIERS) this.appliedUpTo.delete(`${tier}:${seriesId}`);
|
|
177
|
+
}
|
|
178
|
+
async deleteSliced(tier, seriesId, lo, hiInclusive) {
|
|
179
|
+
const yieldNow = this.deps.yieldNow ?? yieldToEventLoop;
|
|
180
|
+
let deleted = 0;
|
|
181
|
+
for (let from = lo; from <= hiInclusive; from += SLICE_MS[tier]) {
|
|
182
|
+
const to = Math.min(from + SLICE_MS[tier] - 1, hiInclusive);
|
|
183
|
+
deleted += await this.deps.store.deleteRange(tier, seriesId, from, to);
|
|
184
|
+
await yieldNow();
|
|
185
|
+
}
|
|
186
|
+
return deleted;
|
|
187
|
+
}
|
|
188
|
+
/** Tier by tier in slices, then the series row (and whatever the slices missed). */
|
|
189
|
+
async purgeSeries(series, now) {
|
|
190
|
+
for (const tier of TIERS) {
|
|
191
|
+
const lo = tier === "raw" || tier === "5m" ? Math.max(series.firstTs - BUCKET_LEAD_MS, now - SHORT_TIER_LOOKBACK_MS) : series.firstTs - BUCKET_LEAD_MS;
|
|
192
|
+
await this.deleteSliced(tier, series.id, Math.max(0, lo), now + DAY_MS);
|
|
193
|
+
}
|
|
194
|
+
await this.deps.store.deleteSeries(series.id);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Delete every series of a DELETED device now (D865). Driven by a tombstone
|
|
198
|
+
* through the `device-removal-hook`, never by a listing: the week-long
|
|
199
|
+
* absence clock above remains the backstop for a device removed before
|
|
200
|
+
* tombstones existed.
|
|
201
|
+
*/
|
|
202
|
+
async purgeDevice(deviceId, now) {
|
|
203
|
+
const { registry, counters } = this.deps;
|
|
204
|
+
let purged = 0;
|
|
205
|
+
for (const series of registry.all()) {
|
|
206
|
+
if (series.deviceId !== deviceId) continue;
|
|
207
|
+
await this.purgeSeries(series, now);
|
|
208
|
+
registry.remove(series.id);
|
|
209
|
+
counters.purgedSeries += 1;
|
|
210
|
+
purged += 1;
|
|
211
|
+
}
|
|
212
|
+
return purged;
|
|
213
|
+
}
|
|
214
|
+
async purgeDeparted(now) {
|
|
215
|
+
const { registry, departures, counters, logger } = this.deps;
|
|
216
|
+
let purged = 0;
|
|
217
|
+
for (const series of registry.all()) {
|
|
218
|
+
if (purged >= MAX_PURGES_PER_RUN) break;
|
|
219
|
+
if (series.deviceId === null) continue;
|
|
220
|
+
const since = departures.missingSince(series.deviceId);
|
|
221
|
+
if (since === void 0 || now - since < DEPARTED_ABSENCE_MS) continue;
|
|
222
|
+
await this.purgeSeries(series, now);
|
|
223
|
+
registry.remove(series.id);
|
|
224
|
+
counters.purgedSeries += 1;
|
|
225
|
+
purged += 1;
|
|
226
|
+
logger.info("device-history: purged the series of an unregistered device", {
|
|
227
|
+
tags: { deviceId: series.deviceId },
|
|
228
|
+
meta: { key: series.key }
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
//#endregion
|
|
2
234
|
//#region src/builtins/device-history/store/series-registry.ts
|
|
3
235
|
function subjectIdOf(subject) {
|
|
4
236
|
return subject.kind === "device" ? `d:${subject.deviceId}` : `n:${subject.nodeId}`;
|
|
@@ -6,16 +238,29 @@ function subjectIdOf(subject) {
|
|
|
6
238
|
function deviceIdOf(subject) {
|
|
7
239
|
return subject.kind === "device" ? subject.deviceId : null;
|
|
8
240
|
}
|
|
241
|
+
/** Every metric key is a cap field (`cap:`), an addon's series (`addon:`) or derived. */
|
|
242
|
+
function isMetricKey(key) {
|
|
243
|
+
return key.startsWith("cap:") || key.startsWith("addon:");
|
|
244
|
+
}
|
|
9
245
|
function identityOf(key, subjectId, scope) {
|
|
10
246
|
return `${key}\u0000${subjectId}\u0000${scope}`;
|
|
11
247
|
}
|
|
12
248
|
var LOAD_PAGE = 5e3;
|
|
249
|
+
/** Append what `next` names that `stored` lacks, in `next`'s order: an ordinal never moves. */
|
|
250
|
+
function mergeEnumValues(stored, next) {
|
|
251
|
+
if (stored === null) return next;
|
|
252
|
+
if (next === null) return stored;
|
|
253
|
+
const missing = next.filter((v) => !stored.includes(v));
|
|
254
|
+
return missing.length === 0 ? stored : [...stored, ...missing];
|
|
255
|
+
}
|
|
13
256
|
var SeriesRegistry = class {
|
|
14
257
|
store;
|
|
15
258
|
metrics = /* @__PURE__ */ new Map();
|
|
16
259
|
byIdentity = /* @__PURE__ */ new Map();
|
|
17
260
|
byId = /* @__PURE__ */ new Map();
|
|
18
261
|
inflight = /* @__PURE__ */ new Map();
|
|
262
|
+
/** Enum metrics whose value list grew in memory and is not stored yet. */
|
|
263
|
+
dirtyMetrics = /* @__PURE__ */ new Set();
|
|
19
264
|
removeListeners = [];
|
|
20
265
|
constructor(store) {
|
|
21
266
|
this.store = store;
|
|
@@ -24,7 +269,10 @@ var SeriesRegistry = class {
|
|
|
24
269
|
this.metrics.clear();
|
|
25
270
|
this.byIdentity.clear();
|
|
26
271
|
this.byId.clear();
|
|
27
|
-
|
|
272
|
+
const stray = [];
|
|
273
|
+
for (const m of await this.store.loadMetrics()) if (isMetricKey(m.key)) this.metrics.set(m.key, m);
|
|
274
|
+
else stray.push(m.key);
|
|
275
|
+
if (stray.length > 0) await this.store.deleteMetrics(stray);
|
|
28
276
|
let afterId = 0;
|
|
29
277
|
for (;;) {
|
|
30
278
|
const page = await this.store.loadSeriesPage(afterId, LOAD_PAGE);
|
|
@@ -44,12 +292,48 @@ var SeriesRegistry = class {
|
|
|
44
292
|
allMetrics() {
|
|
45
293
|
return [...this.metrics.values()];
|
|
46
294
|
}
|
|
47
|
-
/**
|
|
295
|
+
/**
|
|
296
|
+
* Persist and remember a metric; idempotent. An `enum` value list is MERGED
|
|
297
|
+
* append-only with the stored one, so reordering a `z.enum` never changes
|
|
298
|
+
* what a stored ordinal means.
|
|
299
|
+
*/
|
|
48
300
|
async declareMetric(metric) {
|
|
49
301
|
const existing = this.metrics.get(metric.key);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
302
|
+
const merged = {
|
|
303
|
+
...metric,
|
|
304
|
+
values: mergeEnumValues(existing?.values ?? null, metric.values)
|
|
305
|
+
};
|
|
306
|
+
if (existing !== void 0 && sameMetric(existing, merged)) return;
|
|
307
|
+
await this.store.upsertMetric(merged);
|
|
308
|
+
this.metrics.set(metric.key, merged);
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* The ordinal of `value` in an `enum` metric's list, learning (appending) a
|
|
312
|
+
* value seen for the first time. `null` when the key is not an enum metric or
|
|
313
|
+
* its list is full ({@link MAX_ENUM_VALUES}). A learned value is stored by
|
|
314
|
+
* {@link persistDirtyMetrics} BEFORE any point that uses it.
|
|
315
|
+
*/
|
|
316
|
+
ordinalOf(key, value) {
|
|
317
|
+
const metric = this.metrics.get(key);
|
|
318
|
+
if (metric === void 0 || metric.kind !== "enum") return null;
|
|
319
|
+
const values = metric.values ?? [];
|
|
320
|
+
const known = values.indexOf(value);
|
|
321
|
+
if (known >= 0) return known;
|
|
322
|
+
if (values.length >= 1024) return null;
|
|
323
|
+
this.metrics.set(key, {
|
|
324
|
+
...metric,
|
|
325
|
+
values: [...values, value]
|
|
326
|
+
});
|
|
327
|
+
this.dirtyMetrics.add(key);
|
|
328
|
+
return values.length;
|
|
329
|
+
}
|
|
330
|
+
/** Store every value list that grew in memory. Throws (and keeps them dirty) on a failed write. */
|
|
331
|
+
async persistDirtyMetrics() {
|
|
332
|
+
for (const key of this.dirtyMetrics) {
|
|
333
|
+
const metric = this.metrics.get(key);
|
|
334
|
+
if (metric !== void 0) await this.store.upsertMetric(metric);
|
|
335
|
+
this.dirtyMetrics.delete(key);
|
|
336
|
+
}
|
|
53
337
|
}
|
|
54
338
|
find(key, subjectId, scope) {
|
|
55
339
|
return this.byIdentity.get(identityOf(key, subjectId, scope));
|
|
@@ -114,7 +398,7 @@ var SeriesRegistry = class {
|
|
|
114
398
|
}
|
|
115
399
|
};
|
|
116
400
|
function sameMetric(a, b) {
|
|
117
|
-
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;
|
|
401
|
+
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);
|
|
118
402
|
}
|
|
119
403
|
//#endregion
|
|
120
404
|
//#region src/builtins/device-history/capture/capture-filter.ts
|
|
@@ -138,10 +422,10 @@ var DEADBAND_EPSILON = 1e-9;
|
|
|
138
422
|
/** The capture floor of a field: its own `minIntervalMs`, else the kind's default. */
|
|
139
423
|
function minIntervalOf(spec) {
|
|
140
424
|
if (spec.minIntervalMs !== void 0) return spec.minIntervalMs;
|
|
141
|
-
return spec.kind === "boolean" ? 0 : HISTORY_DEFAULT_MIN_INTERVAL_MS;
|
|
425
|
+
return spec.kind === "boolean" || spec.kind === "enum" ? 0 : HISTORY_DEFAULT_MIN_INTERVAL_MS;
|
|
142
426
|
}
|
|
143
427
|
function clearsDeadband(spec, last, value) {
|
|
144
|
-
if (spec.kind === "boolean") return true;
|
|
428
|
+
if (spec.kind === "boolean" || spec.kind === "enum") return true;
|
|
145
429
|
const delta = Math.abs(value - last);
|
|
146
430
|
if (spec.deadband !== void 0 && delta < spec.deadband - DEADBAND_EPSILON) return false;
|
|
147
431
|
if (spec.deadbandPct !== void 0) {
|
|
@@ -326,7 +610,8 @@ function catalogMetrics(catalog, now) {
|
|
|
326
610
|
unit: spec.unit ?? null,
|
|
327
611
|
displayName: spec.displayName ?? humanize(capName, path),
|
|
328
612
|
ingest: "points",
|
|
329
|
-
declaredAt: now
|
|
613
|
+
declaredAt: now,
|
|
614
|
+
values: spec.kind === "enum" ? spec.values ?? [] : null
|
|
330
615
|
})));
|
|
331
616
|
}
|
|
332
617
|
/** Devices waiting on a type lookup; beyond this the oldest is dropped (and counted). */
|
|
@@ -375,11 +660,28 @@ var CaptureEngine = class {
|
|
|
375
660
|
counters.skip("excluded-device-type");
|
|
376
661
|
continue;
|
|
377
662
|
}
|
|
378
|
-
const value =
|
|
663
|
+
const value = this.readValue(slice, capName, path, spec);
|
|
379
664
|
if (value === null) continue;
|
|
380
665
|
this.consider(deviceId, capName, path, spec, value, at, false);
|
|
381
666
|
}
|
|
382
667
|
}
|
|
668
|
+
/**
|
|
669
|
+
* The number a field contributes, or `null` (not a reading — never 0). An
|
|
670
|
+
* `enum` state becomes the ordinal of its value in the metric's list; a value
|
|
671
|
+
* the full list cannot take is refused and counted (`enum-overflow`).
|
|
672
|
+
*/
|
|
673
|
+
readValue(slice, capName, path, spec) {
|
|
674
|
+
if (spec.skipWhenSet !== void 0) {
|
|
675
|
+
const gate = readPath(slice, spec.skipWhenSet);
|
|
676
|
+
if (gate !== void 0 && gate !== null) return null;
|
|
677
|
+
}
|
|
678
|
+
if (spec.kind !== "enum") return readNumeric(slice, path, spec.kind);
|
|
679
|
+
const raw = readPath(slice, path);
|
|
680
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
681
|
+
const ordinal = this.deps.registry.ordinalOf(capSeriesKey(capName, path), raw);
|
|
682
|
+
if (ordinal === null) this.deps.counters.skip("enum-overflow");
|
|
683
|
+
return ordinal;
|
|
684
|
+
}
|
|
383
685
|
consider(deviceId, capName, path, spec, value, at, reconciling) {
|
|
384
686
|
const key = capSeriesKey(capName, path);
|
|
385
687
|
const subject = {
|
|
@@ -485,7 +787,7 @@ var CaptureEngine = class {
|
|
|
485
787
|
if (slice === void 0) continue;
|
|
486
788
|
for (const [path, spec] of Object.entries(fields)) {
|
|
487
789
|
if (spec.excludeDeviceTypes?.includes(type) === true) continue;
|
|
488
|
-
const value =
|
|
790
|
+
const value = this.readValue(slice, capName, path, spec);
|
|
489
791
|
if (value === null) continue;
|
|
490
792
|
this.consider(deviceId, capName, path, spec, value, now, true);
|
|
491
793
|
}
|
|
@@ -568,6 +870,7 @@ var PointWriter = class {
|
|
|
568
870
|
this.queue = [];
|
|
569
871
|
const { registry, store, counters, logger, clock } = this.deps;
|
|
570
872
|
try {
|
|
873
|
+
await registry.persistDirtyMetrics();
|
|
571
874
|
const bySeries = /* @__PURE__ */ new Map();
|
|
572
875
|
for (const point of batch) {
|
|
573
876
|
const series = await registry.ensure(point.key, point.subject, point.scope, point.ts);
|
|
@@ -659,52 +962,27 @@ var PointWriter = class {
|
|
|
659
962
|
}
|
|
660
963
|
};
|
|
661
964
|
//#endregion
|
|
662
|
-
//#region src/builtins/device-history/
|
|
965
|
+
//#region src/builtins/device-history/ingest/producer-ingest.ts
|
|
966
|
+
/** One point per series per this many ms, sustained. */
|
|
967
|
+
var RECORD_MIN_INTERVAL_MS = 1e4;
|
|
663
968
|
/**
|
|
664
|
-
*
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
* the environment here.
|
|
969
|
+
* The slot after accepting a point at `ts`. A point older than the newest
|
|
970
|
+
* accepted one (a late arrival) moves nothing. Otherwise the consumed slot is
|
|
971
|
+
* the later of the one that was due and `ts` itself: an early point uses the
|
|
972
|
+
* due slot (the grid holds, so slack never compounds into a higher rate), a
|
|
973
|
+
* late one restarts the grid at `ts` (no credit is banked for a gap).
|
|
670
974
|
*/
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
return d.getTime();
|
|
682
|
-
}
|
|
683
|
-
/** The local midnight following the local midnight `dayStart`. */
|
|
684
|
-
function nextLocalDayStart(dayStart) {
|
|
685
|
-
const d = new Date(dayStart);
|
|
686
|
-
d.setDate(d.getDate() + 1);
|
|
687
|
-
d.setHours(0, 0, 0, 0);
|
|
688
|
-
return d.getTime();
|
|
689
|
-
}
|
|
690
|
-
/** Start of the `tier` bucket containing `ts`. */
|
|
691
|
-
function bucketStartOf(tier, ts) {
|
|
692
|
-
if (tier === "5m") return floorTo(ts, FIVE_MINUTES_MS);
|
|
693
|
-
if (tier === "1h") return floorTo(ts, HOUR_MS);
|
|
694
|
-
return startOfLocalDay(ts);
|
|
695
|
-
}
|
|
696
|
-
/** Exclusive end of the `tier` bucket that starts at `start`. */
|
|
697
|
-
function bucketEndOf(tier, start) {
|
|
698
|
-
if (tier === "5m") return start + FIVE_MINUTES_MS;
|
|
699
|
-
if (tier === "1h") return start + HOUR_MS;
|
|
700
|
-
return nextLocalDayStart(start);
|
|
975
|
+
function nextSlot(slot, ts) {
|
|
976
|
+
if (slot === void 0) return {
|
|
977
|
+
lastTs: ts,
|
|
978
|
+
nextDue: ts + RECORD_MIN_INTERVAL_MS
|
|
979
|
+
};
|
|
980
|
+
if (ts < slot.lastTs) return slot;
|
|
981
|
+
return {
|
|
982
|
+
lastTs: ts,
|
|
983
|
+
nextDue: Math.max(slot.nextDue, ts) + RECORD_MIN_INTERVAL_MS
|
|
984
|
+
};
|
|
701
985
|
}
|
|
702
|
-
/** Quiet time after a bucket closes before it is rolled up (late writers). */
|
|
703
|
-
var ROLLUP_GRACE_MS = {
|
|
704
|
-
"5m": 3e4,
|
|
705
|
-
"1h": 5 * 6e4,
|
|
706
|
-
"1d": 15 * 6e4
|
|
707
|
-
};
|
|
708
986
|
function addRefusal(refusals, key, reason) {
|
|
709
987
|
const byReason = refusals.get(key) ?? /* @__PURE__ */ new Map();
|
|
710
988
|
byReason.set(reason, (byReason.get(reason) ?? 0) + 1);
|
|
@@ -722,7 +1000,7 @@ function toResult(accepted, refusals) {
|
|
|
722
1000
|
}
|
|
723
1001
|
var ProducerIngest = class {
|
|
724
1002
|
deps;
|
|
725
|
-
/**
|
|
1003
|
+
/** Rate state per series identity, for the sustained-rate bound. */
|
|
726
1004
|
lastAccepted = /* @__PURE__ */ new Map();
|
|
727
1005
|
constructor(deps) {
|
|
728
1006
|
this.deps = deps;
|
|
@@ -743,7 +1021,8 @@ var ProducerIngest = class {
|
|
|
743
1021
|
unit: input.unit ?? null,
|
|
744
1022
|
displayName: input.displayName,
|
|
745
1023
|
ingest: input.ingest,
|
|
746
|
-
declaredAt: this.deps.clock()
|
|
1024
|
+
declaredAt: this.deps.clock(),
|
|
1025
|
+
values: null
|
|
747
1026
|
};
|
|
748
1027
|
await this.deps.registry.declareMetric(metric);
|
|
749
1028
|
return { key };
|
|
@@ -780,13 +1059,13 @@ var ProducerIngest = class {
|
|
|
780
1059
|
continue;
|
|
781
1060
|
}
|
|
782
1061
|
const identity = identityOf(point.key, subjectIdOf(point.subject), point.scope);
|
|
783
|
-
const
|
|
784
|
-
if (
|
|
1062
|
+
const slot = this.lastAccepted.get(identity);
|
|
1063
|
+
if (slot !== void 0 && point.ts >= slot.lastTs && point.ts < slot.nextDue - 1e3) {
|
|
785
1064
|
addRefusal(refusals, point.key, "rate");
|
|
786
1065
|
counters.refuse("rate");
|
|
787
1066
|
continue;
|
|
788
1067
|
}
|
|
789
|
-
this.lastAccepted.set(identity,
|
|
1068
|
+
this.lastAccepted.set(identity, nextSlot(slot, point.ts));
|
|
790
1069
|
this.deps.writer.enqueue({
|
|
791
1070
|
key: point.key,
|
|
792
1071
|
subject: point.subject,
|
|
@@ -830,7 +1109,8 @@ var ProducerIngest = class {
|
|
|
830
1109
|
last: bucket.last,
|
|
831
1110
|
lastTs: bucket.lastTs,
|
|
832
1111
|
twSum: null,
|
|
833
|
-
twMs: null
|
|
1112
|
+
twMs: null,
|
|
1113
|
+
states: null
|
|
834
1114
|
});
|
|
835
1115
|
}
|
|
836
1116
|
await store.upsertRollups("5m", rows);
|
|
@@ -869,7 +1149,109 @@ var ProducerIngest = class {
|
|
|
869
1149
|
}
|
|
870
1150
|
};
|
|
871
1151
|
//#endregion
|
|
1152
|
+
//#region src/builtins/device-history/query/point-shape.ts
|
|
1153
|
+
/** kWh in one W·ms: 1 kWh = 1 000 W × 3 600 000 ms. */
|
|
1154
|
+
var KWH_PER_WATT_MS = 1 / 36e8;
|
|
1155
|
+
/** Growth from `prev` to `cur`; a drop is a reset counted from zero. `fallback` when `prev` is unknown. */
|
|
1156
|
+
function counterDelta(prev, cur, fallback) {
|
|
1157
|
+
if (cur === null) return null;
|
|
1158
|
+
if (prev === null) return fallback;
|
|
1159
|
+
return cur >= prev ? cur - prev : cur;
|
|
1160
|
+
}
|
|
1161
|
+
/** The name of an ordinal, or `null` when the series is not an enum or the ordinal is unknown. */
|
|
1162
|
+
function stateName(meaning, ordinal) {
|
|
1163
|
+
if (meaning.kind !== "enum" || meaning.values === null || ordinal === null) return null;
|
|
1164
|
+
return meaning.values[ordinal] ?? null;
|
|
1165
|
+
}
|
|
1166
|
+
function namedStates(meaning, states) {
|
|
1167
|
+
if (meaning.kind !== "enum" || states === null) return null;
|
|
1168
|
+
const out = {};
|
|
1169
|
+
for (const [ordinal, ms] of Object.entries(states)) {
|
|
1170
|
+
const name = stateName(meaning, Number(ordinal));
|
|
1171
|
+
if (name !== null) out[name] = (out[name] ?? 0) + ms;
|
|
1172
|
+
}
|
|
1173
|
+
return out;
|
|
1174
|
+
}
|
|
1175
|
+
/** Raw points, oldest first; `prev` is the value of the point before the first one. */
|
|
1176
|
+
function rawPoints(rows, meaning, prev) {
|
|
1177
|
+
const isEnum = meaning.kind === "enum";
|
|
1178
|
+
let held = prev;
|
|
1179
|
+
return rows.map((r) => {
|
|
1180
|
+
const delta = meaning.kind === "counter" ? counterDelta(held, r.value, null) : null;
|
|
1181
|
+
held = r.value;
|
|
1182
|
+
return {
|
|
1183
|
+
t: r.ts,
|
|
1184
|
+
samples: 1,
|
|
1185
|
+
mean: isEnum ? null : r.value,
|
|
1186
|
+
twMean: null,
|
|
1187
|
+
min: r.value,
|
|
1188
|
+
max: r.value,
|
|
1189
|
+
last: r.value,
|
|
1190
|
+
delta,
|
|
1191
|
+
state: stateName(meaning, r.value),
|
|
1192
|
+
states: null
|
|
1193
|
+
};
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
/** Rollup rows, oldest first; `prevLast` is the `last` of the bucket before the first one. */
|
|
1197
|
+
function rollupPoints(rows, meaning, prevLast) {
|
|
1198
|
+
const isEnum = meaning.kind === "enum";
|
|
1199
|
+
let held = prevLast;
|
|
1200
|
+
return rows.map((row) => {
|
|
1201
|
+
const delta = meaning.kind === "counter" ? counterDelta(held, row.last, row.last === null ? null : row.last - row.min) : null;
|
|
1202
|
+
if (row.last !== null) held = row.last;
|
|
1203
|
+
return {
|
|
1204
|
+
t: row.bucketStart,
|
|
1205
|
+
samples: row.samples,
|
|
1206
|
+
mean: isEnum || row.samples <= 0 ? null : row.sum / row.samples,
|
|
1207
|
+
twMean: !isEnum && row.twSum !== null && row.twMs !== null && row.twMs > 0 ? row.twSum / row.twMs : null,
|
|
1208
|
+
min: row.min,
|
|
1209
|
+
max: row.max,
|
|
1210
|
+
last: row.last,
|
|
1211
|
+
delta,
|
|
1212
|
+
state: stateName(meaning, row.last),
|
|
1213
|
+
states: namedStates(meaning, row.states)
|
|
1214
|
+
};
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
/** Energy used in each bucket of a `watts` gauge: its time-weighted integral, in kWh. */
|
|
1218
|
+
function integratedEnergyPoints(rows) {
|
|
1219
|
+
return rows.map((row) => ({
|
|
1220
|
+
t: row.bucketStart,
|
|
1221
|
+
samples: row.samples,
|
|
1222
|
+
mean: null,
|
|
1223
|
+
twMean: null,
|
|
1224
|
+
min: row.min,
|
|
1225
|
+
max: row.max,
|
|
1226
|
+
last: null,
|
|
1227
|
+
delta: row.twSum === null ? null : row.twSum * KWH_PER_WATT_MS,
|
|
1228
|
+
state: null,
|
|
1229
|
+
states: null
|
|
1230
|
+
}));
|
|
1231
|
+
}
|
|
1232
|
+
//#endregion
|
|
872
1233
|
//#region src/builtins/device-history/query/history-query.ts
|
|
1234
|
+
/**
|
|
1235
|
+
* Reads: `listSeries`, `query` and `queryMany` (plan §5.1).
|
|
1236
|
+
*
|
|
1237
|
+
* `auto` resolution picks the finest tier whose retention covers `from` and
|
|
1238
|
+
* whose point count fits `maxPoints`: a rollup tier fits when
|
|
1239
|
+
* `ceil(range / bucket) <= maxPoints`, raw fits when a read of `maxPoints + 1`
|
|
1240
|
+
* rows comes back short. The coarsest covering tier is the last resort and is
|
|
1241
|
+
* truncated to its newest `maxPoints` points (`truncated: true`) rather than
|
|
1242
|
+
* answering nothing. A series fed by closed buckets has NO raw rows, so raw is
|
|
1243
|
+
* never a candidate for it: an empty raw read "fits" every window.
|
|
1244
|
+
*
|
|
1245
|
+
* `before` is the last point strictly before the window in the chosen tier, so a
|
|
1246
|
+
* step chart can start at the value in force when the window opens — a quiet
|
|
1247
|
+
* bucket has no row, and the held value is otherwise invisible.
|
|
1248
|
+
*
|
|
1249
|
+
* {@link DEVICE_HISTORY_ENERGY_KEY} is computed here at read time, never stored:
|
|
1250
|
+
* the device's `kwhTotal` meter when it has one (per-bucket growth), else the
|
|
1251
|
+
* time-weighted integral of its `watts` (`tw_sum`, already in every rollup).
|
|
1252
|
+
* One place makes that choice, so two charts of one device cannot disagree, and
|
|
1253
|
+
* it covers every hour of history recorded before the key existed.
|
|
1254
|
+
*/
|
|
873
1255
|
var TIER_ORDER = [
|
|
874
1256
|
"raw",
|
|
875
1257
|
"5m",
|
|
@@ -883,46 +1265,101 @@ var BUCKET_MS = {
|
|
|
883
1265
|
};
|
|
884
1266
|
/** A key with more scopes than this is cut (a zone x class matrix, not a chart). */
|
|
885
1267
|
var MAX_SCOPES = 50;
|
|
1268
|
+
/** The power-meter series the energy key is derived from, meter first. */
|
|
1269
|
+
var ENERGY_METER_KEY = capSeriesKey("power-meter", "kwhTotal");
|
|
1270
|
+
var ENERGY_POWER_KEY = capSeriesKey("power-meter", "watts");
|
|
886
1271
|
function retentionMsOf(tier, policy) {
|
|
887
1272
|
if (tier === "raw") return policy.rawDays * DAY_MS;
|
|
888
1273
|
if (tier === "5m") return policy.fiveMinuteDays * DAY_MS;
|
|
889
1274
|
if (tier === "1h") return policy.hourDays * DAY_MS;
|
|
890
1275
|
return policy.dayDays === null || policy.dayDays <= 0 ? null : policy.dayDays * DAY_MS;
|
|
891
1276
|
}
|
|
892
|
-
|
|
1277
|
+
/** Rows arrive newest first and one over the bound; keep the newest `maxPoints`, oldest first. */
|
|
1278
|
+
function bounded(newestFirst, maxPoints) {
|
|
1279
|
+
const overflow = newestFirst.length > maxPoints;
|
|
893
1280
|
return {
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
mean: value,
|
|
897
|
-
twMean: null,
|
|
898
|
-
min: value,
|
|
899
|
-
max: value,
|
|
900
|
-
last: value
|
|
1281
|
+
rows: (overflow ? newestFirst.slice(0, maxPoints) : newestFirst).toReversed(),
|
|
1282
|
+
overflow
|
|
901
1283
|
};
|
|
902
1284
|
}
|
|
903
|
-
function
|
|
904
|
-
return
|
|
905
|
-
t: row.bucketStart,
|
|
906
|
-
samples: row.samples,
|
|
907
|
-
mean: row.samples > 0 ? row.sum / row.samples : null,
|
|
908
|
-
twMean: row.twSum !== null && row.twMs !== null && row.twMs > 0 ? row.twSum / row.twMs : null,
|
|
909
|
-
min: row.min,
|
|
910
|
-
max: row.max,
|
|
911
|
-
last: row.last
|
|
912
|
-
};
|
|
1285
|
+
function errorText(err) {
|
|
1286
|
+
return err instanceof Error ? err.message : String(err);
|
|
913
1287
|
}
|
|
914
1288
|
var HistoryQuery = class {
|
|
915
1289
|
deps;
|
|
916
1290
|
constructor(deps) {
|
|
917
1291
|
this.deps = deps;
|
|
918
1292
|
}
|
|
919
|
-
async query(input) {
|
|
920
|
-
|
|
921
|
-
const
|
|
922
|
-
const wanted =
|
|
923
|
-
const
|
|
1293
|
+
async query(input) {
|
|
1294
|
+
if (input.key === "derived:energy.kwh") return this.queryEnergy(input);
|
|
1295
|
+
const { registry } = this.deps;
|
|
1296
|
+
const wanted = this.seriesOf(input);
|
|
1297
|
+
const metric = registry.metric(input.key);
|
|
1298
|
+
const meaning = {
|
|
1299
|
+
kind: metric?.kind ?? "gauge",
|
|
1300
|
+
values: metric?.values ?? null
|
|
1301
|
+
};
|
|
1302
|
+
const { chosen, reads } = await this.choose(input, wanted, metric?.ingest === "buckets");
|
|
1303
|
+
const series = [];
|
|
1304
|
+
for (const [s, read] of reads) {
|
|
1305
|
+
const before = await this.before(chosen, s, input.from, meaning);
|
|
1306
|
+
const points = read.tier === "raw" ? rawPoints(read.rows, meaning, before?.value ?? null) : rollupPoints(read.rows, meaning, before?.value ?? null);
|
|
1307
|
+
series.push({
|
|
1308
|
+
scope: s.scope,
|
|
1309
|
+
before,
|
|
1310
|
+
points,
|
|
1311
|
+
derivedFrom: null
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
return {
|
|
1315
|
+
resolution: chosen,
|
|
1316
|
+
series,
|
|
1317
|
+
truncated: reads.some(([, r]) => r.overflow)
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
/** Several reads over one window; one failing item answers its error, not the batch's. */
|
|
1321
|
+
async queryMany(input) {
|
|
1322
|
+
const out = [];
|
|
1323
|
+
for (const item of input.items) {
|
|
1324
|
+
const subject = "deviceId" in item ? {
|
|
1325
|
+
kind: "device",
|
|
1326
|
+
deviceId: item.deviceId
|
|
1327
|
+
} : {
|
|
1328
|
+
kind: "node",
|
|
1329
|
+
nodeId: item.forNodeId
|
|
1330
|
+
};
|
|
1331
|
+
try {
|
|
1332
|
+
const result = await this.query({
|
|
1333
|
+
key: item.key,
|
|
1334
|
+
subject,
|
|
1335
|
+
...item.scope !== void 0 ? { scope: item.scope } : {},
|
|
1336
|
+
from: input.from,
|
|
1337
|
+
to: input.to,
|
|
1338
|
+
resolution: input.resolution,
|
|
1339
|
+
maxPoints: input.maxPoints
|
|
1340
|
+
});
|
|
1341
|
+
out.push({
|
|
1342
|
+
ok: true,
|
|
1343
|
+
result
|
|
1344
|
+
});
|
|
1345
|
+
} catch (err) {
|
|
1346
|
+
out.push({
|
|
1347
|
+
ok: false,
|
|
1348
|
+
error: errorText(err)
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
return out;
|
|
1353
|
+
}
|
|
1354
|
+
seriesOf(input) {
|
|
1355
|
+
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);
|
|
1356
|
+
}
|
|
1357
|
+
/** The tier to answer from and its reads. `noRaw`: the series has no raw rows to read. */
|
|
1358
|
+
async choose(input, wanted, noRaw) {
|
|
1359
|
+
const covering = this.coveringTiers(input.from);
|
|
1360
|
+
const candidates = noRaw ? withoutRaw(covering) : covering;
|
|
924
1361
|
const forced = input.resolution === "auto" ? null : input.resolution;
|
|
925
|
-
const order = forced === null ? candidates : [forced];
|
|
1362
|
+
const order = forced === null ? candidates : [noRaw && forced === "raw" ? "5m" : forced];
|
|
926
1363
|
let chosen = order[order.length - 1] ?? "1d";
|
|
927
1364
|
let reads = [];
|
|
928
1365
|
for (const [index, tier] of order.entries()) {
|
|
@@ -933,16 +1370,9 @@ var HistoryQuery = class {
|
|
|
933
1370
|
if (reads.some(([, r]) => r.overflow) && !last) continue;
|
|
934
1371
|
break;
|
|
935
1372
|
}
|
|
936
|
-
const series = [];
|
|
937
|
-
for (const [s, read] of reads) series.push({
|
|
938
|
-
scope: s.scope,
|
|
939
|
-
before: await this.before(chosen, s, input.from),
|
|
940
|
-
points: read.points
|
|
941
|
-
});
|
|
942
1373
|
return {
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
truncated: reads.some(([, r]) => r.overflow)
|
|
1374
|
+
chosen,
|
|
1375
|
+
reads
|
|
946
1376
|
};
|
|
947
1377
|
}
|
|
948
1378
|
/** Tiers whose retention covers `from`, finest first; the coarsest is always present. */
|
|
@@ -964,35 +1394,79 @@ var HistoryQuery = class {
|
|
|
964
1394
|
async readTier(tier, series, input) {
|
|
965
1395
|
const { store } = this.deps;
|
|
966
1396
|
const limit = input.maxPoints + 1;
|
|
967
|
-
if (tier === "raw") {
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
}
|
|
1397
|
+
if (tier === "raw") return {
|
|
1398
|
+
tier,
|
|
1399
|
+
...bounded(await store.readRaw(series.id, input.from, input.to, limit, "desc"), input.maxPoints)
|
|
1400
|
+
};
|
|
971
1401
|
const lower = bucketStartOf(tier, input.from);
|
|
972
|
-
const rows = await store.readRollups(tier, series.id, lower, input.to, limit, "desc");
|
|
973
|
-
return this.shape(rows.map(rollupPoint), input.maxPoints);
|
|
974
|
-
}
|
|
975
|
-
/** Rows arrive newest first and one over the bound; keep the newest `maxPoints`, oldest first. */
|
|
976
|
-
shape(newestFirst, maxPoints) {
|
|
977
|
-
const overflow = newestFirst.length > maxPoints;
|
|
978
1402
|
return {
|
|
979
|
-
|
|
980
|
-
|
|
1403
|
+
tier,
|
|
1404
|
+
...bounded(await store.readRollups(tier, series.id, lower, input.to, limit, "desc"), input.maxPoints)
|
|
981
1405
|
};
|
|
982
1406
|
}
|
|
983
|
-
async before(tier, series, from) {
|
|
1407
|
+
async before(tier, series, from, meaning) {
|
|
984
1408
|
const { store } = this.deps;
|
|
985
1409
|
if (tier === "raw") {
|
|
986
1410
|
const p = await store.rawBefore(series.id, from);
|
|
987
1411
|
return p === null ? null : {
|
|
988
1412
|
t: p.ts,
|
|
989
|
-
value: p.value
|
|
1413
|
+
value: p.value,
|
|
1414
|
+
state: stateName(meaning, p.value)
|
|
990
1415
|
};
|
|
991
1416
|
}
|
|
992
1417
|
const row = await store.rollupBefore(tier, series.id, bucketStartOf(tier, from));
|
|
993
1418
|
return row === null ? null : {
|
|
994
1419
|
t: row.bucketStart,
|
|
995
|
-
value: row.last
|
|
1420
|
+
value: row.last,
|
|
1421
|
+
state: stateName(meaning, row.last)
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
/** The series energy is derived from for a device: the meter when it has one, else its power. */
|
|
1425
|
+
energySource(subject) {
|
|
1426
|
+
if (subject.kind !== "device") return null;
|
|
1427
|
+
const subjectId = subjectIdOf(subject);
|
|
1428
|
+
const meter = this.deps.registry.find(ENERGY_METER_KEY, subjectId, "");
|
|
1429
|
+
if (meter !== void 0) return {
|
|
1430
|
+
series: meter,
|
|
1431
|
+
from: "meter"
|
|
1432
|
+
};
|
|
1433
|
+
const power = this.deps.registry.find(ENERGY_POWER_KEY, subjectId, "");
|
|
1434
|
+
return power === void 0 ? null : {
|
|
1435
|
+
series: power,
|
|
1436
|
+
from: "power"
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
async queryEnergy(input) {
|
|
1440
|
+
const source = this.energySource(input.subject);
|
|
1441
|
+
if (source === null || input.scope !== void 0 && input.scope !== "") return {
|
|
1442
|
+
resolution: "5m",
|
|
1443
|
+
series: [],
|
|
1444
|
+
truncated: false
|
|
1445
|
+
};
|
|
1446
|
+
const { chosen, reads } = await this.choose(input, [source.series], true);
|
|
1447
|
+
const read = reads[0]?.[1];
|
|
1448
|
+
if (read === void 0 || read.tier === "raw") return {
|
|
1449
|
+
resolution: chosen,
|
|
1450
|
+
series: [],
|
|
1451
|
+
truncated: false
|
|
1452
|
+
};
|
|
1453
|
+
let points;
|
|
1454
|
+
if (source.from === "meter") {
|
|
1455
|
+
const prev = await this.deps.store.rollupBefore(read.tier, source.series.id, bucketStartOf(read.tier, input.from));
|
|
1456
|
+
points = rollupPoints(read.rows, {
|
|
1457
|
+
kind: "counter",
|
|
1458
|
+
values: null
|
|
1459
|
+
}, prev?.last ?? null);
|
|
1460
|
+
} else points = integratedEnergyPoints(read.rows);
|
|
1461
|
+
return {
|
|
1462
|
+
resolution: chosen,
|
|
1463
|
+
series: [{
|
|
1464
|
+
scope: "",
|
|
1465
|
+
before: null,
|
|
1466
|
+
points,
|
|
1467
|
+
derivedFrom: source.series.key
|
|
1468
|
+
}],
|
|
1469
|
+
truncated: read.overflow
|
|
996
1470
|
};
|
|
997
1471
|
}
|
|
998
1472
|
async listSeries(subject) {
|
|
@@ -1009,10 +1483,23 @@ var HistoryQuery = class {
|
|
|
1009
1483
|
unit: await this.unitOf(subject, s, metric.unit, slices),
|
|
1010
1484
|
displayName: metric.displayName,
|
|
1011
1485
|
source: metric.source,
|
|
1486
|
+
values: metric.kind === "enum" ? metric.values ?? [] : null,
|
|
1012
1487
|
firstTs: s.firstTs,
|
|
1013
1488
|
lastTs: s.lastTs
|
|
1014
1489
|
});
|
|
1015
1490
|
}
|
|
1491
|
+
const energy = this.energySource(subject);
|
|
1492
|
+
if (energy !== null) infos.push({
|
|
1493
|
+
key: DEVICE_HISTORY_ENERGY_KEY,
|
|
1494
|
+
scope: "",
|
|
1495
|
+
kind: "counter",
|
|
1496
|
+
unit: "kWh",
|
|
1497
|
+
displayName: "Energy",
|
|
1498
|
+
source: "derived",
|
|
1499
|
+
values: null,
|
|
1500
|
+
firstTs: energy.series.firstTs,
|
|
1501
|
+
lastTs: energy.series.lastTs
|
|
1502
|
+
});
|
|
1016
1503
|
return infos.toSorted((a, b) => a.key.localeCompare(b.key) || a.scope.localeCompare(b.scope));
|
|
1017
1504
|
}
|
|
1018
1505
|
/** The metric's fixed unit, else the device's own (a `unitFrom` field), else unknown. */
|
|
@@ -1033,191 +1520,11 @@ var HistoryQuery = class {
|
|
|
1033
1520
|
return this.deps.catalog[body.slice(0, dot)]?.[body.slice(dot + 1)];
|
|
1034
1521
|
}
|
|
1035
1522
|
};
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
store;
|
|
1041
|
-
registry;
|
|
1042
|
-
logger;
|
|
1043
|
-
missing = /* @__PURE__ */ new Map();
|
|
1044
|
-
constructor(store, registry, logger) {
|
|
1045
|
-
this.store = store;
|
|
1046
|
-
this.registry = registry;
|
|
1047
|
-
this.logger = logger;
|
|
1048
|
-
}
|
|
1049
|
-
async load() {
|
|
1050
|
-
this.missing = await this.store.loadDepartures();
|
|
1051
|
-
}
|
|
1052
|
-
missingSince(deviceId) {
|
|
1053
|
-
return this.missing.get(deviceId);
|
|
1054
|
-
}
|
|
1055
|
-
/** Forget a device's clock — its series were removed because it was DELETED (D865). */
|
|
1056
|
-
async forget(deviceId) {
|
|
1057
|
-
await this.store.clearMissing([deviceId]);
|
|
1058
|
-
const next = new Map(this.missing);
|
|
1059
|
-
next.delete(deviceId);
|
|
1060
|
-
this.missing = next;
|
|
1061
|
-
}
|
|
1062
|
-
/** Feed one SUCCESSFUL listing. A failed read never reaches here. */
|
|
1063
|
-
async observe(listed, now) {
|
|
1064
|
-
const withSeries = new Set(this.registry.all().flatMap((s) => s.deviceId === null ? [] : [s.deviceId]));
|
|
1065
|
-
const back = [...this.missing.keys()].filter((id) => listed.has(id));
|
|
1066
|
-
const gone = [...withSeries].filter((id) => !listed.has(id) && !this.missing.has(id));
|
|
1067
|
-
try {
|
|
1068
|
-
await this.store.clearMissing(back);
|
|
1069
|
-
await this.store.markMissing(gone, now);
|
|
1070
|
-
} catch (err) {
|
|
1071
|
-
this.logger.warn("device-history: departure clock not written", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
1072
|
-
return;
|
|
1073
|
-
}
|
|
1074
|
-
const next = new Map(this.missing);
|
|
1075
|
-
for (const id of back) next.delete(id);
|
|
1076
|
-
for (const id of gone) next.set(id, now);
|
|
1077
|
-
this.missing = next;
|
|
1078
|
-
}
|
|
1079
|
-
};
|
|
1080
|
-
//#endregion
|
|
1081
|
-
//#region src/builtins/device-history/retention/retention.ts
|
|
1082
|
-
var DEFAULT_RETENTION = {
|
|
1083
|
-
rawDays: 7,
|
|
1084
|
-
fiveMinuteDays: 14,
|
|
1085
|
-
hourDays: 365,
|
|
1086
|
-
dayDays: null
|
|
1087
|
-
};
|
|
1088
|
-
/** Cutoff per tier: rows strictly older are deleted. `null` = keep forever. */
|
|
1089
|
-
function retentionCutoffs(policy, now) {
|
|
1090
|
-
return {
|
|
1091
|
-
raw: now - policy.rawDays * DAY_MS,
|
|
1092
|
-
"5m": now - policy.fiveMinuteDays * DAY_MS,
|
|
1093
|
-
"1h": now - policy.hourDays * DAY_MS,
|
|
1094
|
-
"1d": policy.dayDays === null || policy.dayDays <= 0 ? null : now - policy.dayDays * DAY_MS
|
|
1095
|
-
};
|
|
1523
|
+
/** Every covering tier but raw; the coarsest stays when it is the only one. */
|
|
1524
|
+
function withoutRaw(tiers) {
|
|
1525
|
+
const rollups = tiers.filter((t) => t !== "raw");
|
|
1526
|
+
return rollups.length > 0 ? rollups : ["5m"];
|
|
1096
1527
|
}
|
|
1097
|
-
var TIERS = [
|
|
1098
|
-
"raw",
|
|
1099
|
-
"5m",
|
|
1100
|
-
"1h",
|
|
1101
|
-
"1d"
|
|
1102
|
-
];
|
|
1103
|
-
var RETENTION_STEP_MS = {
|
|
1104
|
-
raw: HOUR_MS,
|
|
1105
|
-
"5m": HOUR_MS,
|
|
1106
|
-
"1h": DAY_MS,
|
|
1107
|
-
"1d": DAY_MS
|
|
1108
|
-
};
|
|
1109
|
-
var MAX_PURGES_PER_RUN = 20;
|
|
1110
|
-
/** Width of one delete statement's time range, per tier. */
|
|
1111
|
-
var SLICE_MS = {
|
|
1112
|
-
raw: DAY_MS,
|
|
1113
|
-
"5m": DAY_MS,
|
|
1114
|
-
"1h": 30 * DAY_MS,
|
|
1115
|
-
"1d": 30 * DAY_MS
|
|
1116
|
-
};
|
|
1117
|
-
/** A bucket's start may precede the series' first point by up to a day. */
|
|
1118
|
-
var BUCKET_LEAD_MS = 2 * DAY_MS;
|
|
1119
|
-
/** The short tiers hold nothing older than this unless the operator configured more. */
|
|
1120
|
-
var SHORT_TIER_LOOKBACK_MS = 400 * DAY_MS;
|
|
1121
|
-
var yieldToEventLoop = () => new Promise((resolve) => setImmediate(resolve));
|
|
1122
|
-
var RetentionJob = class {
|
|
1123
|
-
deps;
|
|
1124
|
-
appliedUpTo = /* @__PURE__ */ new Map();
|
|
1125
|
-
running = null;
|
|
1126
|
-
constructor(deps) {
|
|
1127
|
-
this.deps = deps;
|
|
1128
|
-
}
|
|
1129
|
-
run(policy, now) {
|
|
1130
|
-
if (this.running !== null) return this.running;
|
|
1131
|
-
const run = this.doRun(policy, now).finally(() => {
|
|
1132
|
-
this.running = null;
|
|
1133
|
-
});
|
|
1134
|
-
this.running = run;
|
|
1135
|
-
return run;
|
|
1136
|
-
}
|
|
1137
|
-
async doRun(policy, now) {
|
|
1138
|
-
const { registry, counters, logger } = this.deps;
|
|
1139
|
-
const cutoffs = retentionCutoffs(policy, now);
|
|
1140
|
-
try {
|
|
1141
|
-
for (const tier of TIERS) {
|
|
1142
|
-
const cutoff = cutoffs[tier];
|
|
1143
|
-
if (cutoff === null) continue;
|
|
1144
|
-
for (const series of registry.all()) {
|
|
1145
|
-
if (series.firstTs - 864e5 >= cutoff) continue;
|
|
1146
|
-
if (tier === "raw" && registry.metric(series.key)?.ingest === "buckets") continue;
|
|
1147
|
-
const marker = `${tier}:${series.id}`;
|
|
1148
|
-
const applied = this.appliedUpTo.get(marker);
|
|
1149
|
-
if (applied !== void 0 && cutoff - applied < RETENTION_STEP_MS[tier]) continue;
|
|
1150
|
-
const lo = Math.max(0, applied === void 0 ? series.firstTs - BUCKET_LEAD_MS : applied - RETENTION_STEP_MS[tier]);
|
|
1151
|
-
counters.addDeleted(tier, await this.deleteSliced(tier, series.id, lo, cutoff - 1));
|
|
1152
|
-
this.appliedUpTo.set(marker, cutoff);
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
|
-
await this.purgeDeparted(now);
|
|
1156
|
-
counters.retentionLastRunAt = now;
|
|
1157
|
-
} catch (err) {
|
|
1158
|
-
logger.warn("device-history: retention pass failed — retried at the next run", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
/** Delete `[lo, hiInclusive]` of one tier in time slices, yielding between them. */
|
|
1162
|
-
/** Drop the visit markers of a purged series. */
|
|
1163
|
-
forgetSeries(seriesId) {
|
|
1164
|
-
for (const tier of TIERS) this.appliedUpTo.delete(`${tier}:${seriesId}`);
|
|
1165
|
-
}
|
|
1166
|
-
async deleteSliced(tier, seriesId, lo, hiInclusive) {
|
|
1167
|
-
const yieldNow = this.deps.yieldNow ?? yieldToEventLoop;
|
|
1168
|
-
let deleted = 0;
|
|
1169
|
-
for (let from = lo; from <= hiInclusive; from += SLICE_MS[tier]) {
|
|
1170
|
-
const to = Math.min(from + SLICE_MS[tier] - 1, hiInclusive);
|
|
1171
|
-
deleted += await this.deps.store.deleteRange(tier, seriesId, from, to);
|
|
1172
|
-
await yieldNow();
|
|
1173
|
-
}
|
|
1174
|
-
return deleted;
|
|
1175
|
-
}
|
|
1176
|
-
/** Tier by tier in slices, then the series row (and whatever the slices missed). */
|
|
1177
|
-
async purgeSeries(series, now) {
|
|
1178
|
-
for (const tier of TIERS) {
|
|
1179
|
-
const lo = tier === "raw" || tier === "5m" ? Math.max(series.firstTs - BUCKET_LEAD_MS, now - SHORT_TIER_LOOKBACK_MS) : series.firstTs - BUCKET_LEAD_MS;
|
|
1180
|
-
await this.deleteSliced(tier, series.id, Math.max(0, lo), now + DAY_MS);
|
|
1181
|
-
}
|
|
1182
|
-
await this.deps.store.deleteSeries(series.id);
|
|
1183
|
-
}
|
|
1184
|
-
/**
|
|
1185
|
-
* Delete every series of a DELETED device now (D865). Driven by a tombstone
|
|
1186
|
-
* through the `device-removal-hook`, never by a listing: the week-long
|
|
1187
|
-
* absence clock above remains the backstop for a device removed before
|
|
1188
|
-
* tombstones existed.
|
|
1189
|
-
*/
|
|
1190
|
-
async purgeDevice(deviceId, now) {
|
|
1191
|
-
const { registry, counters } = this.deps;
|
|
1192
|
-
let purged = 0;
|
|
1193
|
-
for (const series of registry.all()) {
|
|
1194
|
-
if (series.deviceId !== deviceId) continue;
|
|
1195
|
-
await this.purgeSeries(series, now);
|
|
1196
|
-
registry.remove(series.id);
|
|
1197
|
-
counters.purgedSeries += 1;
|
|
1198
|
-
purged += 1;
|
|
1199
|
-
}
|
|
1200
|
-
return purged;
|
|
1201
|
-
}
|
|
1202
|
-
async purgeDeparted(now) {
|
|
1203
|
-
const { registry, departures, counters, logger } = this.deps;
|
|
1204
|
-
let purged = 0;
|
|
1205
|
-
for (const series of registry.all()) {
|
|
1206
|
-
if (purged >= MAX_PURGES_PER_RUN) break;
|
|
1207
|
-
if (series.deviceId === null) continue;
|
|
1208
|
-
const since = departures.missingSince(series.deviceId);
|
|
1209
|
-
if (since === void 0 || now - since < DEPARTED_ABSENCE_MS) continue;
|
|
1210
|
-
await this.purgeSeries(series, now);
|
|
1211
|
-
registry.remove(series.id);
|
|
1212
|
-
counters.purgedSeries += 1;
|
|
1213
|
-
purged += 1;
|
|
1214
|
-
logger.info("device-history: purged the series of an unregistered device", {
|
|
1215
|
-
tags: { deviceId: series.deviceId },
|
|
1216
|
-
meta: { key: series.key }
|
|
1217
|
-
});
|
|
1218
|
-
}
|
|
1219
|
-
}
|
|
1220
|
-
};
|
|
1221
1528
|
//#endregion
|
|
1222
1529
|
//#region src/builtins/device-history/store/history-schema.ts
|
|
1223
1530
|
var HISTORY_NAMESPACE = "device-history";
|
|
@@ -1287,6 +1594,10 @@ function rollupDeclaration(collection) {
|
|
|
1287
1594
|
{
|
|
1288
1595
|
name: "tw_ms",
|
|
1289
1596
|
type: "INTEGER"
|
|
1597
|
+
},
|
|
1598
|
+
{
|
|
1599
|
+
name: "state_ms",
|
|
1600
|
+
type: "TEXT"
|
|
1290
1601
|
}
|
|
1291
1602
|
]
|
|
1292
1603
|
};
|
|
@@ -1341,6 +1652,10 @@ var HISTORY_DECLARATIONS = [
|
|
|
1341
1652
|
name: "declared_at",
|
|
1342
1653
|
type: "INTEGER",
|
|
1343
1654
|
notNull: true
|
|
1655
|
+
},
|
|
1656
|
+
{
|
|
1657
|
+
name: "enum_values",
|
|
1658
|
+
type: "TEXT"
|
|
1344
1659
|
}
|
|
1345
1660
|
]
|
|
1346
1661
|
},
|
|
@@ -1514,9 +1829,34 @@ function chunks(items, size) {
|
|
|
1514
1829
|
return out;
|
|
1515
1830
|
}
|
|
1516
1831
|
function parseKind(value) {
|
|
1517
|
-
if (value === "gauge" || value === "counter" || value === "boolean") return value;
|
|
1832
|
+
if (value === "gauge" || value === "counter" || value === "boolean" || value === "enum") return value;
|
|
1518
1833
|
throw new Error(`history row: unknown kind "${value}"`);
|
|
1519
1834
|
}
|
|
1835
|
+
/** A JSON TEXT column: the engine hands back the decoded value, or the text itself. */
|
|
1836
|
+
function jsonColumn(data, column) {
|
|
1837
|
+
const raw = data[column];
|
|
1838
|
+
if (raw === null || raw === void 0) return null;
|
|
1839
|
+
return typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
1840
|
+
}
|
|
1841
|
+
/** A JSON column that must hold a string array, or nothing. */
|
|
1842
|
+
function parseValues(data) {
|
|
1843
|
+
const parsed = jsonColumn(data, "enum_values");
|
|
1844
|
+
if (parsed === null) return null;
|
|
1845
|
+
if (Array.isArray(parsed) && parsed.every((v) => typeof v === "string")) return parsed;
|
|
1846
|
+
throw new Error("history row: column \"enum_values\" is not a JSON string array");
|
|
1847
|
+
}
|
|
1848
|
+
/** A JSON column that must hold `{ "<ordinal>": ms }`, or nothing. */
|
|
1849
|
+
function parseStates(data) {
|
|
1850
|
+
const parsed = jsonColumn(data, "state_ms");
|
|
1851
|
+
if (parsed === null) return null;
|
|
1852
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("history row: column \"state_ms\" is not a JSON object");
|
|
1853
|
+
const out = {};
|
|
1854
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
1855
|
+
if (typeof v !== "number" || !Number.isFinite(v)) throw new Error("history row: column \"state_ms\" holds a non-number");
|
|
1856
|
+
out[k] = v;
|
|
1857
|
+
}
|
|
1858
|
+
return out;
|
|
1859
|
+
}
|
|
1520
1860
|
function parseMetric(data) {
|
|
1521
1861
|
const source = readText(data, "source");
|
|
1522
1862
|
const subjectKind = readText(data, "subject_kind");
|
|
@@ -1533,7 +1873,8 @@ function parseMetric(data) {
|
|
|
1533
1873
|
unit: readTextOrNull(data, "unit"),
|
|
1534
1874
|
displayName: readText(data, "display_name"),
|
|
1535
1875
|
ingest,
|
|
1536
|
-
declaredAt: readNumber(data, "declared_at")
|
|
1876
|
+
declaredAt: readNumber(data, "declared_at"),
|
|
1877
|
+
values: parseValues(data)
|
|
1537
1878
|
};
|
|
1538
1879
|
}
|
|
1539
1880
|
function parseSeries(data) {
|
|
@@ -1558,7 +1899,8 @@ function parseRollup(data) {
|
|
|
1558
1899
|
last: readNumberOrNull(data, "last"),
|
|
1559
1900
|
lastTs: readNumberOrNull(data, "last_ts"),
|
|
1560
1901
|
twSum: readNumberOrNull(data, "tw_sum"),
|
|
1561
|
-
twMs: readNumberOrNull(data, "tw_ms")
|
|
1902
|
+
twMs: readNumberOrNull(data, "tw_ms"),
|
|
1903
|
+
states: parseStates(data)
|
|
1562
1904
|
};
|
|
1563
1905
|
}
|
|
1564
1906
|
function rollupData(row) {
|
|
@@ -1572,7 +1914,8 @@ function rollupData(row) {
|
|
|
1572
1914
|
last: row.last,
|
|
1573
1915
|
last_ts: row.lastTs,
|
|
1574
1916
|
tw_sum: row.twSum,
|
|
1575
|
-
tw_ms: row.twMs
|
|
1917
|
+
tw_ms: row.twMs,
|
|
1918
|
+
state_ms: row.states === null ? null : JSON.stringify(row.states)
|
|
1576
1919
|
};
|
|
1577
1920
|
}
|
|
1578
1921
|
function collectionOf(tier) {
|
|
@@ -1596,22 +1939,37 @@ var HistoryStore = class {
|
|
|
1596
1939
|
filter: { limit: 2e4 }
|
|
1597
1940
|
})).map((r) => parseMetric(r.data));
|
|
1598
1941
|
}
|
|
1942
|
+
/**
|
|
1943
|
+
* Delete metric rows no lookup can reach: rows an earlier build wrote under an
|
|
1944
|
+
* engine-minted random key (every metric key is `cap:` / `addon:` prefixed).
|
|
1945
|
+
*/
|
|
1946
|
+
async deleteMetrics(keys) {
|
|
1947
|
+
for (const batch of chunks(keys, WRITE_CHUNK)) await this.door.deleteWhere.mutate({
|
|
1948
|
+
namespace: HISTORY_NAMESPACE,
|
|
1949
|
+
collection: COLLECTIONS.metrics,
|
|
1950
|
+
filter: { whereIn: { key: [...batch] } }
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1599
1953
|
async upsertMetric(metric) {
|
|
1600
1954
|
await this.door.upsertMany.mutate({
|
|
1601
1955
|
namespace: HISTORY_NAMESPACE,
|
|
1602
1956
|
collection: COLLECTIONS.metrics,
|
|
1603
1957
|
conflict: ["key"],
|
|
1604
|
-
records: [{
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1958
|
+
records: [{
|
|
1959
|
+
id: metric.key,
|
|
1960
|
+
data: {
|
|
1961
|
+
key: metric.key,
|
|
1962
|
+
source: metric.source,
|
|
1963
|
+
owner_addon: metric.ownerAddon,
|
|
1964
|
+
subject_kind: metric.subjectKind,
|
|
1965
|
+
kind: metric.kind,
|
|
1966
|
+
unit: metric.unit,
|
|
1967
|
+
display_name: metric.displayName,
|
|
1968
|
+
ingest: metric.ingest,
|
|
1969
|
+
declared_at: metric.declaredAt,
|
|
1970
|
+
enum_values: metric.values === null ? null : JSON.stringify(metric.values)
|
|
1971
|
+
}
|
|
1972
|
+
}]
|
|
1615
1973
|
});
|
|
1616
1974
|
}
|
|
1617
1975
|
/** Series with `id > afterId`, ascending — the boot load pages with this. */
|
|
@@ -1858,6 +2216,11 @@ var HistoryStore = class {
|
|
|
1858
2216
|
* The held value also counts toward `min`/`max` when it was in force at the
|
|
1859
2217
|
* start of a bucket, because it really was the series' value then.
|
|
1860
2218
|
*/
|
|
2219
|
+
function credit(states, value, ms) {
|
|
2220
|
+
if (states === null || ms <= 0) return;
|
|
2221
|
+
const key = String(value);
|
|
2222
|
+
states[key] = (states[key] ?? 0) + ms;
|
|
2223
|
+
}
|
|
1861
2224
|
/** One row per 5-minute bucket that has at least one point. */
|
|
1862
2225
|
function rawToFiveMinuteRows(input) {
|
|
1863
2226
|
const rows = [];
|
|
@@ -1876,6 +2239,7 @@ function rawToFiveMinuteRows(input) {
|
|
|
1876
2239
|
let max = Number.NEGATIVE_INFINITY;
|
|
1877
2240
|
let twSum = 0;
|
|
1878
2241
|
let twMs = 0;
|
|
2242
|
+
const states = input.trackStates === true ? {} : null;
|
|
1879
2243
|
let cursor = bucketStart;
|
|
1880
2244
|
let value = held;
|
|
1881
2245
|
if (value !== null) {
|
|
@@ -1890,6 +2254,7 @@ function rawToFiveMinuteRows(input) {
|
|
|
1890
2254
|
const dt = p.ts - cursor;
|
|
1891
2255
|
twSum += value * dt;
|
|
1892
2256
|
twMs += dt;
|
|
2257
|
+
credit(states, value, dt);
|
|
1893
2258
|
}
|
|
1894
2259
|
cursor = p.ts;
|
|
1895
2260
|
value = p.value;
|
|
@@ -1903,6 +2268,7 @@ function rawToFiveMinuteRows(input) {
|
|
|
1903
2268
|
if (value !== null && coverageEnd > cursor) {
|
|
1904
2269
|
twSum += value * (coverageEnd - cursor);
|
|
1905
2270
|
twMs += coverageEnd - cursor;
|
|
2271
|
+
credit(states, value, coverageEnd - cursor);
|
|
1906
2272
|
}
|
|
1907
2273
|
held = last.value;
|
|
1908
2274
|
rows.push({
|
|
@@ -1915,7 +2281,8 @@ function rawToFiveMinuteRows(input) {
|
|
|
1915
2281
|
last: last.value,
|
|
1916
2282
|
lastTs: last.ts,
|
|
1917
2283
|
twSum,
|
|
1918
|
-
twMs
|
|
2284
|
+
twMs,
|
|
2285
|
+
states
|
|
1919
2286
|
});
|
|
1920
2287
|
}
|
|
1921
2288
|
return rows;
|
|
@@ -1932,6 +2299,7 @@ function combineRows(input) {
|
|
|
1932
2299
|
let max = Number.NEGATIVE_INFINITY;
|
|
1933
2300
|
let twSum = 0;
|
|
1934
2301
|
let twMs = 0;
|
|
2302
|
+
const states = input.trackStates === true ? {} : null;
|
|
1935
2303
|
let held = input.carryIn;
|
|
1936
2304
|
let cursor = input.windowStart;
|
|
1937
2305
|
for (const row of rows) {
|
|
@@ -1943,6 +2311,7 @@ function combineRows(input) {
|
|
|
1943
2311
|
if (hasTw && gap > 0 && held !== null) {
|
|
1944
2312
|
twSum += held * gap;
|
|
1945
2313
|
twMs += gap;
|
|
2314
|
+
credit(states, held, gap);
|
|
1946
2315
|
if (held < min) min = held;
|
|
1947
2316
|
if (held > max) max = held;
|
|
1948
2317
|
}
|
|
@@ -1950,6 +2319,7 @@ function combineRows(input) {
|
|
|
1950
2319
|
twSum += row.twSum;
|
|
1951
2320
|
twMs += row.twMs;
|
|
1952
2321
|
}
|
|
2322
|
+
if (states !== null && row.states !== null) for (const [state, ms] of Object.entries(row.states)) states[state] = (states[state] ?? 0) + ms;
|
|
1953
2323
|
cursor = row.bucketStart + input.childWidthMs;
|
|
1954
2324
|
held = row.last;
|
|
1955
2325
|
}
|
|
@@ -1957,6 +2327,7 @@ function combineRows(input) {
|
|
|
1957
2327
|
if (hasTw && held !== null && coverageEnd > cursor) {
|
|
1958
2328
|
twSum += held * (coverageEnd - cursor);
|
|
1959
2329
|
twMs += coverageEnd - cursor;
|
|
2330
|
+
credit(states, held, coverageEnd - cursor);
|
|
1960
2331
|
}
|
|
1961
2332
|
return {
|
|
1962
2333
|
seriesId: input.seriesId,
|
|
@@ -1968,7 +2339,8 @@ function combineRows(input) {
|
|
|
1968
2339
|
last: lastRow.last,
|
|
1969
2340
|
lastTs: lastRow.lastTs,
|
|
1970
2341
|
twSum: hasTw ? twSum : null,
|
|
1971
|
-
twMs: hasTw ? twMs : null
|
|
2342
|
+
twMs: hasTw ? twMs : null,
|
|
2343
|
+
states
|
|
1972
2344
|
};
|
|
1973
2345
|
}
|
|
1974
2346
|
/** How far back a boot rebuilds from raw and re-marks the upper tiers. */
|
|
@@ -2109,6 +2481,11 @@ var RollupEngine = class {
|
|
|
2109
2481
|
}
|
|
2110
2482
|
if (handled > 0) this.deps.counters.last1dAt = now;
|
|
2111
2483
|
}
|
|
2484
|
+
/** An `enum` series keeps the time spent in each state in its rollups. */
|
|
2485
|
+
isEnum(seriesId) {
|
|
2486
|
+
const series = this.deps.registry.byIdOf(seriesId);
|
|
2487
|
+
return series !== void 0 && this.deps.registry.metric(series.key)?.kind === "enum";
|
|
2488
|
+
}
|
|
2112
2489
|
/** Raw → 5m rows for buckets in `[from, toExclusive)`; writes them and marks their hours. */
|
|
2113
2490
|
async recomputeFiveMinute(seriesId, from, toExclusive, horizon) {
|
|
2114
2491
|
const { store } = this.deps;
|
|
@@ -2125,7 +2502,8 @@ var RollupEngine = class {
|
|
|
2125
2502
|
seriesId,
|
|
2126
2503
|
points,
|
|
2127
2504
|
carryIn: await store.rawBefore(seriesId, from),
|
|
2128
|
-
horizon
|
|
2505
|
+
horizon,
|
|
2506
|
+
trackStates: this.isEnum(seriesId)
|
|
2129
2507
|
});
|
|
2130
2508
|
if (rows.length === 0) return 0;
|
|
2131
2509
|
await store.upsertRollups("5m", rows);
|
|
@@ -2149,7 +2527,8 @@ var RollupEngine = class {
|
|
|
2149
2527
|
childWidthMs,
|
|
2150
2528
|
rows: await store.readRollups(child, seriesId, windowStart, windowEnd - 1, READ_PAGE),
|
|
2151
2529
|
carryIn: (await store.rollupBefore(child, seriesId, windowStart))?.last ?? null,
|
|
2152
|
-
horizon: now
|
|
2530
|
+
horizon: now,
|
|
2531
|
+
trackStates: this.isEnum(seriesId)
|
|
2153
2532
|
});
|
|
2154
2533
|
if (combined === null) return;
|
|
2155
2534
|
await store.upsertRollups(parent, [combined]);
|
|
@@ -2609,6 +2988,13 @@ var HistoryRuntime = class {
|
|
|
2609
2988
|
* are hub capabilities, and a forked runner cannot call them from
|
|
2610
2989
|
* `onInitialize`. Until then the provider answers "starting" rather than an
|
|
2611
2990
|
* empty history (an unanswered query is not an empty one, D315).
|
|
2991
|
+
*
|
|
2992
|
+
* Readiness follows the STORE, not the registration: the provider registers at
|
|
2993
|
+
* `onInitialize`, but `ready` is emitted only once the runtime is open in
|
|
2994
|
+
* `onHubReachable` (`autoEmitReadiness` is off). Producers declare their
|
|
2995
|
+
* series on `ready`; the automatic emit at registration sent every one of
|
|
2996
|
+
* them into "device-history is starting" on each boot (`.50`, 1.2.607:
|
|
2997
|
+
* metrics-native and pipeline-analytics).
|
|
2612
2998
|
*/
|
|
2613
2999
|
var DEFAULT_CONFIG = {
|
|
2614
3000
|
rawDays: DEFAULT_RETENTION.rawDays,
|
|
@@ -2632,9 +3018,40 @@ var DeviceHistoryAddon = class extends BaseAddon {
|
|
|
2632
3018
|
runtime = null;
|
|
2633
3019
|
unsubscribe = null;
|
|
2634
3020
|
removalHook = null;
|
|
3021
|
+
/** This instance's readiness generation (the base class keeps its own private). */
|
|
3022
|
+
readinessGeneration = `device-history-${Date.now().toString(36)}`;
|
|
2635
3023
|
constructor() {
|
|
2636
3024
|
super({ ...DEFAULT_CONFIG });
|
|
2637
3025
|
}
|
|
3026
|
+
/** `ready` is the store being open, emitted by {@link emitStoreReadiness}. */
|
|
3027
|
+
get autoEmitReadiness() {
|
|
3028
|
+
return false;
|
|
3029
|
+
}
|
|
3030
|
+
/** Readiness of both provided caps, on the bare node this runner belongs to. */
|
|
3031
|
+
emitStoreReadiness(state) {
|
|
3032
|
+
const ctx = this.ctxIfReady;
|
|
3033
|
+
if (ctx === null) return;
|
|
3034
|
+
const raw = ctx.kernel?.localNodeId ?? "hub";
|
|
3035
|
+
const nodeId = raw.includes("/") ? raw.split("/")[0] ?? raw : raw;
|
|
3036
|
+
for (const capName of [deviceHistoryCapability.name, deviceRemovalHookCapability.name]) try {
|
|
3037
|
+
emitReadiness(ctx.eventBus, {
|
|
3038
|
+
capName,
|
|
3039
|
+
scope: {
|
|
3040
|
+
type: "node",
|
|
3041
|
+
nodeId
|
|
3042
|
+
},
|
|
3043
|
+
state,
|
|
3044
|
+
generation: this.readinessGeneration,
|
|
3045
|
+
sourceNodeId: nodeId
|
|
3046
|
+
});
|
|
3047
|
+
} catch (err) {
|
|
3048
|
+
ctx.logger.warn("device-history: readiness not emitted", { meta: {
|
|
3049
|
+
capName,
|
|
3050
|
+
state,
|
|
3051
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3052
|
+
} });
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
2638
3055
|
retention() {
|
|
2639
3056
|
const c = this.config;
|
|
2640
3057
|
return {
|
|
@@ -2656,7 +3073,7 @@ var DeviceHistoryAddon = class extends BaseAddon {
|
|
|
2656
3073
|
}),
|
|
2657
3074
|
listNodeSeries: async (input) => this.requireRuntime().query.listSeries({
|
|
2658
3075
|
kind: "node",
|
|
2659
|
-
nodeId: input.
|
|
3076
|
+
nodeId: input.forNodeId
|
|
2660
3077
|
}),
|
|
2661
3078
|
query: async ({ deviceId, ...window }) => this.requireRuntime().query.query({
|
|
2662
3079
|
...window,
|
|
@@ -2665,13 +3082,14 @@ var DeviceHistoryAddon = class extends BaseAddon {
|
|
|
2665
3082
|
deviceId
|
|
2666
3083
|
}
|
|
2667
3084
|
}),
|
|
2668
|
-
queryNode: async ({
|
|
3085
|
+
queryNode: async ({ forNodeId, ...window }) => this.requireRuntime().query.query({
|
|
2669
3086
|
...window,
|
|
2670
3087
|
subject: {
|
|
2671
3088
|
kind: "node",
|
|
2672
|
-
nodeId
|
|
3089
|
+
nodeId: forNodeId
|
|
2673
3090
|
}
|
|
2674
3091
|
}),
|
|
3092
|
+
queryMany: async (input) => ({ results: await this.requireRuntime().query.queryMany(input) }),
|
|
2675
3093
|
declareSeries: async (input) => this.requireRuntime().ingest.declareSeries(callerOf(input), input),
|
|
2676
3094
|
record: async (input) => this.requireRuntime().ingest.record(callerOf(input), input.points),
|
|
2677
3095
|
recordBuckets: async (input) => this.requireRuntime().ingest.recordBuckets(callerOf(input), input.buckets),
|
|
@@ -2679,6 +3097,7 @@ var DeviceHistoryAddon = class extends BaseAddon {
|
|
|
2679
3097
|
setDeviceEnabled: async (input) => ({ enabled: await this.requireRuntime().setEnabled(input.deviceId, input.enabled) }),
|
|
2680
3098
|
getStatus: async () => this.requireRuntime().status()
|
|
2681
3099
|
};
|
|
3100
|
+
this.emitStoreReadiness("starting");
|
|
2682
3101
|
this.ctx.logger.info("Initialized — device-history provider registered (store opens on hub link)");
|
|
2683
3102
|
const removalHook = {
|
|
2684
3103
|
onDeviceRemoved: async (input) => {
|
|
@@ -2755,6 +3174,7 @@ var DeviceHistoryAddon = class extends BaseAddon {
|
|
|
2755
3174
|
logger: this.ctx.logger,
|
|
2756
3175
|
remove: (input) => removalHook.onDeviceRemoved(input)
|
|
2757
3176
|
}));
|
|
3177
|
+
this.emitStoreReadiness("ready");
|
|
2758
3178
|
this.ctx.logger.info("device-history: capturing", { meta: {
|
|
2759
3179
|
series: runtime.status().series,
|
|
2760
3180
|
metrics: runtime.status().metrics
|
|
@@ -2766,6 +3186,7 @@ var DeviceHistoryAddon = class extends BaseAddon {
|
|
|
2766
3186
|
await this.runtime?.stop();
|
|
2767
3187
|
}
|
|
2768
3188
|
async onShutdown() {
|
|
3189
|
+
this.emitStoreReadiness("down");
|
|
2769
3190
|
await this.stopRuntime();
|
|
2770
3191
|
this.runtime = null;
|
|
2771
3192
|
}
|