@camstack/system 1.2.476 → 1.2.478

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/assets/display-palette/icon.svg +7 -0
  2. package/dist/{agent-https-vpf_D97z.mjs → agent-https-CXMsY05a.mjs} +1 -1
  3. package/dist/{agent-https-DkXBXEvz.js → agent-https-D6a1IRPK.js} +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  8. package/dist/builtins/alerts/alerts.addon.js +1 -1
  9. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  10. package/dist/builtins/autotrack/index.js +1 -1
  11. package/dist/builtins/autotrack/index.mjs +1 -1
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
  13. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
  14. package/dist/builtins/calendars/calendars.addon.js +1 -1
  15. package/dist/builtins/calendars/calendars.addon.mjs +1 -1
  16. package/dist/builtins/camera-grid/index.js +1 -1
  17. package/dist/builtins/camera-grid/index.mjs +1 -1
  18. package/dist/builtins/composer/composer-runtime-factory.d.ts +2 -1
  19. package/dist/builtins/composer/composer.addon.js +453 -6
  20. package/dist/builtins/composer/composer.addon.mjs +453 -6
  21. package/dist/builtins/composer/composer.d.ts +6 -0
  22. package/dist/builtins/composer/composition-runtime.d.ts +3 -0
  23. package/dist/builtins/composer/field-record.d.ts +7 -0
  24. package/dist/builtins/composer/pulse-seed-resolve.d.ts +59 -0
  25. package/dist/builtins/composer/pulse-seed.d.ts +75 -0
  26. package/dist/builtins/console-logging/index.js +1 -1
  27. package/dist/builtins/console-logging/index.mjs +1 -1
  28. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  29. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  30. package/dist/builtins/device-history/device-history.addon.js +63 -953
  31. package/dist/builtins/device-history/device-history.addon.mjs +2 -892
  32. package/dist/builtins/device-manager/device-manager.addon.js +1 -1
  33. package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
  34. package/dist/builtins/display-palette/display-palette.addon.d.ts +12 -0
  35. package/dist/builtins/display-palette/display-palette.addon.js +85 -0
  36. package/dist/builtins/display-palette/display-palette.addon.mjs +79 -0
  37. package/dist/builtins/display-palette/index.d.ts +1 -0
  38. package/dist/builtins/display-palette/index.js +8 -0
  39. package/dist/builtins/display-palette/index.mjs +2 -0
  40. package/dist/builtins/hub-forwarder/index.js +1 -1
  41. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  42. package/dist/builtins/keychain/index.js +3 -3
  43. package/dist/builtins/keychain/index.mjs +3 -3
  44. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  45. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  46. package/dist/builtins/local-auth/local-auth.addon.js +2 -2
  47. package/dist/builtins/local-auth/local-auth.addon.mjs +2 -2
  48. package/dist/builtins/local-network/local-network.addon.js +2 -2
  49. package/dist/builtins/local-network/local-network.addon.mjs +2 -2
  50. package/dist/builtins/loki-logging/index.js +1 -1
  51. package/dist/builtins/loki-logging/index.mjs +1 -1
  52. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  53. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  54. package/dist/builtins/platform-probe/index.js +1 -1
  55. package/dist/builtins/platform-probe/index.mjs +1 -1
  56. package/dist/builtins/record-entries/index.js +1 -1
  57. package/dist/builtins/record-entries/index.mjs +1 -1
  58. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  59. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  60. package/dist/builtins/snapshot/index.js +1 -1
  61. package/dist/builtins/snapshot/index.mjs +1 -1
  62. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  63. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  64. package/dist/builtins/sqlite-storage/index.js +1 -1
  65. package/dist/builtins/sqlite-storage/index.mjs +1 -1
  66. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
  67. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
  68. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  69. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  70. package/dist/builtins/system-config/system-config.addon.js +3 -3
  71. package/dist/builtins/system-config/system-config.addon.mjs +3 -3
  72. package/dist/builtins/winston-logging/index.js +1 -1
  73. package/dist/builtins/winston-logging/index.mjs +1 -1
  74. package/dist/{composition-sources-B5nK7pHt.mjs → composition-sources-CQcYYZxL.mjs} +1 -1
  75. package/dist/{composition-sources-BNOP9mFp.js → composition-sources-yfxjp3qV.js} +1 -1
  76. package/dist/{constants-D4VigrMM.js → constants-D--JWd55.js} +1 -1
  77. package/dist/{constants-jbhoFV7w.mjs → constants-lWkcQxVE.mjs} +1 -1
  78. package/dist/{device-ca-DhyQeXsa.js → device-ca-D0KnLHMw.js} +1 -1
  79. package/dist/{device-ca-CvBZeyVX.mjs → device-ca-J9ZsyoFw.mjs} +1 -1
  80. package/dist/{dist-B_V9r61Z.mjs → dist-OC-L50JY.mjs} +751 -70
  81. package/dist/{dist-CNU7cdFf.js → dist-iVUkxljP.js} +792 -69
  82. package/dist/file-host-worker.js +1 -1
  83. package/dist/file-host-worker.mjs +1 -1
  84. package/dist/{hub-mdns-ciao-DFidf52M.js → hub-mdns-ciao-BDKiKV75.js} +2 -2
  85. package/dist/{hub-mdns-ciao-BuUg0gpI.mjs → hub-mdns-ciao-CvXWbJub.mjs} +2 -2
  86. package/dist/index.js +7 -7
  87. package/dist/index.mjs +7 -7
  88. package/dist/{restore-archive-CDo4Zlmx.mjs → restore-archive-D1Uzojhc.mjs} +1 -1
  89. package/dist/{restore-archive-BtCheeWu.js → restore-archive-DxkdJpWd.js} +1 -1
  90. package/dist/retention-B3z_lBP1.js +1024 -0
  91. package/dist/retention-DdVnmXXS.mjs +893 -0
  92. package/dist/{wal-maintenance-DItlr0Yu.mjs → wal-maintenance-DuO0D2H3.mjs} +0 -0
  93. package/dist/{wal-maintenance-B7xBeIFi.js → wal-maintenance-o9TyZeYp.js} +0 -0
  94. package/package.json +19 -1
@@ -1,895 +1,5 @@
1
- import { $r as DeviceType, B as HISTORY_ITEM_CATALOG, Ct as armRemovalReconcile, D as DEVICE_HISTORY_ENERGY_KEY, Oa as EventCategory, Qi as historyItemFieldPath, Yi as emitReadiness, en as deviceHistoryCapability, nn as deviceRemovalHookCapability, ti as BaseAddon, ui as HISTORY_DEFAULT_MIN_INTERVAL_MS, z as HISTORY_CATALOG } from "../../dist-B_V9r61Z.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
- /**
189
- * Tier by tier in slices, then the series row (and whatever the slices missed).
190
- * Answers the rows the slices deleted per tier. The caller removes the series
191
- * from the registry, which releases its per-series state.
192
- */
193
- async purgeSeries(series, now) {
194
- const deleted = {
195
- raw: 0,
196
- "5m": 0,
197
- "1h": 0,
198
- "1d": 0
199
- };
200
- for (const tier of TIERS) {
201
- const lo = tier === "raw" || tier === "5m" ? Math.max(series.firstTs - BUCKET_LEAD_MS, now - SHORT_TIER_LOOKBACK_MS) : series.firstTs - BUCKET_LEAD_MS;
202
- deleted[tier] = await this.deleteSliced(tier, series.id, Math.max(0, lo), now + DAY_MS);
203
- this.deps.counters.addDeleted(tier, deleted[tier]);
204
- }
205
- await this.deps.store.deleteSeries(series.id);
206
- return deleted;
207
- }
208
- /**
209
- * Delete every series of a DELETED device now (D865). Driven by a tombstone
210
- * through the `device-removal-hook`, never by a listing: the week-long
211
- * absence clock above remains the backstop for a device removed before
212
- * tombstones existed.
213
- */
214
- async purgeDevice(deviceId, now) {
215
- const { registry, counters } = this.deps;
216
- let purged = 0;
217
- for (const series of registry.all()) {
218
- if (series.deviceId !== deviceId) continue;
219
- await this.purgeSeries(series, now);
220
- registry.remove(series.id);
221
- counters.purgedSeries += 1;
222
- purged += 1;
223
- }
224
- return purged;
225
- }
226
- async purgeDeparted(now) {
227
- const { registry, departures, counters, logger } = this.deps;
228
- let purged = 0;
229
- for (const series of registry.all()) {
230
- if (purged >= MAX_PURGES_PER_RUN) break;
231
- if (series.deviceId === null) continue;
232
- const since = departures.missingSince(series.deviceId);
233
- if (since === void 0 || now - since < DEPARTED_ABSENCE_MS) continue;
234
- await this.purgeSeries(series, now);
235
- registry.remove(series.id);
236
- counters.purgedSeries += 1;
237
- purged += 1;
238
- logger.info("device-history: purged the series of an unregistered device", {
239
- tags: { deviceId: series.deviceId },
240
- meta: { key: series.key }
241
- });
242
- }
243
- }
244
- };
245
- //#endregion
246
- //#region src/builtins/device-history/store/series-registry.ts
247
- function subjectIdOf(subject) {
248
- return subject.kind === "device" ? `d:${subject.deviceId}` : `n:${subject.nodeId}`;
249
- }
250
- function deviceIdOf(subject) {
251
- return subject.kind === "device" ? subject.deviceId : null;
252
- }
253
- /** Every metric key is a cap field (`cap:`), an addon's series (`addon:`) or derived. */
254
- function isMetricKey(key) {
255
- return key.startsWith("cap:") || key.startsWith("addon:");
256
- }
257
- function identityOf(key, subjectId, scope) {
258
- return `${key}\u0000${subjectId}\u0000${scope}`;
259
- }
260
- var LOAD_PAGE = 5e3;
261
- /** Append what `next` names that `stored` lacks, in `next`'s order: an ordinal never moves. */
262
- function mergeEnumValues(stored, next) {
263
- if (stored === null) return next;
264
- if (next === null) return stored;
265
- const missing = next.filter((v) => !stored.includes(v));
266
- return missing.length === 0 ? stored : [...stored, ...missing];
267
- }
268
- var SeriesRegistry = class {
269
- store;
270
- metrics = /* @__PURE__ */ new Map();
271
- byIdentity = /* @__PURE__ */ new Map();
272
- byId = /* @__PURE__ */ new Map();
273
- inflight = /* @__PURE__ */ new Map();
274
- /** Enum metrics whose value list grew in memory and is not stored yet. */
275
- dirtyMetrics = /* @__PURE__ */ new Set();
276
- removeListeners = [];
277
- constructor(store) {
278
- this.store = store;
279
- }
280
- async load() {
281
- this.metrics.clear();
282
- this.byIdentity.clear();
283
- this.byId.clear();
284
- const stray = [];
285
- for (const m of await this.store.loadMetrics()) if (isMetricKey(m.key)) this.metrics.set(m.key, m);
286
- else stray.push(m.key);
287
- if (stray.length > 0) await this.store.deleteMetrics(stray);
288
- let afterId = 0;
289
- for (;;) {
290
- const page = await this.store.loadSeriesPage(afterId, LOAD_PAGE);
291
- for (const s of page) this.index(s);
292
- const last = page[page.length - 1];
293
- if (last === void 0 || page.length < LOAD_PAGE) break;
294
- afterId = last.id;
295
- }
296
- }
297
- index(series) {
298
- this.byId.set(series.id, series);
299
- this.byIdentity.set(identityOf(series.key, series.subject, series.scope), series);
300
- }
301
- metric(key) {
302
- return this.metrics.get(key);
303
- }
304
- allMetrics() {
305
- return [...this.metrics.values()];
306
- }
307
- /**
308
- * Persist and remember a metric; idempotent. An `enum` value list is MERGED
309
- * append-only with the stored one, so reordering a `z.enum` never changes
310
- * what a stored ordinal means.
311
- */
312
- async declareMetric(metric) {
313
- const existing = this.metrics.get(metric.key);
314
- const merged = {
315
- ...metric,
316
- values: mergeEnumValues(existing?.values ?? null, metric.values)
317
- };
318
- if (existing !== void 0 && sameMetric(existing, merged)) return;
319
- await this.store.upsertMetric(merged);
320
- this.metrics.set(metric.key, merged);
321
- }
322
- /**
323
- * The ordinal of `value` in an `enum` metric's list, learning (appending) a
324
- * value seen for the first time. `null` when the key is not an enum metric or
325
- * its list is full ({@link MAX_ENUM_VALUES}). A learned value is stored by
326
- * {@link persistDirtyMetrics} BEFORE any point that uses it.
327
- */
328
- ordinalOf(key, value) {
329
- const metric = this.metrics.get(key);
330
- if (metric === void 0 || metric.kind !== "enum") return null;
331
- const values = metric.values ?? [];
332
- const known = values.indexOf(value);
333
- if (known >= 0) return known;
334
- if (values.length >= 1024) return null;
335
- this.metrics.set(key, {
336
- ...metric,
337
- values: [...values, value]
338
- });
339
- this.dirtyMetrics.add(key);
340
- return values.length;
341
- }
342
- /** Store every value list that grew in memory. Throws (and keeps them dirty) on a failed write. */
343
- async persistDirtyMetrics() {
344
- for (const key of this.dirtyMetrics) {
345
- const metric = this.metrics.get(key);
346
- if (metric !== void 0) await this.store.upsertMetric(metric);
347
- this.dirtyMetrics.delete(key);
348
- }
349
- }
350
- find(key, subjectId, scope) {
351
- return this.byIdentity.get(identityOf(key, subjectId, scope));
352
- }
353
- byIdOf(id) {
354
- return this.byId.get(id);
355
- }
356
- all() {
357
- return [...this.byId.values()];
358
- }
359
- forSubject(subjectId) {
360
- return this.all().filter((s) => s.subject === subjectId);
361
- }
362
- forKeyAndSubject(key, subjectId) {
363
- return this.all().filter((s) => s.key === key && s.subject === subjectId);
364
- }
365
- /** The series, created on first sight. `ts` stamps `first_ts`/`last_ts` of a new one. */
366
- async ensure(key, subject, scope, ts) {
367
- const subjectId = subjectIdOf(subject);
368
- const identity = identityOf(key, subjectId, scope);
369
- const known = this.byIdentity.get(identity);
370
- if (known !== void 0) return known;
371
- const pending = this.inflight.get(identity);
372
- if (pending !== void 0) return pending;
373
- const created = this.create(key, subjectId, deviceIdOf(subject), scope, ts).finally(() => {
374
- this.inflight.delete(identity);
375
- });
376
- this.inflight.set(identity, created);
377
- return created;
378
- }
379
- async create(key, subjectId, deviceId, scope, ts) {
380
- const stored = await this.store.findSeries(key, subjectId, scope) ?? await this.store.insertSeries({
381
- key,
382
- subject: subjectId,
383
- deviceId,
384
- scope,
385
- ts
386
- });
387
- this.index(stored);
388
- return stored;
389
- }
390
- /** Remember a widened span (the store write is the caller's). */
391
- noteSpan(id, firstTs, lastTs) {
392
- const current = this.byId.get(id);
393
- if (current === void 0) return;
394
- this.index({
395
- ...current,
396
- firstTs: Math.min(current.firstTs, firstTs),
397
- lastTs: Math.max(current.lastTs, lastTs)
398
- });
399
- }
400
- remove(id) {
401
- const current = this.byId.get(id);
402
- if (current === void 0) return;
403
- this.byId.delete(id);
404
- this.byIdentity.delete(identityOf(current.key, current.subject, current.scope));
405
- for (const listener of this.removeListeners) listener(current);
406
- }
407
- /** Per-series state elsewhere (last recorded value, dirty marks, rate limits) is dropped through this. */
408
- onRemove(listener) {
409
- this.removeListeners.push(listener);
410
- }
411
- };
412
- function sameMetric(a, b) {
413
- 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);
414
- }
415
- //#endregion
416
- //#region src/builtins/device-history/capture/capture-filter.ts
417
- /**
418
- * The per-point capture decision for a cap field (D850, plan §5.3). PURE.
419
- *
420
- * A point is written only when the value CHANGED from the last one recorded,
421
- * by more than the field's deadband, and no sooner than its capture floor:
422
- *
423
- * - `unchanged` — the same value again; a change-only series stores nothing;
424
- * - `deadband` — a change smaller than `deadband` (absolute) or
425
- * `deadbandPct` (relative to the last recorded value); a slow
426
- * drift is still caught, because the comparison is against
427
- * the last RECORDED value, not the last seen one;
428
- * - `defer` — changed, but inside `minIntervalMs` of the last recorded
429
- * point. NOT dropped: the caller keeps it pending and emits it
430
- * once the floor has passed, so a burst never loses its end.
431
- */
432
- /** Tolerance for floating-point steps such as 21.3 - 21.2 against a 0.1 deadband. */
433
- var DEADBAND_EPSILON = 1e-9;
434
- /** The capture floor of a field: its own `minIntervalMs`, else the kind's default. */
435
- function minIntervalOf(spec) {
436
- if (spec.minIntervalMs !== void 0) return spec.minIntervalMs;
437
- return spec.kind === "boolean" || spec.kind === "enum" ? 0 : HISTORY_DEFAULT_MIN_INTERVAL_MS;
438
- }
439
- function clearsDeadband(spec, last, value) {
440
- if (spec.kind === "boolean" || spec.kind === "enum") return true;
441
- const delta = Math.abs(value - last);
442
- if (spec.deadband !== void 0 && delta < spec.deadband - DEADBAND_EPSILON) return false;
443
- if (spec.deadbandPct !== void 0) {
444
- if (delta < Math.abs(last) * spec.deadbandPct / 100 - DEADBAND_EPSILON) return false;
445
- }
446
- return true;
447
- }
448
- function decideCapture(spec, recorded, value, now, options = {}) {
449
- if (!Number.isFinite(value)) return {
450
- action: "skip",
451
- reason: "non-finite"
452
- };
453
- if (recorded === null) return { action: "emit" };
454
- if (value === recorded.value) return {
455
- action: "skip",
456
- reason: "unchanged"
457
- };
458
- if (!clearsDeadband(spec, recorded.value, value)) return {
459
- action: "skip",
460
- reason: "deadband"
461
- };
462
- if (options.ignoreFloor !== true) {
463
- const notBefore = recorded.ts + minIntervalOf(spec);
464
- if (now < notBefore) return {
465
- action: "defer",
466
- notBefore
467
- };
468
- }
469
- return { action: "emit" };
470
- }
471
- //#endregion
472
- //#region src/builtins/device-history/capture/device-directory.ts
473
- /**
474
- * Which devices exist and what type they are — the "real devices only" gate
475
- * (`hub` and `container` are not devices anyone charts) and the
476
- * `excludeDeviceTypes` gate of a history field.
477
- *
478
- * Built from `deviceManager.listAll({ projection: 'slim' })` at boot and every
479
- * reconcile, and per device (`getDevice`) the first time a state event names one
480
- * the map does not know. A FAILED read changes nothing (D49): the previous map
481
- * stays, and the failure is counted and logged, never read as "no devices".
482
- */
483
- function isRealDeviceType(type) {
484
- return type !== DeviceType.Hub && type !== DeviceType.Container;
485
- }
486
- var DeviceDirectory = class {
487
- source;
488
- counters;
489
- logger;
490
- clock;
491
- types = /* @__PURE__ */ new Map();
492
- inflight = /* @__PURE__ */ new Map();
493
- refreshInflight = null;
494
- lastFullRefreshAt = null;
495
- constructor(source, counters, logger, clock) {
496
- this.source = source;
497
- this.counters = counters;
498
- this.logger = logger;
499
- this.clock = clock;
500
- }
501
- typeOf(deviceId) {
502
- return this.types.get(deviceId);
503
- }
504
- size() {
505
- return this.types.size;
506
- }
507
- /** When the full listing last ANSWERED; `null` before the first answer. */
508
- lastRefreshAt() {
509
- return this.lastFullRefreshAt;
510
- }
511
- /** Ids in the directory now (the last full listing plus per-device lookups). */
512
- listedIds() {
513
- return new Set(this.types.keys());
514
- }
515
- knows(deviceId) {
516
- return this.types.has(deviceId);
517
- }
518
- /** Replace the map from the full listing. Returns false (map untouched) when the read failed. */
519
- refresh() {
520
- if (this.refreshInflight !== null) return this.refreshInflight;
521
- const run = this.doRefresh().finally(() => {
522
- this.refreshInflight = null;
523
- });
524
- this.refreshInflight = run;
525
- return run;
526
- }
527
- async doRefresh() {
528
- try {
529
- const all = await this.source.listAll();
530
- this.types = new Map(all.map((d) => [d.id, d.type]));
531
- this.lastFullRefreshAt = this.clock();
532
- this.counters.directoryRefreshAt = this.lastFullRefreshAt;
533
- return true;
534
- } catch (err) {
535
- this.logger.warn("device directory: listing failed — keeping the previous map", { meta: { error: err instanceof Error ? err.message : String(err) } });
536
- return false;
537
- }
538
- }
539
- /** The device's type, asking the hub once when the map does not know it. */
540
- ensure(deviceId) {
541
- const known = this.types.get(deviceId);
542
- if (known !== void 0) return Promise.resolve(known);
543
- const pending = this.inflight.get(deviceId);
544
- if (pending !== void 0) return pending;
545
- const run = this.lookup(deviceId).finally(() => {
546
- this.inflight.delete(deviceId);
547
- });
548
- this.inflight.set(deviceId, run);
549
- return run;
550
- }
551
- async lookup(deviceId) {
552
- try {
553
- const device = await this.source.getDevice(deviceId);
554
- if (device === null) return void 0;
555
- this.types.set(device.id, device.type);
556
- return device.type;
557
- } catch (err) {
558
- this.logger.warn("device directory: lookup failed", {
559
- tags: { deviceId },
560
- meta: { error: err instanceof Error ? err.message : String(err) }
561
- });
562
- return;
563
- }
564
- }
565
- };
566
- //#endregion
567
- //#region src/builtins/device-history/capture/field-path.ts
568
- /**
569
- * Reading a history field out of a cap slice. A catalog path is a top-level
570
- * `runtimeState` key or a path into a FIXED object (`dustBin.level`); the
571
- * catalog guard keeps it out of arrays and records, so a plain walk is enough.
572
- */
573
- function isRecord$2(value) {
574
- return typeof value === "object" && value !== null && !Array.isArray(value);
575
- }
576
- /** The value at `path`, or `undefined` when any step is missing or not an object. */
577
- function readPath(slice, path) {
578
- let current = slice;
579
- for (const step of path.split(".")) {
580
- if (!isRecord$2(current)) return void 0;
581
- current = current[step];
582
- }
583
- return current;
584
- }
585
- /**
586
- * The number a field contributes: a finite number for a gauge/counter, 0/1 for
587
- * a boolean field. Anything else (null, undefined, a string, NaN) is NOT a
588
- * reading — `null` means unknown and must never become 0.
589
- */
590
- function readNumeric(slice, path, kind) {
591
- const raw = readPath(slice, path);
592
- if (kind === "boolean") {
593
- if (typeof raw === "boolean") return raw ? 1 : 0;
594
- return null;
595
- }
596
- if (typeof raw === "number" && Number.isFinite(raw)) return raw;
597
- return null;
598
- }
599
- /** A unit string a `unitFrom` sibling field states, or `null` when it states none. */
600
- function readUnit(slice, unitFrom) {
601
- const raw = readPath(slice, unitFrom);
602
- return typeof raw === "string" && raw.length > 0 ? raw : null;
603
- }
604
- //#endregion
605
- //#region src/builtins/device-history/capture/capture-engine.ts
606
- /**
607
- * Capture of capability fields (D850, plan §5.3), driven by the GENERATED
608
- * history catalog — a field is recorded because its `*.cap.ts` says so, never
609
- * because someone listed it here.
610
- *
611
- * For a `DeviceStateChanged { deviceId, capName, slice }` the engine:
612
- * 1. ignores a cap with no catalog entry (the common case — cheap, uncounted);
613
- * 2. skips an opted-out device (counted), a device whose type is not yet
614
- * known (parked, then replayed once the hub answers), and a `hub` /
615
- * `container` (counted: not a real device);
616
- * 3. per field: skips an excluded device type, an absent or non-numeric value
617
- * (null is UNKNOWN, never 0), then runs {@link decideCapture} against the
618
- * last RECORDED value — change-only, deadband, capture floor;
619
- * 4. buffers the point for the batched write.
620
- *
621
- * A change inside the capture floor is kept PENDING and emitted when the floor
622
- * has passed ({@link CaptureEngine.tickPending}), so the end of a burst is never
623
- * lost. Reconcile ({@link CaptureEngine.reconcile}) restores what a dropped
624
- * event missed from the state mirror's snapshot.
625
- */
626
- var CAP_KEY_PREFIX = "cap:";
627
- function capSeriesKey(capName, fieldPath) {
628
- return `${CAP_KEY_PREFIX}${capName}.${fieldPath}`;
629
- }
630
- /** The series key of an item field: `cap:<capName>.<path>[].<field>`. */
631
- function capItemSeriesKey(capName, items, field) {
632
- return capSeriesKey(capName, historyItemFieldPath(items.path, field));
633
- }
634
- function humanize(capName, fieldPath) {
635
- const field = fieldPath.replace(/([A-Z])/g, " $1").replace(/\./g, " ").toLowerCase();
636
- return `${capName.replace(/-/g, " ")} ${field}`;
637
- }
638
- function metricOf(key, capName, path, spec, now) {
639
- return {
640
- key,
641
- source: "cap",
642
- ownerAddon: "device-history",
643
- subjectKind: "device",
644
- kind: spec.kind,
645
- unit: spec.unit ?? null,
646
- displayName: spec.displayName ?? humanize(capName, path),
647
- ingest: "points",
648
- declaredAt: now,
649
- values: spec.kind === "enum" ? spec.values ?? [] : null
650
- };
651
- }
652
- /** The metric rows of every catalog field and keyed-item field (owner `device-history`). */
653
- function catalogMetrics(catalog, itemCatalog, now) {
654
- const fields = Object.entries(catalog).flatMap(([capName, specs]) => Object.entries(specs).map(([path, spec]) => metricOf(capSeriesKey(capName, path), capName, path, spec, now)));
655
- const items = Object.entries(itemCatalog).flatMap(([capName, block]) => Object.entries(block.fields).map(([field, spec]) => metricOf(capItemSeriesKey(capName, block, field), capName, field, spec, now)));
656
- return [...fields, ...items];
657
- }
658
- function isRecord$1(value) {
659
- return typeof value === "object" && value !== null && !Array.isArray(value);
660
- }
661
- /** Devices waiting on a type lookup; beyond this the oldest is dropped (and counted). */
662
- var MAX_PARKED_DEVICES = 5e3;
663
- /** Seeding reads the newest raw row of each cap series, this many at once. */
664
- var SEED_CONCURRENCY = 8;
665
- var CaptureEngine = class {
666
- deps;
667
- recorded = /* @__PURE__ */ new Map();
668
- pending = /* @__PURE__ */ new Map();
669
- parked = /* @__PURE__ */ new Map();
670
- constructor(deps) {
671
- this.deps = deps;
672
- }
673
- /** Learn the last recorded value of every cap series from the store, so a restart repeats nothing. */
674
- async seed() {
675
- const { registry, store } = this.deps;
676
- const capSeries = registry.all().filter((s) => s.key.startsWith(CAP_KEY_PREFIX));
677
- for (let i = 0; i < capSeries.length; i += SEED_CONCURRENCY) await Promise.all(capSeries.slice(i, i + SEED_CONCURRENCY).map(async (series) => {
678
- const newest = await store.rawBefore(series.id, Number.MAX_SAFE_INTEGER);
679
- if (newest === null) return;
680
- this.recorded.set(identityOf(series.key, series.subject, series.scope), newest);
681
- }));
682
- }
683
- /** One `DeviceStateChanged`. Synchronous: the bus must never wait on us. */
684
- handleState(deviceId, capName, slice, at) {
685
- const fields = this.deps.catalog[capName];
686
- const items = this.deps.itemCatalog[capName];
687
- if (fields === void 0 && items === void 0) return;
688
- const { counters, directory } = this.deps;
689
- counters.eventsSeen += 1;
690
- if (this.deps.optedOut().has(deviceId)) {
691
- counters.skip("opted-out");
692
- return;
693
- }
694
- const type = directory.typeOf(deviceId);
695
- if (type === void 0) {
696
- this.park(deviceId, capName, slice, at);
697
- return;
698
- }
699
- if (!isRealDeviceType(type)) {
700
- counters.skip("not-a-real-device");
701
- return;
702
- }
703
- this.captureSlice(deviceId, type, capName, slice, at, false);
704
- }
705
- /** Every catalogued field and keyed item of one cap slice, for a real device. */
706
- captureSlice(deviceId, type, capName, slice, at, reconciling) {
707
- for (const [path, spec] of Object.entries(this.deps.catalog[capName] ?? {})) {
708
- if (spec.excludeDeviceTypes?.includes(type) === true) {
709
- if (!reconciling) this.deps.counters.skip("excluded-device-type");
710
- continue;
711
- }
712
- const key = capSeriesKey(capName, path);
713
- const value = this.readValue(slice, key, path, spec);
714
- if (value === null) continue;
715
- this.consider(deviceId, key, "", spec, value, at, reconciling);
716
- }
717
- const items = this.deps.itemCatalog[capName];
718
- if (items !== void 0) this.captureItems(deviceId, type, capName, items, slice, at, reconciling);
719
- }
720
- /**
721
- * A keyed array (D917): each item with a usable key contributes its fields
722
- * under that key as the series scope. A value that is not an array is no
723
- * reading; an item without a string key cannot be followed and is counted.
724
- */
725
- captureItems(deviceId, type, capName, items, slice, at, reconciling) {
726
- const list = readPath(slice, items.path);
727
- if (!Array.isArray(list)) return;
728
- const { counters } = this.deps;
729
- if (list.length > 64) counters.skip("item-overflow", list.length - 64);
730
- for (const entry of list.slice(0, 64)) {
731
- const scope = isRecord$1(entry) ? entry[items.key] : void 0;
732
- if (!isRecord$1(entry) || typeof scope !== "string" || scope.length === 0 || scope.length > 128) {
733
- counters.skip("item-key-invalid");
734
- continue;
735
- }
736
- for (const [field, spec] of Object.entries(items.fields)) {
737
- if (spec.excludeDeviceTypes?.includes(type) === true) continue;
738
- const key = capItemSeriesKey(capName, items, field);
739
- const value = this.readValue(entry, key, field, spec);
740
- if (value === null) continue;
741
- this.consider(deviceId, key, scope, spec, value, at, reconciling);
742
- }
743
- }
744
- }
745
- /**
746
- * The number a field contributes, or `null` (not a reading — never 0). An
747
- * `enum` state becomes the ordinal of its value in the metric's list; a value
748
- * the full list cannot take is refused and counted (`enum-overflow`).
749
- */
750
- readValue(slice, key, path, spec) {
751
- if (spec.skipWhenSet !== void 0) {
752
- const gate = readPath(slice, spec.skipWhenSet);
753
- if (gate !== void 0 && gate !== null) return null;
754
- }
755
- if (spec.kind !== "enum") return readNumeric(slice, path, spec.kind);
756
- const raw = readPath(slice, path);
757
- if (typeof raw !== "string" || raw.length === 0) return null;
758
- const ordinal = this.deps.registry.ordinalOf(key, raw);
759
- if (ordinal === null) this.deps.counters.skip("enum-overflow");
760
- return ordinal;
761
- }
762
- consider(deviceId, key, scope, spec, value, at, reconciling) {
763
- const subject = {
764
- kind: "device",
765
- deviceId
766
- };
767
- const identity = identityOf(key, `d:${deviceId}`, scope);
768
- const decision = decideCapture(spec, this.recorded.get(identity) ?? null, value, at, { ignoreFloor: reconciling });
769
- if (decision.action === "emit") {
770
- this.emit(identity, key, subject, scope, value, at);
771
- if (reconciling) this.deps.counters.reconciledPoints += 1;
772
- return;
773
- }
774
- if (decision.action === "defer") {
775
- this.pending.set(identity, {
776
- key,
777
- subject,
778
- scope,
779
- spec,
780
- value
781
- });
782
- return;
783
- }
784
- this.pending.delete(identity);
785
- this.deps.counters.skip(decision.reason);
786
- }
787
- emit(identity, key, subject, scope, value, ts) {
788
- this.recorded.set(identity, {
789
- value,
790
- ts
791
- });
792
- this.pending.delete(identity);
793
- this.deps.counters.pointsEmitted += 1;
794
- this.deps.writer.enqueue({
795
- key,
796
- subject,
797
- scope,
798
- ts,
799
- value
800
- });
801
- }
802
- /** Emit every pending change whose capture floor has passed, stamped where the floor ended. */
803
- tickPending(now) {
804
- for (const [identity, p] of this.pending) {
805
- const recorded = this.recorded.get(identity);
806
- const deviceId = p.subject.kind === "device" ? p.subject.deviceId : -1;
807
- if (this.deps.optedOut().has(deviceId)) {
808
- this.pending.delete(identity);
809
- this.deps.counters.skip("opted-out");
810
- continue;
811
- }
812
- const notBefore = (recorded?.ts ?? now) + minIntervalOf(p.spec);
813
- if (now < notBefore) continue;
814
- this.emit(identity, p.key, p.subject, p.scope, p.value, notBefore);
815
- this.deps.counters.trailingEmitted += 1;
816
- }
817
- }
818
- park(deviceId, capName, slice, at) {
819
- const existing = this.parked.get(deviceId);
820
- if (existing !== void 0) {
821
- existing.set(capName, {
822
- capName,
823
- slice,
824
- at
825
- });
826
- return;
827
- }
828
- if (this.parked.size >= MAX_PARKED_DEVICES) {
829
- const oldest = this.parked.keys().next();
830
- if (oldest.done !== true) this.parked.delete(oldest.value);
831
- this.deps.counters.skip("parked-overflow");
832
- }
833
- this.parked.set(deviceId, new Map([[capName, {
834
- capName,
835
- slice,
836
- at
837
- }]]));
838
- this.deps.directory.ensure(deviceId).then((type) => {
839
- const waiting = this.parked.get(deviceId);
840
- this.parked.delete(deviceId);
841
- if (waiting === void 0) return;
842
- if (type === void 0) {
843
- this.deps.counters.skip("unknown-device", waiting.size);
844
- return;
845
- }
846
- for (const p of waiting.values()) this.handleState(deviceId, p.capName, p.slice, p.at);
847
- });
848
- }
849
- /**
850
- * Restore a missed change from the state mirror's whole snapshot: a field whose
851
- * value differs from the last recorded one gets a point stamped `now`.
852
- * Additive only — it never removes anything, and a snapshot that could not be
853
- * read is the CALLER's failure to count (D49), not an empty snapshot.
854
- */
855
- reconcile(snapshots, now) {
856
- const optedOut = this.deps.optedOut();
857
- for (const [deviceKey, caps] of Object.entries(snapshots)) {
858
- const deviceId = Number(deviceKey);
859
- if (!Number.isInteger(deviceId) || optedOut.has(deviceId)) continue;
860
- const type = this.deps.directory.typeOf(deviceId);
861
- if (type === void 0 || !isRealDeviceType(type)) continue;
862
- for (const [capName, slice] of Object.entries(caps)) this.captureSlice(deviceId, type, capName, slice, now, true);
863
- }
864
- }
865
- /**
866
- * Forget what was "recorded" for points that never reached the store (dropped),
867
- * so the next change-only comparison — and the next reconcile — starts from what
868
- * is really there instead of leaving a hole until the value moves.
869
- */
870
- forget(points) {
871
- for (const p of points) {
872
- const identity = identityOf(p.key, subjectIdOf(p.subject), p.scope);
873
- this.recorded.delete(identity);
874
- this.pending.delete(identity);
875
- }
876
- }
877
- /** A purged series leaves no state behind. */
878
- forgetSeries(series) {
879
- const identity = identityOf(series.key, series.subject, series.scope);
880
- this.recorded.delete(identity);
881
- this.pending.delete(identity);
882
- }
883
- /** For status and specs. */
884
- pendingCount() {
885
- return this.pending.size;
886
- }
887
- /** The last recorded state of a series, for specs. */
888
- lastRecorded(key, subjectId, scope = "") {
889
- return this.recorded.get(identityOf(key, subjectId, scope));
890
- }
891
- };
892
- //#endregion
1
+ import { B as HISTORY_CATALOG, D as DEVICE_HISTORY_ENERGY_KEY, Fa as EventCategory, Tt as armRemovalReconcile, V as HISTORY_ITEM_CATALOG, an as deviceRemovalHookCapability, ci as BaseAddon, na as emitReadiness, rn as deviceHistoryCapability } from "../../dist-OC-L50JY.mjs";
2
+ import { S as subjectIdOf, _ as readUnit, a as FIVE_MINUTES_MS, b as SeriesRegistry, c as bucketEndOf, d as CAP_KEY_PREFIX, f as CaptureEngine, g as readPath, h as catalogMetrics, i as DAY_MS, l as bucketStartOf, m as capSeriesKey, n as RetentionJob, o as HOUR_MS, p as capItemSeriesKey, r as DepartureTracker, s as ROLLUP_GRACE_MS, t as DEFAULT_RETENTION, u as floorTo, v as DeviceDirectory, x as identityOf, y as isRealDeviceType } from "../../retention-DdVnmXXS.mjs";
893
3
  //#region src/builtins/device-history/capture/point-writer.ts
894
4
  var FLUSH_INTERVAL_MS = 1e4;
895
5
  var MAX_BUFFER_AGE_MS = 6e4;