@camstack/system 1.2.67 → 1.2.69

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 (55) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  8. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  9. package/dist/builtins/console-logging/index.js +1 -1
  10. package/dist/builtins/console-logging/index.mjs +1 -1
  11. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  12. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  13. package/dist/builtins/device-manager/adoption-job-engine.d.ts +89 -0
  14. package/dist/builtins/device-manager/device-manager.addon.d.ts +8 -0
  15. package/dist/builtins/device-manager/device-manager.addon.js +329 -2
  16. package/dist/builtins/device-manager/device-manager.addon.mjs +329 -2
  17. package/dist/builtins/device-manager/device-provider-context.d.ts +3 -0
  18. package/dist/builtins/device-manager/device-queries.d.ts +3 -0
  19. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
  20. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
  21. package/dist/builtins/hub-forwarder/index.js +1 -1
  22. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  23. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  24. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  25. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  26. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  27. package/dist/builtins/local-network/local-network.addon.js +1 -1
  28. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  29. package/dist/builtins/loki-logging/index.js +1 -1
  30. package/dist/builtins/loki-logging/index.mjs +1 -1
  31. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  32. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  33. package/dist/builtins/platform-probe/index.js +1 -1
  34. package/dist/builtins/platform-probe/index.mjs +1 -1
  35. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  36. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  37. package/dist/builtins/snapshot/index.js +339 -2
  38. package/dist/builtins/snapshot/index.mjs +339 -2
  39. package/dist/builtins/snapshot/snapshot-link-url.d.ts +66 -0
  40. package/dist/builtins/snapshot/snapshot.addon.d.ts +79 -0
  41. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  42. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  43. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
  44. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
  45. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  46. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  47. package/dist/builtins/system-config/system-config.addon.js +1 -1
  48. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  49. package/dist/builtins/winston-logging/index.js +1 -1
  50. package/dist/builtins/winston-logging/index.mjs +1 -1
  51. package/dist/{dist-CbS6uoSb.mjs → dist-C-QzFJ8v.mjs} +313 -1
  52. package/dist/{dist-NRYjb8qK.js → dist-D7wC-S4p.js} +313 -1
  53. package/dist/index.js +1 -1
  54. package/dist/index.mjs +1 -1
  55. package/package.json +1 -1
@@ -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-NRYjb8qK.js");
6
+ const require_dist = require("../../dist-D7wC-S4p.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_fs = require("node:fs");
@@ -1,4 +1,4 @@
1
- import { _ as addonPagesCapability, ft as errMsg, pt as BaseAddon, zt as EventCategory } from "../../dist-CbS6uoSb.mjs";
1
+ import { _ as addonPagesCapability, ft as errMsg, pt as BaseAddon, zt as EventCategory } from "../../dist-C-QzFJ8v.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import * as fs from "node:fs";
@@ -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-NRYjb8qK.js");
6
+ const require_dist = require("../../dist-D7wC-S4p.js");
7
7
  const require_settle_sources = require("../../settle-sources-Bhsy57y-.js");
8
8
  let node_crypto = require("node:crypto");
9
9
  let node_fs = require("node:fs");
@@ -1,4 +1,4 @@
1
- import { ft as errMsg, pt as BaseAddon, y as addonWidgetsCapability, zt as EventCategory } from "../../dist-CbS6uoSb.mjs";
1
+ import { ft as errMsg, pt as BaseAddon, y as addonWidgetsCapability, zt as EventCategory } from "../../dist-C-QzFJ8v.mjs";
2
2
  import { t as settleSourcesWithTimeout } from "../../settle-sources-CDtNC8ub.mjs";
3
3
  import { createHash, randomUUID } from "node:crypto";
4
4
  import * as fs from "node:fs";
@@ -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-NRYjb8qK.js");
6
+ const require_dist = require("../../dist-D7wC-S4p.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 { Et as createEvent, b as alertsCapability, ft as errMsg, n as AlertSchema, pt as BaseAddon, zt as EventCategory } from "../../dist-CbS6uoSb.mjs";
1
+ import { Et as createEvent, b as alertsCapability, ft as errMsg, n as AlertSchema, pt as BaseAddon, zt as EventCategory } from "../../dist-C-QzFJ8v.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-NRYjb8qK.js");
6
+ const require_dist = require("../../dist-D7wC-S4p.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  let node_fs = require("node:fs");
9
9
  let node_fs$1 = require_chunk.__toESM(node_fs, 1);
@@ -1,4 +1,4 @@
1
- import { S as backupCapability, pt as BaseAddon, zt as EventCategory } from "../../dist-CbS6uoSb.mjs";
1
+ import { S as backupCapability, pt as BaseAddon, zt as EventCategory } from "../../dist-C-QzFJ8v.mjs";
2
2
  import { randomBytes, randomUUID } from "node:crypto";
3
3
  import * as fs from "node:fs";
4
4
  import Kt from "node:fs";
@@ -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-NRYjb8qK.js");
6
+ const require_dist = require("../../dist-D7wC-S4p.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 { pt as BaseAddon, q as logDestinationCapability } from "../../dist-CbS6uoSb.mjs";
1
+ import { pt as BaseAddon, q as logDestinationCapability } from "../../dist-C-QzFJ8v.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-NRYjb8qK.js");
3
+ const require_dist = require("../../dist-D7wC-S4p.js");
4
4
  let node_crypto = require("node:crypto");
5
5
  let node_path = require("node:path");
6
6
  node_path = require_chunk.__toESM(node_path);
@@ -1,4 +1,4 @@
1
- import { D as coreBlocksCapability, E as coreBlockIdFromAddonId, T as coreBlockAddonId, c as CoreBlockSchema, l as DeclaredDevices, o as CORE_BLOCKS_ADDON_ID, pt as BaseAddon, s as CORE_BLOCK_ADDON_PREFIX } from "../../dist-CbS6uoSb.mjs";
1
+ import { D as coreBlocksCapability, E as coreBlockIdFromAddonId, T as coreBlockAddonId, c as CoreBlockSchema, l as DeclaredDevices, o as CORE_BLOCKS_ADDON_ID, pt as BaseAddon, s as CORE_BLOCK_ADDON_PREFIX } from "../../dist-C-QzFJ8v.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import * as path$1 from "node:path";
4
4
  import * as fs from "node:fs/promises";
@@ -0,0 +1,89 @@
1
+ import { AdoptionJob, IScopedLogger } from '@camstack/types';
2
+ /**
3
+ * The slice of a `device-adoption` provider this engine drives. Structural on
4
+ * purpose: the real provider satisfies it, and a spec can exercise the whole
5
+ * loop without standing up a registry.
6
+ */
7
+ export interface AdoptionJobProvider {
8
+ listCandidates(input: {
9
+ integrationId: string;
10
+ page: number;
11
+ pageSize: number;
12
+ filter?: string;
13
+ }): Promise<{
14
+ readonly candidates: readonly {
15
+ readonly childNativeId: string;
16
+ readonly alreadyAdopted?: boolean;
17
+ }[];
18
+ }>;
19
+ adopt(input: {
20
+ integrationId: string;
21
+ childNativeIds: readonly string[];
22
+ filter?: string;
23
+ importLocations?: boolean;
24
+ perCandidate?: Record<string, {
25
+ name?: string;
26
+ hiddenChildIds?: string[];
27
+ }>;
28
+ }): Promise<{
29
+ readonly adopted: readonly {
30
+ readonly childNativeId: string;
31
+ readonly parentDeviceId: number;
32
+ readonly accessoryDeviceIds: readonly number[];
33
+ }[];
34
+ }>;
35
+ }
36
+ export interface AdoptionJobEngineDeps {
37
+ /** Resolve the integration's adoption provider for an `addonId` pin. */
38
+ resolveProvider(addonId: string): Promise<AdoptionJobProvider>;
39
+ readonly logger: IScopedLogger;
40
+ readonly now?: () => number;
41
+ readonly newId?: () => string;
42
+ }
43
+ export interface StartAdoptionJobInput {
44
+ readonly addonId: string;
45
+ readonly integrationId: string;
46
+ readonly childNativeIds: readonly string[];
47
+ readonly filter?: string;
48
+ readonly importLocations?: boolean;
49
+ readonly perCandidate?: Record<string, {
50
+ name?: string;
51
+ hiddenChildIds?: string[];
52
+ }>;
53
+ }
54
+ export declare class AdoptionJobEngine {
55
+ private readonly deps;
56
+ private readonly jobs;
57
+ private seq;
58
+ constructor(deps: AdoptionJobEngineDeps);
59
+ private now;
60
+ private newId;
61
+ /**
62
+ * Accept the batch and return immediately. The provider is resolved BEFORE
63
+ * returning so an unroutable `addonId` is refused at the call the operator
64
+ * made, not buried in a job they have to go looking for.
65
+ */
66
+ start(input: StartAdoptionJobInput): Promise<{
67
+ jobId: string;
68
+ }>;
69
+ get(jobId: string): AdoptionJob | null;
70
+ list(filter: {
71
+ addonId?: string;
72
+ integrationId?: string;
73
+ }): readonly AdoptionJob[];
74
+ /** Cooperative cancel — the in-flight candidate finishes, the rest do not start. */
75
+ cancel(jobId: string): boolean;
76
+ /** Test-only: await the detached loop. Never called by production code. */
77
+ waitForTest(jobId: string): Promise<void>;
78
+ private prune;
79
+ private run;
80
+ /**
81
+ * One `listCandidates` snapshot at job start. A read that FAILS must not be
82
+ * read as "nothing is adopted" — that would send every candidate to the
83
+ * provider and turn a safe retry back into a wall of duplicate-stableId
84
+ * errors — so a failure yields an empty set AND a warning, and the
85
+ * per-candidate failure path stays the backstop.
86
+ */
87
+ private alreadyAdopted;
88
+ private adoptOne;
89
+ }
@@ -48,6 +48,8 @@ export declare class DeviceManagerAddon extends BaseAddon {
48
48
  * the D3 re-handshake after device restore is the reliable replacement.
49
49
  */
50
50
  private remoteNativeCaps;
51
+ /** Background adoption jobs — lazily constructed (needs `ctx.logger`). */
52
+ private adoptionJobEngine;
51
53
  /** The runtime-state mirror, asserted present (constructed in onInitialize). */
52
54
  private get stateMirror();
53
55
  /** Build the `ProviderHost` the extracted provider-method modules
@@ -57,6 +59,12 @@ export declare class DeviceManagerAddon extends BaseAddon {
57
59
  * `remoteNativeCaps` is mutated in place (never reassigned) so a direct
58
60
  * reference preserves the live semantics. */
59
61
  private get providerHost();
62
+ /**
63
+ * Background adoption jobs. In-RAM and process-lifetime: a restart forgets
64
+ * them, which is safe because adoption is keyed by a stable id — re-running a
65
+ * job re-adopts nothing (see `AdoptionJobEngine`).
66
+ */
67
+ private get adoptionJobsImpl();
60
68
  /** Wait for a device-provider by addonId, returning null on timeout. */
61
69
  private waitDeviceProvider;
62
70
  /** Require a device-provider by addonId — throws if not found. */
@@ -3,9 +3,297 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-NRYjb8qK.js");
6
+ const require_dist = require("../../dist-D7wC-S4p.js");
7
7
  let _camstack_types_node = require("@camstack/types/node");
8
8
  let node_crypto = require("node:crypto");
9
+ //#region src/builtins/device-manager/adoption-job-engine.ts
10
+ /** How many finished jobs are kept per (addonId, integrationId). */
11
+ var JOB_HISTORY = 20;
12
+ function snapshot(j) {
13
+ let adopted = 0;
14
+ let alreadyAdopted = 0;
15
+ let failed = 0;
16
+ let processed = 0;
17
+ let accessoriesCreated = 0;
18
+ for (const r of j.results) {
19
+ if (r.outcome === "cancelled") continue;
20
+ processed += 1;
21
+ accessoriesCreated += r.accessoryCount;
22
+ if (r.outcome === "adopted") adopted += 1;
23
+ else if (r.outcome === "already-adopted") alreadyAdopted += 1;
24
+ else if (r.outcome === "failed") failed += 1;
25
+ }
26
+ return {
27
+ jobId: j.jobId,
28
+ addonId: j.addonId,
29
+ integrationId: j.integrationId,
30
+ state: j.state,
31
+ total: j.total,
32
+ processed,
33
+ adopted,
34
+ alreadyAdopted,
35
+ failed,
36
+ accessoriesCreated,
37
+ currentChildNativeId: j.currentChildNativeId,
38
+ results: j.results.map((r) => ({
39
+ childNativeId: r.childNativeId,
40
+ outcome: r.outcome,
41
+ parentDeviceId: r.parentDeviceId,
42
+ accessoryCount: r.accessoryCount,
43
+ error: r.error
44
+ })),
45
+ startedAt: j.startedAt,
46
+ finishedAt: j.finishedAt,
47
+ error: j.error
48
+ };
49
+ }
50
+ var AdoptionJobEngine = class {
51
+ deps;
52
+ jobs = /* @__PURE__ */ new Map();
53
+ seq = 0;
54
+ constructor(deps) {
55
+ this.deps = deps;
56
+ }
57
+ now() {
58
+ return this.deps.now?.() ?? Date.now();
59
+ }
60
+ newId() {
61
+ if (this.deps.newId) return this.deps.newId();
62
+ this.seq += 1;
63
+ return `adopt-${Date.now().toString(36)}-${this.seq}`;
64
+ }
65
+ /**
66
+ * Accept the batch and return immediately. The provider is resolved BEFORE
67
+ * returning so an unroutable `addonId` is refused at the call the operator
68
+ * made, not buried in a job they have to go looking for.
69
+ */
70
+ async start(input) {
71
+ const provider = await this.deps.resolveProvider(input.addonId);
72
+ const job = {
73
+ jobId: this.newId(),
74
+ addonId: input.addonId,
75
+ integrationId: input.integrationId,
76
+ state: "running",
77
+ total: input.childNativeIds.length,
78
+ currentChildNativeId: null,
79
+ results: [],
80
+ startedAt: this.now(),
81
+ finishedAt: null,
82
+ error: null,
83
+ cancelRequested: false,
84
+ done: Promise.resolve()
85
+ };
86
+ this.jobs.set(job.jobId, job);
87
+ this.prune(input.addonId, input.integrationId);
88
+ this.deps.logger.info("adoption job started", {
89
+ tags: { addonId: input.addonId },
90
+ meta: {
91
+ jobId: job.jobId,
92
+ integrationId: input.integrationId,
93
+ total: job.total
94
+ }
95
+ });
96
+ job.done = this.run(job, provider, input);
97
+ return { jobId: job.jobId };
98
+ }
99
+ get(jobId) {
100
+ const j = this.jobs.get(jobId);
101
+ return j ? snapshot(j) : null;
102
+ }
103
+ list(filter) {
104
+ return [...this.jobs.values()].filter((j) => (filter.addonId === void 0 || j.addonId === filter.addonId) && (filter.integrationId === void 0 || j.integrationId === filter.integrationId)).sort((a, b) => b.startedAt - a.startedAt || b.jobId.localeCompare(a.jobId)).map(snapshot);
105
+ }
106
+ /** Cooperative cancel — the in-flight candidate finishes, the rest do not start. */
107
+ cancel(jobId) {
108
+ const job = this.jobs.get(jobId);
109
+ if (!job || job.state !== "running") return false;
110
+ job.cancelRequested = true;
111
+ this.deps.logger.info("adoption job cancel requested", {
112
+ tags: { addonId: job.addonId },
113
+ meta: {
114
+ jobId,
115
+ integrationId: job.integrationId
116
+ }
117
+ });
118
+ return true;
119
+ }
120
+ /** Test-only: await the detached loop. Never called by production code. */
121
+ async waitForTest(jobId) {
122
+ await this.jobs.get(jobId)?.done;
123
+ }
124
+ prune(addonId, integrationId) {
125
+ const finished = [...this.jobs.values()].filter((j) => j.addonId === addonId && j.integrationId === integrationId && j.state !== "running").sort((a, b) => b.startedAt - a.startedAt);
126
+ for (const j of finished.slice(JOB_HISTORY)) this.jobs.delete(j.jobId);
127
+ }
128
+ async run(job, provider, input) {
129
+ try {
130
+ const already = await this.alreadyAdopted(provider, input, job);
131
+ for (const childNativeId of input.childNativeIds) {
132
+ if (job.cancelRequested) {
133
+ job.results.push({
134
+ childNativeId,
135
+ outcome: "cancelled",
136
+ parentDeviceId: null,
137
+ accessoryCount: 0,
138
+ error: null
139
+ });
140
+ continue;
141
+ }
142
+ if (already.has(childNativeId)) {
143
+ job.results.push({
144
+ childNativeId,
145
+ outcome: "already-adopted",
146
+ parentDeviceId: null,
147
+ accessoryCount: 0,
148
+ error: null
149
+ });
150
+ this.deps.logger.info("adoption job: candidate already adopted, skipped", {
151
+ tags: { addonId: job.addonId },
152
+ meta: {
153
+ jobId: job.jobId,
154
+ integrationId: job.integrationId,
155
+ childNativeId
156
+ }
157
+ });
158
+ continue;
159
+ }
160
+ job.currentChildNativeId = childNativeId;
161
+ await this.adoptOne(job, provider, input, childNativeId);
162
+ job.currentChildNativeId = null;
163
+ }
164
+ job.state = job.cancelRequested ? "cancelled" : "done";
165
+ } catch (err) {
166
+ job.state = "failed";
167
+ job.error = require_dist.errMsg(err);
168
+ this.deps.logger.error("adoption job failed", {
169
+ tags: { addonId: job.addonId },
170
+ meta: {
171
+ jobId: job.jobId,
172
+ integrationId: job.integrationId,
173
+ error: job.error
174
+ }
175
+ });
176
+ } finally {
177
+ job.currentChildNativeId = null;
178
+ job.finishedAt = this.now();
179
+ const s = snapshot(job);
180
+ this.deps.logger.info("adoption job finished", {
181
+ tags: { addonId: job.addonId },
182
+ meta: {
183
+ jobId: job.jobId,
184
+ integrationId: job.integrationId,
185
+ state: s.state,
186
+ total: s.total,
187
+ adopted: s.adopted,
188
+ alreadyAdopted: s.alreadyAdopted,
189
+ failed: s.failed,
190
+ accessoriesCreated: s.accessoriesCreated,
191
+ durationMs: (s.finishedAt ?? 0) - s.startedAt
192
+ }
193
+ });
194
+ }
195
+ }
196
+ /**
197
+ * One `listCandidates` snapshot at job start. A read that FAILS must not be
198
+ * read as "nothing is adopted" — that would send every candidate to the
199
+ * provider and turn a safe retry back into a wall of duplicate-stableId
200
+ * errors — so a failure yields an empty set AND a warning, and the
201
+ * per-candidate failure path stays the backstop.
202
+ */
203
+ async alreadyAdopted(provider, input, job) {
204
+ try {
205
+ const page = await provider.listCandidates({
206
+ integrationId: input.integrationId,
207
+ page: 1,
208
+ pageSize: 2e4,
209
+ ...input.filter !== void 0 ? { filter: input.filter } : {}
210
+ });
211
+ const out = /* @__PURE__ */ new Set();
212
+ for (const c of page.candidates) if (c.alreadyAdopted === true) out.add(c.childNativeId);
213
+ return out;
214
+ } catch (err) {
215
+ this.deps.logger.warn("adoption job: pre-flight listCandidates failed — every candidate will be attempted", {
216
+ tags: { addonId: job.addonId },
217
+ meta: {
218
+ jobId: job.jobId,
219
+ integrationId: job.integrationId,
220
+ error: require_dist.errMsg(err)
221
+ }
222
+ });
223
+ return /* @__PURE__ */ new Set();
224
+ }
225
+ }
226
+ async adoptOne(job, provider, input, childNativeId) {
227
+ const startedAt = this.now();
228
+ try {
229
+ const per = input.perCandidate?.[childNativeId];
230
+ const entry = (await provider.adopt({
231
+ integrationId: input.integrationId,
232
+ childNativeIds: [childNativeId],
233
+ ...input.filter !== void 0 ? { filter: input.filter } : {},
234
+ ...input.importLocations !== void 0 ? { importLocations: input.importLocations } : {},
235
+ ...per !== void 0 ? { perCandidate: { [childNativeId]: per } } : {}
236
+ })).adopted[0];
237
+ if (!entry) {
238
+ job.results.push({
239
+ childNativeId,
240
+ outcome: "failed",
241
+ parentDeviceId: null,
242
+ accessoryCount: 0,
243
+ error: "provider returned no adopted entry"
244
+ });
245
+ this.deps.logger.warn("adoption job: provider adopted nothing for candidate", {
246
+ tags: { addonId: job.addonId },
247
+ meta: {
248
+ jobId: job.jobId,
249
+ integrationId: job.integrationId,
250
+ childNativeId
251
+ }
252
+ });
253
+ return;
254
+ }
255
+ job.results.push({
256
+ childNativeId,
257
+ outcome: "adopted",
258
+ parentDeviceId: entry.parentDeviceId,
259
+ accessoryCount: entry.accessoryDeviceIds.length,
260
+ error: null
261
+ });
262
+ this.deps.logger.info("adoption job: candidate adopted", {
263
+ tags: {
264
+ deviceId: entry.parentDeviceId,
265
+ addonId: job.addonId
266
+ },
267
+ meta: {
268
+ jobId: job.jobId,
269
+ integrationId: job.integrationId,
270
+ childNativeId,
271
+ accessories: entry.accessoryDeviceIds.length,
272
+ durationMs: this.now() - startedAt
273
+ }
274
+ });
275
+ } catch (err) {
276
+ job.results.push({
277
+ childNativeId,
278
+ outcome: "failed",
279
+ parentDeviceId: null,
280
+ accessoryCount: 0,
281
+ error: require_dist.errMsg(err)
282
+ });
283
+ this.deps.logger.warn("adoption job: candidate failed", {
284
+ tags: { addonId: job.addonId },
285
+ meta: {
286
+ jobId: job.jobId,
287
+ integrationId: job.integrationId,
288
+ childNativeId,
289
+ durationMs: this.now() - startedAt,
290
+ error: require_dist.errMsg(err)
291
+ }
292
+ });
293
+ }
294
+ }
295
+ };
296
+ //#endregion
9
297
  //#region src/builtins/device-manager/day-night-config-schema.ts
10
298
  var MODE_LABELS = {
11
299
  auto: "Auto",
@@ -1128,6 +1416,27 @@ async function adoptionRelease(pctx, input) {
1128
1416
  const { addonId, ...rest } = input;
1129
1417
  return (await pctx.host.requireDeviceAdoptionProvider(addonId)).release(rest);
1130
1418
  }
1419
+ async function adoptionStartJob(pctx, input) {
1420
+ const { addonId, integrationId, childNativeIds, filter, importLocations, perCandidate } = input;
1421
+ return pctx.host.adoptionJobs.start({
1422
+ addonId,
1423
+ integrationId,
1424
+ childNativeIds,
1425
+ ...filter !== void 0 ? { filter } : {},
1426
+ ...importLocations !== void 0 ? { importLocations } : {},
1427
+ ...perCandidate !== void 0 ? { perCandidate } : {}
1428
+ });
1429
+ }
1430
+ async function adoptionListJobs(pctx, input) {
1431
+ const { addonId, integrationId } = input;
1432
+ return pctx.host.adoptionJobs.list({
1433
+ addonId,
1434
+ ...integrationId !== void 0 ? { integrationId } : {}
1435
+ });
1436
+ }
1437
+ async function adoptionCancelJob(pctx, input) {
1438
+ return { cancelled: pctx.host.adoptionJobs.cancel(input.jobId) };
1439
+ }
1131
1440
  async function adoptionResync(pctx, input) {
1132
1441
  const { camDeviceId, resetToSource } = input;
1133
1442
  let owningAddonId = pctx.registry?.getAddonId(camDeviceId) ?? null;
@@ -3929,6 +4238,8 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
3929
4238
  * the D3 re-handshake after device restore is the reliable replacement.
3930
4239
  */
3931
4240
  remoteNativeCaps = /* @__PURE__ */ new Map();
4241
+ /** Background adoption jobs — lazily constructed (needs `ctx.logger`). */
4242
+ adoptionJobEngine = null;
3932
4243
  /** The runtime-state mirror, asserted present (constructed in onInitialize). */
3933
4244
  get stateMirror() {
3934
4245
  if (!this.stateMirrorImpl) throw new Error("DeviceManagerAddon: state mirror accessed before onInitialize");
@@ -3954,9 +4265,22 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
3954
4265
  resolveDeviceProbed: (deviceId) => this.stateMirror.resolveDeviceProbed(deviceId),
3955
4266
  waitDeviceProvider: (addonId, timeoutMs) => this.waitDeviceProvider(addonId, timeoutMs),
3956
4267
  requireDeviceProvider: (addonId) => this.requireDeviceProvider(addonId),
3957
- requireDeviceAdoptionProvider: (addonId) => this.requireDeviceAdoptionProvider(addonId)
4268
+ requireDeviceAdoptionProvider: (addonId) => this.requireDeviceAdoptionProvider(addonId),
4269
+ adoptionJobs: this.adoptionJobsImpl
3958
4270
  };
3959
4271
  }
4272
+ /**
4273
+ * Background adoption jobs. In-RAM and process-lifetime: a restart forgets
4274
+ * them, which is safe because adoption is keyed by a stable id — re-running a
4275
+ * job re-adopts nothing (see `AdoptionJobEngine`).
4276
+ */
4277
+ get adoptionJobsImpl() {
4278
+ if (!this.adoptionJobEngine) this.adoptionJobEngine = new AdoptionJobEngine({
4279
+ resolveProvider: async (addonId) => this.requireDeviceAdoptionProvider(addonId),
4280
+ logger: this.ctx.logger
4281
+ });
4282
+ return this.adoptionJobEngine;
4283
+ }
3960
4284
  /** Wait for a device-provider by addonId, returning null on timeout. */
3961
4285
  async waitDeviceProvider(addonId, timeoutMs = 5e3) {
3962
4286
  const provider = await this.capabilityRegistry?.waitForProvider("device-provider", addonId, timeoutMs);
@@ -4136,6 +4460,9 @@ var DeviceManagerAddon = class extends require_dist.BaseAddon {
4136
4460
  adoptionRefresh: (input) => adoptionRefresh(pctx, input),
4137
4461
  adoptionAdopt: (input) => adoptionAdopt(pctx, input),
4138
4462
  adoptionRelease: (input) => adoptionRelease(pctx, input),
4463
+ adoptionStartJob: (input) => adoptionStartJob(pctx, input),
4464
+ adoptionListJobs: (input) => adoptionListJobs(pctx, input),
4465
+ adoptionCancelJob: (input) => adoptionCancelJob(pctx, input),
4139
4466
  adoptionResync: (input) => adoptionResync(pctx, input),
4140
4467
  discoveryProviders: async () => {
4141
4468
  return { providers: (await this.listDiscoveryCapableProviderIds()).map((addonId) => ({