camstack 1.2.187 → 1.2.188

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.
@@ -24024,9 +24024,9 @@ var require_zod = __commonJS({
24024
24024
  }
24025
24025
  });
24026
24026
 
24027
- // ../system/dist/dist-BkNfEJKa.js
24028
- var require_dist_BkNfEJKa = __commonJS({
24029
- "../system/dist/dist-BkNfEJKa.js"(exports) {
24027
+ // ../system/dist/dist-Ct88catN.js
24028
+ var require_dist_Ct88catN = __commonJS({
24029
+ "../system/dist/dist-Ct88catN.js"(exports) {
24030
24030
  "use strict";
24031
24031
  var zod = require_zod();
24032
24032
  __require("crypto");
@@ -27460,6 +27460,7 @@ var require_dist_BkNfEJKa = __commonJS({
27460
27460
  field: zod.z.string().min(1),
27461
27461
  on: zod.z.literal("advance")
27462
27462
  });
27463
+ var HISTORY_DEFAULT_MIN_INTERVAL_MS = 1e4;
27463
27464
  function resolveCapMount(def) {
27464
27465
  if (def.mount) return def.mount;
27465
27466
  if (def.deviceNative === true) return { kind: "device-native" };
@@ -98797,7 +98798,284 @@ var require_dist_BkNfEJKa = __commonJS({
98797
98798
  durability: "session",
98798
98799
  volatileFields: []
98799
98800
  };
98800
- DeviceType.Camera;
98801
+ var HISTORY_CATALOG = {
98802
+ "air-quality-sensor": {
98803
+ "co2Ppm": {
98804
+ kind: "gauge",
98805
+ unit: "ppm",
98806
+ deadband: 5
98807
+ },
98808
+ "vocPpb": {
98809
+ kind: "gauge",
98810
+ unit: "ppb",
98811
+ deadband: 5
98812
+ },
98813
+ "pm25": {
98814
+ kind: "gauge",
98815
+ unit: "ug/m3",
98816
+ deadband: 1
98817
+ },
98818
+ "pm10": {
98819
+ kind: "gauge",
98820
+ unit: "ug/m3",
98821
+ deadband: 1
98822
+ },
98823
+ "aqi": {
98824
+ kind: "gauge",
98825
+ unit: "count",
98826
+ deadband: 1
98827
+ }
98828
+ },
98829
+ "ambient-light-sensor": { "lux": {
98830
+ kind: "gauge",
98831
+ unit: "lx",
98832
+ deadbandPct: 10,
98833
+ minIntervalMs: 6e4
98834
+ } },
98835
+ "battery": { "percentage": {
98836
+ kind: "gauge",
98837
+ unit: "%"
98838
+ } },
98839
+ "binary": { "on": { kind: "boolean" } },
98840
+ "brightness": { "percentage": {
98841
+ kind: "gauge",
98842
+ unit: "%",
98843
+ minIntervalMs: 0
98844
+ } },
98845
+ "carbon-monoxide": { "detected": { kind: "boolean" } },
98846
+ "climate-control": {
98847
+ "currentTemp": {
98848
+ kind: "gauge",
98849
+ unit: "\xB0C",
98850
+ deadband: 0.1
98851
+ },
98852
+ "currentHumidity": {
98853
+ kind: "gauge",
98854
+ unit: "%",
98855
+ deadband: 0.5
98856
+ },
98857
+ "target": {
98858
+ kind: "gauge",
98859
+ unit: "\xB0C",
98860
+ minIntervalMs: 0
98861
+ },
98862
+ "targetHigh": {
98863
+ kind: "gauge",
98864
+ unit: "\xB0C",
98865
+ minIntervalMs: 0
98866
+ },
98867
+ "targetLow": {
98868
+ kind: "gauge",
98869
+ unit: "\xB0C",
98870
+ minIntervalMs: 0
98871
+ }
98872
+ },
98873
+ "contact": { "entryOpen": { kind: "boolean" } },
98874
+ "cover": { "position": {
98875
+ kind: "gauge",
98876
+ unit: "%",
98877
+ minIntervalMs: 0
98878
+ } },
98879
+ "device-status": { "online": { kind: "boolean" } },
98880
+ "dishwasher": { "progressPercent": {
98881
+ kind: "gauge",
98882
+ unit: "%",
98883
+ deadband: 2
98884
+ } },
98885
+ "fan-control": {
98886
+ "percentage": {
98887
+ kind: "gauge",
98888
+ unit: "%",
98889
+ minIntervalMs: 0
98890
+ },
98891
+ "oscillating": { kind: "boolean" }
98892
+ },
98893
+ "flood": { "flooded": { kind: "boolean" } },
98894
+ "gas": { "detected": { kind: "boolean" } },
98895
+ "heating-valve": { "position": {
98896
+ kind: "gauge",
98897
+ unit: "%",
98898
+ deadband: 1
98899
+ } },
98900
+ "humidifier": {
98901
+ "currentHumidity": {
98902
+ kind: "gauge",
98903
+ unit: "%",
98904
+ deadband: 0.5
98905
+ },
98906
+ "on": { kind: "boolean" }
98907
+ },
98908
+ "humidity-sensor": { "percent": {
98909
+ kind: "gauge",
98910
+ unit: "%",
98911
+ deadband: 0.5
98912
+ } },
98913
+ "lawn-mower-control": { "batteryLevel": {
98914
+ kind: "gauge",
98915
+ unit: "%",
98916
+ deadband: 2
98917
+ } },
98918
+ "motion": { "detected": {
98919
+ kind: "boolean",
98920
+ excludeDeviceTypes: [DeviceType.Camera]
98921
+ } },
98922
+ "network-link": {
98923
+ "signalPercent": {
98924
+ kind: "gauge",
98925
+ unit: "%",
98926
+ deadband: 3,
98927
+ minIntervalMs: 6e4
98928
+ },
98929
+ "rssiDbm": {
98930
+ kind: "gauge",
98931
+ unit: "dBm",
98932
+ deadband: 2,
98933
+ minIntervalMs: 6e4
98934
+ }
98935
+ },
98936
+ "numeric-sensor": { "value": {
98937
+ kind: "gauge",
98938
+ unitFrom: "unit",
98939
+ minIntervalMs: 3e4
98940
+ } },
98941
+ "pet-feeder": {
98942
+ "foodLevel": { kind: "gauge" },
98943
+ "food1": { kind: "gauge" },
98944
+ "food2": { kind: "gauge" },
98945
+ "batteryPower": {
98946
+ kind: "gauge",
98947
+ unit: "%",
98948
+ deadband: 2
98949
+ },
98950
+ "desiccantLeftDays": {
98951
+ kind: "gauge",
98952
+ unit: "days"
98953
+ }
98954
+ },
98955
+ "power-meter": {
98956
+ "watts": {
98957
+ kind: "gauge",
98958
+ unit: "W",
98959
+ deadband: 1,
98960
+ minIntervalMs: 3e4
98961
+ },
98962
+ "kwhTotal": {
98963
+ kind: "counter",
98964
+ unit: "kWh",
98965
+ deadband: 0.01,
98966
+ minIntervalMs: 6e4
98967
+ },
98968
+ "volts": {
98969
+ kind: "gauge",
98970
+ unit: "V",
98971
+ deadband: 1,
98972
+ minIntervalMs: 3e4
98973
+ },
98974
+ "amps": {
98975
+ kind: "gauge",
98976
+ unit: "A",
98977
+ deadband: 0.05,
98978
+ minIntervalMs: 3e4
98979
+ },
98980
+ "va": {
98981
+ kind: "gauge",
98982
+ unit: "VA",
98983
+ deadband: 1,
98984
+ minIntervalMs: 3e4
98985
+ },
98986
+ "powerFactor": {
98987
+ kind: "gauge",
98988
+ unit: "ratio",
98989
+ deadband: 0.01,
98990
+ minIntervalMs: 6e4
98991
+ }
98992
+ },
98993
+ "presence": { "batteryPercent": {
98994
+ kind: "gauge",
98995
+ unit: "%",
98996
+ deadband: 1
98997
+ } },
98998
+ "pressure-sensor": { "hpa": {
98999
+ kind: "gauge",
99000
+ unit: "hPa",
99001
+ deadband: 0.1
99002
+ } },
99003
+ "smoke": { "detected": { kind: "boolean" } },
99004
+ "switch": { "on": { kind: "boolean" } },
99005
+ "tamper": { "tampered": { kind: "boolean" } },
99006
+ "temperature-sensor": { "celsius": {
99007
+ kind: "gauge",
99008
+ unit: "\xB0C",
99009
+ deadband: 0.1
99010
+ } },
99011
+ "vacuum-control": {
99012
+ "batteryLevel": {
99013
+ kind: "gauge",
99014
+ unit: "%",
99015
+ deadband: 2
99016
+ },
99017
+ "cleanWater.level": {
99018
+ kind: "gauge",
99019
+ unit: "%",
99020
+ deadband: 2
99021
+ },
99022
+ "dirtyWater.level": {
99023
+ kind: "gauge",
99024
+ unit: "%",
99025
+ deadband: 2
99026
+ },
99027
+ "detergent.level": {
99028
+ kind: "gauge",
99029
+ unit: "%",
99030
+ deadband: 2
99031
+ },
99032
+ "dustBin.level": {
99033
+ kind: "gauge",
99034
+ unit: "%",
99035
+ deadband: 2
99036
+ }
99037
+ },
99038
+ "valve": { "position": {
99039
+ kind: "gauge",
99040
+ unit: "%",
99041
+ minIntervalMs: 0
99042
+ } },
99043
+ "vibration": { "detected": { kind: "boolean" } },
99044
+ "water-heater": {
99045
+ "currentTemp": {
99046
+ kind: "gauge",
99047
+ unit: "\xB0C",
99048
+ deadband: 0.5
99049
+ },
99050
+ "targetTemp": {
99051
+ kind: "gauge",
99052
+ unit: "\xB0C",
99053
+ minIntervalMs: 0
99054
+ }
99055
+ },
99056
+ "weather": {
99057
+ "temperature": {
99058
+ kind: "gauge",
99059
+ unitFrom: "temperatureUnit",
99060
+ deadband: 0.2
99061
+ },
99062
+ "humidity": {
99063
+ kind: "gauge",
99064
+ unit: "%",
99065
+ deadband: 1
99066
+ },
99067
+ "pressure": {
99068
+ kind: "gauge",
99069
+ unitFrom: "pressureUnit",
99070
+ deadband: 0.5
99071
+ },
99072
+ "windSpeed": {
99073
+ kind: "gauge",
99074
+ unitFrom: "windSpeedUnit",
99075
+ deadband: 0.5
99076
+ }
99077
+ }
99078
+ };
98801
99079
  var G711_SCALE_CORRECTION_DB = {
98802
99080
  PCMU: 20 * Math.log10(4),
98803
99081
  PCMA: 20 * Math.log10(8)
@@ -99724,6 +100002,70 @@ var require_dist_BkNfEJKa = __commonJS({
99724
100002
  }
99725
100003
  return out;
99726
100004
  }
100005
+ var REMOVAL_RECONCILE_INTERVAL_MS = 360 * 60 * 1e3;
100006
+ async function reconcileRemovedDevices(opts) {
100007
+ const now = opts.now ?? (() => Date.now());
100008
+ const api = opts.api;
100009
+ if (api === void 0) {
100010
+ opts.logger.warn("device removal reconcile skipped \u2014 no device-manager api");
100011
+ return {
100012
+ read: false,
100013
+ applied: 0,
100014
+ failed: 0
100015
+ };
100016
+ }
100017
+ let tombstones;
100018
+ try {
100019
+ tombstones = await api.deviceManager.listDeletedSince.query({ sinceMs: now() - DEVICE_TOMBSTONE_RETENTION_MS });
100020
+ } catch (err) {
100021
+ opts.logger.warn("device removal reconcile skipped \u2014 tombstones unreadable, nothing deleted", { meta: { error: errMsg(err) } });
100022
+ return {
100023
+ read: false,
100024
+ applied: 0,
100025
+ failed: 0
100026
+ };
100027
+ }
100028
+ let applied = 0;
100029
+ let failed = 0;
100030
+ for (const tombstone of tombstones) try {
100031
+ const result = await opts.remove(tombstone);
100032
+ applied += 1;
100033
+ if (Object.values(result.counts).reduce((sum, n) => sum + n, 0) > 0) opts.logger.info("device removal reconcile cleaned a missed deletion", {
100034
+ tags: { deviceId: tombstone.deviceId },
100035
+ meta: {
100036
+ counts: result.counts,
100037
+ deletedAt: tombstone.deletedAt
100038
+ }
100039
+ });
100040
+ } catch (err) {
100041
+ failed += 1;
100042
+ opts.logger.warn("device removal reconcile failed for a device \u2014 retried next pass", {
100043
+ tags: { deviceId: tombstone.deviceId },
100044
+ meta: { error: errMsg(err) }
100045
+ });
100046
+ }
100047
+ return {
100048
+ read: true,
100049
+ applied,
100050
+ failed
100051
+ };
100052
+ }
100053
+ function armRemovalReconcile(opts) {
100054
+ let running = false;
100055
+ const pass = () => {
100056
+ if (running) return;
100057
+ running = true;
100058
+ reconcileRemovedDevices(opts).catch((err) => {
100059
+ opts.logger.warn("device removal reconcile pass crashed", { meta: { error: errMsg(err) } });
100060
+ }).finally(() => {
100061
+ running = false;
100062
+ });
100063
+ };
100064
+ pass();
100065
+ const timer = setInterval(pass, REMOVAL_RECONCILE_INTERVAL_MS);
100066
+ timer.unref?.();
100067
+ return () => clearInterval(timer);
100068
+ }
99727
100069
  Object.defineProperty(exports, "AGENT_MDNS_SERVICE_TYPE", {
99728
100070
  enumerable: true,
99729
100071
  get: function() {
@@ -100024,6 +100366,18 @@ var require_dist_BkNfEJKa = __commonJS({
100024
100366
  return ForeignSecretMarkerError;
100025
100367
  }
100026
100368
  });
100369
+ Object.defineProperty(exports, "HISTORY_CATALOG", {
100370
+ enumerable: true,
100371
+ get: function() {
100372
+ return HISTORY_CATALOG;
100373
+ }
100374
+ });
100375
+ Object.defineProperty(exports, "HISTORY_DEFAULT_MIN_INTERVAL_MS", {
100376
+ enumerable: true,
100377
+ get: function() {
100378
+ return HISTORY_DEFAULT_MIN_INTERVAL_MS;
100379
+ }
100380
+ });
100027
100381
  Object.defineProperty(exports, "HUB_MDNS_SERVICE_TYPE", {
100028
100382
  enumerable: true,
100029
100383
  get: function() {
@@ -100330,6 +100684,12 @@ var require_dist_BkNfEJKa = __commonJS({
100330
100684
  return alertsCapability;
100331
100685
  }
100332
100686
  });
100687
+ Object.defineProperty(exports, "armRemovalReconcile", {
100688
+ enumerable: true,
100689
+ get: function() {
100690
+ return armRemovalReconcile;
100691
+ }
100692
+ });
100333
100693
  Object.defineProperty(exports, "asJsonObject", {
100334
100694
  enumerable: true,
100335
100695
  get: function() {
@@ -100648,12 +101008,24 @@ var require_dist_BkNfEJKa = __commonJS({
100648
101008
  return deviceExtensionCapability;
100649
101009
  }
100650
101010
  });
101011
+ Object.defineProperty(exports, "deviceHistoryCapability", {
101012
+ enumerable: true,
101013
+ get: function() {
101014
+ return deviceHistoryCapability;
101015
+ }
101016
+ });
100651
101017
  Object.defineProperty(exports, "deviceManagerCapability", {
100652
101018
  enumerable: true,
100653
101019
  get: function() {
100654
101020
  return deviceManagerCapability;
100655
101021
  }
100656
101022
  });
101023
+ Object.defineProperty(exports, "deviceRemovalHookCapability", {
101024
+ enumerable: true,
101025
+ get: function() {
101026
+ return deviceRemovalHookCapability;
101027
+ }
101028
+ });
100657
101029
  Object.defineProperty(exports, "deviceStateCapability", {
100658
101030
  enumerable: true,
100659
101031
  get: function() {
@@ -112906,11 +113278,11 @@ ${values.join("\n")}` : `${blockName} :`;
112906
113278
  }
112907
113279
  });
112908
113280
 
112909
- // ../system/dist/device-ca-CnbrgfmW.js
112910
- var require_device_ca_CnbrgfmW = __commonJS({
112911
- "../system/dist/device-ca-CnbrgfmW.js"(exports) {
113281
+ // ../system/dist/device-ca-75rt3U_U.js
113282
+ var require_device_ca_75rt3U_U = __commonJS({
113283
+ "../system/dist/device-ca-75rt3U_U.js"(exports) {
112912
113284
  "use strict";
112913
- var require_dist10 = require_dist_BkNfEJKa();
113285
+ var require_dist10 = require_dist_Ct88catN();
112914
113286
  var require_x509_es = require_x509_es_C4cOo48M();
112915
113287
  var node_crypto = __require("crypto");
112916
113288
  var SYSTEM_SECRET_OWNER = "system";
@@ -113325,11 +113697,11 @@ var require_device_ca_CnbrgfmW = __commonJS({
113325
113697
  }
113326
113698
  });
113327
113699
 
113328
- // ../system/dist/phase6-declare-gate-Bl7KocEs.js
113329
- var require_phase6_declare_gate_Bl7KocEs = __commonJS({
113330
- "../system/dist/phase6-declare-gate-Bl7KocEs.js"(exports) {
113700
+ // ../system/dist/phase6-declare-gate-C6WqagkC.js
113701
+ var require_phase6_declare_gate_C6WqagkC = __commonJS({
113702
+ "../system/dist/phase6-declare-gate-C6WqagkC.js"(exports) {
113331
113703
  "use strict";
113332
- var require_dist10 = require_dist_BkNfEJKa();
113704
+ var require_dist10 = require_dist_Ct88catN();
113333
113705
  function legacyTableRowCountFromList(tables, collection) {
113334
113706
  return tables.find((t) => t.name === collection)?.rows ?? 0;
113335
113707
  }
@@ -113423,9 +113795,9 @@ var require_alerts_addon = __commonJS({
113423
113795
  [Symbol.toStringTag]: { value: "Module" }
113424
113796
  });
113425
113797
  require_chunk_Cek0wNdY();
113426
- var require_dist10 = require_dist_BkNfEJKa();
113798
+ var require_dist10 = require_dist_Ct88catN();
113427
113799
  var require_read_all_rows$1 = require_read_all_rows_CVoO1ONM();
113428
- var require_phase6_declare_gate = require_phase6_declare_gate_Bl7KocEs();
113800
+ var require_phase6_declare_gate = require_phase6_declare_gate_C6WqagkC();
113429
113801
  var CRITICAL_CATEGORIES = /* @__PURE__ */ new Set([
113430
113802
  "addon.runner-failed",
113431
113803
  "addon.runner-refused",
@@ -115075,7 +115447,7 @@ var require_calendars_addon = __commonJS({
115075
115447
  [Symbol.toStringTag]: { value: "Module" }
115076
115448
  });
115077
115449
  require_chunk_Cek0wNdY();
115078
- var require_dist10 = require_dist_BkNfEJKa();
115450
+ var require_dist10 = require_dist_Ct88catN();
115079
115451
  var require_settle_sources = require_settle_sources_Bhsy57y();
115080
115452
  var zod = require_zod();
115081
115453
  var CALENDAR_COLOR_PALETTE = [
@@ -115293,11 +115665,11 @@ var require_calendars = __commonJS({
115293
115665
  }
115294
115666
  });
115295
115667
 
115296
- // ../system/dist/composition-sources-BwIIltnZ.js
115297
- var require_composition_sources_BwIIltnZ = __commonJS({
115298
- "../system/dist/composition-sources-BwIIltnZ.js"(exports) {
115668
+ // ../system/dist/composition-sources-BQmhc3se.js
115669
+ var require_composition_sources_BQmhc3se = __commonJS({
115670
+ "../system/dist/composition-sources-BQmhc3se.js"(exports) {
115299
115671
  "use strict";
115300
- var require_dist10 = require_dist_BkNfEJKa();
115672
+ var require_dist10 = require_dist_Ct88catN();
115301
115673
  function normalizedNativeOwner(owner) {
115302
115674
  const trimmed = owner?.trim();
115303
115675
  return trimmed ? trimmed : null;
@@ -115412,8 +115784,8 @@ var require_composer_addon = __commonJS({
115412
115784
  [Symbol.toStringTag]: { value: "Module" }
115413
115785
  });
115414
115786
  require_chunk_Cek0wNdY();
115415
- var require_dist10 = require_dist_BkNfEJKa();
115416
- var require_composition_sources = require_composition_sources_BwIIltnZ();
115787
+ var require_dist10 = require_dist_Ct88catN();
115788
+ var require_composition_sources = require_composition_sources_BQmhc3se();
115417
115789
  var zod = require_zod();
115418
115790
  function isRecord$2(value) {
115419
115791
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -119223,7 +119595,7 @@ var require_console_logging = __commonJS({
119223
119595
  [Symbol.toStringTag]: { value: "Module" }
119224
119596
  });
119225
119597
  require_chunk_Cek0wNdY();
119226
- var require_dist10 = require_dist_BkNfEJKa();
119598
+ var require_dist10 = require_dist_Ct88catN();
119227
119599
  var require_formatter = require_formatter_DqAKDlvN();
119228
119600
  var LEVEL_RANK = {
119229
119601
  debug: 0,
@@ -123616,9 +123988,9 @@ var require_core_blocks_addon = __commonJS({
123616
123988
  "use strict";
123617
123989
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
123618
123990
  var require_chunk = require_chunk_Cek0wNdY();
123619
- var require_dist10 = require_dist_BkNfEJKa();
123991
+ var require_dist10 = require_dist_Ct88catN();
123620
123992
  var require_read_all_rows$1 = require_read_all_rows_CVoO1ONM();
123621
- var require_composition_sources = require_composition_sources_BwIIltnZ();
123993
+ var require_composition_sources = require_composition_sources_BQmhc3se();
123622
123994
  var require_prefix_range = require_prefix_range_D_KueXbQ();
123623
123995
  var require_prefix_walk = require_prefix_walk_Dv6MLZlM();
123624
123996
  var zod = require_zod();
@@ -136160,7 +136532,7 @@ var require_device_manager_addon = __commonJS({
136160
136532
  [Symbol.toStringTag]: { value: "Module" }
136161
136533
  });
136162
136534
  require_chunk_Cek0wNdY();
136163
- var require_dist10 = require_dist_BkNfEJKa();
136535
+ var require_dist10 = require_dist_Ct88catN();
136164
136536
  var require_read_all_rows$1 = require_read_all_rows_CVoO1ONM();
136165
136537
  var require_prefix_range = require_prefix_range_D_KueXbQ();
136166
136538
  var require_prefix_walk = require_prefix_walk_Dv6MLZlM();
@@ -145234,7 +145606,7 @@ var require_hub_forwarder = __commonJS({
145234
145606
  [Symbol.toStringTag]: { value: "Module" }
145235
145607
  });
145236
145608
  require_chunk_Cek0wNdY();
145237
- var require_dist10 = require_dist_BkNfEJKa();
145609
+ var require_dist10 = require_dist_Ct88catN();
145238
145610
  var require_formatter = require_formatter_DqAKDlvN();
145239
145611
  var DEFAULT_OUTBOUND_BUFFER_SIZE = 500;
145240
145612
  var HubForwarderDestination = class {
@@ -145371,7 +145743,7 @@ var require_liveness_monitor_addon = __commonJS({
145371
145743
  "use strict";
145372
145744
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
145373
145745
  require_chunk_Cek0wNdY();
145374
- var require_dist10 = require_dist_BkNfEJKa();
145746
+ var require_dist10 = require_dist_Ct88catN();
145375
145747
  var NO_DEVICES = "liveness:no-devices";
145376
145748
  var ALL_OFFLINE = "liveness:all-devices-offline";
145377
145749
  var NO_FOOTAGE_PREFIX = "liveness:no-footage:";
@@ -145734,12 +146106,12 @@ var require_local_auth_addon = __commonJS({
145734
146106
  [Symbol.toStringTag]: { value: "Module" }
145735
146107
  });
145736
146108
  var require_chunk = require_chunk_Cek0wNdY();
145737
- var require_dist10 = require_dist_BkNfEJKa();
146109
+ var require_dist10 = require_dist_Ct88catN();
145738
146110
  var require_read_all_rows$1 = require_read_all_rows_CVoO1ONM();
145739
146111
  var require_safe_buffer$1 = require_safe_buffer_D3aoa_7X();
145740
146112
  var require_ms$1 = require_ms_LB3giGoU();
145741
- var require_device_ca = require_device_ca_CnbrgfmW();
145742
- var require_phase6_declare_gate = require_phase6_declare_gate_Bl7KocEs();
146113
+ var require_device_ca = require_device_ca_75rt3U_U();
146114
+ var require_phase6_declare_gate = require_phase6_declare_gate_C6WqagkC();
145743
146115
  var zod = require_zod();
145744
146116
  var node_crypto = __require("crypto");
145745
146117
  node_crypto = require_chunk.__toESM(node_crypto);
@@ -156534,7 +156906,7 @@ var require_loki_logging = __commonJS({
156534
156906
  [Symbol.toStringTag]: { value: "Module" }
156535
156907
  });
156536
156908
  require_chunk_Cek0wNdY();
156537
- var require_dist10 = require_dist_BkNfEJKa();
156909
+ var require_dist10 = require_dist_Ct88catN();
156538
156910
  function sanitizeLabelName(raw) {
156539
156911
  const replaced = raw.replaceAll(/[^a-zA-Z0-9_]/g, "_");
156540
156912
  return /^[a-zA-Z_]/.test(replaced) ? replaced : `_${replaced}`;
@@ -157206,7 +157578,7 @@ var require_native_metrics_addon = __commonJS({
157206
157578
  [Symbol.toStringTag]: { value: "Module" }
157207
157579
  });
157208
157580
  var require_chunk = require_chunk_Cek0wNdY();
157209
- var require_dist10 = require_dist_BkNfEJKa();
157581
+ var require_dist10 = require_dist_Ct88catN();
157210
157582
  var node_fs_promises = __require("fs/promises");
157211
157583
  var node_util = __require("util");
157212
157584
  var node_os = __require("os");
@@ -157701,11 +158073,9 @@ var require_native_metrics_addon = __commonJS({
157701
158073
  * rather than a replay of a timestamp this node has already delivered.
157702
158074
  *
157703
158075
  * The return value is not diagnostics. It is the idempotence gate the
157704
- * DURABLE tier rides on (`load-series-store.ts`): only an accepted sample is
157705
- * appended to the table, which is what lets that table carry an `INTEGER`
157706
- * rowid key instead of a composite unique index over two million rows. The
157707
- * gate is one monotonic comparison in memory; the index it replaces was
157708
- * measured at ~40 bytes per row.
158076
+ * DURABLE tier rides on this return value (`node-metrics-history-recorder.ts`):
158077
+ * only an accepted sample is sent to device-history. The gate is one monotonic
158078
+ * comparison in memory.
157709
158079
  */
157710
158080
  record(nodeId, atMs, processes) {
157711
158081
  if (nodeId.length === 0) return false;
@@ -157830,138 +158200,7 @@ var require_native_metrics_addon = __commonJS({
157830
158200
  this.lastWriteSeq.delete(nodeId);
157831
158201
  }
157832
158202
  };
157833
- var CONTAINER_MEMORY_COLLECTION = "metrics:node-memory-samples";
157834
158203
  var CONTAINER_MEMORY_MAX_TOTAL_ROWS = 2880;
157835
- var CONTAINER_MEMORY_MAX_ROWS = 5e4;
157836
- var CONTAINER_MEMORY_COLUMNS = [
157837
- {
157838
- name: "id",
157839
- type: "INTEGER",
157840
- primaryKey: true,
157841
- notNull: true
157842
- },
157843
- {
157844
- name: "nodeId",
157845
- type: "TEXT",
157846
- notNull: true
157847
- },
157848
- /** Shared with the process rows of the same tick — that is the whole point. */
157849
- {
157850
- name: "atMs",
157851
- type: "INTEGER",
157852
- notNull: true
157853
- },
157854
- {
157855
- name: "source",
157856
- type: "TEXT",
157857
- notNull: true
157858
- },
157859
- /**
157860
- * Whole MiB, like `rssMib` on the process row and for the same reason: a
157861
- * byte-exact residency on a value in the tens of gigabytes is precision no
157862
- * chart can draw, and it costs bytes on every row forever.
157863
- */
157864
- {
157865
- name: "currentMib",
157866
- type: "INTEGER",
157867
- notNull: true
157868
- },
157869
- /** `NULL` = NO LIMIT. Never a sentinel. */
157870
- {
157871
- name: "limitMib",
157872
- type: "INTEGER"
157873
- },
157874
- /** `NULL` = UNKNOWN on all four. Never zero. */
157875
- {
157876
- name: "anonMib",
157877
- type: "INTEGER"
157878
- },
157879
- {
157880
- name: "fileMib",
157881
- type: "INTEGER"
157882
- },
157883
- {
157884
- name: "shmemMib",
157885
- type: "INTEGER"
157886
- },
157887
- {
157888
- name: "slabMib",
157889
- type: "INTEGER"
157890
- },
157891
- /** Host-wide i915 GEM bytes. `NULL` wherever debugfs is not mounted. */
157892
- {
157893
- name: "gpuShmemMib",
157894
- type: "INTEGER"
157895
- }
157896
- ];
157897
- var CONTAINER_MEMORY_INDEXES = [
157898
- /** The one index, and it serves both the read and the prune — see the
157899
- * process table's `idx_load_samples_node_at` for the full reasoning. */
157900
- {
157901
- name: "idx_node_memory_node_at",
157902
- columns: ["nodeId", "atMs"]
157903
- }
157904
- ];
157905
- var BYTES_PER_MIB$1 = 1048576;
157906
- function toMib(value) {
157907
- return value === null ? null : Math.round(value / BYTES_PER_MIB$1);
157908
- }
157909
- function fromMib(value) {
157910
- return value === null ? null : value * BYTES_PER_MIB$1;
157911
- }
157912
- function snapshotToRow(nodeId, atMs, snapshot) {
157913
- return {
157914
- nodeId,
157915
- atMs,
157916
- source: snapshot.source,
157917
- currentMib: Math.round(snapshot.currentBytes / BYTES_PER_MIB$1),
157918
- limitMib: toMib(snapshot.limitBytes),
157919
- anonMib: toMib(snapshot.anonBytes),
157920
- fileMib: toMib(snapshot.fileBytes),
157921
- shmemMib: toMib(snapshot.shmemBytes),
157922
- slabMib: toMib(snapshot.slabBytes),
157923
- gpuShmemMib: toMib(snapshot.gpuShmemBytes)
157924
- };
157925
- }
157926
- function rowToPoint(row) {
157927
- if (row.source !== "cgroup-v2" && row.source !== "cgroup-v1") return null;
157928
- return {
157929
- atMs: row.atMs,
157930
- source: row.source,
157931
- currentBytes: row.currentMib * BYTES_PER_MIB$1,
157932
- limitBytes: fromMib(row.limitMib),
157933
- anonBytes: fromMib(row.anonMib),
157934
- fileBytes: fromMib(row.fileMib),
157935
- shmemBytes: fromMib(row.shmemMib),
157936
- slabBytes: fromMib(row.slabMib),
157937
- gpuShmemBytes: fromMib(row.gpuShmemMib)
157938
- };
157939
- }
157940
- function recordToMemoryRow(data) {
157941
- const nodeId = data["nodeId"];
157942
- const source = data["source"];
157943
- if (typeof nodeId !== "string" || typeof source !== "string") return null;
157944
- const atMs = Number(data["atMs"]);
157945
- const currentMib = Number(data["currentMib"]);
157946
- if (!Number.isFinite(atMs) || !Number.isFinite(currentMib)) return null;
157947
- const optional = (raw) => {
157948
- if (raw === null || raw === void 0) return null;
157949
- const n = Number(raw);
157950
- return Number.isFinite(n) ? n : null;
157951
- };
157952
- return {
157953
- nodeId,
157954
- atMs,
157955
- source,
157956
- currentMib,
157957
- limitMib: optional(data["limitMib"]),
157958
- anonMib: optional(data["anonMib"]),
157959
- fileMib: optional(data["fileMib"]),
157960
- shmemMib: optional(data["shmemMib"]),
157961
- slabMib: optional(data["slabMib"]),
157962
- gpuShmemMib: optional(data["gpuShmemMib"])
157963
- };
157964
- }
157965
158204
  var ContainerMemoryRing = class {
157966
158205
  ring;
157967
158206
  constructor(options = {}) {
@@ -157971,17 +158210,9 @@ var require_native_metrics_addon = __commonJS({
157971
158210
  ...options.now !== void 0 ? { now: options.now } : {}
157972
158211
  });
157973
158212
  }
157974
- /** Retain one reading. `false` = a timestamp this node already delivered. */
157975
158213
  record(nodeId, atMs, snapshot) {
157976
158214
  return this.ring.record(nodeId, atMs, [snapshot]);
157977
158215
  }
157978
- /**
157979
- * Read one node's retained readings, oldest-first. `sinceMs` is EXCLUSIVE,
157980
- * matching every other reader in this subsystem.
157981
- *
157982
- * A node nobody has recorded answers EMPTY — which is what "this node has no
157983
- * cgroup" looks like, and it must stay distinguishable from a zero.
157984
- */
157985
158216
  read(nodeId, sinceMs) {
157986
158217
  const series = this.ring.read(nodeId, sinceMs);
157987
158218
  const out = [];
@@ -158015,623 +158246,7 @@ var require_native_metrics_addon = __commonJS({
158015
158246
  for (const point of hot) byAt.set(point.atMs, point);
158016
158247
  return [...byAt.values()].toSorted((a, b) => a.atMs - b.atMs);
158017
158248
  }
158018
- var ContainerMemoryStore = class {
158019
- declared = false;
158020
- store;
158021
- logger;
158022
- nowFn;
158023
- prunePageRows;
158024
- constructor(deps) {
158025
- this.store = deps.store;
158026
- this.logger = deps.logger;
158027
- this.nowFn = deps.now ?? (() => Date.now());
158028
- this.prunePageRows = deps.prunePageRows ?? 2e4;
158029
- }
158030
- /** Idempotently declare the collection. `false` when the store refused. */
158031
- async declare() {
158032
- if (this.declared) return true;
158033
- try {
158034
- await this.store.declareCollection.mutate({
158035
- collection: CONTAINER_MEMORY_COLLECTION,
158036
- columns: [...CONTAINER_MEMORY_COLUMNS],
158037
- indexes: CONTAINER_MEMORY_INDEXES.map((i) => ({
158038
- name: i.name,
158039
- columns: [...i.columns]
158040
- }))
158041
- });
158042
- this.declared = true;
158043
- return true;
158044
- } catch (err) {
158045
- this.logger.warn("container memory declareCollection failed \u2014 nothing will be retained on disk", { meta: {
158046
- collection: CONTAINER_MEMORY_COLLECTION,
158047
- error: require_dist10.errMsg(err)
158048
- } });
158049
- return false;
158050
- }
158051
- }
158052
- /** Append ONE reading. */
158053
- async append(nodeId, atMs, snapshot) {
158054
- if (!await this.declare()) return 0;
158055
- const record = { ...snapshotToRow(nodeId, atMs, snapshot) };
158056
- try {
158057
- const { inserted } = await this.store.insertMany.mutate({
158058
- collection: CONTAINER_MEMORY_COLLECTION,
158059
- records: [{ data: record }]
158060
- });
158061
- return inserted;
158062
- } catch (err) {
158063
- this.logger.warn("container memory reading not retained \u2014 this interval will be missing", { meta: {
158064
- nodeId,
158065
- atMs,
158066
- error: require_dist10.errMsg(err)
158067
- } });
158068
- return 0;
158069
- }
158070
- }
158071
- /** Read one node's cold readings, oldest-first. `sinceMs` is EXCLUSIVE. */
158072
- async read(nodeId, sinceMs, limitRows) {
158073
- if (!await this.declare()) return [];
158074
- try {
158075
- const records = await this.store.query.query({
158076
- collection: CONTAINER_MEMORY_COLLECTION,
158077
- filter: {
158078
- where: { nodeId },
158079
- whereBetween: { atMs: [sinceMs + 1, Number.MAX_SAFE_INTEGER] },
158080
- orderBy: {
158081
- field: "atMs",
158082
- direction: "asc"
158083
- },
158084
- limit: limitRows
158085
- }
158086
- });
158087
- const points = [];
158088
- for (const record of records) {
158089
- const row = recordToMemoryRow(record.data);
158090
- if (row === null) continue;
158091
- const point = rowToPoint(row);
158092
- if (point !== null) points.push(point);
158093
- }
158094
- return points;
158095
- } catch (err) {
158096
- this.logger.warn("container memory cold read failed \u2014 answering from the hot window only", { meta: {
158097
- nodeId,
158098
- sinceMs,
158099
- error: require_dist10.errMsg(err)
158100
- } });
158101
- return [];
158102
- }
158103
- }
158104
- /**
158105
- * Enforce BOTH bounds, oldest-first, through a bounded page each.
158106
- *
158107
- * Not rate-limited here: it is driven by the process store's own prune
158108
- * cadence, so it runs at most once a minute by construction and a second
158109
- * timer would be a second thing to get wrong.
158110
- */
158111
- async prune(nodeIds, retention) {
158112
- if (!await this.declare()) return null;
158113
- const ageCutoff = this.nowFn() - retention.retentionHours * 36e5;
158114
- let deletedByAge = 0;
158115
- let rowsExamined = 0;
158116
- for (const nodeId of nodeIds) {
158117
- const outcome = await this.deleteOldestPage(nodeId, ageCutoff, this.prunePageRows);
158118
- deletedByAge += outcome.deleted;
158119
- rowsExamined += outcome.examined;
158120
- }
158121
- const total = await this.count();
158122
- const excess = total === null ? 0 : total - CONTAINER_MEMORY_MAX_ROWS;
158123
- let deletedByCap = 0;
158124
- if (excess > 0) {
158125
- let remaining = Math.min(excess, this.prunePageRows);
158126
- for (const nodeId of nodeIds) {
158127
- if (remaining <= 0) break;
158128
- const outcome = await this.deleteOldestPage(nodeId, null, remaining);
158129
- deletedByCap += outcome.deleted;
158130
- remaining -= outcome.deleted;
158131
- rowsExamined += outcome.examined;
158132
- }
158133
- this.logger.warn("container memory ROW CAP bit \u2014 evicting the oldest readings", { meta: {
158134
- collection: CONTAINER_MEMORY_COLLECTION,
158135
- rows: total,
158136
- cap: CONTAINER_MEMORY_MAX_ROWS,
158137
- over: excess,
158138
- deleted: deletedByCap,
158139
- hint: "lower the retention or the sampling cadence \u2014 the cap is the guarantee, not the intention"
158140
- } });
158141
- }
158142
- return {
158143
- deletedByAge,
158144
- deletedByCap,
158145
- rowsExamined,
158146
- capBit: excess > 0
158147
- };
158148
- }
158149
- /** Total rows, or `null` when the store could not answer. */
158150
- async count() {
158151
- if (!await this.declare()) return null;
158152
- try {
158153
- return await this.store.count.query({ collection: CONTAINER_MEMORY_COLLECTION });
158154
- } catch (err) {
158155
- this.logger.warn("container memory count failed \u2014 the row cap is not enforced this pass", { meta: { error: require_dist10.errMsg(err) } });
158156
- return null;
158157
- }
158158
- }
158159
- /**
158160
- * Delete this node's oldest rows, at most one page's worth.
158161
- *
158162
- * `cutoff === null` means "the oldest rows, whatever their age" — the
158163
- * row-cap path, which is bounded by `limitRows` instead. Either way the page
158164
- * is READ FIRST, ordered by the one index, and its last `atMs` becomes the
158165
- * effective cutoff for a single bounded delete. An unbounded `deleteWhere`
158166
- * would be one statement and one stalling transaction the first time a
158167
- * retention is lowered.
158168
- */
158169
- async deleteOldestPage(nodeId, cutoff, limitRows) {
158170
- if (limitRows <= 0) return {
158171
- deleted: 0,
158172
- examined: 0
158173
- };
158174
- try {
158175
- const page = await this.store.query.query({
158176
- collection: CONTAINER_MEMORY_COLLECTION,
158177
- filter: {
158178
- where: { nodeId },
158179
- ...cutoff === null ? {} : { whereBetween: { atMs: [0, cutoff] } },
158180
- orderBy: {
158181
- field: "atMs",
158182
- direction: "asc"
158183
- },
158184
- limit: Math.min(limitRows, this.prunePageRows)
158185
- },
158186
- columns: ["atMs"]
158187
- });
158188
- if (page.length === 0) return {
158189
- deleted: 0,
158190
- examined: 0
158191
- };
158192
- const effectiveCutoff = Number(page.at(-1)?.data["atMs"]);
158193
- if (!Number.isFinite(effectiveCutoff)) return {
158194
- deleted: 0,
158195
- examined: page.length
158196
- };
158197
- const { deleted } = await this.store.deleteWhere.mutate({
158198
- collection: CONTAINER_MEMORY_COLLECTION,
158199
- filter: {
158200
- where: { nodeId },
158201
- whereBetween: { atMs: [0, effectiveCutoff] }
158202
- }
158203
- });
158204
- return {
158205
- deleted,
158206
- examined: page.length
158207
- };
158208
- } catch (err) {
158209
- this.logger.warn("container memory prune failed \u2014 the table keeps growing this pass", { meta: {
158210
- nodeId,
158211
- cutoff,
158212
- error: require_dist10.errMsg(err)
158213
- } });
158214
- return {
158215
- deleted: 0,
158216
- examined: 0
158217
- };
158218
- }
158219
- }
158220
- };
158221
- var LOAD_SERIES_COLLECTION = "metrics:node-load-samples";
158222
158249
  var DEFAULT_MAX_ROWS = 5e5;
158223
- var LOAD_SERIES_COLUMNS = [
158224
- /** `INTEGER PRIMARY KEY` = SQLite rowid alias: the key IS the row's address,
158225
- * so it costs no separate index and no stored string. */
158226
- {
158227
- name: "id",
158228
- type: "INTEGER",
158229
- primaryKey: true,
158230
- notNull: true
158231
- },
158232
- {
158233
- name: "nodeId",
158234
- type: "TEXT",
158235
- notNull: true
158236
- },
158237
- /** The EMITTING node's timestamp for the sample this row belongs to. Every
158238
- * row of one sample shares it — that is what makes a sample reassemblable. */
158239
- {
158240
- name: "atMs",
158241
- type: "INTEGER",
158242
- notNull: true
158243
- },
158244
- {
158245
- name: "pid",
158246
- type: "INTEGER",
158247
- notNull: true
158248
- },
158249
- /** `NULL` for a process no addon owns — `root` and `system` both. */
158250
- {
158251
- name: "addonId",
158252
- type: "TEXT"
158253
- },
158254
- {
158255
- name: "classification",
158256
- type: "TEXT",
158257
- notNull: true
158258
- },
158259
- /** `ps pcpu` x 10. A LIFETIME average, not a rate — see `NodeProcess`. */
158260
- {
158261
- name: "cpuDeci",
158262
- type: "INTEGER",
158263
- notNull: true
158264
- },
158265
- {
158266
- name: "rssMib",
158267
- type: "INTEGER",
158268
- notNull: true
158269
- },
158270
- /** Instantaneous main-thread CPU% x 10. `NULL` = UNKNOWN, never zero. */
158271
- {
158272
- name: "cpuMainDeci",
158273
- type: "INTEGER"
158274
- },
158275
- /** Instantaneous V8-helper-pool CPU% x 10. `NULL` = UNKNOWN, never zero. */
158276
- {
158277
- name: "cpuGcDeci",
158278
- type: "INTEGER"
158279
- }
158280
- ];
158281
- var LOAD_SERIES_INDEXES = [
158282
- /**
158283
- * The ONE index, and it serves both jobs.
158284
- *
158285
- * Reads are always "this node, newer than T" — `nodeId` leads so the
158286
- * equality is a range scan and `atMs` supplies the order without a sort.
158287
- * Prunes are always "this node, oldest first" — the same index, walked from
158288
- * the other end. A second index on `atMs` alone was measured and rejected:
158289
- * it cost 23 B/row (196.0 → 172.8 with the two column drops) to serve a
158290
- * cross-node prune that a per-node loop over three nodes already serves.
158291
- */
158292
- {
158293
- name: "idx_load_samples_node_at",
158294
- columns: ["nodeId", "atMs"]
158295
- }
158296
- ];
158297
- var BYTES_PER_MIB = 1048576;
158298
- function fromDeci(value) {
158299
- return value === null ? null : Math.round(value) / 10;
158300
- }
158301
- function toDeci(value) {
158302
- return value === null ? null : Math.round(value * 10);
158303
- }
158304
- function rowToProcess(row) {
158305
- return {
158306
- pid: row.pid,
158307
- addonId: row.addonId,
158308
- classification: row.classification,
158309
- cpuPercent: fromDeci(row.cpuDeci) ?? 0,
158310
- memoryRssBytes: row.rssMib * BYTES_PER_MIB,
158311
- cpuMainPercent: fromDeci(row.cpuMainDeci),
158312
- cpuGcPercent: fromDeci(row.cpuGcDeci)
158313
- };
158314
- }
158315
- function processToRow(nodeId, atMs, process2) {
158316
- return {
158317
- nodeId,
158318
- atMs,
158319
- pid: process2.pid,
158320
- addonId: process2.addonId,
158321
- classification: process2.classification,
158322
- cpuDeci: toDeci(process2.cpuPercent) ?? 0,
158323
- rssMib: Math.round(process2.memoryRssBytes / BYTES_PER_MIB),
158324
- cpuMainDeci: toDeci(process2.cpuMainPercent),
158325
- cpuGcDeci: toDeci(process2.cpuGcPercent)
158326
- };
158327
- }
158328
- function rowsToSamples(rows) {
158329
- const byAt = /* @__PURE__ */ new Map();
158330
- for (const row of rows) {
158331
- const held = byAt.get(row.atMs);
158332
- if (held === void 0) byAt.set(row.atMs, [rowToProcess(row)]);
158333
- else held.push(rowToProcess(row));
158334
- }
158335
- return [...byAt.entries()].toSorted((a, b) => a[0] - b[0]).map(([atMs, processes]) => ({
158336
- atMs,
158337
- processes
158338
- }));
158339
- }
158340
- var LoadSeriesStore = class {
158341
- declared = false;
158342
- lastPruneAtMs = 0;
158343
- store;
158344
- logger;
158345
- nowFn;
158346
- prunePageRows;
158347
- pruneIntervalMs;
158348
- constructor(deps) {
158349
- this.store = deps.store;
158350
- this.logger = deps.logger;
158351
- this.nowFn = deps.now ?? (() => Date.now());
158352
- this.prunePageRows = deps.prunePageRows ?? 2e4;
158353
- this.pruneIntervalMs = deps.pruneIntervalMs ?? 6e4;
158354
- }
158355
- /** Idempotently declare the collection. `false` when the store refused. */
158356
- async declare() {
158357
- if (this.declared) return true;
158358
- try {
158359
- await this.store.declareCollection.mutate({
158360
- collection: LOAD_SERIES_COLLECTION,
158361
- columns: [...LOAD_SERIES_COLUMNS],
158362
- indexes: LOAD_SERIES_INDEXES.map((i) => ({
158363
- name: i.name,
158364
- columns: [...i.columns]
158365
- }))
158366
- });
158367
- this.declared = true;
158368
- return true;
158369
- } catch (err) {
158370
- this.logger.warn("load series declareCollection failed \u2014 nothing will be retained on disk", { meta: {
158371
- collection: LOAD_SERIES_COLLECTION,
158372
- error: require_dist10.errMsg(err)
158373
- } });
158374
- return false;
158375
- }
158376
- }
158377
- /**
158378
- * Append ONE sample — every process row of it — in ONE transaction.
158379
- *
158380
- * Never a write per row. `insertMany` exists for exactly this: at the 10 s
158381
- * default the fleet produces 7.6 rows/s, and 7.6 separate commits per second
158382
- * on the connection that also serves every cluster-wide configuration read
158383
- * is a constant load nobody asked for.
158384
- */
158385
- async append(nodeId, atMs, processes) {
158386
- if (processes.length === 0) return 0;
158387
- if (!await this.declare()) return 0;
158388
- const records = processes.map((p) => ({ data: { ...processToRow(nodeId, atMs, p) } }));
158389
- try {
158390
- const { inserted } = await this.store.insertMany.mutate({
158391
- collection: LOAD_SERIES_COLLECTION,
158392
- records
158393
- });
158394
- return inserted;
158395
- } catch (err) {
158396
- this.logger.warn("load series sample not retained \u2014 this interval will be missing", { meta: {
158397
- nodeId,
158398
- atMs,
158399
- rows: processes.length,
158400
- error: require_dist10.errMsg(err)
158401
- } });
158402
- return 0;
158403
- }
158404
- }
158405
- /**
158406
- * Read one node's cold samples, oldest-first.
158407
- *
158408
- * `sinceMs` is EXCLUSIVE, matching the ring, so a caller passing the newest
158409
- * timestamp it holds gets only what it is missing. `limitRows` bounds the
158410
- * read in ROWS (not samples) because rows are what the query costs.
158411
- */
158412
- async read(nodeId, sinceMs, limitRows) {
158413
- if (!await this.declare()) return [];
158414
- try {
158415
- const records = await this.store.query.query({
158416
- collection: LOAD_SERIES_COLLECTION,
158417
- filter: {
158418
- where: { nodeId },
158419
- whereBetween: { atMs: [sinceMs + 1, Number.MAX_SAFE_INTEGER] },
158420
- orderBy: {
158421
- field: "atMs",
158422
- direction: "asc"
158423
- },
158424
- limit: limitRows
158425
- }
158426
- });
158427
- const rows = [];
158428
- for (const record of records) {
158429
- const row = recordToRow(record.data);
158430
- if (row !== null) rows.push(row);
158431
- }
158432
- return rowsToSamples(rows);
158433
- } catch (err) {
158434
- this.logger.warn("load series cold read failed \u2014 answering from the hot window only", { meta: {
158435
- nodeId,
158436
- sinceMs,
158437
- error: require_dist10.errMsg(err)
158438
- } });
158439
- return [];
158440
- }
158441
- }
158442
- /**
158443
- * Enforce BOTH bounds, oldest-first, through a bounded page each.
158444
- *
158445
- * Rate-limited to {@link pruneIntervalMs}: a bound is not a deadline, and the
158446
- * append path must not pay a sweep on every sample.
158447
- */
158448
- async prune(nodeIds, retention, force = false) {
158449
- const now = this.nowFn();
158450
- if (!force && now - this.lastPruneAtMs < this.pruneIntervalMs) return null;
158451
- this.lastPruneAtMs = now;
158452
- if (!await this.declare()) return null;
158453
- let deletedByAge = 0;
158454
- let deletedByCap = 0;
158455
- let rowsExamined = 0;
158456
- const ageCutoff = now - retention.retentionHours * 36e5;
158457
- for (const nodeId of nodeIds) {
158458
- const outcome = await this.pruneNodeToCutoff(nodeId, ageCutoff);
158459
- deletedByAge += outcome.deleted;
158460
- rowsExamined += outcome.examined;
158461
- }
158462
- const total = await this.count();
158463
- const excess = total === null ? 0 : total - retention.maxRows;
158464
- if (excess > 0) {
158465
- const outcome = await this.pruneOldestRows(nodeIds, excess);
158466
- deletedByCap = outcome.deleted;
158467
- rowsExamined += outcome.examined;
158468
- this.logger.warn("load series ROW CAP bit \u2014 evicting the oldest samples", { meta: {
158469
- collection: LOAD_SERIES_COLLECTION,
158470
- rows: total,
158471
- cap: retention.maxRows,
158472
- over: excess,
158473
- deleted: outcome.deleted,
158474
- retentionHours: retention.retentionHours,
158475
- hint: "lower the retention or the sampling cadence \u2014 the cap is the guarantee, not the intention"
158476
- } });
158477
- }
158478
- return {
158479
- deletedByAge,
158480
- deletedByCap,
158481
- rowsExamined,
158482
- capBit: excess > 0
158483
- };
158484
- }
158485
- /** Total rows, or `null` when the store could not answer. */
158486
- async count() {
158487
- if (!await this.declare()) return null;
158488
- try {
158489
- return await this.store.count.query({ collection: LOAD_SERIES_COLLECTION });
158490
- } catch (err) {
158491
- this.logger.warn("load series count failed \u2014 the row cap is not enforced this pass", { meta: { error: require_dist10.errMsg(err) } });
158492
- return null;
158493
- }
158494
- }
158495
- /**
158496
- * Delete this node's rows older than `cutoff`, at most one page's worth.
158497
- *
158498
- * The page is the whole point. `deleteWhere({ atMs: [0, cutoff] })` on its
158499
- * own is one statement but an UNBOUNDED one — a first pass after a retention
158500
- * change would delete millions of rows inside a single stalling transaction.
158501
- * So the page is read first (keys only, ordered by the index), its last
158502
- * `atMs` becomes the EFFECTIVE cutoff, and the delete is bounded by it.
158503
- */
158504
- async pruneNodeToCutoff(nodeId, cutoff) {
158505
- try {
158506
- const page = await this.store.query.query({
158507
- collection: LOAD_SERIES_COLLECTION,
158508
- filter: {
158509
- where: { nodeId },
158510
- whereBetween: { atMs: [0, cutoff] },
158511
- orderBy: {
158512
- field: "atMs",
158513
- direction: "asc"
158514
- },
158515
- limit: this.prunePageRows
158516
- },
158517
- columns: ["atMs"]
158518
- });
158519
- if (page.length === 0) return {
158520
- deleted: 0,
158521
- examined: 0
158522
- };
158523
- const effectiveCutoff = Number(page.at(-1)?.data["atMs"]);
158524
- if (!Number.isFinite(effectiveCutoff)) return {
158525
- deleted: 0,
158526
- examined: page.length
158527
- };
158528
- const { deleted } = await this.store.deleteWhere.mutate({
158529
- collection: LOAD_SERIES_COLLECTION,
158530
- filter: {
158531
- where: { nodeId },
158532
- whereBetween: { atMs: [0, effectiveCutoff] }
158533
- }
158534
- });
158535
- return {
158536
- deleted,
158537
- examined: page.length
158538
- };
158539
- } catch (err) {
158540
- this.logger.warn("load series age prune failed \u2014 the table keeps growing this pass", { meta: {
158541
- nodeId,
158542
- cutoff,
158543
- error: require_dist10.errMsg(err)
158544
- } });
158545
- return {
158546
- deleted: 0,
158547
- examined: 0
158548
- };
158549
- }
158550
- }
158551
- /**
158552
- * Drop the oldest rows across the known nodes until `excess` is covered.
158553
- *
158554
- * Same bounded-page technique, walked per node so the one index serves it.
158555
- * The node holding the oldest rows pays first, which is also the node
158556
- * producing the pressure when a runaway process count is the cause.
158557
- */
158558
- async pruneOldestRows(nodeIds, excess) {
158559
- let remaining = Math.min(excess, this.prunePageRows);
158560
- let deleted = 0;
158561
- let examined = 0;
158562
- for (const nodeId of nodeIds) {
158563
- if (remaining <= 0) break;
158564
- try {
158565
- const page = await this.store.query.query({
158566
- collection: LOAD_SERIES_COLLECTION,
158567
- filter: {
158568
- where: { nodeId },
158569
- orderBy: {
158570
- field: "atMs",
158571
- direction: "asc"
158572
- },
158573
- limit: remaining
158574
- },
158575
- columns: ["atMs"]
158576
- });
158577
- examined += page.length;
158578
- if (page.length === 0) continue;
158579
- const cutoff = Number(page.at(-1)?.data["atMs"]);
158580
- if (!Number.isFinite(cutoff)) continue;
158581
- const result = await this.store.deleteWhere.mutate({
158582
- collection: LOAD_SERIES_COLLECTION,
158583
- filter: {
158584
- where: { nodeId },
158585
- whereBetween: { atMs: [0, cutoff] }
158586
- }
158587
- });
158588
- deleted += result.deleted;
158589
- remaining -= result.deleted;
158590
- } catch (err) {
158591
- this.logger.warn("load series cap prune failed \u2014 the cap is not enforced this pass", { meta: {
158592
- nodeId,
158593
- error: require_dist10.errMsg(err)
158594
- } });
158595
- }
158596
- }
158597
- return {
158598
- deleted,
158599
- examined
158600
- };
158601
- }
158602
- };
158603
- function recordToRow(data) {
158604
- const nodeId = data["nodeId"];
158605
- const classification = data["classification"];
158606
- if (typeof nodeId !== "string" || typeof classification !== "string") return null;
158607
- const atMs = Number(data["atMs"]);
158608
- const pid = Number(data["pid"]);
158609
- const cpuDeci = Number(data["cpuDeci"]);
158610
- const rssMib = Number(data["rssMib"]);
158611
- if (![
158612
- atMs,
158613
- pid,
158614
- cpuDeci,
158615
- rssMib
158616
- ].every((n) => Number.isFinite(n))) return null;
158617
- const rawAddon = data["addonId"];
158618
- const optional = (raw) => {
158619
- if (raw === null || raw === void 0) return null;
158620
- const n = Number(raw);
158621
- return Number.isFinite(n) ? n : null;
158622
- };
158623
- return {
158624
- nodeId,
158625
- atMs,
158626
- pid,
158627
- addonId: typeof rawAddon === "string" ? rawAddon : null,
158628
- classification,
158629
- cpuDeci,
158630
- rssMib,
158631
- cpuMainDeci: optional(data["cpuMainDeci"]),
158632
- cpuGcDeci: optional(data["cpuGcDeci"])
158633
- };
158634
- }
158635
158250
  var LoadSeriesConfigError = class extends Error {
158636
158251
  constructor(message) {
158637
158252
  super(message);
@@ -158730,6 +158345,201 @@ var require_native_metrics_addon = __commonJS({
158730
158345
  oldestAtMs
158731
158346
  };
158732
158347
  }
158348
+ var ADDON_ID = "metrics-native";
158349
+ var NODE_PROCESS_CPU_KEY = `addon:${ADDON_ID}/process-cpu-percent`;
158350
+ var NODE_PROCESS_RSS_KEY = `addon:${ADDON_ID}/process-rss-mib`;
158351
+ var NODE_CONTAINER_CURRENT_KEY = `addon:${ADDON_ID}/container-current-mib`;
158352
+ function nodeMetricsHistoryClient(api) {
158353
+ return {
158354
+ declareSeries: (input) => api.declareSeries.mutate(input),
158355
+ record: (input) => api.record.mutate({ points: [...input.points] }),
158356
+ queryNode: async (input) => {
158357
+ return { series: (await api.queryNode.query(input)).series.map((s) => ({
158358
+ scope: s.scope,
158359
+ points: s.points.map((p) => ({
158360
+ t: p.t,
158361
+ samples: p.samples,
158362
+ value: p.last ?? p.mean
158363
+ }))
158364
+ })) };
158365
+ }
158366
+ };
158367
+ }
158368
+ var NodeMetricsHistoryRecorder = class {
158369
+ deps;
158370
+ declared = false;
158371
+ constructor(deps) {
158372
+ this.deps = deps;
158373
+ }
158374
+ async declare() {
158375
+ if (this.declared) return;
158376
+ const { deviceHistory } = this.deps;
158377
+ await deviceHistory.declareSeries({
158378
+ name: "process-cpu-percent",
158379
+ subjectKind: "node",
158380
+ kind: "gauge",
158381
+ displayName: "Process CPU %",
158382
+ ingest: "points",
158383
+ unit: "%"
158384
+ });
158385
+ await deviceHistory.declareSeries({
158386
+ name: "process-rss-mib",
158387
+ subjectKind: "node",
158388
+ kind: "gauge",
158389
+ displayName: "Process RSS (MiB)",
158390
+ ingest: "points",
158391
+ unit: "MiB"
158392
+ });
158393
+ await deviceHistory.declareSeries({
158394
+ name: "container-current-mib",
158395
+ subjectKind: "node",
158396
+ kind: "gauge",
158397
+ displayName: "Container memory current (MiB)",
158398
+ ingest: "points",
158399
+ unit: "MiB"
158400
+ });
158401
+ this.declared = true;
158402
+ }
158403
+ async recordSample(nodeId, atMs, processes, containerMemory) {
158404
+ if (!await this.ensureDeclared()) return;
158405
+ const points = [];
158406
+ for (const p of processes) {
158407
+ const scope = String(p.pid);
158408
+ points.push({
158409
+ key: NODE_PROCESS_CPU_KEY,
158410
+ subject: {
158411
+ kind: "node",
158412
+ nodeId
158413
+ },
158414
+ scope,
158415
+ ts: atMs,
158416
+ value: p.cpuPercent
158417
+ });
158418
+ points.push({
158419
+ key: NODE_PROCESS_RSS_KEY,
158420
+ subject: {
158421
+ kind: "node",
158422
+ nodeId
158423
+ },
158424
+ scope,
158425
+ ts: atMs,
158426
+ value: Math.round(p.memoryRssBytes / 1048576)
158427
+ });
158428
+ }
158429
+ if (containerMemory !== null) points.push({
158430
+ key: NODE_CONTAINER_CURRENT_KEY,
158431
+ subject: {
158432
+ kind: "node",
158433
+ nodeId
158434
+ },
158435
+ scope: "",
158436
+ ts: atMs,
158437
+ value: Math.round(containerMemory.currentBytes / 1048576)
158438
+ });
158439
+ if (points.length === 0) return;
158440
+ try {
158441
+ await this.deps.deviceHistory.record({ points });
158442
+ } catch (err) {
158443
+ this.deps.logger.warn("node metrics history sample not stored", { meta: {
158444
+ nodeId,
158445
+ atMs,
158446
+ error: err instanceof Error ? err.message : String(err)
158447
+ } });
158448
+ }
158449
+ }
158450
+ async readContainerPoints(nodeId, sinceMs, maxPoints) {
158451
+ if (!await this.ensureDeclared()) return [];
158452
+ const result = await this.deps.deviceHistory.queryNode({
158453
+ nodeId,
158454
+ key: NODE_CONTAINER_CURRENT_KEY,
158455
+ from: sinceMs + 1,
158456
+ to: Date.now(),
158457
+ resolution: "raw",
158458
+ maxPoints
158459
+ });
158460
+ const out = [];
158461
+ for (const s of result.series) for (const p of s.points) {
158462
+ if (p.value === null || !Number.isFinite(p.value)) continue;
158463
+ out.push({
158464
+ atMs: p.t,
158465
+ source: "cgroup-v2",
158466
+ currentBytes: p.value * 1048576,
158467
+ limitBytes: null,
158468
+ anonBytes: null,
158469
+ fileBytes: null,
158470
+ shmemBytes: null,
158471
+ slabBytes: null,
158472
+ gpuShmemBytes: null
158473
+ });
158474
+ }
158475
+ return out.toSorted((a, b) => a.atMs - b.atMs);
158476
+ }
158477
+ async readProcessSamples(nodeId, sinceMs, maxPoints) {
158478
+ if (!await this.ensureDeclared()) return [];
158479
+ const to = Date.now();
158480
+ const [cpuResult, rssResult] = await Promise.all([this.deps.deviceHistory.queryNode({
158481
+ nodeId,
158482
+ key: NODE_PROCESS_CPU_KEY,
158483
+ from: sinceMs + 1,
158484
+ to,
158485
+ resolution: "raw",
158486
+ maxPoints
158487
+ }), this.deps.deviceHistory.queryNode({
158488
+ nodeId,
158489
+ key: NODE_PROCESS_RSS_KEY,
158490
+ from: sinceMs + 1,
158491
+ to,
158492
+ resolution: "raw",
158493
+ maxPoints
158494
+ })]);
158495
+ const byAt = /* @__PURE__ */ new Map();
158496
+ const ingest = (series, field) => {
158497
+ for (const s of series) {
158498
+ const pid = Number(s.scope);
158499
+ if (!Number.isFinite(pid)) continue;
158500
+ for (const p of s.points) {
158501
+ if (p.value === null || !Number.isFinite(p.value)) continue;
158502
+ let perPid = byAt.get(p.t);
158503
+ if (perPid === void 0) {
158504
+ perPid = /* @__PURE__ */ new Map();
158505
+ byAt.set(p.t, perPid);
158506
+ }
158507
+ const base = perPid.get(pid) ?? {
158508
+ pid,
158509
+ addonId: null,
158510
+ classification: "managed",
158511
+ cpuPercent: 0,
158512
+ memoryRssBytes: 0,
158513
+ cpuMainPercent: null,
158514
+ cpuGcPercent: null
158515
+ };
158516
+ perPid.set(pid, field === "cpuPercent" ? {
158517
+ ...base,
158518
+ cpuPercent: p.value
158519
+ } : {
158520
+ ...base,
158521
+ memoryRssBytes: p.value * 1048576
158522
+ });
158523
+ }
158524
+ }
158525
+ };
158526
+ ingest(cpuResult.series, "cpuPercent");
158527
+ ingest(rssResult.series, "memoryRssBytes");
158528
+ return [...byAt.entries()].toSorted((a, b) => a[0] - b[0]).map(([atMs, perPid]) => ({
158529
+ atMs,
158530
+ processes: [...perPid.values()]
158531
+ }));
158532
+ }
158533
+ async ensureDeclared() {
158534
+ try {
158535
+ await this.declare();
158536
+ return true;
158537
+ } catch (err) {
158538
+ this.deps.logger.warn("node metrics history declare failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
158539
+ return false;
158540
+ }
158541
+ }
158542
+ };
158733
158543
  var nodeAccessCheck = (path) => (0, node_fs_promises.access)(path, node_fs.constants.X_OK);
158734
158544
  async function findOnPath(binary, pathEnv, check = nodeAccessCheck) {
158735
158545
  const dirs = (pathEnv ?? "").split(":").filter((dir) => dir.length > 0);
@@ -159577,7 +159387,7 @@ var require_native_metrics_addon = __commonJS({
159577
159387
  * the `settings-store` singleton would ship its rows over Moleculer, once
159578
159388
  * per sample, to reach that very same table.
159579
159389
  */
159580
- loadStore = null;
159390
+ metricsHistory = null;
159581
159391
  /**
159582
159392
  * The container's OWN memory, sampled on the process-snapshot tick.
159583
159393
  *
@@ -159589,10 +159399,8 @@ var require_native_metrics_addon = __commonJS({
159589
159399
  */
159590
159400
  memoryProbe = null;
159591
159401
  pidsProbe = null;
159592
- /** Hot tier for the container reading. Bounds in `container-memory-series.ts`. */
159402
+ /** Hot tier for the container reading. Bounds in `container-memory-ring.ts`. */
159593
159403
  memoryRing = new ContainerMemoryRing();
159594
- /** Cold tier, HUB only — same reasoning as `loadStore`. */
159595
- memoryStore = null;
159596
159404
  /** The resolved knobs. Re-resolved on every settings write. */
159597
159405
  loadConfig = {
159598
159406
  cadenceSec: 10,
@@ -159665,13 +159473,12 @@ var require_native_metrics_addon = __commonJS({
159665
159473
  };
159666
159474
  this.applyLoadSeriesConfig();
159667
159475
  if (this.isHub) {
159668
- this.loadStore = new LoadSeriesStore({
159669
- store: this.ctx.api.settingsStore,
159670
- logger: this.ctx.logger.child("LoadSeries")
159476
+ this.metricsHistory = new NodeMetricsHistoryRecorder({
159477
+ deviceHistory: nodeMetricsHistoryClient(this.ctx.api.deviceHistory),
159478
+ logger: this.ctx.logger.child("NodeMetricsHistory")
159671
159479
  });
159672
- this.memoryStore = new ContainerMemoryStore({
159673
- store: this.ctx.api.settingsStore,
159674
- logger: this.ctx.logger.child("ContainerMemory")
159480
+ this.metricsHistory.declare().catch((err) => {
159481
+ this.ctx.logger.warn("node metrics history declare failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
159675
159482
  });
159676
159483
  }
159677
159484
  this.memoryProbe = new ContainerMemoryProbe({ logger: this.ctx.logger.child("ContainerMemory") });
@@ -159692,7 +159499,7 @@ var require_native_metrics_addon = __commonJS({
159692
159499
  * this node has already delivered, and only an accepted sample reaches the
159693
159500
  * table. That is what makes the append idempotent without a unique index
159694
159501
  * over two million rows — the composite key it replaces was measured at ~40
159695
- * bytes per row (`load-series-store.ts`).
159502
+ * bytes per row (measured on the old SQLite tier; see D850).
159696
159503
  *
159697
159504
  * The durable append is fire-and-forget: a storage stall must cost a gap in
159698
159505
  * the cold window, never a blocked event-bus handler. Every failure logs.
@@ -159702,26 +159509,9 @@ var require_native_metrics_addon = __commonJS({
159702
159509
  if (!this.loadRing.record(nodeId, atMs, retained)) return;
159703
159510
  this.observedRowsByNode.set(nodeId, retained.length);
159704
159511
  if (containerMemory !== null) this.memoryRing.record(nodeId, atMs, containerMemory);
159705
- const store = this.loadStore;
159706
- if (store === null) return;
159707
- store.append(nodeId, atMs, retained).then(() => store.prune([...this.observedRowsByNode.keys()], this.loadConfig)).then(async (outcome) => {
159708
- if (outcome === null) return;
159709
- await this.memoryStore?.prune([...this.observedRowsByNode.keys()], this.loadConfig);
159710
- }).catch((err) => {
159711
- this.ctx.logger.warn("durable load series write failed", { meta: {
159712
- nodeId,
159713
- atMs,
159714
- error: err instanceof Error ? err.message : String(err)
159715
- } });
159716
- });
159717
- if (containerMemory === null) return;
159718
- this.memoryStore?.append(nodeId, atMs, containerMemory).catch((err) => {
159719
- this.ctx.logger.warn("durable container memory write failed", { meta: {
159720
- nodeId,
159721
- atMs,
159722
- error: err instanceof Error ? err.message : String(err)
159723
- } });
159724
- });
159512
+ const history = this.metricsHistory;
159513
+ if (history === null) return;
159514
+ history.recordSample(nodeId, atMs, retained, containerMemory);
159725
159515
  }
159726
159516
  /**
159727
159517
  * The ONE reader, over both tiers.
@@ -159740,9 +159530,9 @@ var require_native_metrics_addon = __commonJS({
159740
159530
  atMs: sample.atMs,
159741
159531
  processes: sample.processes
159742
159532
  }));
159743
- const store = this.loadStore;
159533
+ const history = this.metricsHistory;
159744
159534
  const merged = mergeLoadSeries({
159745
- cold: store === null ? [] : await store.read(params.forNodeId, sinceMs, this.loadConfig.maxRows),
159535
+ cold: history === null ? [] : await history.readProcessSamples(params.forNodeId, sinceMs, this.loadConfig.maxRows),
159746
159536
  hot: hotSamples,
159747
159537
  cadenceMs,
159748
159538
  ...params.maxPoints !== void 0 ? { maxPoints: params.maxPoints } : {}
@@ -159762,7 +159552,7 @@ var require_native_metrics_addon = __commonJS({
159762
159552
  retainedSamples: merged.retainedSamples,
159763
159553
  oldestAtMs: merged.oldestAtMs,
159764
159554
  cadenceMs,
159765
- durable: store !== null
159555
+ durable: history !== null
159766
159556
  };
159767
159557
  }
159768
159558
  /**
@@ -159778,8 +159568,8 @@ var require_native_metrics_addon = __commonJS({
159778
159568
  */
159779
159569
  async readContainerMemory(params) {
159780
159570
  const hot = this.memoryRing.read(params.forNodeId, params.exclusiveSinceMs);
159781
- const store = this.memoryStore;
159782
- const merged = mergeContainerMemory(store === null ? [] : await store.read(params.forNodeId, params.sinceMs, CONTAINER_MEMORY_MAX_ROWS), hot);
159571
+ const history = this.metricsHistory;
159572
+ const merged = mergeContainerMemory(history === null ? [] : await history.readContainerPoints(params.forNodeId, params.sinceMs, 5e3), hot);
159783
159573
  if (params.bucketMs <= params.cadenceMs) return merged;
159784
159574
  return reduceContainerMemory(merged, params.bucketMs, params.origin ?? 0);
159785
159575
  }
@@ -160160,6 +159950,2685 @@ var require_native_metrics_addon = __commonJS({
160160
159950
  }
160161
159951
  });
160162
159952
 
159953
+ // ../system/dist/builtins/device-history/device-history.addon.js
159954
+ var require_device_history_addon = __commonJS({
159955
+ "../system/dist/builtins/device-history/device-history.addon.js"(exports) {
159956
+ "use strict";
159957
+ Object.defineProperties(exports, {
159958
+ __esModule: { value: true },
159959
+ [Symbol.toStringTag]: { value: "Module" }
159960
+ });
159961
+ require_chunk_Cek0wNdY();
159962
+ var require_dist10 = require_dist_Ct88catN();
159963
+ function subjectIdOf(subject) {
159964
+ return subject.kind === "device" ? `d:${subject.deviceId}` : `n:${subject.nodeId}`;
159965
+ }
159966
+ function deviceIdOf(subject) {
159967
+ return subject.kind === "device" ? subject.deviceId : null;
159968
+ }
159969
+ function identityOf(key, subjectId, scope) {
159970
+ return `${key}\0${subjectId}\0${scope}`;
159971
+ }
159972
+ var LOAD_PAGE = 5e3;
159973
+ var SeriesRegistry = class {
159974
+ store;
159975
+ metrics = /* @__PURE__ */ new Map();
159976
+ byIdentity = /* @__PURE__ */ new Map();
159977
+ byId = /* @__PURE__ */ new Map();
159978
+ inflight = /* @__PURE__ */ new Map();
159979
+ removeListeners = [];
159980
+ constructor(store) {
159981
+ this.store = store;
159982
+ }
159983
+ async load() {
159984
+ this.metrics.clear();
159985
+ this.byIdentity.clear();
159986
+ this.byId.clear();
159987
+ for (const m of await this.store.loadMetrics()) this.metrics.set(m.key, m);
159988
+ let afterId = 0;
159989
+ for (; ; ) {
159990
+ const page = await this.store.loadSeriesPage(afterId, LOAD_PAGE);
159991
+ for (const s of page) this.index(s);
159992
+ const last = page[page.length - 1];
159993
+ if (last === void 0 || page.length < LOAD_PAGE) break;
159994
+ afterId = last.id;
159995
+ }
159996
+ }
159997
+ index(series) {
159998
+ this.byId.set(series.id, series);
159999
+ this.byIdentity.set(identityOf(series.key, series.subject, series.scope), series);
160000
+ }
160001
+ metric(key) {
160002
+ return this.metrics.get(key);
160003
+ }
160004
+ allMetrics() {
160005
+ return [...this.metrics.values()];
160006
+ }
160007
+ /** Persist and remember a metric; idempotent. */
160008
+ async declareMetric(metric) {
160009
+ const existing = this.metrics.get(metric.key);
160010
+ if (existing !== void 0 && sameMetric(existing, metric)) return;
160011
+ await this.store.upsertMetric(metric);
160012
+ this.metrics.set(metric.key, metric);
160013
+ }
160014
+ find(key, subjectId, scope) {
160015
+ return this.byIdentity.get(identityOf(key, subjectId, scope));
160016
+ }
160017
+ byIdOf(id) {
160018
+ return this.byId.get(id);
160019
+ }
160020
+ all() {
160021
+ return [...this.byId.values()];
160022
+ }
160023
+ forSubject(subjectId) {
160024
+ return this.all().filter((s) => s.subject === subjectId);
160025
+ }
160026
+ forKeyAndSubject(key, subjectId) {
160027
+ return this.all().filter((s) => s.key === key && s.subject === subjectId);
160028
+ }
160029
+ /** The series, created on first sight. `ts` stamps `first_ts`/`last_ts` of a new one. */
160030
+ async ensure(key, subject, scope, ts) {
160031
+ const subjectId = subjectIdOf(subject);
160032
+ const identity = identityOf(key, subjectId, scope);
160033
+ const known = this.byIdentity.get(identity);
160034
+ if (known !== void 0) return known;
160035
+ const pending = this.inflight.get(identity);
160036
+ if (pending !== void 0) return pending;
160037
+ const created = this.create(key, subjectId, deviceIdOf(subject), scope, ts).finally(() => {
160038
+ this.inflight.delete(identity);
160039
+ });
160040
+ this.inflight.set(identity, created);
160041
+ return created;
160042
+ }
160043
+ async create(key, subjectId, deviceId, scope, ts) {
160044
+ const stored = await this.store.findSeries(key, subjectId, scope) ?? await this.store.insertSeries({
160045
+ key,
160046
+ subject: subjectId,
160047
+ deviceId,
160048
+ scope,
160049
+ ts
160050
+ });
160051
+ this.index(stored);
160052
+ return stored;
160053
+ }
160054
+ /** Remember a widened span (the store write is the caller's). */
160055
+ noteSpan(id, firstTs, lastTs) {
160056
+ const current = this.byId.get(id);
160057
+ if (current === void 0) return;
160058
+ this.index({
160059
+ ...current,
160060
+ firstTs: Math.min(current.firstTs, firstTs),
160061
+ lastTs: Math.max(current.lastTs, lastTs)
160062
+ });
160063
+ }
160064
+ remove(id) {
160065
+ const current = this.byId.get(id);
160066
+ if (current === void 0) return;
160067
+ this.byId.delete(id);
160068
+ this.byIdentity.delete(identityOf(current.key, current.subject, current.scope));
160069
+ for (const listener of this.removeListeners) listener(current);
160070
+ }
160071
+ /** Per-series state elsewhere (last recorded value, dirty marks, rate limits) is dropped through this. */
160072
+ onRemove(listener) {
160073
+ this.removeListeners.push(listener);
160074
+ }
160075
+ };
160076
+ function sameMetric(a, b) {
160077
+ 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;
160078
+ }
160079
+ var DEADBAND_EPSILON = 1e-9;
160080
+ function minIntervalOf(spec) {
160081
+ if (spec.minIntervalMs !== void 0) return spec.minIntervalMs;
160082
+ return spec.kind === "boolean" ? 0 : require_dist10.HISTORY_DEFAULT_MIN_INTERVAL_MS;
160083
+ }
160084
+ function clearsDeadband(spec, last, value) {
160085
+ if (spec.kind === "boolean") return true;
160086
+ const delta = Math.abs(value - last);
160087
+ if (spec.deadband !== void 0 && delta < spec.deadband - DEADBAND_EPSILON) return false;
160088
+ if (spec.deadbandPct !== void 0) {
160089
+ if (delta < Math.abs(last) * spec.deadbandPct / 100 - DEADBAND_EPSILON) return false;
160090
+ }
160091
+ return true;
160092
+ }
160093
+ function decideCapture(spec, recorded, value, now, options = {}) {
160094
+ if (!Number.isFinite(value)) return {
160095
+ action: "skip",
160096
+ reason: "non-finite"
160097
+ };
160098
+ if (recorded === null) return { action: "emit" };
160099
+ if (value === recorded.value) return {
160100
+ action: "skip",
160101
+ reason: "unchanged"
160102
+ };
160103
+ if (!clearsDeadband(spec, recorded.value, value)) return {
160104
+ action: "skip",
160105
+ reason: "deadband"
160106
+ };
160107
+ if (options.ignoreFloor !== true) {
160108
+ const notBefore = recorded.ts + minIntervalOf(spec);
160109
+ if (now < notBefore) return {
160110
+ action: "defer",
160111
+ notBefore
160112
+ };
160113
+ }
160114
+ return { action: "emit" };
160115
+ }
160116
+ function isRealDeviceType(type) {
160117
+ return type !== require_dist10.DeviceType.Hub && type !== require_dist10.DeviceType.Container;
160118
+ }
160119
+ var DeviceDirectory = class {
160120
+ source;
160121
+ counters;
160122
+ logger;
160123
+ clock;
160124
+ types = /* @__PURE__ */ new Map();
160125
+ inflight = /* @__PURE__ */ new Map();
160126
+ refreshInflight = null;
160127
+ lastFullRefreshAt = null;
160128
+ constructor(source, counters, logger, clock) {
160129
+ this.source = source;
160130
+ this.counters = counters;
160131
+ this.logger = logger;
160132
+ this.clock = clock;
160133
+ }
160134
+ typeOf(deviceId) {
160135
+ return this.types.get(deviceId);
160136
+ }
160137
+ size() {
160138
+ return this.types.size;
160139
+ }
160140
+ /** When the full listing last ANSWERED; `null` before the first answer. */
160141
+ lastRefreshAt() {
160142
+ return this.lastFullRefreshAt;
160143
+ }
160144
+ /** Ids in the directory now (the last full listing plus per-device lookups). */
160145
+ listedIds() {
160146
+ return new Set(this.types.keys());
160147
+ }
160148
+ knows(deviceId) {
160149
+ return this.types.has(deviceId);
160150
+ }
160151
+ /** Replace the map from the full listing. Returns false (map untouched) when the read failed. */
160152
+ refresh() {
160153
+ if (this.refreshInflight !== null) return this.refreshInflight;
160154
+ const run = this.doRefresh().finally(() => {
160155
+ this.refreshInflight = null;
160156
+ });
160157
+ this.refreshInflight = run;
160158
+ return run;
160159
+ }
160160
+ async doRefresh() {
160161
+ try {
160162
+ const all = await this.source.listAll();
160163
+ this.types = new Map(all.map((d) => [d.id, d.type]));
160164
+ this.lastFullRefreshAt = this.clock();
160165
+ this.counters.directoryRefreshAt = this.lastFullRefreshAt;
160166
+ return true;
160167
+ } catch (err) {
160168
+ this.logger.warn("device directory: listing failed \u2014 keeping the previous map", { meta: { error: err instanceof Error ? err.message : String(err) } });
160169
+ return false;
160170
+ }
160171
+ }
160172
+ /** The device's type, asking the hub once when the map does not know it. */
160173
+ ensure(deviceId) {
160174
+ const known = this.types.get(deviceId);
160175
+ if (known !== void 0) return Promise.resolve(known);
160176
+ const pending = this.inflight.get(deviceId);
160177
+ if (pending !== void 0) return pending;
160178
+ const run = this.lookup(deviceId).finally(() => {
160179
+ this.inflight.delete(deviceId);
160180
+ });
160181
+ this.inflight.set(deviceId, run);
160182
+ return run;
160183
+ }
160184
+ async lookup(deviceId) {
160185
+ try {
160186
+ const device = await this.source.getDevice(deviceId);
160187
+ if (device === null) return void 0;
160188
+ this.types.set(device.id, device.type);
160189
+ return device.type;
160190
+ } catch (err) {
160191
+ this.logger.warn("device directory: lookup failed", {
160192
+ tags: { deviceId },
160193
+ meta: { error: err instanceof Error ? err.message : String(err) }
160194
+ });
160195
+ return;
160196
+ }
160197
+ }
160198
+ };
160199
+ function isRecord$1(value) {
160200
+ return typeof value === "object" && value !== null && !Array.isArray(value);
160201
+ }
160202
+ function readPath(slice, path) {
160203
+ let current = slice;
160204
+ for (const step of path.split(".")) {
160205
+ if (!isRecord$1(current)) return void 0;
160206
+ current = current[step];
160207
+ }
160208
+ return current;
160209
+ }
160210
+ function readNumeric(slice, path, kind) {
160211
+ const raw = readPath(slice, path);
160212
+ if (kind === "boolean") {
160213
+ if (typeof raw === "boolean") return raw ? 1 : 0;
160214
+ return null;
160215
+ }
160216
+ if (typeof raw === "number" && Number.isFinite(raw)) return raw;
160217
+ return null;
160218
+ }
160219
+ function readUnit(slice, unitFrom) {
160220
+ const raw = readPath(slice, unitFrom);
160221
+ return typeof raw === "string" && raw.length > 0 ? raw : null;
160222
+ }
160223
+ var CAP_KEY_PREFIX = "cap:";
160224
+ function capSeriesKey(capName, fieldPath) {
160225
+ return `${CAP_KEY_PREFIX}${capName}.${fieldPath}`;
160226
+ }
160227
+ function humanize(capName, fieldPath) {
160228
+ const field = fieldPath.replace(/([A-Z])/g, " $1").replace(/\./g, " ").toLowerCase();
160229
+ return `${capName.replace(/-/g, " ")} ${field}`;
160230
+ }
160231
+ function catalogMetrics(catalog, now) {
160232
+ return Object.entries(catalog).flatMap(([capName, fields]) => Object.entries(fields).map(([path, spec]) => ({
160233
+ key: capSeriesKey(capName, path),
160234
+ source: "cap",
160235
+ ownerAddon: "device-history",
160236
+ subjectKind: "device",
160237
+ kind: spec.kind,
160238
+ unit: spec.unit ?? null,
160239
+ displayName: spec.displayName ?? humanize(capName, path),
160240
+ ingest: "points",
160241
+ declaredAt: now
160242
+ })));
160243
+ }
160244
+ var MAX_PARKED_DEVICES = 5e3;
160245
+ var SEED_CONCURRENCY = 8;
160246
+ var CaptureEngine = class {
160247
+ deps;
160248
+ recorded = /* @__PURE__ */ new Map();
160249
+ pending = /* @__PURE__ */ new Map();
160250
+ parked = /* @__PURE__ */ new Map();
160251
+ constructor(deps) {
160252
+ this.deps = deps;
160253
+ }
160254
+ /** Learn the last recorded value of every cap series from the store, so a restart repeats nothing. */
160255
+ async seed() {
160256
+ const { registry, store } = this.deps;
160257
+ const capSeries = registry.all().filter((s) => s.key.startsWith(CAP_KEY_PREFIX));
160258
+ for (let i = 0; i < capSeries.length; i += SEED_CONCURRENCY) await Promise.all(capSeries.slice(i, i + SEED_CONCURRENCY).map(async (series) => {
160259
+ const newest = await store.rawBefore(series.id, Number.MAX_SAFE_INTEGER);
160260
+ if (newest === null) return;
160261
+ this.recorded.set(identityOf(series.key, series.subject, series.scope), newest);
160262
+ }));
160263
+ }
160264
+ /** One `DeviceStateChanged`. Synchronous: the bus must never wait on us. */
160265
+ handleState(deviceId, capName, slice, at) {
160266
+ const fields = this.deps.catalog[capName];
160267
+ if (fields === void 0) return;
160268
+ const { counters, directory } = this.deps;
160269
+ counters.eventsSeen += 1;
160270
+ if (this.deps.optedOut().has(deviceId)) {
160271
+ counters.skip("opted-out");
160272
+ return;
160273
+ }
160274
+ const type = directory.typeOf(deviceId);
160275
+ if (type === void 0) {
160276
+ this.park(deviceId, capName, slice, at);
160277
+ return;
160278
+ }
160279
+ if (!isRealDeviceType(type)) {
160280
+ counters.skip("not-a-real-device");
160281
+ return;
160282
+ }
160283
+ for (const [path, spec] of Object.entries(fields)) {
160284
+ if (spec.excludeDeviceTypes?.includes(type) === true) {
160285
+ counters.skip("excluded-device-type");
160286
+ continue;
160287
+ }
160288
+ const value = readNumeric(slice, path, spec.kind);
160289
+ if (value === null) continue;
160290
+ this.consider(deviceId, capName, path, spec, value, at, false);
160291
+ }
160292
+ }
160293
+ consider(deviceId, capName, path, spec, value, at, reconciling) {
160294
+ const key = capSeriesKey(capName, path);
160295
+ const subject = {
160296
+ kind: "device",
160297
+ deviceId
160298
+ };
160299
+ const identity = identityOf(key, `d:${deviceId}`, "");
160300
+ const decision = decideCapture(spec, this.recorded.get(identity) ?? null, value, at, { ignoreFloor: reconciling });
160301
+ if (decision.action === "emit") {
160302
+ this.emit(identity, key, subject, value, at);
160303
+ if (reconciling) this.deps.counters.reconciledPoints += 1;
160304
+ return;
160305
+ }
160306
+ if (decision.action === "defer") {
160307
+ this.pending.set(identity, {
160308
+ key,
160309
+ subject,
160310
+ spec,
160311
+ value
160312
+ });
160313
+ return;
160314
+ }
160315
+ this.pending.delete(identity);
160316
+ this.deps.counters.skip(decision.reason);
160317
+ }
160318
+ emit(identity, key, subject, value, ts) {
160319
+ this.recorded.set(identity, {
160320
+ value,
160321
+ ts
160322
+ });
160323
+ this.pending.delete(identity);
160324
+ this.deps.counters.pointsEmitted += 1;
160325
+ this.deps.writer.enqueue({
160326
+ key,
160327
+ subject,
160328
+ scope: "",
160329
+ ts,
160330
+ value
160331
+ });
160332
+ }
160333
+ /** Emit every pending change whose capture floor has passed, stamped where the floor ended. */
160334
+ tickPending(now) {
160335
+ for (const [identity, p] of this.pending) {
160336
+ const recorded = this.recorded.get(identity);
160337
+ const deviceId = p.subject.kind === "device" ? p.subject.deviceId : -1;
160338
+ if (this.deps.optedOut().has(deviceId)) {
160339
+ this.pending.delete(identity);
160340
+ this.deps.counters.skip("opted-out");
160341
+ continue;
160342
+ }
160343
+ const notBefore = (recorded?.ts ?? now) + minIntervalOf(p.spec);
160344
+ if (now < notBefore) continue;
160345
+ this.emit(identity, p.key, p.subject, p.value, notBefore);
160346
+ this.deps.counters.trailingEmitted += 1;
160347
+ }
160348
+ }
160349
+ park(deviceId, capName, slice, at) {
160350
+ const existing = this.parked.get(deviceId);
160351
+ if (existing !== void 0) {
160352
+ existing.set(capName, {
160353
+ capName,
160354
+ slice,
160355
+ at
160356
+ });
160357
+ return;
160358
+ }
160359
+ if (this.parked.size >= MAX_PARKED_DEVICES) {
160360
+ const oldest = this.parked.keys().next();
160361
+ if (oldest.done !== true) this.parked.delete(oldest.value);
160362
+ this.deps.counters.skip("parked-overflow");
160363
+ }
160364
+ this.parked.set(deviceId, /* @__PURE__ */ new Map([[capName, {
160365
+ capName,
160366
+ slice,
160367
+ at
160368
+ }]]));
160369
+ this.deps.directory.ensure(deviceId).then((type) => {
160370
+ const waiting = this.parked.get(deviceId);
160371
+ this.parked.delete(deviceId);
160372
+ if (waiting === void 0) return;
160373
+ if (type === void 0) {
160374
+ this.deps.counters.skip("unknown-device", waiting.size);
160375
+ return;
160376
+ }
160377
+ for (const p of waiting.values()) this.handleState(deviceId, p.capName, p.slice, p.at);
160378
+ });
160379
+ }
160380
+ /**
160381
+ * Restore a missed change from the state mirror's whole snapshot: a field whose
160382
+ * value differs from the last recorded one gets a point stamped `now`.
160383
+ * Additive only — it never removes anything, and a snapshot that could not be
160384
+ * read is the CALLER's failure to count (D49), not an empty snapshot.
160385
+ */
160386
+ reconcile(snapshots, now) {
160387
+ const optedOut = this.deps.optedOut();
160388
+ for (const [deviceKey, caps] of Object.entries(snapshots)) {
160389
+ const deviceId = Number(deviceKey);
160390
+ if (!Number.isInteger(deviceId) || optedOut.has(deviceId)) continue;
160391
+ const type = this.deps.directory.typeOf(deviceId);
160392
+ if (type === void 0 || !isRealDeviceType(type)) continue;
160393
+ for (const [capName, fields] of Object.entries(this.deps.catalog)) {
160394
+ const slice = caps[capName];
160395
+ if (slice === void 0) continue;
160396
+ for (const [path, spec] of Object.entries(fields)) {
160397
+ if (spec.excludeDeviceTypes?.includes(type) === true) continue;
160398
+ const value = readNumeric(slice, path, spec.kind);
160399
+ if (value === null) continue;
160400
+ this.consider(deviceId, capName, path, spec, value, now, true);
160401
+ }
160402
+ }
160403
+ }
160404
+ }
160405
+ /**
160406
+ * Forget what was "recorded" for points that never reached the store (dropped),
160407
+ * so the next change-only comparison — and the next reconcile — starts from what
160408
+ * is really there instead of leaving a hole until the value moves.
160409
+ */
160410
+ forget(points) {
160411
+ for (const p of points) {
160412
+ const identity = identityOf(p.key, subjectIdOf(p.subject), p.scope);
160413
+ this.recorded.delete(identity);
160414
+ this.pending.delete(identity);
160415
+ }
160416
+ }
160417
+ /** A purged series leaves no state behind. */
160418
+ forgetSeries(series) {
160419
+ const identity = identityOf(series.key, series.subject, series.scope);
160420
+ this.recorded.delete(identity);
160421
+ this.pending.delete(identity);
160422
+ }
160423
+ /** For status and specs. */
160424
+ pendingCount() {
160425
+ return this.pending.size;
160426
+ }
160427
+ /** The last recorded state of a series, for specs. */
160428
+ lastRecorded(key, subjectId) {
160429
+ return this.recorded.get(identityOf(key, subjectId, ""));
160430
+ }
160431
+ };
160432
+ var FLUSH_INTERVAL_MS = 1e4;
160433
+ var MAX_BUFFER_AGE_MS = 6e4;
160434
+ var MAX_QUEUE_POINTS = 1e5;
160435
+ var QUEUE_DROP_FRACTION = 0.1;
160436
+ var MAX_DEVICES_IN_DROP_LINE = 20;
160437
+ var MAX_DEVICE_LINES_PER_DROP = 10;
160438
+ var DEVICE_LINES_MIN_GAP_MS = 1e4;
160439
+ var PointWriter = class {
160440
+ deps;
160441
+ queue = [];
160442
+ flushing = null;
160443
+ lastDeviceLinesAt = Number.NEGATIVE_INFINITY;
160444
+ constructor(deps) {
160445
+ this.deps = deps;
160446
+ }
160447
+ enqueue(point) {
160448
+ this.queue.push({
160449
+ ...point,
160450
+ bufferedAt: this.deps.clock()
160451
+ });
160452
+ if (this.queue.length > 1e5) {
160453
+ const count = Math.ceil(MAX_QUEUE_POINTS * QUEUE_DROP_FRACTION);
160454
+ this.drop(this.queue.splice(0, count), "queue full");
160455
+ }
160456
+ }
160457
+ depth() {
160458
+ return this.queue.length;
160459
+ }
160460
+ /** One flush at a time; a call while one runs waits for it and returns. */
160461
+ flush() {
160462
+ if (this.flushing !== null) return this.flushing;
160463
+ const run = this.doFlush().finally(() => {
160464
+ this.flushing = null;
160465
+ });
160466
+ this.flushing = run;
160467
+ return run;
160468
+ }
160469
+ async doFlush() {
160470
+ const batch = this.queue;
160471
+ if (batch.length === 0) return;
160472
+ this.queue = [];
160473
+ const { registry, store, counters, logger, clock } = this.deps;
160474
+ try {
160475
+ const bySeries = /* @__PURE__ */ new Map();
160476
+ for (const point of batch) {
160477
+ const series = await registry.ensure(point.key, point.subject, point.scope, point.ts);
160478
+ const list = bySeries.get(series.id) ?? [];
160479
+ list.push({
160480
+ ts: point.ts,
160481
+ value: point.value
160482
+ });
160483
+ bySeries.set(series.id, list);
160484
+ }
160485
+ const rows = [...bySeries].flatMap(([seriesId, points]) => points.map((p) => ({
160486
+ seriesId,
160487
+ ts: p.ts,
160488
+ value: p.value
160489
+ })));
160490
+ await store.upsertRaw(rows);
160491
+ await this.recordSpans(bySeries);
160492
+ counters.pointsWritten += rows.length;
160493
+ counters.addRawRows(rows.length);
160494
+ counters.lastFlushAt = clock();
160495
+ this.deps.onWritten([...bySeries].map(([seriesId, points]) => ({
160496
+ seriesId,
160497
+ timestamps: points.map((p) => p.ts)
160498
+ })));
160499
+ } catch (err) {
160500
+ counters.flushFailures += 1;
160501
+ logger.warn("device-history: raw flush failed \u2014 keeping the batch", { meta: {
160502
+ points: batch.length,
160503
+ error: err instanceof Error ? err.message : String(err)
160504
+ } });
160505
+ this.requeue(batch);
160506
+ }
160507
+ }
160508
+ async recordSpans(bySeries) {
160509
+ const { registry, store } = this.deps;
160510
+ const touch = [];
160511
+ for (const [seriesId, points] of bySeries) {
160512
+ const current = registry.byIdOf(seriesId);
160513
+ if (current === void 0) continue;
160514
+ let minTs = Number.POSITIVE_INFINITY;
160515
+ let maxTs = Number.NEGATIVE_INFINITY;
160516
+ for (const p of points) {
160517
+ if (p.ts < minTs) minTs = p.ts;
160518
+ if (p.ts > maxTs) maxTs = p.ts;
160519
+ }
160520
+ if (minTs < current.firstTs) await store.widenSeriesStart(seriesId, minTs);
160521
+ if (maxTs > current.lastTs) touch.push({
160522
+ id: seriesId,
160523
+ lastTs: maxTs
160524
+ });
160525
+ registry.noteSpan(seriesId, minTs, maxTs);
160526
+ }
160527
+ await store.touchSeries(touch);
160528
+ }
160529
+ requeue(batch) {
160530
+ const now = this.deps.clock();
160531
+ const keep = [];
160532
+ const dropped = [];
160533
+ for (const point of batch) if (now - point.bufferedAt > 6e4) dropped.push(point);
160534
+ else keep.push(point);
160535
+ this.queue = [...keep, ...this.queue];
160536
+ if (dropped.length > 0) this.drop(dropped, `older than ${MAX_BUFFER_AGE_MS} ms`);
160537
+ }
160538
+ /** Account for, say, and report to the owner a set of points that will never be written. */
160539
+ drop(dropped, reason) {
160540
+ if (dropped.length === 0) return;
160541
+ const { counters, logger, clock } = this.deps;
160542
+ counters.pointsDropped += dropped.length;
160543
+ const perDevice = /* @__PURE__ */ new Map();
160544
+ for (const p of dropped) if (p.subject.kind === "device") perDevice.set(p.subject.deviceId, (perDevice.get(p.subject.deviceId) ?? 0) + 1);
160545
+ logger.error("device-history: dropped points the store would not take", { meta: {
160546
+ dropped: dropped.length,
160547
+ reason,
160548
+ maxAgeMs: MAX_BUFFER_AGE_MS,
160549
+ deviceIds: [...perDevice.keys()].slice(0, MAX_DEVICES_IN_DROP_LINE)
160550
+ } });
160551
+ const now = clock();
160552
+ if (now - this.lastDeviceLinesAt >= DEVICE_LINES_MIN_GAP_MS) {
160553
+ this.lastDeviceLinesAt = now;
160554
+ for (const [deviceId, count] of [...perDevice].slice(0, MAX_DEVICE_LINES_PER_DROP)) logger.warn("device-history: points dropped for this device", {
160555
+ tags: { deviceId },
160556
+ meta: {
160557
+ dropped: count,
160558
+ reason
160559
+ }
160560
+ });
160561
+ }
160562
+ this.deps.onDropped(dropped);
160563
+ }
160564
+ };
160565
+ var FIVE_MINUTES_MS = 5 * 6e4;
160566
+ var HOUR_MS = 60 * 6e4;
160567
+ var DAY_MS = 24 * HOUR_MS;
160568
+ function floorTo(ts, widthMs) {
160569
+ return Math.floor(ts / widthMs) * widthMs;
160570
+ }
160571
+ function startOfLocalDay(ts) {
160572
+ const d = new Date(ts);
160573
+ d.setHours(0, 0, 0, 0);
160574
+ return d.getTime();
160575
+ }
160576
+ function nextLocalDayStart(dayStart) {
160577
+ const d = new Date(dayStart);
160578
+ d.setDate(d.getDate() + 1);
160579
+ d.setHours(0, 0, 0, 0);
160580
+ return d.getTime();
160581
+ }
160582
+ function bucketStartOf(tier, ts) {
160583
+ if (tier === "5m") return floorTo(ts, FIVE_MINUTES_MS);
160584
+ if (tier === "1h") return floorTo(ts, HOUR_MS);
160585
+ return startOfLocalDay(ts);
160586
+ }
160587
+ function bucketEndOf(tier, start) {
160588
+ if (tier === "5m") return start + FIVE_MINUTES_MS;
160589
+ if (tier === "1h") return start + HOUR_MS;
160590
+ return nextLocalDayStart(start);
160591
+ }
160592
+ var ROLLUP_GRACE_MS = {
160593
+ "5m": 3e4,
160594
+ "1h": 5 * 6e4,
160595
+ "1d": 15 * 6e4
160596
+ };
160597
+ function addRefusal(refusals, key, reason) {
160598
+ const byReason = refusals.get(key) ?? /* @__PURE__ */ new Map();
160599
+ byReason.set(reason, (byReason.get(reason) ?? 0) + 1);
160600
+ refusals.set(key, byReason);
160601
+ }
160602
+ function toResult(accepted, refusals) {
160603
+ return {
160604
+ accepted,
160605
+ refused: [...refusals].flatMap(([key, byReason]) => [...byReason].map(([reason, count]) => ({
160606
+ key,
160607
+ reason,
160608
+ count
160609
+ })))
160610
+ };
160611
+ }
160612
+ var ProducerIngest = class {
160613
+ deps;
160614
+ /** Last accepted point time per series, for the sustained-rate bound. */
160615
+ lastAccepted = /* @__PURE__ */ new Map();
160616
+ constructor(deps) {
160617
+ this.deps = deps;
160618
+ }
160619
+ /** A purged series leaves no rate-limit state behind. */
160620
+ forgetSeries(series) {
160621
+ this.lastAccepted.delete(identityOf(series.key, series.subject, series.scope));
160622
+ }
160623
+ async declareSeries(callerAddonId, input) {
160624
+ if (callerAddonId === null) throw new Error("declareSeries: the caller addon could not be identified");
160625
+ const key = `addon:${callerAddonId}/${input.name}`;
160626
+ const metric = {
160627
+ key,
160628
+ source: "addon",
160629
+ ownerAddon: callerAddonId,
160630
+ subjectKind: input.subjectKind,
160631
+ kind: input.kind,
160632
+ unit: input.unit ?? null,
160633
+ displayName: input.displayName,
160634
+ ingest: input.ingest,
160635
+ declaredAt: this.deps.clock()
160636
+ };
160637
+ await this.deps.registry.declareMetric(metric);
160638
+ return { key };
160639
+ }
160640
+ /** Why a write to `metric` by `caller` for this subject must be refused, or null. */
160641
+ async refusalFor(callerAddonId, metric, ingest, subject) {
160642
+ if (metric === void 0) return "undeclared";
160643
+ if (callerAddonId === null || metric.ownerAddon !== callerAddonId) return "not-owner";
160644
+ if (metric.ingest !== ingest) return "ingest-mismatch";
160645
+ if (metric.subjectKind !== subject.kind) return "undeclared";
160646
+ if (subject.kind === "device") {
160647
+ if (this.deps.optedOut().has(subject.deviceId)) return "opted-out";
160648
+ const type = await this.deps.directory.ensure(subject.deviceId);
160649
+ if (type === void 0 || !isRealDeviceType(type)) return "not-a-real-device";
160650
+ }
160651
+ return null;
160652
+ }
160653
+ async record(callerAddonId, points) {
160654
+ const { clock, counters } = this.deps;
160655
+ const now = clock();
160656
+ const { rawMs } = this.deps.retention();
160657
+ const refusals = /* @__PURE__ */ new Map();
160658
+ let accepted = 0;
160659
+ for (const point of points) {
160660
+ const reason = await this.refusalFor(callerAddonId, this.deps.registry.metric(point.key), "points", point.subject);
160661
+ if (reason !== null) {
160662
+ addRefusal(refusals, point.key, reason);
160663
+ counters.refuse(reason);
160664
+ continue;
160665
+ }
160666
+ if (!Number.isFinite(point.value) || !Number.isFinite(point.ts) || point.ts > now + 3e5 || point.ts < now - rawMs) {
160667
+ addRefusal(refusals, point.key, "out-of-range");
160668
+ counters.refuse("out-of-range");
160669
+ continue;
160670
+ }
160671
+ const identity = identityOf(point.key, subjectIdOf(point.subject), point.scope);
160672
+ const last = this.lastAccepted.get(identity);
160673
+ if (last !== void 0 && point.ts >= last && point.ts - last < 1e4) {
160674
+ addRefusal(refusals, point.key, "rate");
160675
+ counters.refuse("rate");
160676
+ continue;
160677
+ }
160678
+ this.lastAccepted.set(identity, Math.max(last ?? point.ts, point.ts));
160679
+ this.deps.writer.enqueue({
160680
+ key: point.key,
160681
+ subject: point.subject,
160682
+ scope: point.scope,
160683
+ ts: point.ts,
160684
+ value: point.value
160685
+ });
160686
+ accepted += 1;
160687
+ }
160688
+ counters.ingestAccepted += accepted;
160689
+ return toResult(accepted, refusals);
160690
+ }
160691
+ async recordBuckets(callerAddonId, buckets) {
160692
+ const { clock, counters, registry, store } = this.deps;
160693
+ const now = clock();
160694
+ const { fiveMinuteMs } = this.deps.retention();
160695
+ const refusals = /* @__PURE__ */ new Map();
160696
+ const rows = [];
160697
+ for (const bucket of buckets) {
160698
+ const reason = await this.refusalFor(callerAddonId, registry.metric(bucket.key), "buckets", bucket.subject);
160699
+ if (reason !== null) {
160700
+ addRefusal(refusals, bucket.key, reason);
160701
+ counters.refuse(reason);
160702
+ continue;
160703
+ }
160704
+ const aligned = bucket.bucketStart % FIVE_MINUTES_MS === 0;
160705
+ const consistent = Number.isFinite(bucket.sum) && Number.isFinite(bucket.min) && Number.isFinite(bucket.max) && bucket.min <= bucket.max;
160706
+ if (!aligned || !consistent || bucket.bucketStart > now + 3e5 || bucket.bucketStart < now - fiveMinuteMs) {
160707
+ addRefusal(refusals, bucket.key, "out-of-range");
160708
+ counters.refuse("out-of-range");
160709
+ continue;
160710
+ }
160711
+ const series = await registry.ensure(bucket.key, bucket.subject, bucket.scope, bucket.bucketStart);
160712
+ rows.push({
160713
+ seriesId: series.id,
160714
+ bucketStart: bucket.bucketStart,
160715
+ samples: bucket.samples,
160716
+ sum: bucket.sum,
160717
+ min: bucket.min,
160718
+ max: bucket.max,
160719
+ last: bucket.last,
160720
+ lastTs: bucket.lastTs,
160721
+ twSum: null,
160722
+ twMs: null
160723
+ });
160724
+ }
160725
+ await store.upsertRollups("5m", rows);
160726
+ const bySeries = /* @__PURE__ */ new Map();
160727
+ for (const row of rows) bySeries.set(row.seriesId, [...bySeries.get(row.seriesId) ?? [], row.bucketStart]);
160728
+ for (const [seriesId, starts] of bySeries) counters.noteRollupRows("5m", seriesId, starts.toSorted((a, b) => a - b));
160729
+ await this.recordSpans(rows);
160730
+ for (const row of rows) this.deps.onBucketWritten(row.seriesId, row.bucketStart);
160731
+ counters.ingestAccepted += rows.length;
160732
+ return toResult(rows.length, refusals);
160733
+ }
160734
+ /** Persist the widened span of every series a bucket batch touched, then remember it. */
160735
+ async recordSpans(rows) {
160736
+ const { registry, store } = this.deps;
160737
+ const spans = /* @__PURE__ */ new Map();
160738
+ for (const row of rows) {
160739
+ const end = row.lastTs ?? row.bucketStart;
160740
+ const span = spans.get(row.seriesId);
160741
+ spans.set(row.seriesId, {
160742
+ first: span === void 0 ? row.bucketStart : Math.min(span.first, row.bucketStart),
160743
+ last: span === void 0 ? end : Math.max(span.last, end)
160744
+ });
160745
+ }
160746
+ const touch = [];
160747
+ for (const [id, span] of spans) {
160748
+ const current = registry.byIdOf(id);
160749
+ if (current === void 0) continue;
160750
+ if (span.first < current.firstTs) await store.widenSeriesStart(id, span.first);
160751
+ if (span.last > current.lastTs) touch.push({
160752
+ id,
160753
+ lastTs: span.last
160754
+ });
160755
+ registry.noteSpan(id, span.first, span.last);
160756
+ }
160757
+ await store.touchSeries(touch);
160758
+ }
160759
+ };
160760
+ var TIER_ORDER = [
160761
+ "raw",
160762
+ "5m",
160763
+ "1h",
160764
+ "1d"
160765
+ ];
160766
+ var BUCKET_MS = {
160767
+ "5m": FIVE_MINUTES_MS,
160768
+ "1h": HOUR_MS,
160769
+ "1d": DAY_MS
160770
+ };
160771
+ var MAX_SCOPES = 50;
160772
+ function retentionMsOf(tier, policy) {
160773
+ if (tier === "raw") return policy.rawDays * DAY_MS;
160774
+ if (tier === "5m") return policy.fiveMinuteDays * DAY_MS;
160775
+ if (tier === "1h") return policy.hourDays * DAY_MS;
160776
+ return policy.dayDays === null || policy.dayDays <= 0 ? null : policy.dayDays * DAY_MS;
160777
+ }
160778
+ function rawPoint(ts, value) {
160779
+ return {
160780
+ t: ts,
160781
+ samples: 1,
160782
+ mean: value,
160783
+ twMean: null,
160784
+ min: value,
160785
+ max: value,
160786
+ last: value
160787
+ };
160788
+ }
160789
+ function rollupPoint(row) {
160790
+ return {
160791
+ t: row.bucketStart,
160792
+ samples: row.samples,
160793
+ mean: row.samples > 0 ? row.sum / row.samples : null,
160794
+ twMean: row.twSum !== null && row.twMs !== null && row.twMs > 0 ? row.twSum / row.twMs : null,
160795
+ min: row.min,
160796
+ max: row.max,
160797
+ last: row.last
160798
+ };
160799
+ }
160800
+ var HistoryQuery = class {
160801
+ deps;
160802
+ constructor(deps) {
160803
+ this.deps = deps;
160804
+ }
160805
+ async query(input) {
160806
+ const { registry } = this.deps;
160807
+ const subjectId = subjectIdOf(input.subject);
160808
+ const wanted = registry.forKeyAndSubject(input.key, subjectId).filter((s) => input.scope === void 0 || s.scope === input.scope).toSorted((a, b) => a.scope.localeCompare(b.scope)).slice(0, MAX_SCOPES);
160809
+ const candidates = this.coveringTiers(input.from);
160810
+ const forced = input.resolution === "auto" ? null : input.resolution;
160811
+ const order = forced === null ? candidates : [forced];
160812
+ let chosen = order[order.length - 1] ?? "1d";
160813
+ let reads = [];
160814
+ for (const [index, tier] of order.entries()) {
160815
+ const last = index === order.length - 1;
160816
+ if (!this.fits(tier, input, wanted) && !last) continue;
160817
+ chosen = tier;
160818
+ reads = await Promise.all(wanted.map(async (s) => [s, await this.readTier(tier, s, input)]));
160819
+ if (reads.some(([, r]) => r.overflow) && !last) continue;
160820
+ break;
160821
+ }
160822
+ const series = [];
160823
+ for (const [s, read] of reads) series.push({
160824
+ scope: s.scope,
160825
+ before: await this.before(chosen, s, input.from),
160826
+ points: read.points
160827
+ });
160828
+ return {
160829
+ resolution: chosen,
160830
+ series,
160831
+ truncated: reads.some(([, r]) => r.overflow)
160832
+ };
160833
+ }
160834
+ /** Tiers whose retention covers `from`, finest first; the coarsest is always present. */
160835
+ coveringTiers(from) {
160836
+ const policy = this.deps.retention();
160837
+ const now = this.deps.clock();
160838
+ const covering = TIER_ORDER.filter((tier) => {
160839
+ const ms = retentionMsOf(tier, policy);
160840
+ return ms === null || from >= now - ms;
160841
+ });
160842
+ return covering.length > 0 ? covering : ["1d"];
160843
+ }
160844
+ /** Cheap fit test for rollup tiers; raw is decided by the read itself. */
160845
+ fits(tier, input, series) {
160846
+ if (tier === "raw" || series.length === 0) return true;
160847
+ const width = BUCKET_MS[tier];
160848
+ return Math.ceil((input.to - input.from) / width) <= input.maxPoints;
160849
+ }
160850
+ async readTier(tier, series, input) {
160851
+ const { store } = this.deps;
160852
+ const limit = input.maxPoints + 1;
160853
+ if (tier === "raw") {
160854
+ const rows2 = await store.readRaw(series.id, input.from, input.to, limit, "desc");
160855
+ return this.shape(rows2.map((r) => rawPoint(r.ts, r.value)), input.maxPoints);
160856
+ }
160857
+ const lower = bucketStartOf(tier, input.from);
160858
+ const rows = await store.readRollups(tier, series.id, lower, input.to, limit, "desc");
160859
+ return this.shape(rows.map(rollupPoint), input.maxPoints);
160860
+ }
160861
+ /** Rows arrive newest first and one over the bound; keep the newest `maxPoints`, oldest first. */
160862
+ shape(newestFirst, maxPoints) {
160863
+ const overflow = newestFirst.length > maxPoints;
160864
+ return {
160865
+ points: (overflow ? newestFirst.slice(0, maxPoints) : newestFirst).toReversed(),
160866
+ overflow
160867
+ };
160868
+ }
160869
+ async before(tier, series, from) {
160870
+ const { store } = this.deps;
160871
+ if (tier === "raw") {
160872
+ const p = await store.rawBefore(series.id, from);
160873
+ return p === null ? null : {
160874
+ t: p.ts,
160875
+ value: p.value
160876
+ };
160877
+ }
160878
+ const row = await store.rollupBefore(tier, series.id, bucketStartOf(tier, from));
160879
+ return row === null ? null : {
160880
+ t: row.bucketStart,
160881
+ value: row.last
160882
+ };
160883
+ }
160884
+ async listSeries(subject) {
160885
+ const { registry } = this.deps;
160886
+ const slices = /* @__PURE__ */ new Map();
160887
+ const infos = [];
160888
+ for (const s of registry.forSubject(subjectIdOf(subject))) {
160889
+ const metric = registry.metric(s.key);
160890
+ if (metric === void 0) continue;
160891
+ infos.push({
160892
+ key: s.key,
160893
+ scope: s.scope,
160894
+ kind: metric.kind,
160895
+ unit: await this.unitOf(subject, s, metric.unit, slices),
160896
+ displayName: metric.displayName,
160897
+ source: metric.source,
160898
+ firstTs: s.firstTs,
160899
+ lastTs: s.lastTs
160900
+ });
160901
+ }
160902
+ return infos.toSorted((a, b) => a.key.localeCompare(b.key) || a.scope.localeCompare(b.scope));
160903
+ }
160904
+ /** The metric's fixed unit, else the device's own (a `unitFrom` field), else unknown. */
160905
+ async unitOf(subject, series, fixed, slices) {
160906
+ if (fixed !== null || subject.kind !== "device" || !series.key.startsWith("cap:")) return fixed;
160907
+ const spec = this.specOf(series.key);
160908
+ if (spec?.unitFrom === void 0) return null;
160909
+ const capName = series.key.slice(CAP_KEY_PREFIX.length, series.key.indexOf("."));
160910
+ const pending = slices.get(capName) ?? this.deps.readCapSlice(subject.deviceId, capName).catch(() => null);
160911
+ slices.set(capName, pending);
160912
+ const slice = await pending;
160913
+ return slice === null ? null : readUnit(slice, spec.unitFrom);
160914
+ }
160915
+ specOf(key) {
160916
+ const body = key.slice(CAP_KEY_PREFIX.length);
160917
+ const dot = body.indexOf(".");
160918
+ if (dot < 0) return void 0;
160919
+ return this.deps.catalog[body.slice(0, dot)]?.[body.slice(dot + 1)];
160920
+ }
160921
+ };
160922
+ var DEPARTED_ABSENCE_MS = 7 * DAY_MS;
160923
+ var DepartureTracker = class {
160924
+ store;
160925
+ registry;
160926
+ logger;
160927
+ missing = /* @__PURE__ */ new Map();
160928
+ constructor(store, registry, logger) {
160929
+ this.store = store;
160930
+ this.registry = registry;
160931
+ this.logger = logger;
160932
+ }
160933
+ async load() {
160934
+ this.missing = await this.store.loadDepartures();
160935
+ }
160936
+ missingSince(deviceId) {
160937
+ return this.missing.get(deviceId);
160938
+ }
160939
+ /** Forget a device's clock — its series were removed because it was DELETED (D865). */
160940
+ async forget(deviceId) {
160941
+ await this.store.clearMissing([deviceId]);
160942
+ const next = new Map(this.missing);
160943
+ next.delete(deviceId);
160944
+ this.missing = next;
160945
+ }
160946
+ /** Feed one SUCCESSFUL listing. A failed read never reaches here. */
160947
+ async observe(listed, now) {
160948
+ const withSeries = new Set(this.registry.all().flatMap((s) => s.deviceId === null ? [] : [s.deviceId]));
160949
+ const back = [...this.missing.keys()].filter((id) => listed.has(id));
160950
+ const gone = [...withSeries].filter((id) => !listed.has(id) && !this.missing.has(id));
160951
+ try {
160952
+ await this.store.clearMissing(back);
160953
+ await this.store.markMissing(gone, now);
160954
+ } catch (err) {
160955
+ this.logger.warn("device-history: departure clock not written", { meta: { error: err instanceof Error ? err.message : String(err) } });
160956
+ return;
160957
+ }
160958
+ const next = new Map(this.missing);
160959
+ for (const id of back) next.delete(id);
160960
+ for (const id of gone) next.set(id, now);
160961
+ this.missing = next;
160962
+ }
160963
+ };
160964
+ var DEFAULT_RETENTION = {
160965
+ rawDays: 7,
160966
+ fiveMinuteDays: 14,
160967
+ hourDays: 365,
160968
+ dayDays: null
160969
+ };
160970
+ function retentionCutoffs(policy, now) {
160971
+ return {
160972
+ raw: now - policy.rawDays * DAY_MS,
160973
+ "5m": now - policy.fiveMinuteDays * DAY_MS,
160974
+ "1h": now - policy.hourDays * DAY_MS,
160975
+ "1d": policy.dayDays === null || policy.dayDays <= 0 ? null : now - policy.dayDays * DAY_MS
160976
+ };
160977
+ }
160978
+ var TIERS = [
160979
+ "raw",
160980
+ "5m",
160981
+ "1h",
160982
+ "1d"
160983
+ ];
160984
+ var RETENTION_STEP_MS = {
160985
+ raw: HOUR_MS,
160986
+ "5m": HOUR_MS,
160987
+ "1h": DAY_MS,
160988
+ "1d": DAY_MS
160989
+ };
160990
+ var MAX_PURGES_PER_RUN = 20;
160991
+ var SLICE_MS = {
160992
+ raw: DAY_MS,
160993
+ "5m": DAY_MS,
160994
+ "1h": 30 * DAY_MS,
160995
+ "1d": 30 * DAY_MS
160996
+ };
160997
+ var BUCKET_LEAD_MS = 2 * DAY_MS;
160998
+ var SHORT_TIER_LOOKBACK_MS = 400 * DAY_MS;
160999
+ var yieldToEventLoop = () => new Promise((resolve) => setImmediate(resolve));
161000
+ var RetentionJob = class {
161001
+ deps;
161002
+ appliedUpTo = /* @__PURE__ */ new Map();
161003
+ running = null;
161004
+ constructor(deps) {
161005
+ this.deps = deps;
161006
+ }
161007
+ run(policy, now) {
161008
+ if (this.running !== null) return this.running;
161009
+ const run = this.doRun(policy, now).finally(() => {
161010
+ this.running = null;
161011
+ });
161012
+ this.running = run;
161013
+ return run;
161014
+ }
161015
+ async doRun(policy, now) {
161016
+ const { registry, counters, logger } = this.deps;
161017
+ const cutoffs = retentionCutoffs(policy, now);
161018
+ try {
161019
+ for (const tier of TIERS) {
161020
+ const cutoff = cutoffs[tier];
161021
+ if (cutoff === null) continue;
161022
+ for (const series of registry.all()) {
161023
+ if (series.firstTs - 864e5 >= cutoff) continue;
161024
+ if (tier === "raw" && registry.metric(series.key)?.ingest === "buckets") continue;
161025
+ const marker = `${tier}:${series.id}`;
161026
+ const applied = this.appliedUpTo.get(marker);
161027
+ if (applied !== void 0 && cutoff - applied < RETENTION_STEP_MS[tier]) continue;
161028
+ const lo = Math.max(0, applied === void 0 ? series.firstTs - BUCKET_LEAD_MS : applied - RETENTION_STEP_MS[tier]);
161029
+ counters.addDeleted(tier, await this.deleteSliced(tier, series.id, lo, cutoff - 1));
161030
+ this.appliedUpTo.set(marker, cutoff);
161031
+ }
161032
+ }
161033
+ await this.purgeDeparted(now);
161034
+ counters.retentionLastRunAt = now;
161035
+ } catch (err) {
161036
+ logger.warn("device-history: retention pass failed \u2014 retried at the next run", { meta: { error: err instanceof Error ? err.message : String(err) } });
161037
+ }
161038
+ }
161039
+ /** Delete `[lo, hiInclusive]` of one tier in time slices, yielding between them. */
161040
+ /** Drop the visit markers of a purged series. */
161041
+ forgetSeries(seriesId) {
161042
+ for (const tier of TIERS) this.appliedUpTo.delete(`${tier}:${seriesId}`);
161043
+ }
161044
+ async deleteSliced(tier, seriesId, lo, hiInclusive) {
161045
+ const yieldNow = this.deps.yieldNow ?? yieldToEventLoop;
161046
+ let deleted = 0;
161047
+ for (let from = lo; from <= hiInclusive; from += SLICE_MS[tier]) {
161048
+ const to = Math.min(from + SLICE_MS[tier] - 1, hiInclusive);
161049
+ deleted += await this.deps.store.deleteRange(tier, seriesId, from, to);
161050
+ await yieldNow();
161051
+ }
161052
+ return deleted;
161053
+ }
161054
+ /** Tier by tier in slices, then the series row (and whatever the slices missed). */
161055
+ async purgeSeries(series, now) {
161056
+ for (const tier of TIERS) {
161057
+ const lo = tier === "raw" || tier === "5m" ? Math.max(series.firstTs - BUCKET_LEAD_MS, now - SHORT_TIER_LOOKBACK_MS) : series.firstTs - BUCKET_LEAD_MS;
161058
+ await this.deleteSliced(tier, series.id, Math.max(0, lo), now + DAY_MS);
161059
+ }
161060
+ await this.deps.store.deleteSeries(series.id);
161061
+ }
161062
+ /**
161063
+ * Delete every series of a DELETED device now (D865). Driven by a tombstone
161064
+ * through the `device-removal-hook`, never by a listing: the week-long
161065
+ * absence clock above remains the backstop for a device removed before
161066
+ * tombstones existed.
161067
+ */
161068
+ async purgeDevice(deviceId, now) {
161069
+ const { registry, counters } = this.deps;
161070
+ let purged = 0;
161071
+ for (const series of registry.all()) {
161072
+ if (series.deviceId !== deviceId) continue;
161073
+ await this.purgeSeries(series, now);
161074
+ registry.remove(series.id);
161075
+ counters.purgedSeries += 1;
161076
+ purged += 1;
161077
+ }
161078
+ return purged;
161079
+ }
161080
+ async purgeDeparted(now) {
161081
+ const { registry, departures, counters, logger } = this.deps;
161082
+ let purged = 0;
161083
+ for (const series of registry.all()) {
161084
+ if (purged >= MAX_PURGES_PER_RUN) break;
161085
+ if (series.deviceId === null) continue;
161086
+ const since = departures.missingSince(series.deviceId);
161087
+ if (since === void 0 || now - since < DEPARTED_ABSENCE_MS) continue;
161088
+ await this.purgeSeries(series, now);
161089
+ registry.remove(series.id);
161090
+ counters.purgedSeries += 1;
161091
+ purged += 1;
161092
+ logger.info("device-history: purged the series of an unregistered device", {
161093
+ tags: { deviceId: series.deviceId },
161094
+ meta: { key: series.key }
161095
+ });
161096
+ }
161097
+ }
161098
+ };
161099
+ var HISTORY_NAMESPACE = "device-history";
161100
+ var COLLECTIONS = {
161101
+ metrics: "metrics",
161102
+ series: "series",
161103
+ raw: "raw",
161104
+ rollup5m: "rollup-5m",
161105
+ rollup1h: "rollup-1h",
161106
+ rollup1d: "rollup-1d",
161107
+ optOuts: "device-opt-outs",
161108
+ departures: "device-departures"
161109
+ };
161110
+ function rollupCollectionOf(tier) {
161111
+ if (tier === "5m") return COLLECTIONS.rollup5m;
161112
+ if (tier === "1h") return COLLECTIONS.rollup1h;
161113
+ return COLLECTIONS.rollup1d;
161114
+ }
161115
+ function rollupDeclaration(collection) {
161116
+ return {
161117
+ namespace: HISTORY_NAMESPACE,
161118
+ collection,
161119
+ database: "history",
161120
+ primaryKey: ["series_id", "bucket_start"],
161121
+ withoutRowid: true,
161122
+ columns: [
161123
+ {
161124
+ name: "series_id",
161125
+ type: "INTEGER"
161126
+ },
161127
+ {
161128
+ name: "bucket_start",
161129
+ type: "INTEGER"
161130
+ },
161131
+ {
161132
+ name: "samples",
161133
+ type: "INTEGER",
161134
+ notNull: true
161135
+ },
161136
+ {
161137
+ name: "sum",
161138
+ type: "REAL",
161139
+ notNull: true
161140
+ },
161141
+ {
161142
+ name: "min",
161143
+ type: "REAL",
161144
+ notNull: true
161145
+ },
161146
+ {
161147
+ name: "max",
161148
+ type: "REAL",
161149
+ notNull: true
161150
+ },
161151
+ {
161152
+ name: "last",
161153
+ type: "REAL"
161154
+ },
161155
+ {
161156
+ name: "last_ts",
161157
+ type: "INTEGER"
161158
+ },
161159
+ {
161160
+ name: "tw_sum",
161161
+ type: "REAL"
161162
+ },
161163
+ {
161164
+ name: "tw_ms",
161165
+ type: "INTEGER"
161166
+ }
161167
+ ]
161168
+ };
161169
+ }
161170
+ var HISTORY_DECLARATIONS = [
161171
+ {
161172
+ namespace: HISTORY_NAMESPACE,
161173
+ collection: COLLECTIONS.metrics,
161174
+ database: "history",
161175
+ columns: [
161176
+ {
161177
+ name: "key",
161178
+ type: "TEXT",
161179
+ primaryKey: true,
161180
+ notNull: true
161181
+ },
161182
+ {
161183
+ name: "source",
161184
+ type: "TEXT",
161185
+ notNull: true
161186
+ },
161187
+ {
161188
+ name: "owner_addon",
161189
+ type: "TEXT",
161190
+ notNull: true
161191
+ },
161192
+ {
161193
+ name: "subject_kind",
161194
+ type: "TEXT",
161195
+ notNull: true
161196
+ },
161197
+ {
161198
+ name: "kind",
161199
+ type: "TEXT",
161200
+ notNull: true
161201
+ },
161202
+ {
161203
+ name: "unit",
161204
+ type: "TEXT"
161205
+ },
161206
+ {
161207
+ name: "display_name",
161208
+ type: "TEXT",
161209
+ notNull: true
161210
+ },
161211
+ {
161212
+ name: "ingest",
161213
+ type: "TEXT",
161214
+ notNull: true
161215
+ },
161216
+ {
161217
+ name: "declared_at",
161218
+ type: "INTEGER",
161219
+ notNull: true
161220
+ }
161221
+ ]
161222
+ },
161223
+ {
161224
+ namespace: HISTORY_NAMESPACE,
161225
+ collection: COLLECTIONS.series,
161226
+ database: "history",
161227
+ columns: [
161228
+ {
161229
+ name: "id",
161230
+ type: "INTEGER",
161231
+ primaryKey: true
161232
+ },
161233
+ {
161234
+ name: "key",
161235
+ type: "TEXT",
161236
+ notNull: true
161237
+ },
161238
+ {
161239
+ name: "subject",
161240
+ type: "TEXT",
161241
+ notNull: true
161242
+ },
161243
+ {
161244
+ name: "device_id",
161245
+ type: "INTEGER"
161246
+ },
161247
+ {
161248
+ name: "scope",
161249
+ type: "TEXT",
161250
+ notNull: true,
161251
+ defaultValue: ""
161252
+ },
161253
+ {
161254
+ name: "first_ts",
161255
+ type: "INTEGER",
161256
+ notNull: true
161257
+ },
161258
+ {
161259
+ name: "last_ts",
161260
+ type: "INTEGER",
161261
+ notNull: true
161262
+ }
161263
+ ],
161264
+ indexes: [{
161265
+ name: "idx_dh_series_identity",
161266
+ columns: [
161267
+ "key",
161268
+ "subject",
161269
+ "scope"
161270
+ ],
161271
+ unique: true
161272
+ }, {
161273
+ name: "idx_dh_series_device",
161274
+ columns: ["device_id"]
161275
+ }]
161276
+ },
161277
+ {
161278
+ namespace: HISTORY_NAMESPACE,
161279
+ collection: COLLECTIONS.raw,
161280
+ database: "history",
161281
+ primaryKey: ["series_id", "ts"],
161282
+ withoutRowid: true,
161283
+ columns: [
161284
+ {
161285
+ name: "series_id",
161286
+ type: "INTEGER"
161287
+ },
161288
+ {
161289
+ name: "ts",
161290
+ type: "INTEGER"
161291
+ },
161292
+ {
161293
+ name: "value",
161294
+ type: "REAL",
161295
+ notNull: true
161296
+ }
161297
+ ]
161298
+ },
161299
+ rollupDeclaration(COLLECTIONS.rollup5m),
161300
+ rollupDeclaration(COLLECTIONS.rollup1h),
161301
+ rollupDeclaration(COLLECTIONS.rollup1d),
161302
+ {
161303
+ namespace: HISTORY_NAMESPACE,
161304
+ collection: COLLECTIONS.optOuts,
161305
+ columns: [
161306
+ {
161307
+ name: "id",
161308
+ type: "INTEGER",
161309
+ primaryKey: true
161310
+ },
161311
+ {
161312
+ name: "device_id",
161313
+ type: "INTEGER",
161314
+ notNull: true
161315
+ },
161316
+ {
161317
+ name: "opted_out_at",
161318
+ type: "INTEGER",
161319
+ notNull: true
161320
+ }
161321
+ ],
161322
+ indexes: [{
161323
+ name: "idx_dh_opt_outs_device",
161324
+ columns: ["device_id"],
161325
+ unique: true
161326
+ }]
161327
+ },
161328
+ {
161329
+ namespace: HISTORY_NAMESPACE,
161330
+ collection: COLLECTIONS.departures,
161331
+ columns: [
161332
+ {
161333
+ name: "id",
161334
+ type: "INTEGER",
161335
+ primaryKey: true
161336
+ },
161337
+ {
161338
+ name: "device_id",
161339
+ type: "INTEGER",
161340
+ notNull: true
161341
+ },
161342
+ {
161343
+ name: "missing_since",
161344
+ type: "INTEGER",
161345
+ notNull: true
161346
+ }
161347
+ ],
161348
+ indexes: [{
161349
+ name: "idx_dh_departures_device",
161350
+ columns: ["device_id"],
161351
+ unique: true
161352
+ }]
161353
+ }
161354
+ ];
161355
+ function readNumber(data, column) {
161356
+ const v = data[column];
161357
+ if (typeof v === "number" && Number.isFinite(v)) return v;
161358
+ throw new Error(`history row: column "${column}" is not a number (${typeof v})`);
161359
+ }
161360
+ function readNumberOrNull(data, column) {
161361
+ const v = data[column];
161362
+ if (v === null || v === void 0) return null;
161363
+ if (typeof v === "number" && Number.isFinite(v)) return v;
161364
+ throw new Error(`history row: column "${column}" is not a number or null (${typeof v})`);
161365
+ }
161366
+ function readText(data, column) {
161367
+ const v = data[column];
161368
+ if (typeof v === "string") return v;
161369
+ throw new Error(`history row: column "${column}" is not a string (${typeof v})`);
161370
+ }
161371
+ function readTextOrNull(data, column) {
161372
+ const v = data[column];
161373
+ if (v === null || v === void 0) return null;
161374
+ if (typeof v === "string") return v;
161375
+ throw new Error(`history row: column "${column}" is not a string or null (${typeof v})`);
161376
+ }
161377
+ var WRITE_CHUNK = 1e3;
161378
+ var TRANSACT_CHUNK = 200;
161379
+ var READ_PAGE = 5e3;
161380
+ function chunks(items, size) {
161381
+ const out = [];
161382
+ for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
161383
+ return out;
161384
+ }
161385
+ function parseKind(value) {
161386
+ if (value === "gauge" || value === "counter" || value === "boolean") return value;
161387
+ throw new Error(`history row: unknown kind "${value}"`);
161388
+ }
161389
+ function parseMetric(data) {
161390
+ const source = readText(data, "source");
161391
+ const subjectKind = readText(data, "subject_kind");
161392
+ const ingest = readText(data, "ingest");
161393
+ if (source !== "cap" && source !== "addon") throw new Error(`history row: unknown source "${source}"`);
161394
+ if (subjectKind !== "device" && subjectKind !== "node") throw new Error(`history row: unknown subject_kind "${subjectKind}"`);
161395
+ if (ingest !== "points" && ingest !== "buckets") throw new Error(`history row: unknown ingest "${ingest}"`);
161396
+ return {
161397
+ key: readText(data, "key"),
161398
+ source,
161399
+ ownerAddon: readText(data, "owner_addon"),
161400
+ subjectKind,
161401
+ kind: parseKind(readText(data, "kind")),
161402
+ unit: readTextOrNull(data, "unit"),
161403
+ displayName: readText(data, "display_name"),
161404
+ ingest,
161405
+ declaredAt: readNumber(data, "declared_at")
161406
+ };
161407
+ }
161408
+ function parseSeries(data) {
161409
+ return {
161410
+ id: readNumber(data, "id"),
161411
+ key: readText(data, "key"),
161412
+ subject: readText(data, "subject"),
161413
+ deviceId: readNumberOrNull(data, "device_id"),
161414
+ scope: readText(data, "scope"),
161415
+ firstTs: readNumber(data, "first_ts"),
161416
+ lastTs: readNumber(data, "last_ts")
161417
+ };
161418
+ }
161419
+ function parseRollup(data) {
161420
+ return {
161421
+ seriesId: readNumber(data, "series_id"),
161422
+ bucketStart: readNumber(data, "bucket_start"),
161423
+ samples: readNumber(data, "samples"),
161424
+ sum: readNumber(data, "sum"),
161425
+ min: readNumber(data, "min"),
161426
+ max: readNumber(data, "max"),
161427
+ last: readNumberOrNull(data, "last"),
161428
+ lastTs: readNumberOrNull(data, "last_ts"),
161429
+ twSum: readNumberOrNull(data, "tw_sum"),
161430
+ twMs: readNumberOrNull(data, "tw_ms")
161431
+ };
161432
+ }
161433
+ function rollupData(row) {
161434
+ return {
161435
+ series_id: row.seriesId,
161436
+ bucket_start: row.bucketStart,
161437
+ samples: row.samples,
161438
+ sum: row.sum,
161439
+ min: row.min,
161440
+ max: row.max,
161441
+ last: row.last,
161442
+ last_ts: row.lastTs,
161443
+ tw_sum: row.twSum,
161444
+ tw_ms: row.twMs
161445
+ };
161446
+ }
161447
+ function collectionOf(tier) {
161448
+ return tier === "raw" ? COLLECTIONS.raw : rollupCollectionOf(tier);
161449
+ }
161450
+ function orderColumnOf(tier) {
161451
+ return tier === "raw" ? "ts" : "bucket_start";
161452
+ }
161453
+ var HistoryStore = class {
161454
+ door;
161455
+ constructor(door) {
161456
+ this.door = door;
161457
+ }
161458
+ async declare() {
161459
+ for (const declaration of HISTORY_DECLARATIONS) await this.door.declareCollection.mutate(declaration);
161460
+ }
161461
+ async loadMetrics() {
161462
+ return (await this.door.query.query({
161463
+ namespace: HISTORY_NAMESPACE,
161464
+ collection: COLLECTIONS.metrics,
161465
+ filter: { limit: 2e4 }
161466
+ })).map((r) => parseMetric(r.data));
161467
+ }
161468
+ async upsertMetric(metric) {
161469
+ await this.door.upsertMany.mutate({
161470
+ namespace: HISTORY_NAMESPACE,
161471
+ collection: COLLECTIONS.metrics,
161472
+ conflict: ["key"],
161473
+ records: [{ data: {
161474
+ key: metric.key,
161475
+ source: metric.source,
161476
+ owner_addon: metric.ownerAddon,
161477
+ subject_kind: metric.subjectKind,
161478
+ kind: metric.kind,
161479
+ unit: metric.unit,
161480
+ display_name: metric.displayName,
161481
+ ingest: metric.ingest,
161482
+ declared_at: metric.declaredAt
161483
+ } }]
161484
+ });
161485
+ }
161486
+ /** Series with `id > afterId`, ascending — the boot load pages with this. */
161487
+ async loadSeriesPage(afterId, limit) {
161488
+ return (await this.door.query.query({
161489
+ namespace: HISTORY_NAMESPACE,
161490
+ collection: COLLECTIONS.series,
161491
+ filter: {
161492
+ whereBetween: { id: [afterId + 1, Number.MAX_SAFE_INTEGER] },
161493
+ orderBy: {
161494
+ field: "id",
161495
+ direction: "asc"
161496
+ },
161497
+ limit
161498
+ }
161499
+ })).map((r) => parseSeries(r.data));
161500
+ }
161501
+ async findSeries(key, subject, scope) {
161502
+ const first = (await this.door.query.query({
161503
+ namespace: HISTORY_NAMESPACE,
161504
+ collection: COLLECTIONS.series,
161505
+ filter: {
161506
+ where: {
161507
+ key,
161508
+ subject,
161509
+ scope
161510
+ },
161511
+ limit: 1
161512
+ }
161513
+ }))[0];
161514
+ return first === void 0 ? null : parseSeries(first.data);
161515
+ }
161516
+ async insertSeries(series) {
161517
+ const { id } = await this.door.insert.mutate({
161518
+ namespace: HISTORY_NAMESPACE,
161519
+ collection: COLLECTIONS.series,
161520
+ record: { data: {
161521
+ key: series.key,
161522
+ subject: series.subject,
161523
+ device_id: series.deviceId,
161524
+ scope: series.scope,
161525
+ first_ts: series.ts,
161526
+ last_ts: series.ts
161527
+ } }
161528
+ });
161529
+ return {
161530
+ id: typeof id === "number" ? id : Number(id),
161531
+ key: series.key,
161532
+ subject: series.subject,
161533
+ deviceId: series.deviceId,
161534
+ scope: series.scope,
161535
+ firstTs: series.ts,
161536
+ lastTs: series.ts
161537
+ };
161538
+ }
161539
+ async touchSeries(updates) {
161540
+ for (const batch of chunks(updates, TRANSACT_CHUNK)) await this.door.transact.mutate({
161541
+ namespace: HISTORY_NAMESPACE,
161542
+ ops: batch.map((u) => ({
161543
+ op: "update",
161544
+ collection: COLLECTIONS.series,
161545
+ id: String(u.id),
161546
+ data: { last_ts: u.lastTs }
161547
+ }))
161548
+ });
161549
+ }
161550
+ async widenSeriesStart(id, firstTs) {
161551
+ await this.door.update.mutate({
161552
+ namespace: HISTORY_NAMESPACE,
161553
+ collection: COLLECTIONS.series,
161554
+ id: String(id),
161555
+ data: { first_ts: firstTs }
161556
+ });
161557
+ }
161558
+ async upsertRaw(rows) {
161559
+ for (const batch of chunks(rows, WRITE_CHUNK)) await this.door.upsertMany.mutate({
161560
+ namespace: HISTORY_NAMESPACE,
161561
+ collection: COLLECTIONS.raw,
161562
+ conflict: ["series_id", "ts"],
161563
+ records: batch.map((r) => ({ data: {
161564
+ series_id: r.seriesId,
161565
+ ts: r.ts,
161566
+ value: r.value
161567
+ } }))
161568
+ });
161569
+ }
161570
+ /** Points of a series in `[from, toInclusive]`, ascending, at most `limit`. */
161571
+ async readRaw(seriesId, from, toInclusive, limit, direction = "asc") {
161572
+ return (await this.door.query.query({
161573
+ namespace: HISTORY_NAMESPACE,
161574
+ collection: COLLECTIONS.raw,
161575
+ filter: {
161576
+ where: { series_id: seriesId },
161577
+ whereBetween: { ts: [from, toInclusive] },
161578
+ orderBy: {
161579
+ field: "ts",
161580
+ direction
161581
+ },
161582
+ limit
161583
+ }
161584
+ })).map((r) => ({
161585
+ ts: readNumber(r.data, "ts"),
161586
+ value: readNumber(r.data, "value")
161587
+ }));
161588
+ }
161589
+ /** The newest raw point strictly before `ts`, if any. */
161590
+ async rawBefore(seriesId, ts) {
161591
+ return (await this.readRaw(seriesId, 0, ts - 1, 1, "desc"))[0] ?? null;
161592
+ }
161593
+ async upsertRollups(tier, rows) {
161594
+ for (const batch of chunks(rows, WRITE_CHUNK)) await this.door.upsertMany.mutate({
161595
+ namespace: HISTORY_NAMESPACE,
161596
+ collection: rollupCollectionOf(tier),
161597
+ conflict: ["series_id", "bucket_start"],
161598
+ records: batch.map((r) => ({ data: rollupData(r) }))
161599
+ });
161600
+ }
161601
+ async readRollups(tier, seriesId, from, toInclusive, limit, direction = "asc") {
161602
+ return (await this.door.query.query({
161603
+ namespace: HISTORY_NAMESPACE,
161604
+ collection: rollupCollectionOf(tier),
161605
+ filter: {
161606
+ where: { series_id: seriesId },
161607
+ whereBetween: { bucket_start: [from, toInclusive] },
161608
+ orderBy: {
161609
+ field: "bucket_start",
161610
+ direction
161611
+ },
161612
+ limit
161613
+ }
161614
+ })).map((r) => parseRollup(r.data));
161615
+ }
161616
+ /** The newest rollup row of `tier` strictly before `bucketStart`, if any. */
161617
+ async rollupBefore(tier, seriesId, bucketStart) {
161618
+ return (await this.readRollups(tier, seriesId, 0, bucketStart - 1, 1, "desc"))[0] ?? null;
161619
+ }
161620
+ /** Delete one series' rows of a tier with the order column in `[lo, hiInclusive]`. */
161621
+ async deleteRange(tier, seriesId, lo, hiInclusive) {
161622
+ const { deleted } = await this.door.deleteWhere.mutate({
161623
+ namespace: HISTORY_NAMESPACE,
161624
+ collection: collectionOf(tier),
161625
+ filter: {
161626
+ where: { series_id: seriesId },
161627
+ whereBetween: { [orderColumnOf(tier)]: [lo, hiInclusive] }
161628
+ }
161629
+ });
161630
+ return deleted;
161631
+ }
161632
+ /** Delete every row of one series, in every tier, then the series row. */
161633
+ async deleteSeries(seriesId) {
161634
+ for (const tier of [
161635
+ "raw",
161636
+ "5m",
161637
+ "1h",
161638
+ "1d"
161639
+ ]) await this.door.deleteWhere.mutate({
161640
+ namespace: HISTORY_NAMESPACE,
161641
+ collection: collectionOf(tier),
161642
+ filter: { where: { series_id: seriesId } }
161643
+ });
161644
+ await this.door.delete.mutate({
161645
+ namespace: HISTORY_NAMESPACE,
161646
+ collection: COLLECTIONS.series,
161647
+ key: String(seriesId)
161648
+ });
161649
+ }
161650
+ async countRows(tier) {
161651
+ return this.door.count.query({
161652
+ namespace: HISTORY_NAMESPACE,
161653
+ collection: collectionOf(tier)
161654
+ });
161655
+ }
161656
+ async loadOptOuts() {
161657
+ const rows = await this.door.query.query({
161658
+ namespace: HISTORY_NAMESPACE,
161659
+ collection: COLLECTIONS.optOuts,
161660
+ filter: { limit: 2e4 }
161661
+ });
161662
+ return new Set(rows.map((r) => readNumber(r.data, "device_id")));
161663
+ }
161664
+ async setOptOut(deviceId, at) {
161665
+ await this.door.upsertMany.mutate({
161666
+ namespace: HISTORY_NAMESPACE,
161667
+ collection: COLLECTIONS.optOuts,
161668
+ conflict: ["device_id"],
161669
+ records: [{ data: {
161670
+ device_id: deviceId,
161671
+ opted_out_at: at
161672
+ } }]
161673
+ });
161674
+ }
161675
+ async loadDepartures() {
161676
+ const rows = await this.door.query.query({
161677
+ namespace: HISTORY_NAMESPACE,
161678
+ collection: COLLECTIONS.departures,
161679
+ filter: { limit: 2e4 }
161680
+ });
161681
+ return new Map(rows.map((r) => [readNumber(r.data, "device_id"), readNumber(r.data, "missing_since")]));
161682
+ }
161683
+ async markMissing(deviceIds, at) {
161684
+ for (const batch of chunks(deviceIds, WRITE_CHUNK)) await this.door.upsertMany.mutate({
161685
+ namespace: HISTORY_NAMESPACE,
161686
+ collection: COLLECTIONS.departures,
161687
+ conflict: ["device_id"],
161688
+ records: batch.map((id) => ({ data: {
161689
+ device_id: id,
161690
+ missing_since: at
161691
+ } }))
161692
+ });
161693
+ }
161694
+ async clearMissing(deviceIds) {
161695
+ for (const batch of chunks(deviceIds, WRITE_CHUNK)) await this.door.deleteWhere.mutate({
161696
+ namespace: HISTORY_NAMESPACE,
161697
+ collection: COLLECTIONS.departures,
161698
+ filter: { whereIn: { device_id: [...batch] } }
161699
+ });
161700
+ }
161701
+ async clearOptOut(deviceId) {
161702
+ await this.door.deleteWhere.mutate({
161703
+ namespace: HISTORY_NAMESPACE,
161704
+ collection: COLLECTIONS.optOuts,
161705
+ filter: { where: { device_id: deviceId } }
161706
+ });
161707
+ }
161708
+ };
161709
+ function rawToFiveMinuteRows(input) {
161710
+ const rows = [];
161711
+ const { points, seriesId, horizon } = input;
161712
+ let held = input.carryIn === null ? null : input.carryIn.value;
161713
+ let i = 0;
161714
+ while (i < points.length) {
161715
+ const first = points[i];
161716
+ if (first === void 0) break;
161717
+ const bucketStart = floorTo(first.ts, FIVE_MINUTES_MS);
161718
+ const bucketEnd = bucketStart + FIVE_MINUTES_MS;
161719
+ const coverageEnd = Math.min(bucketEnd, Math.max(horizon, bucketStart));
161720
+ let samples = 0;
161721
+ let sum = 0;
161722
+ let min = Number.POSITIVE_INFINITY;
161723
+ let max = Number.NEGATIVE_INFINITY;
161724
+ let twSum = 0;
161725
+ let twMs = 0;
161726
+ let cursor = bucketStart;
161727
+ let value = held;
161728
+ if (value !== null) {
161729
+ min = value;
161730
+ max = value;
161731
+ }
161732
+ let last = first;
161733
+ while (i < points.length) {
161734
+ const p = points[i];
161735
+ if (p === void 0 || p.ts >= bucketEnd) break;
161736
+ if (value !== null) {
161737
+ const dt = p.ts - cursor;
161738
+ twSum += value * dt;
161739
+ twMs += dt;
161740
+ }
161741
+ cursor = p.ts;
161742
+ value = p.value;
161743
+ samples += 1;
161744
+ sum += p.value;
161745
+ if (p.value < min) min = p.value;
161746
+ if (p.value > max) max = p.value;
161747
+ last = p;
161748
+ i += 1;
161749
+ }
161750
+ if (value !== null && coverageEnd > cursor) {
161751
+ twSum += value * (coverageEnd - cursor);
161752
+ twMs += coverageEnd - cursor;
161753
+ }
161754
+ held = last.value;
161755
+ rows.push({
161756
+ seriesId,
161757
+ bucketStart,
161758
+ samples,
161759
+ sum,
161760
+ min,
161761
+ max,
161762
+ last: last.value,
161763
+ lastTs: last.ts,
161764
+ twSum,
161765
+ twMs
161766
+ });
161767
+ }
161768
+ return rows;
161769
+ }
161770
+ function combineRows(input) {
161771
+ const { rows } = input;
161772
+ const lastRow = rows[rows.length - 1];
161773
+ if (lastRow === void 0) return null;
161774
+ const hasTw = rows.some((r) => r.twSum !== null && r.twMs !== null);
161775
+ let samples = 0;
161776
+ let sum = 0;
161777
+ let min = Number.POSITIVE_INFINITY;
161778
+ let max = Number.NEGATIVE_INFINITY;
161779
+ let twSum = 0;
161780
+ let twMs = 0;
161781
+ let held = input.carryIn;
161782
+ let cursor = input.windowStart;
161783
+ for (const row of rows) {
161784
+ samples += row.samples;
161785
+ sum += row.sum;
161786
+ if (row.min < min) min = row.min;
161787
+ if (row.max > max) max = row.max;
161788
+ const gap = row.bucketStart - cursor;
161789
+ if (hasTw && gap > 0 && held !== null) {
161790
+ twSum += held * gap;
161791
+ twMs += gap;
161792
+ if (held < min) min = held;
161793
+ if (held > max) max = held;
161794
+ }
161795
+ if (row.twSum !== null && row.twMs !== null) {
161796
+ twSum += row.twSum;
161797
+ twMs += row.twMs;
161798
+ }
161799
+ cursor = row.bucketStart + input.childWidthMs;
161800
+ held = row.last;
161801
+ }
161802
+ const coverageEnd = Math.min(input.windowEnd, input.horizon);
161803
+ if (hasTw && held !== null && coverageEnd > cursor) {
161804
+ twSum += held * (coverageEnd - cursor);
161805
+ twMs += coverageEnd - cursor;
161806
+ }
161807
+ return {
161808
+ seriesId: input.seriesId,
161809
+ bucketStart: input.windowStart,
161810
+ samples,
161811
+ sum,
161812
+ min,
161813
+ max,
161814
+ last: lastRow.last,
161815
+ lastTs: lastRow.lastTs,
161816
+ twSum: hasTw ? twSum : null,
161817
+ twMs: hasTw ? twMs : null
161818
+ };
161819
+ }
161820
+ var BOOT_WINDOW_MS = 3 * HOUR_MS;
161821
+ var markVersion = 0;
161822
+ function mark(sets, seriesId, bucketStart) {
161823
+ const map = sets.get(seriesId) ?? /* @__PURE__ */ new Map();
161824
+ markVersion += 1;
161825
+ map.set(bucketStart, markVersion);
161826
+ sets.set(seriesId, map);
161827
+ }
161828
+ function settle(sets, seriesId, handled) {
161829
+ const map = sets.get(seriesId);
161830
+ if (map === void 0) return;
161831
+ for (const { bucket, version } of handled) if (map.get(bucket) === version) map.delete(bucket);
161832
+ if (map.size === 0) sets.delete(seriesId);
161833
+ }
161834
+ var MAX_RANGE_GAP_MS = HOUR_MS;
161835
+ function clusterRanges(sorted) {
161836
+ const ranges = [];
161837
+ for (const start of sorted) {
161838
+ const current = ranges[ranges.length - 1];
161839
+ if (current !== void 0 && start - current[1] <= MAX_RANGE_GAP_MS) current[1] = start + FIVE_MINUTES_MS;
161840
+ else ranges.push([start, start + FIVE_MINUTES_MS]);
161841
+ }
161842
+ return ranges;
161843
+ }
161844
+ var RollupEngine = class {
161845
+ deps;
161846
+ dirty5m = /* @__PURE__ */ new Map();
161847
+ dirty1h = /* @__PURE__ */ new Map();
161848
+ dirty1d = /* @__PURE__ */ new Map();
161849
+ ticking = null;
161850
+ constructor(deps) {
161851
+ this.deps = deps;
161852
+ }
161853
+ dirtyCounts() {
161854
+ const count = (sets) => [...sets.values()].reduce((n, set) => n + set.size, 0);
161855
+ return {
161856
+ m5: count(this.dirty5m),
161857
+ h1: count(this.dirty1h),
161858
+ d1: count(this.dirty1d)
161859
+ };
161860
+ }
161861
+ /** Raw points landed: their 5-minute buckets (and a late bucket's successor) are dirty. */
161862
+ markPoints(written) {
161863
+ const now = this.deps.clock();
161864
+ for (const { seriesId, timestamps } of written) for (const ts of timestamps) {
161865
+ const bucket = floorTo(ts, FIVE_MINUTES_MS);
161866
+ mark(this.dirty5m, seriesId, bucket);
161867
+ if (bucket + 3e5 + ROLLUP_GRACE_MS["5m"] <= now) mark(this.dirty5m, seriesId, bucket + FIVE_MINUTES_MS);
161868
+ }
161869
+ }
161870
+ /** A 5m row landed (computed, or a producer bucket): its hour is dirty. */
161871
+ markFiveMinuteRow(seriesId, bucketStart) {
161872
+ mark(this.dirty1h, seriesId, bucketStartOf("1h", bucketStart));
161873
+ }
161874
+ /** One pass over the three tiers. Single-flight; errors are counted and the work stays dirty. */
161875
+ tick() {
161876
+ if (this.ticking !== null) return this.ticking;
161877
+ const run = this.doTick().finally(() => {
161878
+ this.ticking = null;
161879
+ });
161880
+ this.ticking = run;
161881
+ return run;
161882
+ }
161883
+ async doTick() {
161884
+ const now = this.deps.clock();
161885
+ await this.guarded("5m", () => this.tickFiveMinute(now));
161886
+ await this.guarded("1h", () => this.tickHour(now));
161887
+ await this.guarded("1d", () => this.tickDay(now));
161888
+ }
161889
+ async guarded(tier, run) {
161890
+ try {
161891
+ await run();
161892
+ } catch (err) {
161893
+ this.deps.counters.rollupErrors += 1;
161894
+ this.deps.logger.warn(`device-history: ${tier} rollup pass failed \u2014 work stays dirty`, { meta: { error: err instanceof Error ? err.message : String(err) } });
161895
+ }
161896
+ }
161897
+ closedBuckets(map, tier, now) {
161898
+ return [...map].filter(([start]) => bucketEndOf(tier, start) + ROLLUP_GRACE_MS[tier] <= now).map(([bucket, version]) => ({
161899
+ bucket,
161900
+ version
161901
+ })).toSorted((a, b) => a.bucket - b.bucket);
161902
+ }
161903
+ /** Forget everything held for a series that was purged. */
161904
+ forgetSeries(seriesId) {
161905
+ this.dirty5m.delete(seriesId);
161906
+ this.dirty1h.delete(seriesId);
161907
+ this.dirty1d.delete(seriesId);
161908
+ this.deps.counters.forgetSeries(seriesId);
161909
+ }
161910
+ async tickFiveMinute(now) {
161911
+ let handled = 0;
161912
+ for (const [seriesId, set] of this.dirty5m) {
161913
+ if (handled >= 400) break;
161914
+ const closed = this.closedBuckets(set, "5m", now);
161915
+ if (closed.length === 0) continue;
161916
+ handled += 1;
161917
+ for (const [from, to] of clusterRanges(closed.map((c) => c.bucket))) await this.recomputeFiveMinute(seriesId, from, to, now);
161918
+ settle(this.dirty5m, seriesId, closed);
161919
+ }
161920
+ if (handled > 0) this.deps.counters.last5mAt = now;
161921
+ }
161922
+ async tickHour(now) {
161923
+ let handled = 0;
161924
+ for (const [seriesId, set] of this.dirty1h) {
161925
+ if (handled >= 400) break;
161926
+ const closed = this.closedBuckets(set, "1h", now);
161927
+ if (closed.length === 0) continue;
161928
+ handled += 1;
161929
+ for (const hour of closed) {
161930
+ await this.recomputeHour(seriesId, hour.bucket, now);
161931
+ settle(this.dirty1h, seriesId, [hour]);
161932
+ }
161933
+ }
161934
+ if (handled > 0) this.deps.counters.last1hAt = now;
161935
+ }
161936
+ async tickDay(now) {
161937
+ let handled = 0;
161938
+ for (const [seriesId, set] of this.dirty1d) {
161939
+ if (handled >= 400) break;
161940
+ const closed = this.closedBuckets(set, "1d", now);
161941
+ if (closed.length === 0) continue;
161942
+ handled += 1;
161943
+ for (const day of closed) {
161944
+ await this.recomputeDay(seriesId, day.bucket, now);
161945
+ settle(this.dirty1d, seriesId, [day]);
161946
+ }
161947
+ }
161948
+ if (handled > 0) this.deps.counters.last1dAt = now;
161949
+ }
161950
+ /** Raw → 5m rows for buckets in `[from, toExclusive)`; writes them and marks their hours. */
161951
+ async recomputeFiveMinute(seriesId, from, toExclusive, horizon) {
161952
+ const { store } = this.deps;
161953
+ const points = [];
161954
+ let cursor = from;
161955
+ for (; ; ) {
161956
+ const page = await store.readRaw(seriesId, cursor, toExclusive - 1, READ_PAGE);
161957
+ points.push(...page);
161958
+ const last = page[page.length - 1];
161959
+ if (last === void 0 || page.length < 5e3) break;
161960
+ cursor = last.ts + 1;
161961
+ }
161962
+ const rows = rawToFiveMinuteRows({
161963
+ seriesId,
161964
+ points,
161965
+ carryIn: await store.rawBefore(seriesId, from),
161966
+ horizon
161967
+ });
161968
+ if (rows.length === 0) return 0;
161969
+ await store.upsertRollups("5m", rows);
161970
+ this.deps.counters.noteRollupRows("5m", seriesId, rows.map((r) => r.bucketStart));
161971
+ for (const row of rows) this.markFiveMinuteRow(seriesId, row.bucketStart);
161972
+ return rows.length;
161973
+ }
161974
+ async recomputeHour(seriesId, hour, now) {
161975
+ await this.recomputeFrom("5m", "1h", FIVE_MINUTES_MS, seriesId, hour, now);
161976
+ }
161977
+ async recomputeDay(seriesId, day, now) {
161978
+ await this.recomputeFrom("1h", "1d", HOUR_MS, seriesId, day, now);
161979
+ }
161980
+ async recomputeFrom(child, parent, childWidthMs, seriesId, windowStart, now) {
161981
+ const { store } = this.deps;
161982
+ const windowEnd = bucketEndOf(parent, windowStart);
161983
+ const combined = combineRows({
161984
+ seriesId,
161985
+ windowStart,
161986
+ windowEnd,
161987
+ childWidthMs,
161988
+ rows: await store.readRollups(child, seriesId, windowStart, windowEnd - 1, READ_PAGE),
161989
+ carryIn: (await store.rollupBefore(child, seriesId, windowStart))?.last ?? null,
161990
+ horizon: now
161991
+ });
161992
+ if (combined === null) return;
161993
+ await store.upsertRollups(parent, [combined]);
161994
+ this.deps.counters.noteRollupRows(parent, seriesId, [combined.bucketStart]);
161995
+ if (parent === "1h") mark(this.dirty1d, seriesId, bucketStartOf("1d", windowStart));
161996
+ }
161997
+ /**
161998
+ * After a restart the dirty sets are gone. Rebuild the recent window from raw
161999
+ * (the still-open bucket included, time-weighted up to `now`), then mark the
162000
+ * 1h / 1d buckets of that window dirty so the next ticks recompute them.
162001
+ */
162002
+ async bootRecompute(now) {
162003
+ const windowStart = floorTo(now - BOOT_WINDOW_MS, FIVE_MINUTES_MS);
162004
+ const openBucket = floorTo(now, FIVE_MINUTES_MS);
162005
+ let series = 0;
162006
+ for (const s of this.deps.registry.all()) {
162007
+ if (s.lastTs < windowStart) continue;
162008
+ const metric = this.deps.registry.metric(s.key);
162009
+ series += 1;
162010
+ if (metric?.ingest === "points") {
162011
+ await this.recomputeFiveMinute(s.id, windowStart, openBucket + FIVE_MINUTES_MS, now);
162012
+ mark(this.dirty5m, s.id, openBucket);
162013
+ mark(this.dirty5m, s.id, openBucket - FIVE_MINUTES_MS);
162014
+ }
162015
+ for (let hour = floorTo(windowStart, HOUR_MS); hour <= now; hour += HOUR_MS) mark(this.dirty1h, s.id, hour);
162016
+ mark(this.dirty1d, s.id, bucketStartOf("1d", windowStart));
162017
+ mark(this.dirty1d, s.id, bucketStartOf("1d", now));
162018
+ }
162019
+ return series;
162020
+ }
162021
+ };
162022
+ function deltas(now, before) {
162023
+ const out = {};
162024
+ for (const [k, v] of now) {
162025
+ const d = v - (before.get(k) ?? 0);
162026
+ if (d > 0) out[k] = d;
162027
+ }
162028
+ return out;
162029
+ }
162030
+ var HistoryCounters = class {
162031
+ eventsSeen = 0;
162032
+ pointsEmitted = 0;
162033
+ pointsWritten = 0;
162034
+ pointsDropped = 0;
162035
+ trailingEmitted = 0;
162036
+ reconciledPoints = 0;
162037
+ reconcileRuns = 0;
162038
+ reconcileFailures = 0;
162039
+ lastReconcileAt = null;
162040
+ lastFlushAt = null;
162041
+ flushFailures = 0;
162042
+ ingestAccepted = 0;
162043
+ rollupErrors = 0;
162044
+ last5mAt = null;
162045
+ last1hAt = null;
162046
+ last1dAt = null;
162047
+ retentionLastRunAt = null;
162048
+ purgedSeries = 0;
162049
+ directoryRefreshAt = null;
162050
+ skipped = /* @__PURE__ */ new Map();
162051
+ refused = /* @__PURE__ */ new Map();
162052
+ deleted = /* @__PURE__ */ new Map();
162053
+ reported = {
162054
+ skipped: /* @__PURE__ */ new Map(),
162055
+ refused: /* @__PURE__ */ new Map(),
162056
+ deleted: /* @__PURE__ */ new Map(),
162057
+ flushFailures: 0
162058
+ };
162059
+ /** Net rows written minus deleted per tier since boot — the estimate behind `getStatus` row counts. */
162060
+ rowDelta = /* @__PURE__ */ new Map();
162061
+ /** Newest bucket seen per (tier, series): a row beyond it is NEW, a rewrite of an older one is not. */
162062
+ newestBucket = /* @__PURE__ */ new Map();
162063
+ skip(reason, count = 1) {
162064
+ this.skipped.set(reason, (this.skipped.get(reason) ?? 0) + count);
162065
+ }
162066
+ refuse(reason, count = 1) {
162067
+ this.refused.set(reason, (this.refused.get(reason) ?? 0) + count);
162068
+ }
162069
+ addDeleted(tier, count) {
162070
+ if (count <= 0) return;
162071
+ this.deleted.set(tier, (this.deleted.get(tier) ?? 0) + count);
162072
+ if (tier === "raw" || tier === "5m" || tier === "1h" || tier === "1d") this.rowDelta.set(tier, (this.rowDelta.get(tier) ?? 0) - count);
162073
+ }
162074
+ addRawRows(count) {
162075
+ this.rowDelta.set("raw", (this.rowDelta.get("raw") ?? 0) + count);
162076
+ }
162077
+ /** Rollup rows written: only a bucket newer than the series' newest known is counted as a new row. */
162078
+ noteRollupRows(tier, seriesId, bucketStarts) {
162079
+ const marker = `${tier}:${seriesId}`;
162080
+ let newest = this.newestBucket.get(marker) ?? Number.NEGATIVE_INFINITY;
162081
+ let added = 0;
162082
+ for (const b of bucketStarts) if (b > newest) {
162083
+ added += 1;
162084
+ newest = b;
162085
+ }
162086
+ this.newestBucket.set(marker, newest);
162087
+ if (added > 0) this.rowDelta.set(tier, (this.rowDelta.get(tier) ?? 0) + added);
162088
+ }
162089
+ rowDeltaOf(tier) {
162090
+ return this.rowDelta.get(tier) ?? 0;
162091
+ }
162092
+ forgetSeries(seriesId) {
162093
+ for (const tier of [
162094
+ "5m",
162095
+ "1h",
162096
+ "1d"
162097
+ ]) this.newestBucket.delete(`${tier}:${seriesId}`);
162098
+ }
162099
+ skippedRecord() {
162100
+ return Object.fromEntries(this.skipped);
162101
+ }
162102
+ refusedRecord() {
162103
+ return Object.fromEntries(this.refused);
162104
+ }
162105
+ deletedRecord() {
162106
+ return Object.fromEntries(this.deleted);
162107
+ }
162108
+ /** What was skipped, refused, deleted or failed since the last call; `null` when nothing. */
162109
+ summarySinceLast() {
162110
+ const summary = {
162111
+ skipped: deltas(this.skipped, this.reported.skipped),
162112
+ refused: deltas(this.refused, this.reported.refused),
162113
+ deleted: deltas(this.deleted, this.reported.deleted),
162114
+ flushFailures: this.flushFailures - this.reported.flushFailures
162115
+ };
162116
+ this.reported = {
162117
+ skipped: new Map(this.skipped),
162118
+ refused: new Map(this.refused),
162119
+ deleted: new Map(this.deleted),
162120
+ flushFailures: this.flushFailures
162121
+ };
162122
+ return Object.keys(summary.skipped).length === 0 && Object.keys(summary.refused).length === 0 && Object.keys(summary.deleted).length === 0 && summary.flushFailures === 0 ? null : summary;
162123
+ }
162124
+ };
162125
+ var ROLLUP_INTERVAL_MS = 6e4;
162126
+ var RECONCILE_INTERVAL_MS = 5 * 6e4;
162127
+ var RETENTION_INTERVAL_MS = 10 * 6e4;
162128
+ var FIRST_COUNT_DELAY_MS = 9e4;
162129
+ var APPROX_BYTES_PER_ROW = {
162130
+ raw: 24,
162131
+ "5m": 72,
162132
+ "1h": 72,
162133
+ "1d": 72
162134
+ };
162135
+ var HistoryRuntime = class {
162136
+ deps;
162137
+ counters = new HistoryCounters();
162138
+ store;
162139
+ registry;
162140
+ directory;
162141
+ rollup;
162142
+ writer;
162143
+ capture;
162144
+ ingest;
162145
+ query;
162146
+ retentionJob;
162147
+ departures;
162148
+ optedOutDevices = /* @__PURE__ */ new Set();
162149
+ tierCounts = /* @__PURE__ */ new Map();
162150
+ startedAt;
162151
+ timers = [];
162152
+ countTimeout = null;
162153
+ ready = false;
162154
+ constructor(deps) {
162155
+ this.deps = deps;
162156
+ const { logger, clock, catalog } = deps;
162157
+ this.startedAt = clock();
162158
+ this.store = new HistoryStore(deps.door);
162159
+ this.registry = new SeriesRegistry(this.store);
162160
+ this.directory = new DeviceDirectory(deps.deviceSource, this.counters, logger, clock);
162161
+ this.rollup = new RollupEngine({
162162
+ store: this.store,
162163
+ registry: this.registry,
162164
+ counters: this.counters,
162165
+ logger,
162166
+ clock
162167
+ });
162168
+ this.writer = new PointWriter({
162169
+ registry: this.registry,
162170
+ store: this.store,
162171
+ counters: this.counters,
162172
+ logger,
162173
+ clock,
162174
+ onWritten: (written) => this.rollup.markPoints(written),
162175
+ onDropped: (dropped) => this.capture.forget(dropped)
162176
+ });
162177
+ const optedOut = () => this.optedOutDevices;
162178
+ this.capture = new CaptureEngine({
162179
+ catalog,
162180
+ registry: this.registry,
162181
+ store: this.store,
162182
+ directory: this.directory,
162183
+ writer: this.writer,
162184
+ optedOut,
162185
+ counters: this.counters,
162186
+ logger,
162187
+ clock
162188
+ });
162189
+ this.ingest = new ProducerIngest({
162190
+ registry: this.registry,
162191
+ store: this.store,
162192
+ directory: this.directory,
162193
+ writer: this.writer,
162194
+ counters: this.counters,
162195
+ logger,
162196
+ clock,
162197
+ optedOut,
162198
+ retention: () => {
162199
+ const policy = deps.retention();
162200
+ return {
162201
+ rawMs: policy.rawDays * DAY_MS,
162202
+ fiveMinuteMs: policy.fiveMinuteDays * DAY_MS
162203
+ };
162204
+ },
162205
+ onBucketWritten: (seriesId, bucketStart) => this.rollup.markFiveMinuteRow(seriesId, bucketStart)
162206
+ });
162207
+ this.query = new HistoryQuery({
162208
+ registry: this.registry,
162209
+ store: this.store,
162210
+ clock,
162211
+ retention: deps.retention,
162212
+ catalog,
162213
+ readCapSlice: deps.readCapSlice
162214
+ });
162215
+ this.departures = new DepartureTracker(this.store, this.registry, logger);
162216
+ this.registry.onRemove((series) => {
162217
+ this.capture.forgetSeries(series);
162218
+ this.ingest.forgetSeries(series);
162219
+ this.rollup.forgetSeries(series.id);
162220
+ this.retentionJob.forgetSeries(series.id);
162221
+ });
162222
+ this.retentionJob = new RetentionJob({
162223
+ store: this.store,
162224
+ registry: this.registry,
162225
+ departures: this.departures,
162226
+ counters: this.counters,
162227
+ logger
162228
+ });
162229
+ }
162230
+ async prepare() {
162231
+ await this.store.declare();
162232
+ await this.registry.load();
162233
+ for (const metric of catalogMetrics(this.deps.catalog, this.deps.clock())) await this.registry.declareMetric(metric);
162234
+ this.optedOutDevices = await this.store.loadOptOuts();
162235
+ await this.departures.load();
162236
+ await this.directory.refresh();
162237
+ await this.capture.seed();
162238
+ }
162239
+ async finishBoot() {
162240
+ const series = await this.rollup.bootRecompute(this.deps.clock());
162241
+ this.deps.logger.info("device-history: recent rollup window rebuilt", { meta: { series } });
162242
+ await this.reconcile();
162243
+ }
162244
+ startTimers() {
162245
+ const every = (ms, run) => {
162246
+ const timer = setInterval(() => {
162247
+ run().catch((err) => {
162248
+ this.deps.logger.warn("device-history: scheduled pass threw", { meta: { error: err instanceof Error ? err.message : String(err) } });
162249
+ });
162250
+ }, ms);
162251
+ timer.unref();
162252
+ this.timers.push(timer);
162253
+ };
162254
+ every(FLUSH_INTERVAL_MS, () => this.flushNow());
162255
+ every(ROLLUP_INTERVAL_MS, async () => {
162256
+ await this.rollup.tick();
162257
+ this.reportWindow();
162258
+ });
162259
+ every(RECONCILE_INTERVAL_MS, async () => {
162260
+ await this.reconcile();
162261
+ this.reportWindow();
162262
+ });
162263
+ every(RETENTION_INTERVAL_MS, async () => {
162264
+ await this.retentionJob.run(this.deps.retention(), this.deps.clock());
162265
+ this.reportWindow();
162266
+ });
162267
+ this.countTimeout = setTimeout(() => {
162268
+ this.refreshCounts().catch(() => void 0);
162269
+ }, FIRST_COUNT_DELAY_MS);
162270
+ this.countTimeout.unref();
162271
+ }
162272
+ async stop() {
162273
+ for (const t of this.timers) clearInterval(t);
162274
+ this.timers = [];
162275
+ if (this.countTimeout !== null) clearTimeout(this.countTimeout);
162276
+ this.countTimeout = null;
162277
+ await this.writer.flush();
162278
+ }
162279
+ /** Emit pending trailing points, then write the buffer. */
162280
+ async flushNow() {
162281
+ this.capture.tickPending(this.deps.clock());
162282
+ await this.writer.flush();
162283
+ }
162284
+ /**
162285
+ * Restore changes a dropped event missed. A FAILED read changes nothing (D49):
162286
+ * it is counted and logged, and the next pass tries again.
162287
+ */
162288
+ async reconcile() {
162289
+ const { logger, clock } = this.deps;
162290
+ if (await this.directory.refresh()) await this.departures.observe(this.directory.listedIds(), clock());
162291
+ let snapshots;
162292
+ try {
162293
+ snapshots = await this.deps.readSnapshots();
162294
+ } catch (err) {
162295
+ this.counters.reconcileFailures += 1;
162296
+ logger.warn("device-history: reconcile read failed \u2014 nothing changed", { meta: { error: err instanceof Error ? err.message : String(err) } });
162297
+ return;
162298
+ }
162299
+ const now = clock();
162300
+ this.capture.reconcile(snapshots, now);
162301
+ this.counters.reconcileRuns += 1;
162302
+ this.counters.lastReconcileAt = now;
162303
+ this.ready = true;
162304
+ }
162305
+ async refreshCounts() {
162306
+ for (const tier of [
162307
+ "raw",
162308
+ "5m",
162309
+ "1h",
162310
+ "1d"
162311
+ ]) {
162312
+ const deltaAtCount = this.counters.rowDeltaOf(tier);
162313
+ const rows = await this.store.countRows(tier);
162314
+ this.tierCounts.set(tier, {
162315
+ rows,
162316
+ countedAt: this.deps.clock(),
162317
+ deltaAtCount
162318
+ });
162319
+ }
162320
+ }
162321
+ /**
162322
+ * ONE aggregated INFO naming what was skipped, refused, deleted or failed since the
162323
+ * previous report (D391) — the counts a per-event line would drown. Silent when
162324
+ * nothing happened.
162325
+ */
162326
+ reportWindow() {
162327
+ const summary = this.counters.summarySinceLast();
162328
+ if (summary === null) return;
162329
+ this.deps.logger.info("device-history: since the last report", { meta: { ...summary } });
162330
+ }
162331
+ isEnabled(deviceId) {
162332
+ return !this.optedOutDevices.has(deviceId);
162333
+ }
162334
+ async setEnabled(deviceId, enabled) {
162335
+ if (enabled) {
162336
+ await this.store.clearOptOut(deviceId);
162337
+ this.optedOutDevices = new Set([...this.optedOutDevices].filter((id) => id !== deviceId));
162338
+ } else {
162339
+ await this.store.setOptOut(deviceId, this.deps.clock());
162340
+ this.optedOutDevices = /* @__PURE__ */ new Set([...this.optedOutDevices, deviceId]);
162341
+ }
162342
+ return enabled;
162343
+ }
162344
+ /** Delete the history of a deleted device: its series, opt-out and absence clock. */
162345
+ async removeDevice(deviceId) {
162346
+ const series = await this.retentionJob.purgeDevice(deviceId, this.deps.clock());
162347
+ await this.store.clearOptOut(deviceId);
162348
+ await this.departures.forget(deviceId);
162349
+ this.optedOutDevices = new Set([...this.optedOutDevices].filter((id) => id !== deviceId));
162350
+ return { series };
162351
+ }
162352
+ /** Series held per device, for the admin clean-up listing. */
162353
+ seriesByDevice() {
162354
+ const out = /* @__PURE__ */ new Map();
162355
+ for (const s of this.registry.all()) {
162356
+ if (s.deviceId === null) continue;
162357
+ out.set(s.deviceId, (out.get(s.deviceId) ?? 0) + 1);
162358
+ }
162359
+ return out;
162360
+ }
162361
+ status() {
162362
+ const c = this.counters;
162363
+ const dirty = this.rollup.dirtyCounts();
162364
+ return {
162365
+ startedAt: this.startedAt,
162366
+ ready: this.ready,
162367
+ series: this.registry.all().length,
162368
+ metrics: this.registry.allMetrics().length,
162369
+ optedOutDevices: this.optedOutDevices.size,
162370
+ tiers: [
162371
+ "raw",
162372
+ "5m",
162373
+ "1h",
162374
+ "1d"
162375
+ ].map((tier) => {
162376
+ const count = this.tierCounts.get(tier);
162377
+ const rows = count === void 0 ? null : Math.max(0, count.rows + this.counters.rowDeltaOf(tier) - count.deltaAtCount);
162378
+ return {
162379
+ tier,
162380
+ rows,
162381
+ approxBytes: rows === null ? null : rows * APPROX_BYTES_PER_ROW[tier],
162382
+ countedAt: count?.countedAt ?? null
162383
+ };
162384
+ }),
162385
+ capture: {
162386
+ eventsSeen: c.eventsSeen,
162387
+ pointsEmitted: c.pointsEmitted,
162388
+ pointsWritten: c.pointsWritten,
162389
+ pointsDropped: c.pointsDropped,
162390
+ bufferDepth: this.writer.depth(),
162391
+ trailingEmitted: c.trailingEmitted,
162392
+ reconciledPoints: c.reconciledPoints,
162393
+ reconcileRuns: c.reconcileRuns,
162394
+ reconcileFailures: c.reconcileFailures,
162395
+ lastReconcileAt: c.lastReconcileAt,
162396
+ lastFlushAt: c.lastFlushAt,
162397
+ flushFailures: c.flushFailures,
162398
+ skipped: c.skippedRecord()
162399
+ },
162400
+ ingest: {
162401
+ accepted: c.ingestAccepted,
162402
+ refused: c.refusedRecord()
162403
+ },
162404
+ rollup: {
162405
+ last5mAt: c.last5mAt,
162406
+ last1hAt: c.last1hAt,
162407
+ last1dAt: c.last1dAt,
162408
+ errors: c.rollupErrors,
162409
+ dirty5m: dirty.m5,
162410
+ dirty1h: dirty.h1,
162411
+ dirty1d: dirty.d1
162412
+ },
162413
+ retention: {
162414
+ lastRunAt: c.retentionLastRunAt,
162415
+ deleted: c.deletedRecord(),
162416
+ purgedSeries: c.purgedSeries
162417
+ },
162418
+ directory: {
162419
+ devices: this.directory.size(),
162420
+ lastRefreshAt: this.directory.lastRefreshAt()
162421
+ }
162422
+ };
162423
+ }
162424
+ };
162425
+ var DEFAULT_CONFIG = {
162426
+ rawDays: DEFAULT_RETENTION.rawDays,
162427
+ fiveMinuteDays: DEFAULT_RETENTION.fiveMinuteDays,
162428
+ hourDays: DEFAULT_RETENTION.hourDays,
162429
+ dayDays: 0
162430
+ };
162431
+ function isRecord(value) {
162432
+ return typeof value === "object" && value !== null && !Array.isArray(value);
162433
+ }
162434
+ function callerOf(input) {
162435
+ if (!isRecord(input)) return null;
162436
+ const raw = input["callerAddonId"];
162437
+ return typeof raw === "string" && raw.length > 0 ? raw : null;
162438
+ }
162439
+ function positiveOr(value, fallback) {
162440
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
162441
+ }
162442
+ var DeviceHistoryAddon = class extends require_dist10.BaseAddon {
162443
+ runtime = null;
162444
+ unsubscribe = null;
162445
+ removalHook = null;
162446
+ constructor() {
162447
+ super({ ...DEFAULT_CONFIG });
162448
+ }
162449
+ retention() {
162450
+ const c = this.config;
162451
+ return {
162452
+ rawDays: positiveOr(c.rawDays, DEFAULT_RETENTION.rawDays),
162453
+ fiveMinuteDays: positiveOr(c.fiveMinuteDays, DEFAULT_RETENTION.fiveMinuteDays),
162454
+ hourDays: positiveOr(c.hourDays, DEFAULT_RETENTION.hourDays),
162455
+ dayDays: positiveOr(c.dayDays, 0) > 0 ? c.dayDays : null
162456
+ };
162457
+ }
162458
+ requireRuntime() {
162459
+ if (this.runtime === null) throw new Error("device-history is starting \u2014 the history store is not open yet");
162460
+ return this.runtime;
162461
+ }
162462
+ async onInitialize() {
162463
+ const provider = {
162464
+ listSeries: async (input) => this.requireRuntime().query.listSeries({
162465
+ kind: "device",
162466
+ deviceId: input.deviceId
162467
+ }),
162468
+ listNodeSeries: async (input) => this.requireRuntime().query.listSeries({
162469
+ kind: "node",
162470
+ nodeId: input.nodeId
162471
+ }),
162472
+ query: async ({ deviceId, ...window2 }) => this.requireRuntime().query.query({
162473
+ ...window2,
162474
+ subject: {
162475
+ kind: "device",
162476
+ deviceId
162477
+ }
162478
+ }),
162479
+ queryNode: async ({ nodeId, ...window2 }) => this.requireRuntime().query.query({
162480
+ ...window2,
162481
+ subject: {
162482
+ kind: "node",
162483
+ nodeId
162484
+ }
162485
+ }),
162486
+ declareSeries: async (input) => this.requireRuntime().ingest.declareSeries(callerOf(input), input),
162487
+ record: async (input) => this.requireRuntime().ingest.record(callerOf(input), input.points),
162488
+ recordBuckets: async (input) => this.requireRuntime().ingest.recordBuckets(callerOf(input), input.buckets),
162489
+ getDeviceEnabled: async (input) => ({ enabled: this.requireRuntime().isEnabled(input.deviceId) }),
162490
+ setDeviceEnabled: async (input) => ({ enabled: await this.requireRuntime().setEnabled(input.deviceId, input.enabled) }),
162491
+ getStatus: async () => this.requireRuntime().status()
162492
+ };
162493
+ this.ctx.logger.info("Initialized \u2014 device-history provider registered (store opens on hub link)");
162494
+ const removalHook = {
162495
+ onDeviceRemoved: async (input) => {
162496
+ const { series } = await this.requireRuntime().removeDevice(input.deviceId);
162497
+ if (series > 0) this.ctx.logger.info("device-history: removed the history of a deleted device", {
162498
+ tags: { deviceId: input.deviceId },
162499
+ meta: {
162500
+ counts: { series },
162501
+ deletedAt: input.deletedAt
162502
+ }
162503
+ });
162504
+ return { counts: { series } };
162505
+ },
162506
+ listDeviceData: async () => [...this.requireRuntime().seriesByDevice()].map(([deviceId, count]) => ({
162507
+ store: "series",
162508
+ deviceId,
162509
+ count
162510
+ }))
162511
+ };
162512
+ this.removalHook = removalHook;
162513
+ return [{
162514
+ capability: require_dist10.deviceHistoryCapability,
162515
+ provider
162516
+ }, {
162517
+ capability: require_dist10.deviceRemovalHookCapability,
162518
+ provider: removalHook
162519
+ }];
162520
+ }
162521
+ async onHubReachable() {
162522
+ const api = this.ctx.api;
162523
+ const runtime = new HistoryRuntime({
162524
+ door: api.settingsStore,
162525
+ catalog: require_dist10.HISTORY_CATALOG,
162526
+ deviceSource: {
162527
+ listAll: async () => {
162528
+ return (await api.deviceManager.listAll.query({ projection: "slim" })).map((d) => ({
162529
+ id: d.id,
162530
+ type: d.type
162531
+ }));
162532
+ },
162533
+ getDevice: async (deviceId) => {
162534
+ const d = await api.deviceManager.getDevice.query({ deviceId });
162535
+ return d === null ? null : {
162536
+ id: d.id,
162537
+ type: d.type
162538
+ };
162539
+ }
162540
+ },
162541
+ readSnapshots: async () => api.deviceState.getAllSnapshots.query({}),
162542
+ readCapSlice: (deviceId, capName) => api.deviceState.getCapSlice.query({
162543
+ deviceId,
162544
+ capName
162545
+ }),
162546
+ retention: () => this.retention(),
162547
+ logger: this.ctx.logger,
162548
+ clock: () => Date.now()
162549
+ });
162550
+ await runtime.prepare();
162551
+ this.unsubscribe = this.ctx.eventBus.subscribe({ category: require_dist10.EventCategory.DeviceStateChanged }, (event) => {
162552
+ const data = event.data;
162553
+ if (!isRecord(data)) return;
162554
+ const { deviceId, capName, slice } = data;
162555
+ if (typeof deviceId !== "number" || typeof capName !== "string" || !isRecord(slice)) return;
162556
+ const at = event.timestamp instanceof Date ? event.timestamp.getTime() : Date.now();
162557
+ runtime.capture.handleState(deviceId, capName, slice, at);
162558
+ });
162559
+ this.ctx.addDisposer(async () => this.stopRuntime());
162560
+ this.runtime = runtime;
162561
+ await runtime.finishBoot();
162562
+ runtime.startTimers();
162563
+ const removalHook = this.removalHook;
162564
+ if (removalHook !== null) this.ctx.addDisposer(require_dist10.armRemovalReconcile({
162565
+ api: this.ctx.api,
162566
+ logger: this.ctx.logger,
162567
+ remove: (input) => removalHook.onDeviceRemoved(input)
162568
+ }));
162569
+ this.ctx.logger.info("device-history: capturing", { meta: {
162570
+ series: runtime.status().series,
162571
+ metrics: runtime.status().metrics
162572
+ } });
162573
+ }
162574
+ async stopRuntime() {
162575
+ this.unsubscribe?.();
162576
+ this.unsubscribe = null;
162577
+ await this.runtime?.stop();
162578
+ }
162579
+ async onShutdown() {
162580
+ await this.stopRuntime();
162581
+ this.runtime = null;
162582
+ }
162583
+ globalSettingsSchema() {
162584
+ return this.schema({ sections: [{
162585
+ id: "retention",
162586
+ title: "Retention",
162587
+ description: "How long each tier is kept. The raw points feed the 5-minute rollup; the 1-day tier is kept forever unless a number of days is set.",
162588
+ columns: 2,
162589
+ fields: [
162590
+ {
162591
+ type: "number",
162592
+ key: "rawDays",
162593
+ label: "Raw points (days)",
162594
+ min: 1,
162595
+ max: 60,
162596
+ step: 1
162597
+ },
162598
+ {
162599
+ type: "number",
162600
+ key: "fiveMinuteDays",
162601
+ label: "5-minute rollup (days)",
162602
+ min: 1,
162603
+ max: 365,
162604
+ step: 1
162605
+ },
162606
+ {
162607
+ type: "number",
162608
+ key: "hourDays",
162609
+ label: "1-hour rollup (days)",
162610
+ min: 7,
162611
+ max: 3650,
162612
+ step: 1
162613
+ },
162614
+ {
162615
+ type: "number",
162616
+ key: "dayDays",
162617
+ label: "1-day rollup (days, 0 = forever)",
162618
+ min: 0,
162619
+ max: 36500,
162620
+ step: 1
162621
+ }
162622
+ ]
162623
+ }] });
162624
+ }
162625
+ };
162626
+ exports.DeviceHistoryAddon = DeviceHistoryAddon;
162627
+ exports.default = DeviceHistoryAddon;
162628
+ exports.callerOf = callerOf;
162629
+ }
162630
+ });
162631
+
160163
162632
  // ../system/dist/builtins/sqlite-storage/filesystem-storage.addon.js
160164
162633
  var require_filesystem_storage_addon = __commonJS({
160165
162634
  "../system/dist/builtins/sqlite-storage/filesystem-storage.addon.js"(exports) {
@@ -160169,7 +162638,7 @@ var require_filesystem_storage_addon = __commonJS({
160169
162638
  [Symbol.toStringTag]: { value: "Module" }
160170
162639
  });
160171
162640
  var require_chunk = require_chunk_Cek0wNdY();
160172
- var require_dist10 = require_dist_BkNfEJKa();
162641
+ var require_dist10 = require_dist_Ct88catN();
160173
162642
  var node_crypto = __require("crypto");
160174
162643
  var node_fs_promises = __require("fs/promises");
160175
162644
  var node_path = __require("path");
@@ -161279,12 +163748,12 @@ var require_lib = __commonJS({
161279
163748
  }
161280
163749
  });
161281
163750
 
161282
- // ../system/dist/wal-maintenance-CuG1DUca.js
161283
- var require_wal_maintenance_CuG1DUca = __commonJS({
161284
- "../system/dist/wal-maintenance-CuG1DUca.js"(exports) {
163751
+ // ../system/dist/wal-maintenance-mwCBe3Vr.js
163752
+ var require_wal_maintenance_mwCBe3Vr = __commonJS({
163753
+ "../system/dist/wal-maintenance-mwCBe3Vr.js"(exports) {
161285
163754
  "use strict";
161286
163755
  var require_chunk = require_chunk_Cek0wNdY();
161287
- var require_dist10 = require_dist_BkNfEJKa();
163756
+ var require_dist10 = require_dist_Ct88catN();
161288
163757
  var require_prefix_range = require_prefix_range_D_KueXbQ();
161289
163758
  var node_crypto = __require("crypto");
161290
163759
  var node_module = __require("module");
@@ -164825,6 +167294,22 @@ CREATE TABLE IF NOT EXISTS _record_sequences (
164825
167294
  ...profiler ? { profiler } : {}
164826
167295
  });
164827
167296
  }
167297
+ function vectorProviderOfOwnedFile(openEvents) {
167298
+ const call = async (op, input) => {
167299
+ return (await openEvents()).vector(op, input);
167300
+ };
167301
+ return {
167302
+ declareIndex: async (input) => call("declareIndex", input),
167303
+ upsert: async (input) => call("upsert", input),
167304
+ query: async (input) => call("query", input),
167305
+ getByIds: async (input) => call("getByIds", input),
167306
+ fetchByIds: async (input) => call("fetchByIds", input),
167307
+ scan: async (input) => call("scan", input),
167308
+ deleteByIds: async (input) => call("deleteByIds", input),
167309
+ deleteByFilter: async (input) => call("deleteByFilter", input),
167310
+ stats: async (input) => call("stats", input)
167311
+ };
167312
+ }
164828
167313
  function vectorProviderOf(vectorIndex) {
164829
167314
  return {
164830
167315
  declareIndex: async (input) => {
@@ -165268,6 +167753,12 @@ CREATE TABLE IF NOT EXISTS _record_sequences (
165268
167753
  return vectorProviderOf;
165269
167754
  }
165270
167755
  });
167756
+ Object.defineProperty(exports, "vectorProviderOfOwnedFile", {
167757
+ enumerable: true,
167758
+ get: function() {
167759
+ return vectorProviderOfOwnedFile;
167760
+ }
167761
+ });
165271
167762
  }
165272
167763
  });
165273
167764
 
@@ -165446,9 +167937,9 @@ var require_sqlite_settings_addon = __commonJS({
165446
167937
  [Symbol.toStringTag]: { value: "Module" }
165447
167938
  });
165448
167939
  var require_chunk = require_chunk_Cek0wNdY();
165449
- var require_dist10 = require_dist_BkNfEJKa();
167940
+ var require_dist10 = require_dist_Ct88catN();
165450
167941
  var require_retired_settings_keys = require_retired_settings_keys_D_VwZiDt();
165451
- var require_wal_maintenance = require_wal_maintenance_CuG1DUca();
167942
+ var require_wal_maintenance = require_wal_maintenance_mwCBe3Vr();
165452
167943
  var require_sqlite_snapshot_protocol = require_sqlite_snapshot_protocol_B9OndqZv();
165453
167944
  var require_wal_checkpoint_policy = require_wal_checkpoint_policy_d2z2qPqx();
165454
167945
  var node_fs_promises = __require("fs/promises");
@@ -167145,7 +169636,7 @@ var require_sqlite_settings_addon = __commonJS({
167145
169636
  livePath: settingsPath,
167146
169637
  thread: "main",
167147
169638
  autoVacuum: "none",
167148
- loadVectorExtension: true
169639
+ loadVectorExtension: false
167149
169640
  }, deps);
167150
169641
  if (!(settings instanceof InlineOwnedFile)) throw new Error("sqlite-settings: the settings file must be served on the main thread");
167151
169642
  this.settingsFile = settings;
@@ -167158,8 +169649,7 @@ var require_sqlite_settings_addon = __commonJS({
167158
169649
  logger: this.ctx.logger.child("Placement")
167159
169650
  });
167160
169651
  this.router = router;
167161
- const vectorIndex = settings.getVectorIndex();
167162
- if (vectorIndex === null) throw new Error("vector-store: the settings file has no vector index");
169652
+ const vectorProvider = require_wal_maintenance.vectorProviderOfOwnedFile(() => router.recordsFileFor("events"));
167163
169653
  const records = new RecordsProvider(router, this.ctx.logger.child("Records"));
167164
169654
  await records.init();
167165
169655
  this.startRecordsSweep(records);
@@ -167175,7 +169665,7 @@ var require_sqlite_settings_addon = __commonJS({
167175
169665
  },
167176
169666
  {
167177
169667
  capability: require_dist10.vectorStoreCapability,
167178
- provider: require_wal_maintenance.vectorProviderOf(vectorIndex)
169668
+ provider: vectorProvider
167179
169669
  },
167180
169670
  {
167181
169671
  capability: require_dist10.databaseSnapshotCapability,
@@ -167244,9 +169734,9 @@ var require_sqlite_settings_addon = __commonJS({
167244
169734
  getRouter() {
167245
169735
  return this.router;
167246
169736
  }
167247
- /** Exposed for tests; null before `onInitialize`. */
169737
+ /** Exposed for tests; vectors are served from the events file (P5). */
167248
169738
  getVectorIndex() {
167249
- return this.settingsFile?.getVectorIndex() ?? null;
169739
+ return null;
167250
169740
  }
167251
169741
  };
167252
169742
  function workerSpec(databaseId, livePath) {
@@ -167255,7 +169745,7 @@ var require_sqlite_settings_addon = __commonJS({
167255
169745
  livePath,
167256
169746
  thread: "worker",
167257
169747
  autoVacuum: "incremental",
167258
- loadVectorExtension: false
169748
+ loadVectorExtension: databaseId === "events"
167259
169749
  };
167260
169750
  }
167261
169751
  exports.SqliteSettingsAddon = SqliteSettingsAddon;
@@ -167273,7 +169763,7 @@ var require_sqlite_storage = __commonJS({
167273
169763
  });
167274
169764
  require_chunk_Cek0wNdY();
167275
169765
  var require_builtins_sqlite_storage_filesystem_storage_addon = require_filesystem_storage_addon();
167276
- var require_wal_maintenance = require_wal_maintenance_CuG1DUca();
169766
+ var require_wal_maintenance = require_wal_maintenance_mwCBe3Vr();
167277
169767
  var require_builtins_sqlite_storage_sqlite_settings_addon = require_sqlite_settings_addon();
167278
169768
  var ConfigStore = class {
167279
169769
  db;
@@ -167357,7 +169847,7 @@ var require_storage_orchestrator_addon = __commonJS({
167357
169847
  [Symbol.toStringTag]: { value: "Module" }
167358
169848
  });
167359
169849
  var require_chunk = require_chunk_Cek0wNdY();
167360
- var require_dist10 = require_dist_BkNfEJKa();
169850
+ var require_dist10 = require_dist_Ct88catN();
167361
169851
  var require_read_all_rows$1 = require_read_all_rows_CVoO1ONM();
167362
169852
  var zod = require_zod();
167363
169853
  var node_crypto = __require("crypto");
@@ -171488,11 +173978,11 @@ var require_storage_orchestrator = __commonJS({
171488
173978
  }
171489
173979
  });
171490
173980
 
171491
- // ../system/dist/constants-DA_tjzIk.js
171492
- var require_constants_DA_tjzIk = __commonJS({
171493
- "../system/dist/constants-DA_tjzIk.js"(exports) {
173981
+ // ../system/dist/constants-BPWA-r-Y.js
173982
+ var require_constants_BPWA_r_Y = __commonJS({
173983
+ "../system/dist/constants-BPWA-r-Y.js"(exports) {
171494
173984
  "use strict";
171495
- var MESH_CA_KEY_REF = require_dist_BkNfEJKa().buildSecretRef({
173985
+ var MESH_CA_KEY_REF = require_dist_Ct88catN().buildSecretRef({
171496
173986
  owner: "system",
171497
173987
  scope: { kind: "global" },
171498
173988
  fieldPath: "meshCaKey"
@@ -171555,14 +174045,14 @@ var require_constants_DA_tjzIk = __commonJS({
171555
174045
  }
171556
174046
  });
171557
174047
 
171558
- // ../system/dist/agent-https-5DYAOJMO.js
171559
- var require_agent_https_5DYAOJMO = __commonJS({
171560
- "../system/dist/agent-https-5DYAOJMO.js"(exports) {
174048
+ // ../system/dist/agent-https-CMz4S56_.js
174049
+ var require_agent_https_CMz4S56 = __commonJS({
174050
+ "../system/dist/agent-https-CMz4S56_.js"(exports) {
171561
174051
  "use strict";
171562
174052
  var require_chunk = require_chunk_Cek0wNdY();
171563
- var require_dist10 = require_dist_BkNfEJKa();
174053
+ var require_dist10 = require_dist_Ct88catN();
171564
174054
  var require_x509_es = require_x509_es_C4cOo48M();
171565
- var require_constants15 = require_constants_DA_tjzIk();
174055
+ var require_constants15 = require_constants_BPWA_r_Y();
171566
174056
  var zod = require_zod();
171567
174057
  var node_crypto = __require("crypto");
171568
174058
  var _camstack_types_node = require_node();
@@ -190305,11 +192795,11 @@ var require_system_config_addon = __commonJS({
190305
192795
  [Symbol.toStringTag]: { value: "Module" }
190306
192796
  });
190307
192797
  var require_chunk = require_chunk_Cek0wNdY();
190308
- var require_dist10 = require_dist_BkNfEJKa();
192798
+ var require_dist10 = require_dist_Ct88catN();
190309
192799
  var require_safe_buffer$1 = require_safe_buffer_D3aoa_7X();
190310
192800
  var require_x509_es = require_x509_es_C4cOo48M();
190311
- var require_constants$8 = require_constants_DA_tjzIk();
190312
- var require_agent_https = require_agent_https_5DYAOJMO();
192801
+ var require_constants$8 = require_constants_BPWA_r_Y();
192802
+ var require_agent_https = require_agent_https_CMz4S56();
190313
192803
  var require_src$1 = require_src_CnAqG6EF();
190314
192804
  var zod = require_zod();
190315
192805
  var node_crypto = __require("crypto");
@@ -270022,7 +272512,7 @@ var require_winston_logging = __commonJS({
270022
272512
  [Symbol.toStringTag]: { value: "Module" }
270023
272513
  });
270024
272514
  var require_chunk = require_chunk_Cek0wNdY();
270025
- var require_dist10 = require_dist_BkNfEJKa();
272515
+ var require_dist10 = require_dist_Ct88catN();
270026
272516
  var require_formatter = require_formatter_DqAKDlvN();
270027
272517
  var node_path = __require("path");
270028
272518
  node_path = require_chunk.__toESM(node_path);
@@ -277672,14 +280162,14 @@ var require_lib_8oZrELLm = __commonJS({
277672
280162
  }
277673
280163
  });
277674
280164
 
277675
- // ../system/dist/hub-mdns-ciao-m5LIr4oA.js
277676
- var require_hub_mdns_ciao_m5LIr4oA = __commonJS({
277677
- "../system/dist/hub-mdns-ciao-m5LIr4oA.js"(exports) {
280165
+ // ../system/dist/hub-mdns-ciao-DAU1WttG.js
280166
+ var require_hub_mdns_ciao_DAU1WttG = __commonJS({
280167
+ "../system/dist/hub-mdns-ciao-DAU1WttG.js"(exports) {
277678
280168
  "use strict";
277679
280169
  var require_chunk = require_chunk_Cek0wNdY();
277680
- var require_dist10 = require_dist_BkNfEJKa();
280170
+ var require_dist10 = require_dist_Ct88catN();
277681
280171
  var require_x509_es = require_x509_es_C4cOo48M();
277682
- var require_agent_https = require_agent_https_5DYAOJMO();
280172
+ var require_agent_https = require_agent_https_CMz4S56();
277683
280173
  var node_crypto = __require("crypto");
277684
280174
  var _camstack_types_node = require_node();
277685
280175
  var node_fs_promises = __require("fs/promises");
@@ -278678,12 +281168,12 @@ var require_platform_key_source_Di7HrlGR = __commonJS({
278678
281168
  }
278679
281169
  });
278680
281170
 
278681
- // ../system/dist/restore-archive-B57xI5Ue.js
278682
- var require_restore_archive_B57xI5Ue = __commonJS({
278683
- "../system/dist/restore-archive-B57xI5Ue.js"(exports) {
281171
+ // ../system/dist/restore-archive-Cprdh3P9.js
281172
+ var require_restore_archive_Cprdh3P9 = __commonJS({
281173
+ "../system/dist/restore-archive-Cprdh3P9.js"(exports) {
278684
281174
  "use strict";
278685
281175
  var require_chunk = require_chunk_Cek0wNdY();
278686
- var require_dist10 = require_dist_BkNfEJKa();
281176
+ var require_dist10 = require_dist_Ct88catN();
278687
281177
  var require_read_all_rows$1 = require_read_all_rows_CVoO1ONM();
278688
281178
  var zod = require_zod();
278689
281179
  var node_crypto = __require("crypto");
@@ -290004,9 +292494,9 @@ var require_dist3 = __commonJS({
290004
292494
  "use strict";
290005
292495
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
290006
292496
  var require_chunk = require_chunk_Cek0wNdY();
290007
- var require_dist10 = require_dist_BkNfEJKa();
292497
+ var require_dist10 = require_dist_Ct88catN();
290008
292498
  var require_read_all_rows$1 = require_read_all_rows_CVoO1ONM();
290009
- var require_device_ca = require_device_ca_CnbrgfmW();
292499
+ var require_device_ca = require_device_ca_75rt3U_U();
290010
292500
  var require_builtins_alerts_alerts_addon = require_alerts_addon();
290011
292501
  require_alerts();
290012
292502
  var require_builtins_calendars_calendars_addon = require_calendars_addon();
@@ -290029,22 +292519,23 @@ var require_dist3 = __commonJS({
290029
292519
  require_local_auth();
290030
292520
  var require_builtins_loki_logging_index = require_loki_logging();
290031
292521
  var require_builtins_native_metrics_native_metrics_addon = require_native_metrics_addon();
292522
+ var require_builtins_device_history_device_history_addon = require_device_history_addon();
290032
292523
  var require_builtins_sqlite_storage_filesystem_storage_addon = require_filesystem_storage_addon();
290033
- var require_wal_maintenance = require_wal_maintenance_CuG1DUca();
292524
+ var require_wal_maintenance = require_wal_maintenance_mwCBe3Vr();
290034
292525
  var require_builtins_sqlite_storage_sqlite_settings_addon = require_sqlite_settings_addon();
290035
292526
  var require_builtins_sqlite_storage_index = require_sqlite_storage();
290036
292527
  var require_builtins_storage_orchestrator_storage_orchestrator_addon = require_storage_orchestrator_addon();
290037
292528
  require_storage_orchestrator();
290038
- var require_constants15 = require_constants_DA_tjzIk();
290039
- var require_agent_https = require_agent_https_5DYAOJMO();
292529
+ var require_constants15 = require_constants_BPWA_r_Y();
292530
+ var require_agent_https = require_agent_https_CMz4S56();
290040
292531
  var require_builtins_system_config_system_config_addon = require_system_config_addon();
290041
292532
  require_system_config();
290042
292533
  var require_builtins_winston_logging_index = require_winston_logging();
290043
292534
  var require_custom_action_registry = require_custom_action_registry_COyb49zm();
290044
- var require_hub_mdns_ciao = require_hub_mdns_ciao_m5LIr4oA();
292535
+ var require_hub_mdns_ciao = require_hub_mdns_ciao_DAU1WttG();
290045
292536
  var require_platform_key_source = require_platform_key_source_Di7HrlGR();
290046
292537
  var require_resource_monitor = require_resource_monitor_mio9dSCT();
290047
- var require_restore_archive = require_restore_archive_B57xI5Ue();
292538
+ var require_restore_archive = require_restore_archive_Cprdh3P9();
290048
292539
  var zod = require_zod();
290049
292540
  var node_crypto = __require("crypto");
290050
292541
  node_crypto = require_chunk.__toESM(node_crypto);
@@ -312707,6 +315198,7 @@ var require_dist3 = __commonJS({
312707
315198
  exports.DataPlaneRegistry = DataPlaneRegistry;
312708
315199
  exports.DeviceCaStore = require_builtins_local_auth_local_auth_addon.DeviceCaStore;
312709
315200
  exports.DeviceCredentialManager = require_builtins_local_auth_local_auth_addon.DeviceCredentialManager;
315201
+ exports.DeviceHistoryAddon = require_builtins_device_history_device_history_addon.DeviceHistoryAddon;
312710
315202
  exports.DeviceManagerAddon = require_builtins_device_manager_device_manager_addon.DeviceManagerAddon;
312711
315203
  exports.DeviceRegistry = DeviceRegistry;
312712
315204
  exports.DeviceStore = require_builtins_sqlite_storage_index.DeviceStore$1;