camstack 1.2.34 → 1.2.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -38,7 +38,7 @@ async function runServe(args) {
38
38
  ...typeof values.data === "string" ? { data: values.data } : {}
39
39
  };
40
40
  Object.assign(process.env, buildServeEnv(opts));
41
- await import("./launcher-CSBOA3IK.js");
41
+ await import("./launcher-IU5FZCJX.js");
42
42
  }
43
43
 
44
44
  // src/commands/agent.ts
@@ -83,7 +83,7 @@ async function runAgent(args) {
83
83
  ...typeof values.port === "string" ? { port: values.port } : {}
84
84
  };
85
85
  Object.assign(process.env, buildAgentEnv(opts));
86
- await import("./launcher-CSBOA3IK.js");
86
+ await import("./launcher-IU5FZCJX.js");
87
87
  }
88
88
 
89
89
  // src/commands/setup.ts
@@ -60548,7 +60548,7 @@ var require_device_manager_addon = __commonJS({
60548
60548
  }
60549
60549
  async function listPersistedByAddon(pctx, input) {
60550
60550
  const { addonId } = input;
60551
- const [index, meta] = await Promise.all([pctx.metaStore.readIndex(), pctx.metaStore.readMeta()]);
60551
+ const { index, meta } = await pctx.metaStore.readAll();
60552
60552
  const stableIds = index[addonId] ?? [];
60553
60553
  const byStableId = /* @__PURE__ */ new Map();
60554
60554
  for (const m of Object.values(meta)) if (m.addonId === addonId) byStableId.set(m.stableId, m);
@@ -60573,8 +60573,7 @@ var require_device_manager_addon = __commonJS({
60573
60573
  const camerasOnly = input.isCamera === true;
60574
60574
  const results = [];
60575
60575
  const seen = /* @__PURE__ */ new Set();
60576
- const meta = await pctx.metaStore.readMeta();
60577
- const metadataMap = await pctx.metaStore.readMetadataMap();
60576
+ const { meta, metadata: metadataMap, index } = await pctx.metaStore.readAll();
60578
60577
  if (pctx.registry) {
60579
60578
  const liveEntries = addonId ? pctx.registry.getAllForAddon(addonId).map((device) => ({
60580
60579
  addonId,
@@ -60592,7 +60591,6 @@ var require_device_manager_addon = __commonJS({
60592
60591
  } : info);
60593
60592
  }
60594
60593
  }
60595
- const index = await pctx.metaStore.readIndex();
60596
60594
  const metaByAddonStable = /* @__PURE__ */ new Map();
60597
60595
  for (const m of Object.values(meta)) metaByAddonStable.set(`${m.addonId}\0${m.stableId}`, m);
60598
60596
  const targetAddons = addonId ? [addonId] : Object.keys(index);
@@ -60690,11 +60688,7 @@ var require_device_manager_addon = __commonJS({
60690
60688
  }
60691
60689
  const results = [];
60692
60690
  const seen = /* @__PURE__ */ new Set();
60693
- const [index, meta, metadataMap] = await Promise.all([
60694
- pctx.metaStore.readIndex(),
60695
- pctx.metaStore.readMeta(),
60696
- pctx.metaStore.readMetadataMap()
60697
- ]);
60691
+ const { index, meta, metadata: metadataMap } = await pctx.metaStore.readAll();
60698
60692
  if (pctx.registry) {
60699
60693
  const liveChildren = pctx.registry.getChildren(parentDeviceId);
60700
60694
  for (const device of liveChildren) {
@@ -62863,8 +62857,61 @@ var require_device_manager_addon = __commonJS({
62863
62857
  this.settings = settings;
62864
62858
  this.registry = registry;
62865
62859
  }
62860
+ /** The read currently in flight, or null. Never a settled value — see
62861
+ * {@link readStore}. */
62862
+ inFlightRead = null;
62863
+ /**
62864
+ * The whole persisted addon store.
62865
+ *
62866
+ * **Concurrent callers join the read already in flight.** This is not a
62867
+ * cache and nothing survives settlement: a caller that awaited the running
62868
+ * promise could not have observed anything older than its result, so the
62869
+ * only thing that changes is cost. What that cost was, measured on the
62870
+ * 2026-08-19 boot: `readAddonStore` lands in `SqliteSettingsBackend
62871
+ * .getAllAddon`, which reads and `JSON.parse`s this addon's rows —
62872
+ * 625 KB on the live hub (`deviceMeta` 467 KB + `deviceMetadata` 87 KB +
62873
+ * `deviceIndex` 70 KB) — synchronously, on the hub's event loop. A V8
62874
+ * profile of that boot had hub-main's JS thread 99.9% busy with 64% of it
62875
+ * inside `getAllAddon`, ~89% of that entered here, and every runner's first
62876
+ * store read queued behind it (the notification centre's six parallel reads
62877
+ * all resolved together at t+44.7 s).
62878
+ *
62879
+ * A rejection is NOT latched: the slot is cleared before the promise
62880
+ * settles either way, so a failed read costs the joiners that one failure
62881
+ * and the next caller reaches the store again.
62882
+ */
62866
62883
  readStore = async () => {
62867
- return await this.settings.readAddonStore();
62884
+ const existing = this.inFlightRead;
62885
+ if (existing !== null) return existing;
62886
+ const read = (async () => {
62887
+ try {
62888
+ return await this.settings.readAddonStore();
62889
+ } finally {
62890
+ this.inFlightRead = null;
62891
+ }
62892
+ })();
62893
+ this.inFlightRead = read;
62894
+ return read;
62895
+ };
62896
+ /**
62897
+ * The three fleet projections from ONE read.
62898
+ *
62899
+ * `listAll` asked for `deviceMeta`, then `deviceMetadata`, then
62900
+ * `deviceIndex` — three SEQUENTIAL awaits, which {@link readStore}'s
62901
+ * in-flight join cannot collapse because each starts after the previous one
62902
+ * settled. Three full 625 KB parses per call, on a call made once per device
62903
+ * lifecycle event during a 974-device boot: 23% of hub-main's CPU.
62904
+ *
62905
+ * It is also ONE snapshot. Three separate reads could straddle a write and
62906
+ * hand back an index that names a device the meta map no longer has.
62907
+ */
62908
+ readAll = async () => {
62909
+ const store = await this.readStore();
62910
+ return {
62911
+ index: store.deviceIndex ?? {},
62912
+ meta: store.deviceMeta ?? {},
62913
+ metadata: store.deviceMetadata ?? {}
62914
+ };
62868
62915
  };
62869
62916
  readIndex = async () => {
62870
62917
  return (await this.readStore()).deviceIndex ?? {};
@@ -74542,6 +74589,20 @@ var require_sqlite_settings_addon = __commonJS({
74542
74589
  params
74543
74590
  };
74544
74591
  }
74592
+ var MAX_ASCII = 127;
74593
+ function prefixRange(prefix) {
74594
+ if (prefix.length === 0) return null;
74595
+ for (let i = 0; i < prefix.length; i++) {
74596
+ const code = prefix.charCodeAt(i);
74597
+ if (code === 0 || code > MAX_ASCII) return null;
74598
+ }
74599
+ const last = prefix.charCodeAt(prefix.length - 1);
74600
+ if (last >= MAX_ASCII) return null;
74601
+ return {
74602
+ lo: prefix,
74603
+ hi: prefix.slice(0, -1) + String.fromCharCode(last + 1)
74604
+ };
74605
+ }
74545
74606
  var SETTINGS_QUERY_DEFAULT_ROW_CAP = 2e3;
74546
74607
  var SETTINGS_QUERY_HARD_ROW_CAP = 2e4;
74547
74608
  function resolveRowBound(requested) {
@@ -74562,20 +74623,6 @@ var require_sqlite_settings_addon = __commonJS({
74562
74623
  function isImposedBound(source) {
74563
74624
  return source !== "caller";
74564
74625
  }
74565
- var MAX_ASCII = 127;
74566
- function prefixRange(prefix) {
74567
- if (prefix.length === 0) return null;
74568
- for (let i = 0; i < prefix.length; i++) {
74569
- const code = prefix.charCodeAt(i);
74570
- if (code === 0 || code > MAX_ASCII) return null;
74571
- }
74572
- const last = prefix.charCodeAt(prefix.length - 1);
74573
- if (last >= MAX_ASCII) return null;
74574
- return {
74575
- lo: prefix,
74576
- hi: prefix.slice(0, -1) + String.fromCharCode(last + 1)
74577
- };
74578
- }
74579
74626
  function parseRowData(raw) {
74580
74627
  return require_dist10.asJsonObject(require_dist10.parseJsonUnknown(raw)) ?? {};
74581
74628
  }
@@ -74958,10 +75005,33 @@ var require_sqlite_settings_addon = __commonJS({
74958
75005
  const rows = this.getDb().prepare('SELECT id, data FROM "system-settings"').all();
74959
75006
  return Object.fromEntries(rows.map((r) => [r.id, JSON.parse(r.data)]));
74960
75007
  }
74961
- /** Get all settings for an addon */
75008
+ /**
75009
+ * Get all settings for an addon.
75010
+ *
75011
+ * Selected by the SAME key range {@link setAllAddon} deletes and re-inserts
75012
+ * — `prefixWhere("<addonId>.")` — and for the same reason `getAllScoped`
75013
+ * uses it: `"<addonId>.<key>"` is the PRIMARY KEY, so the range is a
75014
+ * `SEARCH … USING INDEX` while anything else is a full `SCAN`.
75015
+ *
75016
+ * It used to select on `json_extract(data, '$.addonId')`, which is a second
75017
+ * authority for "this addon's rows" and disagreed with the writer in both
75018
+ * directions: a row inside the JSON's idea of the addon but outside the key
75019
+ * range was READ and never DELETED (a config key that could not be cleared),
75020
+ * and `json_extract` was evaluated on EVERY row — so a four-key addon paid
75021
+ * for the whole table and one unparseable neighbour aborted the statement,
75022
+ * taking out every addon's config read at once.
75023
+ *
75024
+ * The cost was not theoretical. On the live hub `addon-settings` is 627 KB
75025
+ * in 24 rows, 625 KB of it device-manager's three fleet blobs; a V8 profile
75026
+ * of a boot (2026-08-19) had hub-main's JS thread 99.9% busy with **64% of
75027
+ * it inside this method**, which is what put a ~45 s queue in front of every
75028
+ * runner's first store read. Measured on that table: 0.947 ms → ~0.02 ms for
75029
+ * a four-key addon, 2.90 ms → 2.13 ms for device-manager's own.
75030
+ */
74962
75031
  getAllAddon(addonId) {
74963
75032
  this.requireDeclared("addon-settings");
74964
- const rows = this.getDb().prepare(`SELECT id, data FROM "addon-settings" WHERE json_extract(data, '$.addonId') = ?`).all(addonId);
75033
+ const where = this.prefixWhere(`${addonId}.`);
75034
+ const rows = this.getDb().prepare(`SELECT id, data FROM "addon-settings" WHERE ${where.sql}`).all(...where.params);
74965
75035
  if (rows.length === 0) return {};
74966
75036
  const result = {};
74967
75037
  for (const row of rows) {
@@ -401908,6 +401978,58 @@ var require_runner_spawn_fanout = __commonJS({
401908
401978
  }
401909
401979
  });
401910
401980
 
401981
+ // ../../server/backend/dist/core/addon/single-flight-refresh.js
401982
+ var require_single_flight_refresh = __commonJS({
401983
+ "../../server/backend/dist/core/addon/single-flight-refresh.js"(exports) {
401984
+ "use strict";
401985
+ Object.defineProperty(exports, "__esModule", { value: true });
401986
+ exports.createSingleFlightRefresh = createSingleFlightRefresh;
401987
+ function createSingleFlightRefresh(run) {
401988
+ let inFlight = null;
401989
+ let pending = false;
401990
+ let pendingPromise = null;
401991
+ let releasePending = () => {
401992
+ };
401993
+ const start = () => {
401994
+ const active = (async () => {
401995
+ try {
401996
+ await run();
401997
+ } finally {
401998
+ inFlight = null;
401999
+ if (pending) {
402000
+ pending = false;
402001
+ const release = releasePending;
402002
+ releasePending = () => {
402003
+ };
402004
+ pendingPromise = null;
402005
+ void start().finally(release);
402006
+ }
402007
+ }
402008
+ })();
402009
+ inFlight = active;
402010
+ return active;
402011
+ };
402012
+ return {
402013
+ request: async () => {
402014
+ if (inFlight === null) {
402015
+ await start();
402016
+ return;
402017
+ }
402018
+ if (!pending) {
402019
+ pending = true;
402020
+ pendingPromise = new Promise((resolve) => {
402021
+ releasePending = resolve;
402022
+ });
402023
+ }
402024
+ const wait = pendingPromise;
402025
+ if (wait !== null)
402026
+ await wait;
402027
+ }
402028
+ };
402029
+ }
402030
+ }
402031
+ });
402032
+
401911
402033
  // ../../server/backend/dist/core/addon/addon-registry.service.js
401912
402034
  var require_addon_registry_service = __commonJS({
401913
402035
  "../../server/backend/dist/core/addon/addon-registry.service.js"(exports) {
@@ -401970,6 +402092,7 @@ var require_addon_registry_service = __commonJS({
401970
402092
  var require_cache_js_1 = require_require_cache();
401971
402093
  var runner_convergence_1 = require_runner_convergence();
401972
402094
  var runner_spawn_fanout_js_1 = require_runner_spawn_fanout();
402095
+ var single_flight_refresh_js_1 = require_single_flight_refresh();
401973
402096
  function shouldEvictMissingOnDisk(entry, id, onDiskIds) {
401974
402097
  return entry.source === "installed" && entry.packageName !== "@camstack/system" && !onDiskIds.has(id);
401975
402098
  }
@@ -402765,11 +402888,18 @@ var require_addon_registry_service = __commonJS({
402765
402888
  }
402766
402889
  return out;
402767
402890
  };
402891
+ /** One full-fleet rebuild in flight, at most one queued behind it. Every
402892
+ * event below asks for the SAME total rebuild, so running it once per event
402893
+ * was ~1 000 rebuilds of a 974-row mirror on a boot — each one a 625 KB read
402894
+ * and parse on hub-main's event loop. No timer: the first event still
402895
+ * refreshes immediately, because this mirror backs scope enforcement.
402896
+ * See `single-flight-refresh.ts`. */
402897
+ deviceMirrorGate = (0, single_flight_refresh_js_1.createSingleFlightRefresh)(() => this.refreshDeviceParentMirror());
402768
402898
  /** Subscribe the mirror to every device-meta lifecycle event, and warm it once
402769
402899
  * the addon set (device-manager included) is up. */
402770
402900
  wireDeviceParentMirror() {
402771
402901
  const refresh = () => {
402772
- void this.refreshDeviceParentMirror();
402902
+ void this.deviceMirrorGate.request();
402773
402903
  };
402774
402904
  for (const category of [
402775
402905
  types_1.EventCategory.DeviceMetaChanged,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "camstack",
3
- "version": "1.2.34",
3
+ "version": "1.2.35",
4
4
  "description": "CLI tool for managing and running CamStack server",
5
5
  "keywords": [
6
6
  "camstack",