@camstack/system 1.2.232 → 1.2.234

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/addon-utils.js +1 -1
  4. package/dist/addon-utils.mjs +1 -1
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  9. package/dist/builtins/alerts/alerts.addon.js +1 -1
  10. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  13. package/dist/builtins/console-logging/index.js +1 -1
  14. package/dist/builtins/console-logging/index.mjs +1 -1
  15. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  16. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  17. package/dist/builtins/device-manager/contribution-cache.d.ts +68 -0
  18. package/dist/builtins/device-manager/device-manager.addon.js +175 -39
  19. package/dist/builtins/device-manager/device-manager.addon.mjs +176 -40
  20. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  21. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  22. package/dist/builtins/hub-forwarder/index.js +1 -1
  23. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  24. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  25. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  26. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  27. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  28. package/dist/builtins/local-network/local-network.addon.js +1 -1
  29. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  30. package/dist/builtins/loki-logging/index.js +1 -1
  31. package/dist/builtins/loki-logging/index.mjs +1 -1
  32. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  33. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  34. package/dist/builtins/platform-probe/index.js +1 -1
  35. package/dist/builtins/platform-probe/index.mjs +1 -1
  36. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  37. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  38. package/dist/builtins/snapshot/index.js +1 -1
  39. package/dist/builtins/snapshot/index.mjs +1 -1
  40. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  41. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  42. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  43. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  44. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  45. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  46. package/dist/builtins/system-config/system-config.addon.js +1 -1
  47. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  48. package/dist/builtins/winston-logging/index.js +1 -1
  49. package/dist/builtins/winston-logging/index.mjs +1 -1
  50. package/dist/{custom-action-registry-gaO1AlT2.mjs → custom-action-registry-CVsXD56p.mjs} +1 -1
  51. package/dist/{custom-action-registry-YHpAdksL.js → custom-action-registry-DG-QIWxd.js} +1 -1
  52. package/dist/{dist-CEIkHEdy.js → dist-DaSCLJVH.js} +114 -10
  53. package/dist/{dist-BX6SpI0i.mjs → dist-at9PGh6X.mjs} +114 -10
  54. package/dist/index.js +2 -2
  55. package/dist/index.mjs +2 -2
  56. package/dist/{retired-settings-keys-FLZlwa2U.js → retired-settings-keys-CZISf623.js} +1 -1
  57. package/dist/{retired-settings-keys-DDV06eUr.mjs → retired-settings-keys-X5zRxcV9.mjs} +1 -1
  58. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  const require_chunk = require("./chunk-Cek0wNdY.js");
2
2
  const require_manifest_system_deps = require("./manifest-system-deps-CrSP0Kkp.js");
3
- const require_custom_action_registry = require("./custom-action-registry-YHpAdksL.js");
3
+ const require_custom_action_registry = require("./custom-action-registry-DG-QIWxd.js");
4
4
  let node_path = require("node:path");
5
5
  node_path = require_chunk.__toESM(node_path);
6
6
  let node_fs = require("node:fs");
@@ -1,5 +1,5 @@
1
1
  import { A as createUdsLoggerWithControl, It as RUNNER_RSS_BUDGET_ENV, L as LocalChildClient, Yt as startRunnerHeapWatch, a as createUdsAddonContext, bt as resolveAddonClass, it as setWorkerNativeCapsChangeListener, n as installManifestPythonDeps, nt as getWorkerNativeCapSnapshot, ot as validateProviderRegistrations, sn as emitHeapDiagnosticReport, t as installManifestSystemDeps, tt as getWorkerNativeCapProvider, yt as installManifestNativeDeps } from "./manifest-system-deps-BEj9Dpu1.mjs";
2
- import { t as CustomActionRegistry } from "./custom-action-registry-gaO1AlT2.mjs";
2
+ import { t as CustomActionRegistry } from "./custom-action-registry-CVsXD56p.mjs";
3
3
  import { register } from "node:module";
4
4
  import * as path$1 from "node:path";
5
5
  import * as fs from "node:fs";
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  require("./chunk-Cek0wNdY.js");
3
3
  const require_event_loop_stall_monitor = require("./event-loop-stall-monitor-CAsHdMwP.js");
4
- const require_custom_action_registry = require("./custom-action-registry-YHpAdksL.js");
4
+ const require_custom_action_registry = require("./custom-action-registry-DG-QIWxd.js");
5
5
  const require_snapshot_courtesy = require("./snapshot-courtesy-oeTWCXPv.js");
6
6
  exports.COURTESY_DEFAULT_WIDTH = require_snapshot_courtesy.COURTESY_DEFAULT_WIDTH;
7
7
  exports.CustomActionRegistry = require_custom_action_registry.CustomActionRegistry;
@@ -1,4 +1,4 @@
1
1
  import { a as createFileDataPlaneHandler, c as parseRangeHeader, d as ModelDownloadService, f as collectModelFiles, g as ensureModel, i as startEventLoopStallMonitor, m as downloadFile, n as STALL_TICK_MS, o as contentTypeFor, p as deleteModelFromDisk, r as attributeStall, t as STALL_REPORT_MS, v as getModelFilePath, y as isModelDownloaded } from "./event-loop-stall-monitor-GPrVvx14.mjs";
2
- import { t as CustomActionRegistry } from "./custom-action-registry-gaO1AlT2.mjs";
2
+ import { t as CustomActionRegistry } from "./custom-action-registry-CVsXD56p.mjs";
3
3
  import { i as renderCourtesyOverlayJpeg, n as courtesyCacheKey, r as renderCourtesyJpeg, t as COURTESY_DEFAULT_WIDTH } from "./snapshot-courtesy-LoJNgHHZ.mjs";
4
4
  export { COURTESY_DEFAULT_WIDTH, CustomActionRegistry, ModelDownloadService, STALL_REPORT_MS, STALL_TICK_MS, attributeStall, collectModelFiles, contentTypeFor, courtesyCacheKey, createFileDataPlaneHandler, deleteModelFromDisk, downloadFile, ensureModel, getModelFilePath, isModelDownloaded, parseRangeHeader, renderCourtesyJpeg, renderCourtesyOverlayJpeg, startEventLoopStallMonitor };
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CEIkHEdy.js");
6
+ const require_dist = require("../../dist-DaSCLJVH.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Jt as errMsg, Yt as BaseAddon, k as addonPagesCapability, xn as EventCategory } from "../../dist-BX6SpI0i.mjs";
1
+ import { Jt as errMsg, Yt as BaseAddon, k as addonPagesCapability, xn as EventCategory } from "../../dist-at9PGh6X.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CEIkHEdy.js");
6
+ const require_dist = require("../../dist-DaSCLJVH.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Jt as errMsg, Yt as BaseAddon, j as addonWidgetsCapability, xn as EventCategory } from "../../dist-BX6SpI0i.mjs";
1
+ import { Jt as errMsg, Yt as BaseAddon, j as addonWidgetsCapability, xn as EventCategory } from "../../dist-at9PGh6X.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import * as path$1 from "node:path";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CEIkHEdy.js");
6
+ const require_dist = require("../../dist-DaSCLJVH.js");
7
7
  //#region src/builtins/alerts/prune-policy.ts
8
8
  /**
9
9
  * Ids older than `cutoffMs`, oldest first.
@@ -1,4 +1,4 @@
1
- import { Jt as errMsg, M as alertsCapability, Yt as BaseAddon, cn as createEvent, n as AlertSchema, xn as EventCategory } from "../../dist-BX6SpI0i.mjs";
1
+ import { Jt as errMsg, M as alertsCapability, Yt as BaseAddon, cn as createEvent, n as AlertSchema, xn as EventCategory } from "../../dist-at9PGh6X.mjs";
2
2
  //#region src/builtins/alerts/prune-policy.ts
3
3
  /**
4
4
  * Ids older than `cutoffMs`, oldest first.
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  const require_chunk = require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CEIkHEdy.js");
6
+ const require_dist = require("../../dist-DaSCLJVH.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let buffer = require("buffer");
9
9
  let node_fs_promises = require("node:fs/promises");
@@ -1,4 +1,4 @@
1
- import { P as backupCapability, Vt as storageOccupancyCapability, Yt as BaseAddon, bt as mayWriteToLocation, xn as EventCategory } from "../../dist-BX6SpI0i.mjs";
1
+ import { P as backupCapability, Vt as storageOccupancyCapability, Yt as BaseAddon, bt as mayWriteToLocation, xn as EventCategory } from "../../dist-at9PGh6X.mjs";
2
2
  import { randomBytes, randomUUID } from "node:crypto";
3
3
  import { Buffer as Buffer$1 } from "buffer";
4
4
  import * as fsp from "node:fs/promises";
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CEIkHEdy.js");
6
+ const require_dist = require("../../dist-DaSCLJVH.js");
7
7
  const require_formatter = require("../../formatter-DqAKDlvN.js");
8
8
  //#region src/builtins/console-logging/console-destination.ts
9
9
  var LEVEL_RANK = {
@@ -1,4 +1,4 @@
1
- import { Yt as BaseAddon, gt as logDestinationCapability } from "../../dist-BX6SpI0i.mjs";
1
+ import { Yt as BaseAddon, gt as logDestinationCapability } from "../../dist-at9PGh6X.mjs";
2
2
  import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
3
3
  //#region src/builtins/console-logging/console-destination.ts
4
4
  var LEVEL_RANK = {
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-CEIkHEdy.js");
3
+ const require_dist = require("../../dist-DaSCLJVH.js");
4
4
  let node_crypto = require("node:crypto");
5
5
  let node_fs_promises = require("node:fs/promises");
6
6
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
@@ -1,4 +1,4 @@
1
- import { B as coreBlockIdFromAddonId, Jt as errMsg, V as coreBlocksCapability, Yt as BaseAddon, c as CORE_BLOCK_ADDON_PREFIX, d as DeclaredDevices, l as CoreBlockSchema, s as CORE_BLOCKS_ADDON_ID, z as coreBlockAddonId } from "../../dist-BX6SpI0i.mjs";
1
+ import { B as coreBlockIdFromAddonId, Jt as errMsg, V as coreBlocksCapability, Yt as BaseAddon, c as CORE_BLOCK_ADDON_PREFIX, d as DeclaredDevices, l as CoreBlockSchema, s as CORE_BLOCKS_ADDON_ID, z as coreBlockAddonId } from "../../dist-at9PGh6X.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path$1 from "node:path";
@@ -0,0 +1,68 @@
1
+ /**
2
+ * The device-details fan-out, made survivable: budgeted, cached, single-flight.
3
+ *
4
+ * ## What it is for
5
+ *
6
+ * `getDeviceAggregate` asks every contributing provider for this device's
7
+ * settings panel and waits for all of them. Measured on this hub (2026-09-14):
8
+ * 10 595 ms on a battery camera against 331 ms on a mains one, while the page
9
+ * re-polls every 2 500 ms. One slow contributor stalled the whole device page,
10
+ * the poll stacked a fresh 10 s call on top of the one still running, and not
11
+ * one line named which provider was holding it.
12
+ *
13
+ * ## Why a budget alone would have been worse
14
+ *
15
+ * Dropping the provider that overran makes its settings SECTION disappear and
16
+ * come back between polls. A section of settings that flickers is worse than a
17
+ * slow one — and it is also what moved the operator's page: the schema loses a
18
+ * tab for a beat, and everything downstream reacts to a tab that is not there.
19
+ *
20
+ * So the budget is paired with the last answer that provider gave. A slow
21
+ * provider costs the page its budget ONCE; after that the page is instant and
22
+ * shows what that provider last said, until it says something new.
23
+ *
24
+ * ## The rules
25
+ *
26
+ * - **A read never joins a refresh already in flight** (D224). Without this the
27
+ * 2.5 s poll starts a new 10 s call every tick and they pile up.
28
+ * - **An explicit `null` is an ANSWER**, not a miss: it means "I have nothing
29
+ * for this device", and it replaces what was cached. A cache that only ever
30
+ * remembers successes would serve a stale panel around a provider that has
31
+ * since disowned the device.
32
+ * - **A failure falls back**, but a failure with nothing behind it is ABSENT
33
+ * and says which of the two it was — "never answered" and "answered once and
34
+ * is now broken" are different operator problems.
35
+ */
36
+ /** How long the page waits for ONE provider before serving what it has. */
37
+ export declare const CONTRIBUTION_BUDGET_MS = 1500;
38
+ /**
39
+ * Entries held across all devices. A device page touches a dozen or so, and an
40
+ * operator walks a fleet — bounded so a long session cannot grow it without
41
+ * limit. Oldest-inserted is evicted first; a re-read of an evicted key simply
42
+ * pays the budget again.
43
+ */
44
+ export declare const CONTRIBUTION_CACHE_MAX_ENTRIES = 512;
45
+ /** How a single provider's contribution was resolved. */
46
+ export type ContributionOutcome<T> = {
47
+ readonly kind: 'fresh';
48
+ readonly value: T | null;
49
+ } | {
50
+ readonly kind: 'stale';
51
+ readonly value: T;
52
+ } | {
53
+ readonly kind: 'absent';
54
+ readonly reason: 'slow' | 'failed';
55
+ };
56
+ export declare class ContributionCache<T = unknown> {
57
+ private readonly maxEntries;
58
+ private readonly entries;
59
+ constructor(maxEntries?: number);
60
+ /**
61
+ * Resolve one provider's contribution within `budgetMs`.
62
+ *
63
+ * The call is never cancelled: it keeps running and its answer refreshes the
64
+ * cache, so the provider that overran this page pays for the next one.
65
+ */
66
+ resolve(key: string, call: () => Promise<T | null>, budgetMs?: number): Promise<ContributionOutcome<T>>;
67
+ private remember;
68
+ }
@@ -3,8 +3,8 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-CEIkHEdy.js");
7
- const require_retired_settings_keys = require("../../retired-settings-keys-FLZlwa2U.js");
6
+ const require_dist = require("../../dist-DaSCLJVH.js");
7
+ const require_retired_settings_keys = require("../../retired-settings-keys-CZISf623.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let _camstack_types_node = require("@camstack/types/node");
10
10
  //#region src/builtins/device-manager/adoption-job-engine.ts
@@ -295,6 +295,113 @@ var AdoptionJobEngine = class {
295
295
  }
296
296
  };
297
297
  //#endregion
298
+ //#region src/builtins/device-manager/contribution-cache.ts
299
+ /**
300
+ * The device-details fan-out, made survivable: budgeted, cached, single-flight.
301
+ *
302
+ * ## What it is for
303
+ *
304
+ * `getDeviceAggregate` asks every contributing provider for this device's
305
+ * settings panel and waits for all of them. Measured on this hub (2026-09-14):
306
+ * 10 595 ms on a battery camera against 331 ms on a mains one, while the page
307
+ * re-polls every 2 500 ms. One slow contributor stalled the whole device page,
308
+ * the poll stacked a fresh 10 s call on top of the one still running, and not
309
+ * one line named which provider was holding it.
310
+ *
311
+ * ## Why a budget alone would have been worse
312
+ *
313
+ * Dropping the provider that overran makes its settings SECTION disappear and
314
+ * come back between polls. A section of settings that flickers is worse than a
315
+ * slow one — and it is also what moved the operator's page: the schema loses a
316
+ * tab for a beat, and everything downstream reacts to a tab that is not there.
317
+ *
318
+ * So the budget is paired with the last answer that provider gave. A slow
319
+ * provider costs the page its budget ONCE; after that the page is instant and
320
+ * shows what that provider last said, until it says something new.
321
+ *
322
+ * ## The rules
323
+ *
324
+ * - **A read never joins a refresh already in flight** (D224). Without this the
325
+ * 2.5 s poll starts a new 10 s call every tick and they pile up.
326
+ * - **An explicit `null` is an ANSWER**, not a miss: it means "I have nothing
327
+ * for this device", and it replaces what was cached. A cache that only ever
328
+ * remembers successes would serve a stale panel around a provider that has
329
+ * since disowned the device.
330
+ * - **A failure falls back**, but a failure with nothing behind it is ABSENT
331
+ * and says which of the two it was — "never answered" and "answered once and
332
+ * is now broken" are different operator problems.
333
+ */
334
+ /** How long the page waits for ONE provider before serving what it has. */
335
+ var CONTRIBUTION_BUDGET_MS = 1500;
336
+ var ContributionCache = class {
337
+ maxEntries;
338
+ entries = /* @__PURE__ */ new Map();
339
+ constructor(maxEntries = 512) {
340
+ this.maxEntries = maxEntries;
341
+ }
342
+ /**
343
+ * Resolve one provider's contribution within `budgetMs`.
344
+ *
345
+ * The call is never cancelled: it keeps running and its answer refreshes the
346
+ * cache, so the provider that overran this page pays for the next one.
347
+ */
348
+ async resolve(key, call, budgetMs = CONTRIBUTION_BUDGET_MS) {
349
+ const existing = this.entries.get(key);
350
+ if (existing?.inFlight) return existing.value !== null ? {
351
+ kind: "stale",
352
+ value: existing.value
353
+ } : {
354
+ kind: "absent",
355
+ reason: "slow"
356
+ };
357
+ const entry = existing ?? {
358
+ value: null,
359
+ inFlight: null
360
+ };
361
+ if (!existing) this.remember(key, entry);
362
+ let settled = false;
363
+ let answer = null;
364
+ let failed = false;
365
+ const refresh = call().then((value) => {
366
+ answer = value;
367
+ settled = true;
368
+ entry.value = value;
369
+ entry.inFlight = null;
370
+ }, () => {
371
+ failed = true;
372
+ settled = true;
373
+ entry.inFlight = null;
374
+ });
375
+ entry.inFlight = refresh;
376
+ await Promise.race([refresh, sleep(budgetMs)]);
377
+ if (settled && !failed) return {
378
+ kind: "fresh",
379
+ value: answer
380
+ };
381
+ if (entry.value !== null) return {
382
+ kind: "stale",
383
+ value: entry.value
384
+ };
385
+ return {
386
+ kind: "absent",
387
+ reason: failed ? "failed" : "slow"
388
+ };
389
+ }
390
+ remember(key, entry) {
391
+ this.entries.set(key, entry);
392
+ while (this.entries.size > this.maxEntries) {
393
+ const oldest = this.entries.keys().next();
394
+ if (oldest.done === true) break;
395
+ this.entries.delete(oldest.value);
396
+ }
397
+ }
398
+ };
399
+ function sleep(ms) {
400
+ return new Promise((resolve) => {
401
+ setTimeout(resolve, ms).unref?.();
402
+ });
403
+ }
404
+ //#endregion
298
405
  //#region src/builtins/device-manager/device-aggregation-merge.ts
299
406
  /**
300
407
  * Pure aggregator-merge + field-tagging helpers for the device-manager
@@ -2438,6 +2545,52 @@ function splitDeviceStoreKeys(patch) {
2438
2545
  * and side effects of the original class methods are preserved; the addon keeps
2439
2546
  * thin delegator methods over these functions.
2440
2547
  */
2548
+ /**
2549
+ * One process-wide cache behind every device-details fan-out.
2550
+ *
2551
+ * Module-level on purpose: the page re-polls every 2.5 s and a per-call cache
2552
+ * would be empty every time, which is the same as having none.
2553
+ */
2554
+ var contributionCache = new ContributionCache();
2555
+ /**
2556
+ * Ask ONE provider for its contribution, bounded, and say what happened.
2557
+ *
2558
+ * The two non-fresh outcomes are logged because they are the two questions an
2559
+ * operator asks of a slow device page, and neither had an answer before: WHICH
2560
+ * provider is slow, and whether the panel in front of them is current.
2561
+ */
2562
+ async function resolveContribution(deps, input) {
2563
+ const { deviceId, kind, capName, addonId } = input;
2564
+ const outcome = await contributionCache.resolve(`${String(deviceId)}:${kind}:${capName}:${addonId}`, input.call);
2565
+ if (outcome.kind === "fresh") return outcome.value === null ? null : tagContribution(outcome.value, capName, addonId, kind);
2566
+ if (outcome.kind === "stale") {
2567
+ deps.ctx.logger.info("device contribution served from the last answer — the provider is slow", {
2568
+ tags: {
2569
+ deviceId,
2570
+ addonId
2571
+ },
2572
+ meta: {
2573
+ capName,
2574
+ method: kind,
2575
+ budgetMs: CONTRIBUTION_BUDGET_MS
2576
+ }
2577
+ });
2578
+ return tagContribution(outcome.value, capName, addonId, kind);
2579
+ }
2580
+ deps.ctx.logger.warn("device contribution unavailable — the panel is missing this section", {
2581
+ tags: {
2582
+ deviceId,
2583
+ addonId
2584
+ },
2585
+ meta: {
2586
+ capName,
2587
+ method: kind,
2588
+ reason: outcome.reason,
2589
+ budgetMs: CONTRIBUTION_BUDGET_MS
2590
+ }
2591
+ });
2592
+ return null;
2593
+ }
2441
2594
  async function getDeviceAggregate(deps, deviceId, kind) {
2442
2595
  const registry = deps.capabilityRegistry;
2443
2596
  if (!registry) {
@@ -2453,25 +2606,16 @@ async function getDeviceAggregate(deps, deviceId, kind) {
2453
2606
  if (!def?.exposesDeviceSettings) return null;
2454
2607
  const provider = registry.getProviderByAddon(entry.capName, entry.providerAddonId);
2455
2608
  if (!provider) return null;
2456
- try {
2457
- const contribution = await provider[method]({ deviceId });
2458
- if (!contribution) return null;
2459
- return tagContribution(toWireShape(contribution), entry.capName, entry.providerAddonId, kind);
2460
- } catch (err) {
2461
- const msg = err instanceof Error ? err.message : String(err);
2462
- deps.ctx.logger.warn("contribution method failed", {
2463
- tags: {
2464
- deviceId,
2465
- addonId: entry.providerAddonId
2466
- },
2467
- meta: {
2468
- capName: entry.capName,
2469
- method,
2470
- error: msg
2471
- }
2472
- });
2473
- return null;
2474
- }
2609
+ return await resolveContribution(deps, {
2610
+ deviceId,
2611
+ kind,
2612
+ capName: entry.capName,
2613
+ addonId: entry.providerAddonId,
2614
+ call: async () => {
2615
+ const contribution = await provider[method]({ deviceId });
2616
+ return contribution ? toWireShape(contribution) : null;
2617
+ }
2618
+ });
2475
2619
  }));
2476
2620
  const base = kind === "settings" ? await buildBaseDeviceSection(deps, deviceId) : null;
2477
2621
  const systemResults = await collectSystemDeviceContributions(deps, deviceId, kind, seenCaps);
@@ -2521,24 +2665,16 @@ async function collectSystemDeviceContributions(deps, deviceId, kind, seenCaps)
2521
2665
  for (const addonId of addonIds) tasks.push((async () => {
2522
2666
  const provider = registry.getProviderByAddon(info.name, addonId);
2523
2667
  if (!provider) return null;
2524
- try {
2525
- const contribution = await provider[method]({ deviceId });
2526
- if (!contribution) return null;
2527
- return tagContribution(toWireShape(contribution), info.name, addonId, kind);
2528
- } catch (err) {
2529
- deps.ctx.logger.warn("system device-settings contribution skipped", {
2530
- tags: {
2531
- deviceId,
2532
- addonId
2533
- },
2534
- meta: {
2535
- capName: info.name,
2536
- method,
2537
- error: err instanceof Error ? err.message : String(err)
2538
- }
2539
- });
2540
- return null;
2541
- }
2668
+ return await resolveContribution(deps, {
2669
+ deviceId,
2670
+ kind,
2671
+ capName: info.name,
2672
+ addonId,
2673
+ call: async () => {
2674
+ const contribution = await provider[method]({ deviceId });
2675
+ return contribution ? toWireShape(contribution) : null;
2676
+ }
2677
+ });
2542
2678
  })());
2543
2679
  }
2544
2680
  return (await Promise.all(tasks)).filter((r) => r !== null);