camstack 1.2.33 → 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-LXP37XH2.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-LXP37XH2.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) {
@@ -396909,7 +396979,7 @@ var require_trpc_context = __commonJS({
396909
396979
  }
396910
396980
  return null;
396911
396981
  }
396912
- async function resolveUser(token2, authService, addonRegistry, shareTokens = null) {
396982
+ async function resolveUser(token2, authService, addonRegistry, shareTokens = null, logger = null) {
396913
396983
  if (!token2)
396914
396984
  return null;
396915
396985
  if (token2.startsWith(share_token_service_js_1.SHARE_TOKEN_PREFIX)) {
@@ -396971,8 +397041,14 @@ var require_trpc_context = __commonJS({
396971
397041
  if (typeof payload.isAdmin !== "boolean") {
396972
397042
  return null;
396973
397043
  }
396974
- const credential = (0, trpc_error_principal_js_1.readCredentialIdentity)(payload);
396975
397044
  const principal = (0, types_1.classifyBearerPrincipal)(payload);
397045
+ if (principal.kind === "not-a-credential") {
397046
+ logger?.warn("trpc: refused a bearer that is not an API credential", {
397047
+ meta: { reason: principal.reason, userId: payload.userId ?? payload.keyId ?? "unknown" }
397048
+ });
397049
+ return null;
397050
+ }
397051
+ const credential = (0, trpc_error_principal_js_1.readCredentialIdentity)(payload);
396976
397052
  return {
396977
397053
  id: payload.userId ?? payload.keyId ?? "unknown",
396978
397054
  username: payload.username ?? "unknown",
@@ -397035,19 +397111,19 @@ var require_trpc_context = __commonJS({
397035
397111
  };
397036
397112
  return { user };
397037
397113
  }
397038
- async function createTrpcContext(req, authService, addonRegistry, shareTokens = null) {
397114
+ async function createTrpcContext(req, authService, addonRegistry, shareTokens = null, logger = null) {
397039
397115
  const token2 = extractTokenFromRequest(req);
397040
397116
  return {
397041
- user: await resolveUser(token2, authService, addonRegistry, shareTokens),
397117
+ user: await resolveUser(token2, authService, addonRegistry, shareTokens, logger),
397042
397118
  req,
397043
397119
  deviceScopeLookup: makeDeviceScopeLookup(addonRegistry),
397044
397120
  deviceFleet: () => addonRegistry.getPersistedDeviceList()
397045
397121
  };
397046
397122
  }
397047
- async function createWsTrpcContext(opts, authService, addonRegistry, shareTokens = null) {
397123
+ async function createWsTrpcContext(opts, authService, addonRegistry, shareTokens = null, logger = null) {
397048
397124
  const paramToken = opts.info.connectionParams?.["token"];
397049
397125
  const token2 = (typeof paramToken === "string" ? paramToken : null) ?? extractTokenFromRequest(opts.req);
397050
- const user = await resolveUser(token2, authService, addonRegistry, shareTokens);
397126
+ const user = await resolveUser(token2, authService, addonRegistry, shareTokens, logger);
397051
397127
  return {
397052
397128
  user,
397053
397129
  req: opts.req,
@@ -401902,6 +401978,58 @@ var require_runner_spawn_fanout = __commonJS({
401902
401978
  }
401903
401979
  });
401904
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
+
401905
402033
  // ../../server/backend/dist/core/addon/addon-registry.service.js
401906
402034
  var require_addon_registry_service = __commonJS({
401907
402035
  "../../server/backend/dist/core/addon/addon-registry.service.js"(exports) {
@@ -401964,6 +402092,7 @@ var require_addon_registry_service = __commonJS({
401964
402092
  var require_cache_js_1 = require_require_cache();
401965
402093
  var runner_convergence_1 = require_runner_convergence();
401966
402094
  var runner_spawn_fanout_js_1 = require_runner_spawn_fanout();
402095
+ var single_flight_refresh_js_1 = require_single_flight_refresh();
401967
402096
  function shouldEvictMissingOnDisk(entry, id, onDiskIds) {
401968
402097
  return entry.source === "installed" && entry.packageName !== "@camstack/system" && !onDiskIds.has(id);
401969
402098
  }
@@ -402759,11 +402888,18 @@ var require_addon_registry_service = __commonJS({
402759
402888
  }
402760
402889
  return out;
402761
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());
402762
402898
  /** Subscribe the mirror to every device-meta lifecycle event, and warm it once
402763
402899
  * the addon set (device-manager included) is up. */
402764
402900
  wireDeviceParentMirror() {
402765
402901
  const refresh = () => {
402766
- void this.refreshDeviceParentMirror();
402902
+ void this.deviceMirrorGate.request();
402767
402903
  };
402768
402904
  for (const category of [
402769
402905
  types_1.EventCategory.DeviceMetaChanged,
@@ -408917,7 +409053,16 @@ var require_main4 = __commonJS({
408917
409053
  prefix: "/trpc",
408918
409054
  trpcOptions: {
408919
409055
  router: appRouter,
408920
- createContext: ({ req }) => (0, trpc_context_1.createTrpcContext)(req, authService, addonRegistry, shareTokenService),
409056
+ createContext: ({ req }) => (0, trpc_context_1.createTrpcContext)(
409057
+ req,
409058
+ authService,
409059
+ addonRegistry,
409060
+ shareTokenService,
409061
+ // A bearer refused for NOT being an API credential never reaches a
409062
+ // procedure, so `onError` below never sees it — the refusal would
409063
+ // be silent without this. Same reason `gateAddonJwt` logs (D103).
409064
+ app.get(logging_service_1.LoggingService).createLogger("tRPC")
409065
+ ),
408921
409066
  onError: ({ path: trpcPath, error, ctx }) => {
408922
409067
  const trpcLogger = app.get(logging_service_1.LoggingService).createLogger("tRPC");
408923
409068
  trpcLogger.warn("tRPC error", {
@@ -409311,7 +409456,7 @@ var require_main4 = __commonJS({
409311
409456
  (0, ws_1.applyWSSHandler)({
409312
409457
  wss,
409313
409458
  router: appRouter,
409314
- createContext: (opts) => (0, trpc_context_1.createWsTrpcContext)(opts, authService, addonRegistry, shareTokenService),
409459
+ createContext: (opts) => (0, trpc_context_1.createWsTrpcContext)(opts, authService, addonRegistry, shareTokenService, app.get(logging_service_1.LoggingService).createLogger("tRPC:ws")),
409315
409460
  onError: ({ path: trpcPath, error, ctx }) => {
409316
409461
  const trpcLogger = app.get(logging_service_1.LoggingService).createLogger("tRPC:ws");
409317
409462
  trpcLogger.warn("tRPC error", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "camstack",
3
- "version": "1.2.33",
3
+ "version": "1.2.35",
4
4
  "description": "CLI tool for managing and running CamStack server",
5
5
  "keywords": [
6
6
  "camstack",