@camstack/system 1.2.107 → 1.2.108
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/builtins/device-manager/device-aggregation.d.ts +2 -2
- package/dist/builtins/device-manager/device-bindings-store.d.ts +12 -6
- package/dist/builtins/device-manager/device-manager.addon.d.ts +6 -0
- package/dist/builtins/device-manager/device-manager.addon.js +1177 -824
- package/dist/builtins/device-manager/device-manager.addon.mjs +1177 -824
- package/dist/builtins/device-manager/device-meta-actions.d.ts +4 -4
- package/dist/builtins/device-manager/device-meta-store.d.ts +18 -43
- package/dist/builtins/device-manager/device-meta-types.d.ts +19 -17
- package/dist/builtins/device-manager/device-row-store.d.ts +229 -0
- package/dist/builtins/sqlite-storage/retired-settings-keys.d.ts +99 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -176
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -175
- package/dist/retired-settings-keys-Bsjf7HQ-.mjs +278 -0
- package/dist/retired-settings-keys-pNmoHPTg.js +295 -0
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Ct as DeviceFeature, E as buildStreamParamsConfigSchema, F as deviceStatusCapability, Gt as EventCategory, Lt as isDeviceConfigCap, N as deviceManagerCapability, Ot as WELL_KNOWN_TAB_MAP, P as deviceStateCapability, R as enumerateItemArrayFields, Tt as DeviceType, Wt as sleep, a as CAP_NAMES_WITH_STATUS, at as runtimeStatePolicyFor, et as normalizeUnit, nt as parseStreamParamsFormPatch, p as STREAM_PROFILE_META, t as ALL_CAPABILITY_DEFINITIONS, u as DeviceStatusSchema, vt as errMsg, wt as DeviceRole, yt as BaseAddon, z as enumerateSchemaFields } from "../../dist-_oC_QkQA.mjs";
|
|
2
|
+
import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-Bsjf7HQ-.mjs";
|
|
2
3
|
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { canonicalDeviceFingerprint } from "@camstack/types/node";
|
|
4
5
|
//#region src/builtins/device-manager/adoption-job-engine.ts
|
|
@@ -289,6 +290,442 @@ var AdoptionJobEngine = class {
|
|
|
289
290
|
}
|
|
290
291
|
};
|
|
291
292
|
//#endregion
|
|
293
|
+
//#region src/builtins/device-manager/device-aggregation-merge.ts
|
|
294
|
+
/**
|
|
295
|
+
* Pure aggregator-merge + field-tagging helpers for the device-manager
|
|
296
|
+
* device-details aggregator. Extracted verbatim from
|
|
297
|
+
* `device-manager.addon.ts`. These functions are stateless: they take
|
|
298
|
+
* contributions in and return new wire-shape objects, attaching writer
|
|
299
|
+
* provenance to editable fields. No addon-instance dependency.
|
|
300
|
+
*
|
|
301
|
+
* `mergeAggregates` is re-exported from the addon module to preserve the
|
|
302
|
+
* existing public export surface the test-suite imports.
|
|
303
|
+
*/
|
|
304
|
+
/**
|
|
305
|
+
* Walk the sections/fields of a contribution and inject `writerCapName` +
|
|
306
|
+
* `writerAddonId` + `source` on each editable field. Readonly fields and
|
|
307
|
+
* structural fields (separator/info/button) pass through untouched. The
|
|
308
|
+
* aggregator is the single place that knows provenance — provider schemas
|
|
309
|
+
* stay clean, UI-bound metadata is attached once at the boundary.
|
|
310
|
+
*/
|
|
311
|
+
function tagContribution(contribution, capName, addonId, kind) {
|
|
312
|
+
const source = kind === "settings" ? "settings" : "live";
|
|
313
|
+
return {
|
|
314
|
+
...contribution.tabs ? { tabs: [...contribution.tabs] } : {},
|
|
315
|
+
sections: contribution.sections.map((section) => ({
|
|
316
|
+
...section,
|
|
317
|
+
fields: section.fields.map((field) => tagField(field, capName, addonId, source, kind))
|
|
318
|
+
}))
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function isFieldRecord(value) {
|
|
322
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Convert a strict `ConfigUISchemaWithValues` (readonly arrays, typed
|
|
326
|
+
* field union) into the cap wire shape `ContributionShape` (mutable
|
|
327
|
+
* arrays, opaque field records). Required because the cap method z.infer
|
|
328
|
+
* uses mutable arrays — readonly arrays are not assignable to mutable
|
|
329
|
+
* even when structurally identical, so a structural copy bridges the gap
|
|
330
|
+
* without disabling the type checker.
|
|
331
|
+
*/
|
|
332
|
+
function toWireShape(input) {
|
|
333
|
+
const out = { sections: input.sections.map((s) => ({
|
|
334
|
+
id: s.id,
|
|
335
|
+
title: s.title,
|
|
336
|
+
...s.description !== void 0 ? { description: s.description } : {},
|
|
337
|
+
...s.style !== void 0 ? { style: s.style } : {},
|
|
338
|
+
...s.defaultCollapsed !== void 0 ? { defaultCollapsed: s.defaultCollapsed } : {},
|
|
339
|
+
...s.columns !== void 0 ? { columns: s.columns } : {},
|
|
340
|
+
...s.tab !== void 0 ? { tab: s.tab } : {},
|
|
341
|
+
...s.location !== void 0 ? { location: s.location } : {},
|
|
342
|
+
...s.order !== void 0 ? { order: s.order } : {},
|
|
343
|
+
fields: [...s.fields]
|
|
344
|
+
})) };
|
|
345
|
+
if (input.tabs) out.tabs = [...input.tabs];
|
|
346
|
+
return out;
|
|
347
|
+
}
|
|
348
|
+
function tagField(field, capName, addonId, source, kind) {
|
|
349
|
+
if (!isFieldRecord(field)) return field;
|
|
350
|
+
const f = field;
|
|
351
|
+
const structuralTypes = new Set([
|
|
352
|
+
"separator",
|
|
353
|
+
"info",
|
|
354
|
+
"button"
|
|
355
|
+
]);
|
|
356
|
+
if (typeof f.type === "string" && structuralTypes.has(f.type)) return field;
|
|
357
|
+
const tagged = {
|
|
358
|
+
...f,
|
|
359
|
+
source
|
|
360
|
+
};
|
|
361
|
+
if (kind === "live" || f.readonlyField === true) tagged.readonlyField = true;
|
|
362
|
+
else {
|
|
363
|
+
tagged.writerCapName = capName;
|
|
364
|
+
tagged.writerAddonId = addonId;
|
|
365
|
+
}
|
|
366
|
+
if (f.type === "group") {
|
|
367
|
+
const children = Array.isArray(f.fields) ? f.fields : [];
|
|
368
|
+
if (children.length > 0) tagged.fields = children.map((child) => tagField(child, capName, addonId, source, kind));
|
|
369
|
+
} else if (f.type === "sub-tabs") {
|
|
370
|
+
const rawTabs = Array.isArray(f.tabs) ? f.tabs : [];
|
|
371
|
+
if (rawTabs.length > 0) tagged.tabs = rawTabs.map((tab) => {
|
|
372
|
+
if (!isFieldRecord(tab)) return tab;
|
|
373
|
+
const tabChildren = Array.isArray(tab.fields) ? tab.fields : [];
|
|
374
|
+
return {
|
|
375
|
+
...tab,
|
|
376
|
+
fields: tabChildren.map((child) => tagField(child, capName, addonId, source, kind))
|
|
377
|
+
};
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
return tagged;
|
|
381
|
+
}
|
|
382
|
+
function mergeAggregates(parts) {
|
|
383
|
+
const tabDecls = /* @__PURE__ */ new Map();
|
|
384
|
+
const sections = [];
|
|
385
|
+
const seenSectionIds = /* @__PURE__ */ new Set();
|
|
386
|
+
for (const part of parts) {
|
|
387
|
+
if (part.tabs) {
|
|
388
|
+
for (const t of part.tabs) if (!tabDecls.has(t.id)) tabDecls.set(t.id, t);
|
|
389
|
+
}
|
|
390
|
+
for (const s of part.sections) {
|
|
391
|
+
if (s.id !== void 0) {
|
|
392
|
+
if (seenSectionIds.has(s.id)) continue;
|
|
393
|
+
seenSectionIds.add(s.id);
|
|
394
|
+
}
|
|
395
|
+
sections.push(s);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
for (const s of sections) {
|
|
399
|
+
const tabId = s.tab ?? "general";
|
|
400
|
+
if (tabDecls.has(tabId)) continue;
|
|
401
|
+
const known = WELL_KNOWN_TAB_MAP[tabId];
|
|
402
|
+
if (known) tabDecls.set(tabId, {
|
|
403
|
+
id: known.id,
|
|
404
|
+
label: known.label,
|
|
405
|
+
icon: known.icon,
|
|
406
|
+
order: known.order
|
|
407
|
+
});
|
|
408
|
+
else tabDecls.set(tabId, {
|
|
409
|
+
id: tabId,
|
|
410
|
+
label: tabId,
|
|
411
|
+
icon: "wrench",
|
|
412
|
+
order: 100
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
sections.sort((a, b) => {
|
|
416
|
+
const tabA = a.tab ?? "general";
|
|
417
|
+
const tabB = b.tab ?? "general";
|
|
418
|
+
if (tabA !== tabB) {
|
|
419
|
+
const orderA = tabDecls.get(tabA)?.order ?? 100;
|
|
420
|
+
const orderB = tabDecls.get(tabB)?.order ?? 100;
|
|
421
|
+
if (orderA !== orderB) return orderA - orderB;
|
|
422
|
+
return tabA.localeCompare(tabB);
|
|
423
|
+
}
|
|
424
|
+
return (a.order ?? 0) - (b.order ?? 0);
|
|
425
|
+
});
|
|
426
|
+
const sortedTabs = [...tabDecls.values()].toSorted((a, b) => (a.order ?? 100) - (b.order ?? 100));
|
|
427
|
+
const out = { sections };
|
|
428
|
+
if (sortedTabs.length > 0) out.tabs = sortedTabs;
|
|
429
|
+
return out;
|
|
430
|
+
}
|
|
431
|
+
//#endregion
|
|
432
|
+
//#region src/builtins/device-manager/device-bindings-store.ts
|
|
433
|
+
/**
|
|
434
|
+
* Wire the push-fed cross-process native-cap cache (`remoteNativeCaps`).
|
|
435
|
+
* Workers emit `DeviceBindingsChanged` on `ctx.registerNativeCap` / device
|
|
436
|
+
* removal; we mirror those into `remoteNativeCaps` so `getBindings` returns the
|
|
437
|
+
* full cluster view. Events from the local node are ignored: hub-local natives
|
|
438
|
+
* live in `capabilityRegistry` and are folded in directly by getBindings.
|
|
439
|
+
*
|
|
440
|
+
* Push events are accurate in the steady state but can be lost during the
|
|
441
|
+
* Moleculer transport handshake window (hub restart, crash-respawn,
|
|
442
|
+
* restartAddon). The reliable replacement for lost events is the D3 re-handshake
|
|
443
|
+
* (`listClusterNativeCaps()`), which `resolveNativeCapOwnerSync` /
|
|
444
|
+
* `getBindings` step 4 fall through to on a push miss. The `$node.disconnected`
|
|
445
|
+
* handler purges a gone node's entries; the worker re-handshakes (and re-emits
|
|
446
|
+
* `native-registered`) on its next boot.
|
|
447
|
+
*/
|
|
448
|
+
function wireRemoteNativeCapSync(ctx, remoteNativeCaps) {
|
|
449
|
+
const localNodeId = ctx.kernel.localNodeId ?? "hub";
|
|
450
|
+
ctx.eventBus.subscribe({ category: EventCategory.DeviceBindingsChanged }, (event) => {
|
|
451
|
+
const { deviceId, capName, reason, addonId, nodeId } = event.data;
|
|
452
|
+
if (nodeId === localNodeId) return;
|
|
453
|
+
if (reason === "native-registered") {
|
|
454
|
+
let perDevice = remoteNativeCaps.get(deviceId);
|
|
455
|
+
if (!perDevice) {
|
|
456
|
+
perDevice = /* @__PURE__ */ new Map();
|
|
457
|
+
remoteNativeCaps.set(deviceId, perDevice);
|
|
458
|
+
}
|
|
459
|
+
perDevice.set(capName, {
|
|
460
|
+
addonId,
|
|
461
|
+
nodeId
|
|
462
|
+
});
|
|
463
|
+
} else if (reason === "native-unregistered") {
|
|
464
|
+
const perDevice = remoteNativeCaps.get(deviceId);
|
|
465
|
+
if (!perDevice) return;
|
|
466
|
+
perDevice.delete(capName);
|
|
467
|
+
if (perDevice.size === 0) remoteNativeCaps.delete(deviceId);
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
const cluster = ctx.kernel.cluster;
|
|
471
|
+
if (cluster) cluster.broker.localBus.on("$node.disconnected", (payload) => {
|
|
472
|
+
const gone = payload.node.id;
|
|
473
|
+
const emptyDevices = [];
|
|
474
|
+
for (const [deviceId, perDevice] of remoteNativeCaps) {
|
|
475
|
+
const toDelete = [];
|
|
476
|
+
for (const [capName, entry] of perDevice) if (entry.nodeId === gone) toDelete.push(capName);
|
|
477
|
+
for (const capName of toDelete) perDevice.delete(capName);
|
|
478
|
+
if (perDevice.size === 0) emptyDevices.push(deviceId);
|
|
479
|
+
}
|
|
480
|
+
for (const deviceId of emptyDevices) remoteNativeCaps.delete(deviceId);
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
async function readBindingsStore(deps) {
|
|
484
|
+
return { deviceBindings: (await deps.ctx.settings.readAddonStore()).deviceBindings ?? {} };
|
|
485
|
+
}
|
|
486
|
+
async function writeBindingsStore(deps, next) {
|
|
487
|
+
await deps.ctx.settings.writeAddonStore({ deviceBindings: next.deviceBindings });
|
|
488
|
+
}
|
|
489
|
+
function resolveWrapperNodeId(_wrapperAddonId) {
|
|
490
|
+
return "hub";
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Reduce a provider node id to the routable form `DeviceProxy` can pin.
|
|
494
|
+
*
|
|
495
|
+
* Every addon runs in its own `addon-runner` with the composite node id
|
|
496
|
+
* `${parentNodeId}/${runnerId}` (see `addon-runner.ts`) — e.g.
|
|
497
|
+
* `hub/provider-reolink` or `dev-agent-0/provider-reolink`. That composite is
|
|
498
|
+
* an IDENTITY, not a routable target: the `CapRouteResolver` reaches a child
|
|
499
|
+
* only THROUGH its parent (the hub resolves a hub-local-uds child by
|
|
500
|
+
* cap+device; an agent forwards to its own child). `DeviceProxy` pins
|
|
501
|
+
* `entry.providerNodeId` on every cap call, so a binding entry must expose the
|
|
502
|
+
* parent node id — otherwise the explicit pin classifies as `remote-moleculer`
|
|
503
|
+
* to an unknown node → `no-provider`, which surfaces as
|
|
504
|
+
* "this camera doesn't expose …" for client-proxy-driven widget caps
|
|
505
|
+
* (motion-zones, privacy-mask). Wrappers already report the parent via
|
|
506
|
+
* `resolveWrapperNodeId`; this aligns natives with the same contract.
|
|
507
|
+
*
|
|
508
|
+
* A flat node id (a genuine standalone node with no `/`) is returned
|
|
509
|
+
* unchanged.
|
|
510
|
+
*/
|
|
511
|
+
function toRoutableProviderNodeId(nodeId) {
|
|
512
|
+
const slash = nodeId.indexOf("/");
|
|
513
|
+
return slash === -1 ? nodeId : nodeId.slice(0, slash);
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* Resolve a remote native cap entry for a given `(capName, deviceId)` by
|
|
517
|
+
* consulting the handshake-fed `HubNodeRegistry` via
|
|
518
|
+
* `ctx.kernel.listClusterNativeCaps()`. Called when the push-based
|
|
519
|
+
* `remoteNativeCaps` cache misses — covers the Moleculer transport
|
|
520
|
+
* handshake window where `DeviceBindingsChanged` events were lost but the
|
|
521
|
+
* D3 re-handshake (post device restore) has already populated the registry.
|
|
522
|
+
*
|
|
523
|
+
* Returns `null` when the entry is genuinely not present in the cluster
|
|
524
|
+
* view (cap not registered on any worker for that device).
|
|
525
|
+
*/
|
|
526
|
+
function resolveRemoteNativeCapFromRegistry(deps, capName, deviceId) {
|
|
527
|
+
const clusterCaps = deps.ctx.kernel.listClusterNativeCapsForDevice?.(deviceId) ?? deps.ctx.kernel.listClusterNativeCaps?.();
|
|
528
|
+
if (!clusterCaps) return null;
|
|
529
|
+
for (const entry of clusterCaps) if (entry.capName === capName && entry.deviceId === deviceId && entry.addonId) return {
|
|
530
|
+
addonId: entry.addonId,
|
|
531
|
+
nodeId: entry.nodeId
|
|
532
|
+
};
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Resolve the device's declared TYPE (`'camera'`, `'event-emitter'`, …), or
|
|
537
|
+
* `undefined` when it cannot be established.
|
|
538
|
+
*
|
|
539
|
+
* The PERSISTED row is the authority: `ctx.kernel.deviceRegistry` is hub-only
|
|
540
|
+
* and has been observed empty in the very process that answers `getBindings`
|
|
541
|
+
* correctly (see the note on `getAllBindings`). The registry is consulted only
|
|
542
|
+
* as a secondary source, for a device constructed but not yet persisted.
|
|
543
|
+
*
|
|
544
|
+
* `undefined` is a first-class answer and callers must treat it as "no
|
|
545
|
+
* filtering" — an absent row must never be the reason a device loses bindings.
|
|
546
|
+
*/
|
|
547
|
+
function resolveDeviceType(deps, row, deviceId) {
|
|
548
|
+
const persisted = row?.meta.type;
|
|
549
|
+
if (typeof persisted === "string" && persisted.length > 0) return persisted;
|
|
550
|
+
const live = deps.ctx.kernel?.deviceRegistry?.getById(deviceId)?.type;
|
|
551
|
+
return typeof live === "string" && live.length > 0 ? live : void 0;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Is this device still part of the fleet?
|
|
555
|
+
*
|
|
556
|
+
* The device-manager's own stores are the authority in-process — no RPC. Two
|
|
557
|
+
* sources, in the same order `resolveDeviceType` uses them: the live registry
|
|
558
|
+
* first (a device constructed but not yet persisted is present), then the
|
|
559
|
+
* PERSISTED meta, which is the index `getBindings` already reads and is present
|
|
560
|
+
* wherever this provider runs.
|
|
561
|
+
*
|
|
562
|
+
* `'absent'` is only ever returned against a NON-EMPTY ledger. An empty one
|
|
563
|
+
* means device restore has not run, not that the fleet was deleted (D49) — the
|
|
564
|
+
* same reason `getAllBindings` warns instead of reporting zero devices. That is
|
|
565
|
+
* the only reason this is async: the `COUNT(*)` runs solely on the miss path,
|
|
566
|
+
* where the alternative is to call a device gone because the store is empty.
|
|
567
|
+
*/
|
|
568
|
+
async function resolveDevicePresence(deps, row, deviceId) {
|
|
569
|
+
if (deps.ctx.kernel?.deviceRegistry?.getById(deviceId)) return "present";
|
|
570
|
+
if (row !== null) return "present";
|
|
571
|
+
return await deps.rows.count() > 0 ? "absent" : "unknown";
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Does a capability apply to a device of type `deviceType`?
|
|
575
|
+
*
|
|
576
|
+
* The cap's `deviceTypes` is the ONLY declaration consulted (D4: behavioural
|
|
577
|
+
* cap metadata lives in the `*.cap.ts`, never in a manifest). Two deliberate
|
|
578
|
+
* fail-open cases:
|
|
579
|
+
*
|
|
580
|
+
* - a cap that declares no `deviceTypes` (or an empty list) applies to every
|
|
581
|
+
* device — the pre-existing, back-compatible semantics;
|
|
582
|
+
* - an UNKNOWN `deviceType` never filters, so a missing/failed meta lookup
|
|
583
|
+
* changes nothing (D49: a read that fails must not destroy work).
|
|
584
|
+
*/
|
|
585
|
+
function capAppliesToDeviceType(def, deviceType) {
|
|
586
|
+
if (deviceType === void 0) return true;
|
|
587
|
+
const declared = def?.deviceTypes;
|
|
588
|
+
if (!declared || declared.length === 0) return true;
|
|
589
|
+
return declared.some((t) => t === deviceType);
|
|
590
|
+
}
|
|
591
|
+
async function getBindings(deps, input) {
|
|
592
|
+
const storeKey = String(input.deviceId);
|
|
593
|
+
const perDevice = (await readBindingsStore(deps)).deviceBindings[storeKey] ?? {};
|
|
594
|
+
const row = await deps.rows.get(input.deviceId);
|
|
595
|
+
const deviceType = resolveDeviceType(deps, row, input.deviceId);
|
|
596
|
+
const presence = await resolveDevicePresence(deps, row, input.deviceId);
|
|
597
|
+
const entries = [];
|
|
598
|
+
const seenCaps = /* @__PURE__ */ new Set();
|
|
599
|
+
const resolveRemote = (capName) => deps.remoteNativeCaps.get(input.deviceId)?.get(capName) ?? resolveRemoteNativeCapFromRegistry(deps, capName, input.deviceId);
|
|
600
|
+
for (const [capName, { wrapperAddonId }] of Object.entries(perDevice)) {
|
|
601
|
+
const hubLocalNative = deps.capabilityRegistry?.getNativeAddonId(capName, input.deviceId) ?? null;
|
|
602
|
+
const remoteNative = resolveRemote(capName);
|
|
603
|
+
const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
|
|
604
|
+
const nativeNodeId = hubLocalNative ? deps.ctx.kernel.localNodeId ?? "hub" : remoteNative?.nodeId ?? deps.ctx.kernel.localNodeId ?? "hub";
|
|
605
|
+
if (wrapperAddonId === null && !nativeAddonId) {
|
|
606
|
+
seenCaps.add(capName);
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
entries.push({
|
|
610
|
+
capName,
|
|
611
|
+
kind: wrapperAddonId ? "wrapped" : "native",
|
|
612
|
+
providerAddonId: wrapperAddonId ?? nativeAddonId,
|
|
613
|
+
providerNodeId: wrapperAddonId ? resolveWrapperNodeId(wrapperAddonId) : toRoutableProviderNodeId(nativeNodeId),
|
|
614
|
+
nativeAddonId
|
|
615
|
+
});
|
|
616
|
+
seenCaps.add(capName);
|
|
617
|
+
}
|
|
618
|
+
if (presence === "absent") deps.ctx.logger.debug("bindings requested for absent device — returning none", { tags: { deviceId: input.deviceId } });
|
|
619
|
+
else if (deps.capabilityRegistry) {
|
|
620
|
+
const skippedForType = [];
|
|
621
|
+
for (const capName of deps.capabilityRegistry.getCapsWithDefaultWrapper()) {
|
|
622
|
+
if (seenCaps.has(capName)) continue;
|
|
623
|
+
if (!capAppliesToDeviceType(deps.capabilityRegistry.getDefinition(capName), deviceType)) {
|
|
624
|
+
skippedForType.push(capName);
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
const defaultWrapperAddonId = deps.capabilityRegistry.getDefaultWrapperForCap(capName);
|
|
628
|
+
if (!defaultWrapperAddonId) continue;
|
|
629
|
+
const hubLocalNative = deps.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? null;
|
|
630
|
+
const remoteNative = resolveRemote(capName);
|
|
631
|
+
const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
|
|
632
|
+
entries.push({
|
|
633
|
+
capName,
|
|
634
|
+
kind: "wrapped",
|
|
635
|
+
providerAddonId: defaultWrapperAddonId,
|
|
636
|
+
providerNodeId: resolveWrapperNodeId(defaultWrapperAddonId),
|
|
637
|
+
nativeAddonId
|
|
638
|
+
});
|
|
639
|
+
seenCaps.add(capName);
|
|
640
|
+
}
|
|
641
|
+
if (skippedForType.length > 0) deps.ctx.logger.debug("getBindings: default wrappers skipped — deviceTypes mismatch", {
|
|
642
|
+
tags: { deviceId: input.deviceId },
|
|
643
|
+
meta: {
|
|
644
|
+
deviceType,
|
|
645
|
+
skipped: skippedForType
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
if (deps.capabilityRegistry) for (const capName of deps.capabilityRegistry.getNativeCapsForDevice(input.deviceId)) {
|
|
650
|
+
if (seenCaps.has(capName)) continue;
|
|
651
|
+
const nativeAddonId = deps.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? "";
|
|
652
|
+
entries.push({
|
|
653
|
+
capName,
|
|
654
|
+
kind: "native",
|
|
655
|
+
providerAddonId: nativeAddonId,
|
|
656
|
+
providerNodeId: deps.ctx.kernel.localNodeId ?? "hub",
|
|
657
|
+
nativeAddonId
|
|
658
|
+
});
|
|
659
|
+
seenCaps.add(capName);
|
|
660
|
+
}
|
|
661
|
+
const pushFed = deps.remoteNativeCaps.get(input.deviceId);
|
|
662
|
+
if (pushFed) for (const [capName, info] of pushFed) {
|
|
663
|
+
if (seenCaps.has(capName)) continue;
|
|
664
|
+
entries.push({
|
|
665
|
+
capName,
|
|
666
|
+
kind: "native",
|
|
667
|
+
providerAddonId: info.addonId,
|
|
668
|
+
providerNodeId: toRoutableProviderNodeId(info.nodeId),
|
|
669
|
+
nativeAddonId: info.addonId
|
|
670
|
+
});
|
|
671
|
+
seenCaps.add(capName);
|
|
672
|
+
}
|
|
673
|
+
const clusterCaps = deps.ctx.kernel.listClusterNativeCapsForDevice?.(input.deviceId) ?? deps.ctx.kernel.listClusterNativeCaps?.();
|
|
674
|
+
if (clusterCaps) for (const entry of clusterCaps) {
|
|
675
|
+
if (entry.deviceId !== input.deviceId) continue;
|
|
676
|
+
if (seenCaps.has(entry.capName)) continue;
|
|
677
|
+
if (!entry.addonId) continue;
|
|
678
|
+
const localNodeId = deps.ctx.kernel.localNodeId ?? "hub";
|
|
679
|
+
if (entry.nodeId === localNodeId) continue;
|
|
680
|
+
entries.push({
|
|
681
|
+
capName: entry.capName,
|
|
682
|
+
kind: "native",
|
|
683
|
+
providerAddonId: entry.addonId,
|
|
684
|
+
providerNodeId: toRoutableProviderNodeId(entry.nodeId),
|
|
685
|
+
nativeAddonId: entry.addonId
|
|
686
|
+
});
|
|
687
|
+
seenCaps.add(entry.capName);
|
|
688
|
+
}
|
|
689
|
+
return {
|
|
690
|
+
deviceId: input.deviceId,
|
|
691
|
+
entries
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Whole-fleet binding dump. Iterates every device known to the
|
|
696
|
+
* deviceRegistry and reuses the per-device `getBindings` resolver
|
|
697
|
+
* for each — same routing rules, single round-trip. Used by
|
|
698
|
+
* `SystemManager.init()` for warm-boot.
|
|
699
|
+
*
|
|
700
|
+
* Bindings change rarely (wrapper toggle, device add/remove) so
|
|
701
|
+
* clients invalidate via the existing
|
|
702
|
+
* `capability.binding-changed` event rather than re-fetching this
|
|
703
|
+
* payload periodically.
|
|
704
|
+
*/
|
|
705
|
+
async function getAllBindings(deps) {
|
|
706
|
+
const ids = /* @__PURE__ */ new Set();
|
|
707
|
+
for (const row of await deps.rows.listAll()) ids.add(row.meta.id);
|
|
708
|
+
const registered = deps.ctx.kernel?.deviceRegistry?.getAll() ?? [];
|
|
709
|
+
for (const device of registered) ids.add(device.id);
|
|
710
|
+
if (ids.size === 0) {
|
|
711
|
+
deps.ctx.logger.warn("getAllBindings found no devices — warm boot will see nothing", { meta: { hasRegistry: deps.ctx.kernel?.deviceRegistry !== void 0 } });
|
|
712
|
+
return [];
|
|
713
|
+
}
|
|
714
|
+
const out = [];
|
|
715
|
+
for (const deviceId of [...ids].sort((a, b) => a - b)) out.push(await getBindings(deps, { deviceId }));
|
|
716
|
+
return out;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Resolve a numeric deviceId to a stableId via persisted meta.
|
|
720
|
+
* Used only by the device-identity section of the device-details
|
|
721
|
+
* aggregator (see `buildBaseDeviceSection`) to surface the stableId as
|
|
722
|
+
* a readonly display field. All runtime/registry lookups are keyed by
|
|
723
|
+
* numeric deviceId; this helper is display-only.
|
|
724
|
+
*/
|
|
725
|
+
async function lookupPersistedStableId(deps, deviceId) {
|
|
726
|
+
return (await deps.rows.get(deviceId))?.meta.stableId;
|
|
727
|
+
}
|
|
728
|
+
//#endregion
|
|
292
729
|
//#region src/builtins/device-manager/day-night-config-schema.ts
|
|
293
730
|
var MODE_LABELS = {
|
|
294
731
|
auto: "Auto",
|
|
@@ -678,145 +1115,6 @@ function parseDerivedFormSettingsPatch(builderId, patch) {
|
|
|
678
1115
|
return reducer.parsePatch(patch);
|
|
679
1116
|
}
|
|
680
1117
|
//#endregion
|
|
681
|
-
//#region src/builtins/device-manager/device-aggregation-merge.ts
|
|
682
|
-
/**
|
|
683
|
-
* Pure aggregator-merge + field-tagging helpers for the device-manager
|
|
684
|
-
* device-details aggregator. Extracted verbatim from
|
|
685
|
-
* `device-manager.addon.ts`. These functions are stateless: they take
|
|
686
|
-
* contributions in and return new wire-shape objects, attaching writer
|
|
687
|
-
* provenance to editable fields. No addon-instance dependency.
|
|
688
|
-
*
|
|
689
|
-
* `mergeAggregates` is re-exported from the addon module to preserve the
|
|
690
|
-
* existing public export surface the test-suite imports.
|
|
691
|
-
*/
|
|
692
|
-
/**
|
|
693
|
-
* Walk the sections/fields of a contribution and inject `writerCapName` +
|
|
694
|
-
* `writerAddonId` + `source` on each editable field. Readonly fields and
|
|
695
|
-
* structural fields (separator/info/button) pass through untouched. The
|
|
696
|
-
* aggregator is the single place that knows provenance — provider schemas
|
|
697
|
-
* stay clean, UI-bound metadata is attached once at the boundary.
|
|
698
|
-
*/
|
|
699
|
-
function tagContribution(contribution, capName, addonId, kind) {
|
|
700
|
-
const source = kind === "settings" ? "settings" : "live";
|
|
701
|
-
return {
|
|
702
|
-
...contribution.tabs ? { tabs: [...contribution.tabs] } : {},
|
|
703
|
-
sections: contribution.sections.map((section) => ({
|
|
704
|
-
...section,
|
|
705
|
-
fields: section.fields.map((field) => tagField(field, capName, addonId, source, kind))
|
|
706
|
-
}))
|
|
707
|
-
};
|
|
708
|
-
}
|
|
709
|
-
function isFieldRecord(value) {
|
|
710
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
711
|
-
}
|
|
712
|
-
/**
|
|
713
|
-
* Convert a strict `ConfigUISchemaWithValues` (readonly arrays, typed
|
|
714
|
-
* field union) into the cap wire shape `ContributionShape` (mutable
|
|
715
|
-
* arrays, opaque field records). Required because the cap method z.infer
|
|
716
|
-
* uses mutable arrays — readonly arrays are not assignable to mutable
|
|
717
|
-
* even when structurally identical, so a structural copy bridges the gap
|
|
718
|
-
* without disabling the type checker.
|
|
719
|
-
*/
|
|
720
|
-
function toWireShape(input) {
|
|
721
|
-
const out = { sections: input.sections.map((s) => ({
|
|
722
|
-
id: s.id,
|
|
723
|
-
title: s.title,
|
|
724
|
-
...s.description !== void 0 ? { description: s.description } : {},
|
|
725
|
-
...s.style !== void 0 ? { style: s.style } : {},
|
|
726
|
-
...s.defaultCollapsed !== void 0 ? { defaultCollapsed: s.defaultCollapsed } : {},
|
|
727
|
-
...s.columns !== void 0 ? { columns: s.columns } : {},
|
|
728
|
-
...s.tab !== void 0 ? { tab: s.tab } : {},
|
|
729
|
-
...s.location !== void 0 ? { location: s.location } : {},
|
|
730
|
-
...s.order !== void 0 ? { order: s.order } : {},
|
|
731
|
-
fields: [...s.fields]
|
|
732
|
-
})) };
|
|
733
|
-
if (input.tabs) out.tabs = [...input.tabs];
|
|
734
|
-
return out;
|
|
735
|
-
}
|
|
736
|
-
function tagField(field, capName, addonId, source, kind) {
|
|
737
|
-
if (!isFieldRecord(field)) return field;
|
|
738
|
-
const f = field;
|
|
739
|
-
const structuralTypes = new Set([
|
|
740
|
-
"separator",
|
|
741
|
-
"info",
|
|
742
|
-
"button"
|
|
743
|
-
]);
|
|
744
|
-
if (typeof f.type === "string" && structuralTypes.has(f.type)) return field;
|
|
745
|
-
const tagged = {
|
|
746
|
-
...f,
|
|
747
|
-
source
|
|
748
|
-
};
|
|
749
|
-
if (kind === "live" || f.readonlyField === true) tagged.readonlyField = true;
|
|
750
|
-
else {
|
|
751
|
-
tagged.writerCapName = capName;
|
|
752
|
-
tagged.writerAddonId = addonId;
|
|
753
|
-
}
|
|
754
|
-
if (f.type === "group") {
|
|
755
|
-
const children = Array.isArray(f.fields) ? f.fields : [];
|
|
756
|
-
if (children.length > 0) tagged.fields = children.map((child) => tagField(child, capName, addonId, source, kind));
|
|
757
|
-
} else if (f.type === "sub-tabs") {
|
|
758
|
-
const rawTabs = Array.isArray(f.tabs) ? f.tabs : [];
|
|
759
|
-
if (rawTabs.length > 0) tagged.tabs = rawTabs.map((tab) => {
|
|
760
|
-
if (!isFieldRecord(tab)) return tab;
|
|
761
|
-
const tabChildren = Array.isArray(tab.fields) ? tab.fields : [];
|
|
762
|
-
return {
|
|
763
|
-
...tab,
|
|
764
|
-
fields: tabChildren.map((child) => tagField(child, capName, addonId, source, kind))
|
|
765
|
-
};
|
|
766
|
-
});
|
|
767
|
-
}
|
|
768
|
-
return tagged;
|
|
769
|
-
}
|
|
770
|
-
function mergeAggregates(parts) {
|
|
771
|
-
const tabDecls = /* @__PURE__ */ new Map();
|
|
772
|
-
const sections = [];
|
|
773
|
-
const seenSectionIds = /* @__PURE__ */ new Set();
|
|
774
|
-
for (const part of parts) {
|
|
775
|
-
if (part.tabs) {
|
|
776
|
-
for (const t of part.tabs) if (!tabDecls.has(t.id)) tabDecls.set(t.id, t);
|
|
777
|
-
}
|
|
778
|
-
for (const s of part.sections) {
|
|
779
|
-
if (s.id !== void 0) {
|
|
780
|
-
if (seenSectionIds.has(s.id)) continue;
|
|
781
|
-
seenSectionIds.add(s.id);
|
|
782
|
-
}
|
|
783
|
-
sections.push(s);
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
for (const s of sections) {
|
|
787
|
-
const tabId = s.tab ?? "general";
|
|
788
|
-
if (tabDecls.has(tabId)) continue;
|
|
789
|
-
const known = WELL_KNOWN_TAB_MAP[tabId];
|
|
790
|
-
if (known) tabDecls.set(tabId, {
|
|
791
|
-
id: known.id,
|
|
792
|
-
label: known.label,
|
|
793
|
-
icon: known.icon,
|
|
794
|
-
order: known.order
|
|
795
|
-
});
|
|
796
|
-
else tabDecls.set(tabId, {
|
|
797
|
-
id: tabId,
|
|
798
|
-
label: tabId,
|
|
799
|
-
icon: "wrench",
|
|
800
|
-
order: 100
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
|
-
sections.sort((a, b) => {
|
|
804
|
-
const tabA = a.tab ?? "general";
|
|
805
|
-
const tabB = b.tab ?? "general";
|
|
806
|
-
if (tabA !== tabB) {
|
|
807
|
-
const orderA = tabDecls.get(tabA)?.order ?? 100;
|
|
808
|
-
const orderB = tabDecls.get(tabB)?.order ?? 100;
|
|
809
|
-
if (orderA !== orderB) return orderA - orderB;
|
|
810
|
-
return tabA.localeCompare(tabB);
|
|
811
|
-
}
|
|
812
|
-
return (a.order ?? 0) - (b.order ?? 0);
|
|
813
|
-
});
|
|
814
|
-
const sortedTabs = [...tabDecls.values()].toSorted((a, b) => (a.order ?? 100) - (b.order ?? 100));
|
|
815
|
-
const out = { sections };
|
|
816
|
-
if (sortedTabs.length > 0) out.tabs = sortedTabs;
|
|
817
|
-
return out;
|
|
818
|
-
}
|
|
819
|
-
//#endregion
|
|
820
1118
|
//#region src/builtins/device-manager/device-projection.ts
|
|
821
1119
|
/**
|
|
822
1120
|
* Return true when `err` is a transient Moleculer error that is worth
|
|
@@ -948,22 +1246,15 @@ function resolveDeviceById(registry, deviceId) {
|
|
|
948
1246
|
//#region src/builtins/device-manager/device-queries.ts
|
|
949
1247
|
async function listPersistedByAddon(pctx, input) {
|
|
950
1248
|
const { addonId } = input;
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
type: m.type,
|
|
961
|
-
name: m.name,
|
|
962
|
-
location: m.location ?? null,
|
|
963
|
-
disabled: m.disabled ?? false,
|
|
964
|
-
parentDeviceId: m.parentDeviceId
|
|
965
|
-
};
|
|
966
|
-
});
|
|
1249
|
+
return (await pctx.metaStore.rows.listByAddon(addonId)).map(({ meta: m }) => ({
|
|
1250
|
+
id: m.id,
|
|
1251
|
+
stableId: m.stableId,
|
|
1252
|
+
type: m.type,
|
|
1253
|
+
name: m.name,
|
|
1254
|
+
location: m.location ?? null,
|
|
1255
|
+
disabled: m.disabled ?? false,
|
|
1256
|
+
parentDeviceId: m.parentDeviceId
|
|
1257
|
+
}));
|
|
967
1258
|
}
|
|
968
1259
|
async function listAll(pctx, input) {
|
|
969
1260
|
const ownerFilter = Reflect.get(input, "addonId");
|
|
@@ -973,7 +1264,9 @@ async function listAll(pctx, input) {
|
|
|
973
1264
|
const camerasOnly = input.isCamera === true;
|
|
974
1265
|
const results = [];
|
|
975
1266
|
const seen = /* @__PURE__ */ new Set();
|
|
976
|
-
const
|
|
1267
|
+
const fleet = addonId ? await pctx.metaStore.rows.listByAddon(addonId) : await pctx.metaStore.rows.listAll();
|
|
1268
|
+
const rowById = /* @__PURE__ */ new Map();
|
|
1269
|
+
for (const row of fleet) rowById.set(row.meta.id, row);
|
|
977
1270
|
if (pctx.registry) {
|
|
978
1271
|
const liveEntries = addonId ? pctx.registry.getAllForAddon(addonId).map((device) => ({
|
|
979
1272
|
addonId,
|
|
@@ -981,7 +1274,8 @@ async function listAll(pctx, input) {
|
|
|
981
1274
|
})) : pctx.registry.getAllWithAddonId();
|
|
982
1275
|
for (const { addonId: aid, device } of liveEntries) {
|
|
983
1276
|
const key = String(device.id);
|
|
984
|
-
const
|
|
1277
|
+
const row = rowById.get(device.id);
|
|
1278
|
+
const info = toDeviceInfo(aid, device, row?.metadata ?? null, row?.meta ?? null);
|
|
985
1279
|
seen.add(key);
|
|
986
1280
|
if (camerasOnly && !info.isCamera) continue;
|
|
987
1281
|
results.push(slim ? {
|
|
@@ -991,38 +1285,36 @@ async function listAll(pctx, input) {
|
|
|
991
1285
|
} : info);
|
|
992
1286
|
}
|
|
993
1287
|
}
|
|
994
|
-
const
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
const key = String(m.id);
|
|
1000
|
-
if (seen.has(key)) continue;
|
|
1288
|
+
for (const row of fleet) {
|
|
1289
|
+
const m = row.meta;
|
|
1290
|
+
const aid = m.addonId;
|
|
1291
|
+
const stableId = m.stableId;
|
|
1292
|
+
if (seen.has(String(m.id))) continue;
|
|
1001
1293
|
const persistedType = m.type;
|
|
1002
1294
|
if (camerasOnly && persistedType !== DeviceType.Camera) continue;
|
|
1003
1295
|
const persistedConfig = slim ? {} : await pctx.settings.readDeviceStore(m.id);
|
|
1004
|
-
const metadata = slim ? null :
|
|
1296
|
+
const metadata = slim ? null : row.metadata;
|
|
1005
1297
|
results.push({
|
|
1006
1298
|
id: m.id,
|
|
1007
1299
|
stableId,
|
|
1008
1300
|
addonId: aid,
|
|
1009
1301
|
type: persistedType,
|
|
1010
|
-
name: m
|
|
1011
|
-
location: m
|
|
1012
|
-
disabled: m
|
|
1013
|
-
parentDeviceId: m
|
|
1014
|
-
role: toDeviceRole(m
|
|
1302
|
+
name: m.name,
|
|
1303
|
+
location: m.location ?? null,
|
|
1304
|
+
disabled: m.disabled ?? false,
|
|
1305
|
+
parentDeviceId: m.parentDeviceId ?? null,
|
|
1306
|
+
role: toDeviceRole(m.role),
|
|
1015
1307
|
online: pctx.host.resolveDeviceOnline(m.id, pctx.registry !== null),
|
|
1016
1308
|
probed: pctx.host.resolveDeviceProbed(m.id),
|
|
1017
|
-
features: persistedFeatures(m
|
|
1309
|
+
features: persistedFeatures(m.features),
|
|
1018
1310
|
isCamera: persistedType === DeviceType.Camera,
|
|
1019
1311
|
config: persistedConfig ?? {},
|
|
1020
1312
|
metadata,
|
|
1021
|
-
...m
|
|
1022
|
-
...m
|
|
1023
|
-
...m
|
|
1024
|
-
...m
|
|
1025
|
-
...m
|
|
1313
|
+
...m.integrationId !== void 0 ? { integrationId: m.integrationId } : {},
|
|
1314
|
+
...m.linkDeviceId !== void 0 ? { linkDeviceId: m.linkDeviceId } : {},
|
|
1315
|
+
...m.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
|
|
1316
|
+
...m.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
|
|
1317
|
+
...m.display !== void 0 ? { display: m.display } : {},
|
|
1026
1318
|
...(() => {
|
|
1027
1319
|
if (slim) return {};
|
|
1028
1320
|
const si = buildSourceInfoFromConfig(persistedConfig ?? {}, stableId, aid);
|
|
@@ -1037,19 +1329,15 @@ async function getDevice(pctx, input) {
|
|
|
1037
1329
|
if (pctx.registry) {
|
|
1038
1330
|
const found = resolveDeviceById(pctx.registry, deviceId);
|
|
1039
1331
|
if (found) {
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
const metadata = map[key] ?? null;
|
|
1043
|
-
const metaRow = metaMap[key] ?? null;
|
|
1044
|
-
return toDeviceInfo(found.addonId, found.device, metadata, metaRow);
|
|
1332
|
+
const row = await pctx.metaStore.getRow(found.device.id);
|
|
1333
|
+
return toDeviceInfo(found.addonId, found.device, row?.metadata ?? null, row?.meta ?? null);
|
|
1045
1334
|
}
|
|
1046
1335
|
}
|
|
1047
|
-
const
|
|
1048
|
-
if (
|
|
1049
|
-
const {
|
|
1336
|
+
const row = await pctx.metaStore.getRow(deviceId);
|
|
1337
|
+
if (row === null) return null;
|
|
1338
|
+
const { meta: m, metadata } = row;
|
|
1339
|
+
const { addonId: aid, stableId } = m;
|
|
1050
1340
|
const persistedConfig = await pctx.settings.readDeviceStore(m.id);
|
|
1051
|
-
const key = String(deviceId);
|
|
1052
|
-
const metadata = (await pctx.metaStore.readMetadataMap())[key] ?? null;
|
|
1053
1341
|
const sourceInfoGetDevice = buildSourceInfoFromConfig(persistedConfig ?? {}, stableId, aid);
|
|
1054
1342
|
return {
|
|
1055
1343
|
id: deviceId,
|
|
@@ -1088,26 +1376,25 @@ async function getChildren(pctx, input) {
|
|
|
1088
1376
|
}
|
|
1089
1377
|
const results = [];
|
|
1090
1378
|
const seen = /* @__PURE__ */ new Set();
|
|
1091
|
-
const
|
|
1379
|
+
const childRows = await pctx.metaStore.rows.listByParent(parentDeviceId);
|
|
1380
|
+
const rowById = /* @__PURE__ */ new Map();
|
|
1381
|
+
for (const row of childRows) rowById.set(row.meta.id, row);
|
|
1092
1382
|
if (pctx.registry) {
|
|
1093
1383
|
const liveChildren = pctx.registry.getChildren(parentDeviceId);
|
|
1094
1384
|
for (const device of liveChildren) {
|
|
1095
1385
|
const key = String(device.id);
|
|
1096
|
-
const
|
|
1097
|
-
|
|
1098
|
-
results.push(toDeviceInfo(ownerAddonId, device, metadata, metaRow));
|
|
1386
|
+
const row = rowById.get(device.id);
|
|
1387
|
+
results.push(toDeviceInfo(ownerAddonId, device, row?.metadata ?? null, row?.meta ?? null));
|
|
1099
1388
|
seen.add(key);
|
|
1100
1389
|
}
|
|
1101
1390
|
}
|
|
1102
|
-
const
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
for (const childStableId of persistedChildren) {
|
|
1106
|
-
const m = ownerMetaByStableId.get(childStableId);
|
|
1391
|
+
for (const row of childRows) {
|
|
1392
|
+
const m = row.meta;
|
|
1393
|
+
const childStableId = m.stableId;
|
|
1107
1394
|
const key = String(m.id);
|
|
1108
1395
|
if (seen.has(key)) continue;
|
|
1109
1396
|
const persistedConfig = await pctx.settings.readDeviceStore(m.id);
|
|
1110
|
-
const metadata =
|
|
1397
|
+
const metadata = row.metadata;
|
|
1111
1398
|
const sourceInfoChild = buildSourceInfoFromConfig(persistedConfig ?? {}, childStableId, ownerAddonId);
|
|
1112
1399
|
results.push({
|
|
1113
1400
|
id: m.id,
|
|
@@ -1259,16 +1546,10 @@ async function remove(pctx, input) {
|
|
|
1259
1546
|
*/
|
|
1260
1547
|
async function removeByIntegration(pctx, input) {
|
|
1261
1548
|
const { integrationId } = input;
|
|
1262
|
-
const
|
|
1263
|
-
const parentKeys = Object.keys(meta).filter((key) => {
|
|
1264
|
-
const m = meta[key];
|
|
1265
|
-
return m !== void 0 && m.integrationId === integrationId && m.parentDeviceId === null;
|
|
1266
|
-
});
|
|
1549
|
+
const parents = (await pctx.metaStore.rows.listByIntegration(integrationId)).filter((row) => row.meta.parentDeviceId === null);
|
|
1267
1550
|
let removed = 0;
|
|
1268
|
-
for (const
|
|
1269
|
-
|
|
1270
|
-
if (!m) continue;
|
|
1271
|
-
await pctx.provider.remove({ deviceId: m.id });
|
|
1551
|
+
for (const parent of parents) {
|
|
1552
|
+
await pctx.provider.remove({ deviceId: parent.meta.id });
|
|
1272
1553
|
removed++;
|
|
1273
1554
|
}
|
|
1274
1555
|
return { removed };
|
|
@@ -1692,316 +1973,16 @@ function splitDeviceStoreKeys(patch) {
|
|
|
1692
1973
|
}
|
|
1693
1974
|
if (Object.keys(slice).length > 0) storeGroups.push({
|
|
1694
1975
|
section,
|
|
1695
|
-
patch: slice
|
|
1696
|
-
});
|
|
1697
|
-
}
|
|
1698
|
-
const driverPatch = {};
|
|
1699
|
-
for (const [key, value] of Object.entries(patch)) if (!claimed.has(key)) driverPatch[key] = value;
|
|
1700
|
-
return {
|
|
1701
|
-
storeGroups,
|
|
1702
|
-
driverPatch
|
|
1703
|
-
};
|
|
1704
|
-
}
|
|
1705
|
-
//#endregion
|
|
1706
|
-
//#region src/builtins/device-manager/device-bindings-store.ts
|
|
1707
|
-
/**
|
|
1708
|
-
* Wire the push-fed cross-process native-cap cache (`remoteNativeCaps`).
|
|
1709
|
-
* Workers emit `DeviceBindingsChanged` on `ctx.registerNativeCap` / device
|
|
1710
|
-
* removal; we mirror those into `remoteNativeCaps` so `getBindings` returns the
|
|
1711
|
-
* full cluster view. Events from the local node are ignored: hub-local natives
|
|
1712
|
-
* live in `capabilityRegistry` and are folded in directly by getBindings.
|
|
1713
|
-
*
|
|
1714
|
-
* Push events are accurate in the steady state but can be lost during the
|
|
1715
|
-
* Moleculer transport handshake window (hub restart, crash-respawn,
|
|
1716
|
-
* restartAddon). The reliable replacement for lost events is the D3 re-handshake
|
|
1717
|
-
* (`listClusterNativeCaps()`), which `resolveNativeCapOwnerSync` /
|
|
1718
|
-
* `getBindings` step 4 fall through to on a push miss. The `$node.disconnected`
|
|
1719
|
-
* handler purges a gone node's entries; the worker re-handshakes (and re-emits
|
|
1720
|
-
* `native-registered`) on its next boot.
|
|
1721
|
-
*/
|
|
1722
|
-
function wireRemoteNativeCapSync(ctx, remoteNativeCaps) {
|
|
1723
|
-
const localNodeId = ctx.kernel.localNodeId ?? "hub";
|
|
1724
|
-
ctx.eventBus.subscribe({ category: EventCategory.DeviceBindingsChanged }, (event) => {
|
|
1725
|
-
const { deviceId, capName, reason, addonId, nodeId } = event.data;
|
|
1726
|
-
if (nodeId === localNodeId) return;
|
|
1727
|
-
if (reason === "native-registered") {
|
|
1728
|
-
let perDevice = remoteNativeCaps.get(deviceId);
|
|
1729
|
-
if (!perDevice) {
|
|
1730
|
-
perDevice = /* @__PURE__ */ new Map();
|
|
1731
|
-
remoteNativeCaps.set(deviceId, perDevice);
|
|
1732
|
-
}
|
|
1733
|
-
perDevice.set(capName, {
|
|
1734
|
-
addonId,
|
|
1735
|
-
nodeId
|
|
1736
|
-
});
|
|
1737
|
-
} else if (reason === "native-unregistered") {
|
|
1738
|
-
const perDevice = remoteNativeCaps.get(deviceId);
|
|
1739
|
-
if (!perDevice) return;
|
|
1740
|
-
perDevice.delete(capName);
|
|
1741
|
-
if (perDevice.size === 0) remoteNativeCaps.delete(deviceId);
|
|
1742
|
-
}
|
|
1743
|
-
});
|
|
1744
|
-
const cluster = ctx.kernel.cluster;
|
|
1745
|
-
if (cluster) cluster.broker.localBus.on("$node.disconnected", (payload) => {
|
|
1746
|
-
const gone = payload.node.id;
|
|
1747
|
-
const emptyDevices = [];
|
|
1748
|
-
for (const [deviceId, perDevice] of remoteNativeCaps) {
|
|
1749
|
-
const toDelete = [];
|
|
1750
|
-
for (const [capName, entry] of perDevice) if (entry.nodeId === gone) toDelete.push(capName);
|
|
1751
|
-
for (const capName of toDelete) perDevice.delete(capName);
|
|
1752
|
-
if (perDevice.size === 0) emptyDevices.push(deviceId);
|
|
1753
|
-
}
|
|
1754
|
-
for (const deviceId of emptyDevices) remoteNativeCaps.delete(deviceId);
|
|
1755
|
-
});
|
|
1756
|
-
}
|
|
1757
|
-
async function readBindingsStore(deps) {
|
|
1758
|
-
return { deviceBindings: (await deps.ctx.settings.readAddonStore()).deviceBindings ?? {} };
|
|
1759
|
-
}
|
|
1760
|
-
async function writeBindingsStore(deps, next) {
|
|
1761
|
-
await deps.ctx.settings.writeAddonStore({ deviceBindings: next.deviceBindings });
|
|
1762
|
-
}
|
|
1763
|
-
function resolveWrapperNodeId(_wrapperAddonId) {
|
|
1764
|
-
return "hub";
|
|
1765
|
-
}
|
|
1766
|
-
/**
|
|
1767
|
-
* Reduce a provider node id to the routable form `DeviceProxy` can pin.
|
|
1768
|
-
*
|
|
1769
|
-
* Every addon runs in its own `addon-runner` with the composite node id
|
|
1770
|
-
* `${parentNodeId}/${runnerId}` (see `addon-runner.ts`) — e.g.
|
|
1771
|
-
* `hub/provider-reolink` or `dev-agent-0/provider-reolink`. That composite is
|
|
1772
|
-
* an IDENTITY, not a routable target: the `CapRouteResolver` reaches a child
|
|
1773
|
-
* only THROUGH its parent (the hub resolves a hub-local-uds child by
|
|
1774
|
-
* cap+device; an agent forwards to its own child). `DeviceProxy` pins
|
|
1775
|
-
* `entry.providerNodeId` on every cap call, so a binding entry must expose the
|
|
1776
|
-
* parent node id — otherwise the explicit pin classifies as `remote-moleculer`
|
|
1777
|
-
* to an unknown node → `no-provider`, which surfaces as
|
|
1778
|
-
* "this camera doesn't expose …" for client-proxy-driven widget caps
|
|
1779
|
-
* (motion-zones, privacy-mask). Wrappers already report the parent via
|
|
1780
|
-
* `resolveWrapperNodeId`; this aligns natives with the same contract.
|
|
1781
|
-
*
|
|
1782
|
-
* A flat node id (a genuine standalone node with no `/`) is returned
|
|
1783
|
-
* unchanged.
|
|
1784
|
-
*/
|
|
1785
|
-
function toRoutableProviderNodeId(nodeId) {
|
|
1786
|
-
const slash = nodeId.indexOf("/");
|
|
1787
|
-
return slash === -1 ? nodeId : nodeId.slice(0, slash);
|
|
1788
|
-
}
|
|
1789
|
-
/**
|
|
1790
|
-
* Resolve a remote native cap entry for a given `(capName, deviceId)` by
|
|
1791
|
-
* consulting the handshake-fed `HubNodeRegistry` via
|
|
1792
|
-
* `ctx.kernel.listClusterNativeCaps()`. Called when the push-based
|
|
1793
|
-
* `remoteNativeCaps` cache misses — covers the Moleculer transport
|
|
1794
|
-
* handshake window where `DeviceBindingsChanged` events were lost but the
|
|
1795
|
-
* D3 re-handshake (post device restore) has already populated the registry.
|
|
1796
|
-
*
|
|
1797
|
-
* Returns `null` when the entry is genuinely not present in the cluster
|
|
1798
|
-
* view (cap not registered on any worker for that device).
|
|
1799
|
-
*/
|
|
1800
|
-
function resolveRemoteNativeCapFromRegistry(deps, capName, deviceId) {
|
|
1801
|
-
const clusterCaps = deps.ctx.kernel.listClusterNativeCapsForDevice?.(deviceId) ?? deps.ctx.kernel.listClusterNativeCaps?.();
|
|
1802
|
-
if (!clusterCaps) return null;
|
|
1803
|
-
for (const entry of clusterCaps) if (entry.capName === capName && entry.deviceId === deviceId && entry.addonId) return {
|
|
1804
|
-
addonId: entry.addonId,
|
|
1805
|
-
nodeId: entry.nodeId
|
|
1806
|
-
};
|
|
1807
|
-
return null;
|
|
1808
|
-
}
|
|
1809
|
-
/**
|
|
1810
|
-
* Resolve the device's declared TYPE (`'camera'`, `'event-emitter'`, …), or
|
|
1811
|
-
* `undefined` when it cannot be established.
|
|
1812
|
-
*
|
|
1813
|
-
* The PERSISTED meta is the authority: `ctx.kernel.deviceRegistry` is hub-only
|
|
1814
|
-
* and has been observed empty in the very process that answers `getBindings`
|
|
1815
|
-
* correctly (see the note on `getAllBindings`). The registry is consulted only
|
|
1816
|
-
* as a secondary source, for a device constructed but not yet persisted.
|
|
1817
|
-
*
|
|
1818
|
-
* `undefined` is a first-class answer and callers must treat it as "no
|
|
1819
|
-
* filtering" — an absent row must never be the reason a device loses bindings.
|
|
1820
|
-
*/
|
|
1821
|
-
function resolveDeviceType(deps, rawStore, deviceId) {
|
|
1822
|
-
const persisted = rawStore.deviceMeta?.[String(deviceId)]?.type;
|
|
1823
|
-
if (typeof persisted === "string" && persisted.length > 0) return persisted;
|
|
1824
|
-
const live = deps.ctx.kernel?.deviceRegistry?.getById(deviceId)?.type;
|
|
1825
|
-
return typeof live === "string" && live.length > 0 ? live : void 0;
|
|
1826
|
-
}
|
|
1827
|
-
/**
|
|
1828
|
-
* Is this device still part of the fleet?
|
|
1829
|
-
*
|
|
1830
|
-
* The device-manager's own stores are the authority in-process — no RPC. Two
|
|
1831
|
-
* sources, in the same order `resolveDeviceType` uses them: the live registry
|
|
1832
|
-
* first (a device constructed but not yet persisted is present), then the
|
|
1833
|
-
* PERSISTED meta, which is the index `getBindings` already reads and is present
|
|
1834
|
-
* wherever this provider runs.
|
|
1835
|
-
*
|
|
1836
|
-
* `'absent'` is only ever returned against a NON-EMPTY meta store. An empty one
|
|
1837
|
-
* means device restore has not run, not that the fleet was deleted (D49) — the
|
|
1838
|
-
* same reason `getAllBindings` warns instead of reporting zero devices.
|
|
1839
|
-
*/
|
|
1840
|
-
function resolveDevicePresence(deps, rawStore, deviceId) {
|
|
1841
|
-
if (deps.ctx.kernel?.deviceRegistry?.getById(deviceId)) return "present";
|
|
1842
|
-
const meta = rawStore.deviceMeta;
|
|
1843
|
-
if (meta && Object.keys(meta).length > 0) return meta[String(deviceId)] === void 0 ? "absent" : "present";
|
|
1844
|
-
return "unknown";
|
|
1845
|
-
}
|
|
1846
|
-
/**
|
|
1847
|
-
* Does a capability apply to a device of type `deviceType`?
|
|
1848
|
-
*
|
|
1849
|
-
* The cap's `deviceTypes` is the ONLY declaration consulted (D4: behavioural
|
|
1850
|
-
* cap metadata lives in the `*.cap.ts`, never in a manifest). Two deliberate
|
|
1851
|
-
* fail-open cases:
|
|
1852
|
-
*
|
|
1853
|
-
* - a cap that declares no `deviceTypes` (or an empty list) applies to every
|
|
1854
|
-
* device — the pre-existing, back-compatible semantics;
|
|
1855
|
-
* - an UNKNOWN `deviceType` never filters, so a missing/failed meta lookup
|
|
1856
|
-
* changes nothing (D49: a read that fails must not destroy work).
|
|
1857
|
-
*/
|
|
1858
|
-
function capAppliesToDeviceType(def, deviceType) {
|
|
1859
|
-
if (deviceType === void 0) return true;
|
|
1860
|
-
const declared = def?.deviceTypes;
|
|
1861
|
-
if (!declared || declared.length === 0) return true;
|
|
1862
|
-
return declared.some((t) => t === deviceType);
|
|
1863
|
-
}
|
|
1864
|
-
async function getBindings(deps, input) {
|
|
1865
|
-
const storeKey = String(input.deviceId);
|
|
1866
|
-
const rawStore = await deps.ctx.settings.readAddonStore();
|
|
1867
|
-
const perDevice = (rawStore.deviceBindings ?? {})[storeKey] ?? {};
|
|
1868
|
-
const deviceType = resolveDeviceType(deps, rawStore, input.deviceId);
|
|
1869
|
-
const presence = resolveDevicePresence(deps, rawStore, input.deviceId);
|
|
1870
|
-
const entries = [];
|
|
1871
|
-
const seenCaps = /* @__PURE__ */ new Set();
|
|
1872
|
-
const resolveRemote = (capName) => deps.remoteNativeCaps.get(input.deviceId)?.get(capName) ?? resolveRemoteNativeCapFromRegistry(deps, capName, input.deviceId);
|
|
1873
|
-
for (const [capName, { wrapperAddonId }] of Object.entries(perDevice)) {
|
|
1874
|
-
const hubLocalNative = deps.capabilityRegistry?.getNativeAddonId(capName, input.deviceId) ?? null;
|
|
1875
|
-
const remoteNative = resolveRemote(capName);
|
|
1876
|
-
const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
|
|
1877
|
-
const nativeNodeId = hubLocalNative ? deps.ctx.kernel.localNodeId ?? "hub" : remoteNative?.nodeId ?? deps.ctx.kernel.localNodeId ?? "hub";
|
|
1878
|
-
if (wrapperAddonId === null && !nativeAddonId) {
|
|
1879
|
-
seenCaps.add(capName);
|
|
1880
|
-
continue;
|
|
1881
|
-
}
|
|
1882
|
-
entries.push({
|
|
1883
|
-
capName,
|
|
1884
|
-
kind: wrapperAddonId ? "wrapped" : "native",
|
|
1885
|
-
providerAddonId: wrapperAddonId ?? nativeAddonId,
|
|
1886
|
-
providerNodeId: wrapperAddonId ? resolveWrapperNodeId(wrapperAddonId) : toRoutableProviderNodeId(nativeNodeId),
|
|
1887
|
-
nativeAddonId
|
|
1888
|
-
});
|
|
1889
|
-
seenCaps.add(capName);
|
|
1890
|
-
}
|
|
1891
|
-
if (presence === "absent") deps.ctx.logger.debug("bindings requested for absent device — returning none", { tags: { deviceId: input.deviceId } });
|
|
1892
|
-
else if (deps.capabilityRegistry) {
|
|
1893
|
-
const skippedForType = [];
|
|
1894
|
-
for (const capName of deps.capabilityRegistry.getCapsWithDefaultWrapper()) {
|
|
1895
|
-
if (seenCaps.has(capName)) continue;
|
|
1896
|
-
if (!capAppliesToDeviceType(deps.capabilityRegistry.getDefinition(capName), deviceType)) {
|
|
1897
|
-
skippedForType.push(capName);
|
|
1898
|
-
continue;
|
|
1899
|
-
}
|
|
1900
|
-
const defaultWrapperAddonId = deps.capabilityRegistry.getDefaultWrapperForCap(capName);
|
|
1901
|
-
if (!defaultWrapperAddonId) continue;
|
|
1902
|
-
const hubLocalNative = deps.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? null;
|
|
1903
|
-
const remoteNative = resolveRemote(capName);
|
|
1904
|
-
const nativeAddonId = hubLocalNative ?? remoteNative?.addonId ?? "";
|
|
1905
|
-
entries.push({
|
|
1906
|
-
capName,
|
|
1907
|
-
kind: "wrapped",
|
|
1908
|
-
providerAddonId: defaultWrapperAddonId,
|
|
1909
|
-
providerNodeId: resolveWrapperNodeId(defaultWrapperAddonId),
|
|
1910
|
-
nativeAddonId
|
|
1911
|
-
});
|
|
1912
|
-
seenCaps.add(capName);
|
|
1913
|
-
}
|
|
1914
|
-
if (skippedForType.length > 0) deps.ctx.logger.debug("getBindings: default wrappers skipped — deviceTypes mismatch", {
|
|
1915
|
-
tags: { deviceId: input.deviceId },
|
|
1916
|
-
meta: {
|
|
1917
|
-
deviceType,
|
|
1918
|
-
skipped: skippedForType
|
|
1919
|
-
}
|
|
1920
|
-
});
|
|
1921
|
-
}
|
|
1922
|
-
if (deps.capabilityRegistry) for (const capName of deps.capabilityRegistry.getNativeCapsForDevice(input.deviceId)) {
|
|
1923
|
-
if (seenCaps.has(capName)) continue;
|
|
1924
|
-
const nativeAddonId = deps.capabilityRegistry.getNativeAddonId(capName, input.deviceId) ?? "";
|
|
1925
|
-
entries.push({
|
|
1926
|
-
capName,
|
|
1927
|
-
kind: "native",
|
|
1928
|
-
providerAddonId: nativeAddonId,
|
|
1929
|
-
providerNodeId: deps.ctx.kernel.localNodeId ?? "hub",
|
|
1930
|
-
nativeAddonId
|
|
1931
|
-
});
|
|
1932
|
-
seenCaps.add(capName);
|
|
1933
|
-
}
|
|
1934
|
-
const pushFed = deps.remoteNativeCaps.get(input.deviceId);
|
|
1935
|
-
if (pushFed) for (const [capName, info] of pushFed) {
|
|
1936
|
-
if (seenCaps.has(capName)) continue;
|
|
1937
|
-
entries.push({
|
|
1938
|
-
capName,
|
|
1939
|
-
kind: "native",
|
|
1940
|
-
providerAddonId: info.addonId,
|
|
1941
|
-
providerNodeId: toRoutableProviderNodeId(info.nodeId),
|
|
1942
|
-
nativeAddonId: info.addonId
|
|
1943
|
-
});
|
|
1944
|
-
seenCaps.add(capName);
|
|
1945
|
-
}
|
|
1946
|
-
const clusterCaps = deps.ctx.kernel.listClusterNativeCapsForDevice?.(input.deviceId) ?? deps.ctx.kernel.listClusterNativeCaps?.();
|
|
1947
|
-
if (clusterCaps) for (const entry of clusterCaps) {
|
|
1948
|
-
if (entry.deviceId !== input.deviceId) continue;
|
|
1949
|
-
if (seenCaps.has(entry.capName)) continue;
|
|
1950
|
-
if (!entry.addonId) continue;
|
|
1951
|
-
const localNodeId = deps.ctx.kernel.localNodeId ?? "hub";
|
|
1952
|
-
if (entry.nodeId === localNodeId) continue;
|
|
1953
|
-
entries.push({
|
|
1954
|
-
capName: entry.capName,
|
|
1955
|
-
kind: "native",
|
|
1956
|
-
providerAddonId: entry.addonId,
|
|
1957
|
-
providerNodeId: toRoutableProviderNodeId(entry.nodeId),
|
|
1958
|
-
nativeAddonId: entry.addonId
|
|
1976
|
+
patch: slice
|
|
1959
1977
|
});
|
|
1960
|
-
seenCaps.add(entry.capName);
|
|
1961
1978
|
}
|
|
1979
|
+
const driverPatch = {};
|
|
1980
|
+
for (const [key, value] of Object.entries(patch)) if (!claimed.has(key)) driverPatch[key] = value;
|
|
1962
1981
|
return {
|
|
1963
|
-
|
|
1964
|
-
|
|
1982
|
+
storeGroups,
|
|
1983
|
+
driverPatch
|
|
1965
1984
|
};
|
|
1966
1985
|
}
|
|
1967
|
-
/**
|
|
1968
|
-
* Whole-fleet binding dump. Iterates every device known to the
|
|
1969
|
-
* deviceRegistry and reuses the per-device `getBindings` resolver
|
|
1970
|
-
* for each — same routing rules, single round-trip. Used by
|
|
1971
|
-
* `SystemManager.init()` for warm-boot.
|
|
1972
|
-
*
|
|
1973
|
-
* Bindings change rarely (wrapper toggle, device add/remove) so
|
|
1974
|
-
* clients invalidate via the existing
|
|
1975
|
-
* `capability.binding-changed` event rather than re-fetching this
|
|
1976
|
-
* payload periodically.
|
|
1977
|
-
*/
|
|
1978
|
-
async function getAllBindings(deps) {
|
|
1979
|
-
const store = await deps.ctx.settings.readAddonStore();
|
|
1980
|
-
const ids = /* @__PURE__ */ new Set();
|
|
1981
|
-
for (const key of Object.keys(store.deviceMeta ?? {})) {
|
|
1982
|
-
const id = Number(key);
|
|
1983
|
-
if (Number.isInteger(id)) ids.add(id);
|
|
1984
|
-
}
|
|
1985
|
-
const registered = deps.ctx.kernel?.deviceRegistry?.getAll() ?? [];
|
|
1986
|
-
for (const device of registered) ids.add(device.id);
|
|
1987
|
-
if (ids.size === 0) {
|
|
1988
|
-
deps.ctx.logger.warn("getAllBindings found no devices — warm boot will see nothing", { meta: { hasRegistry: deps.ctx.kernel?.deviceRegistry !== void 0 } });
|
|
1989
|
-
return [];
|
|
1990
|
-
}
|
|
1991
|
-
const out = [];
|
|
1992
|
-
for (const deviceId of [...ids].sort((a, b) => a - b)) out.push(await getBindings(deps, { deviceId }));
|
|
1993
|
-
return out;
|
|
1994
|
-
}
|
|
1995
|
-
/**
|
|
1996
|
-
* Resolve a numeric deviceId to a stableId via persisted meta.
|
|
1997
|
-
* Used only by the device-identity section of the device-details
|
|
1998
|
-
* aggregator (see `buildBaseDeviceSection`) to surface the stableId as
|
|
1999
|
-
* a readonly display field. All runtime/registry lookups are keyed by
|
|
2000
|
-
* numeric deviceId; this helper is display-only.
|
|
2001
|
-
*/
|
|
2002
|
-
async function lookupPersistedStableId(deps, deviceId) {
|
|
2003
|
-
return ((await deps.ctx.settings.readAddonStore()).deviceMeta ?? {})[String(deviceId)]?.stableId;
|
|
2004
|
-
}
|
|
2005
1986
|
//#endregion
|
|
2006
1987
|
//#region src/builtins/device-manager/device-aggregation.ts
|
|
2007
1988
|
/**
|
|
@@ -2790,7 +2771,7 @@ async function getWireableFields(deps, input) {
|
|
|
2790
2771
|
if (wireable) caps.push(wireable);
|
|
2791
2772
|
}
|
|
2792
2773
|
if (input.includeSynthesizable === true) {
|
|
2793
|
-
const deviceType = (
|
|
2774
|
+
const deviceType = (await deps.bindingsDeps.rows.get(deviceId))?.meta.type;
|
|
2794
2775
|
if (deviceType !== void 0) for (const def of ALL_CAPABILITY_DEFINITIONS) {
|
|
2795
2776
|
if (seen.has(def.name)) continue;
|
|
2796
2777
|
if (def.scope !== "device" || def.kind === "wrapper") continue;
|
|
@@ -2944,20 +2925,10 @@ var DeviceEventPropagator = class {
|
|
|
2944
2925
|
* because `ProviderContext.stampIntegrationId` delegates HERE — passing the
|
|
2945
2926
|
* context would be a capture cycle.
|
|
2946
2927
|
*/
|
|
2947
|
-
async function stampIntegrationId(metaStore,
|
|
2928
|
+
async function stampIntegrationId(metaStore, ctx, deviceId, integrationId) {
|
|
2948
2929
|
await metaStore.withMetaWriteLock(async () => {
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
const { meta: m } = persisted;
|
|
2952
|
-
const key = String(deviceId);
|
|
2953
|
-
const allMeta = await metaStore.readMeta();
|
|
2954
|
-
await settings.writeAddonStore({ deviceMeta: {
|
|
2955
|
-
...allMeta,
|
|
2956
|
-
[key]: {
|
|
2957
|
-
...m,
|
|
2958
|
-
integrationId
|
|
2959
|
-
}
|
|
2960
|
-
} });
|
|
2930
|
+
if (!await metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] stampIntegrationId: unknown device id=${deviceId}`);
|
|
2931
|
+
await metaStore.rows.patch(deviceId, { integrationId });
|
|
2961
2932
|
});
|
|
2962
2933
|
ctx.eventBus.emit({
|
|
2963
2934
|
id: randomUUID(),
|
|
@@ -2977,70 +2948,54 @@ async function stampIntegrationId(metaStore, settings, ctx, deviceId, integratio
|
|
|
2977
2948
|
async function allocateDeviceId(pctx, input) {
|
|
2978
2949
|
const { addonId, stableId } = input;
|
|
2979
2950
|
return await pctx.metaStore.withMetaWriteLock(async () => {
|
|
2980
|
-
const
|
|
2981
|
-
|
|
2982
|
-
if (existing) return { id: existing.id };
|
|
2951
|
+
const existing = await pctx.metaStore.rows.findByStableId(addonId, stableId);
|
|
2952
|
+
if (existing) return { id: existing.meta.id };
|
|
2983
2953
|
const id = await pctx.metaStore.allocateNextDeviceId();
|
|
2984
|
-
await pctx.
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
}
|
|
2954
|
+
await pctx.metaStore.rows.upsert({
|
|
2955
|
+
addonId,
|
|
2956
|
+
stableId,
|
|
2957
|
+
type: "generic",
|
|
2958
|
+
name: stableId,
|
|
2959
|
+
location: null,
|
|
2960
|
+
disabled: false,
|
|
2961
|
+
parentDeviceId: null,
|
|
2962
|
+
id
|
|
2963
|
+
}, {
|
|
2964
|
+
registered: false,
|
|
2965
|
+
metadata: null
|
|
2966
|
+
});
|
|
2997
2967
|
return { id };
|
|
2998
2968
|
});
|
|
2999
2969
|
}
|
|
3000
2970
|
async function registerDevice(pctx, input) {
|
|
3001
2971
|
const { addonId, stableId, id, type, name, parentDeviceId, features, config } = input;
|
|
3002
|
-
const key = String(id);
|
|
3003
2972
|
const featuresArr = Array.isArray(features) ? [...features] : [];
|
|
3004
2973
|
const { isFirstRegistration, exportFingerprint, fingerprintChanged } = await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3005
|
-
const
|
|
3006
|
-
const
|
|
3007
|
-
const
|
|
3008
|
-
if (!wasInIndex) await pctx.settings.writeAddonStore({ deviceIndex: {
|
|
3009
|
-
...index,
|
|
3010
|
-
[addonId]: [...existing, stableId]
|
|
3011
|
-
} });
|
|
3012
|
-
const meta = await pctx.metaStore.readMeta();
|
|
3013
|
-
const existingMeta = meta[key];
|
|
2974
|
+
const existingRow = await pctx.metaStore.rows.get(id);
|
|
2975
|
+
const existingMeta = existingRow?.meta;
|
|
2976
|
+
const wasRegistered = existingRow?.registered ?? false;
|
|
3014
2977
|
const wasUserNamed = existingMeta?.userNamed ?? (existingMeta !== void 0 && existingMeta.name !== stableId);
|
|
3015
2978
|
const resolvedName = wasUserNamed && existingMeta !== void 0 ? existingMeta.name : name;
|
|
3016
|
-
const isFirst = !existingMeta || !
|
|
2979
|
+
const isFirst = !existingMeta || !wasRegistered;
|
|
3017
2980
|
const fingerprint = canonicalDeviceFingerprint({
|
|
3018
2981
|
deviceId: id,
|
|
3019
2982
|
deviceType: type,
|
|
3020
2983
|
features: featuresArr
|
|
3021
2984
|
});
|
|
3022
|
-
await pctx.
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
...existingMeta?.role !== void 0 ? { role: existingMeta.role } : {},
|
|
3037
|
-
...existingMeta?.display !== void 0 ? { display: existingMeta.display } : {},
|
|
3038
|
-
parentDeviceId,
|
|
3039
|
-
id,
|
|
3040
|
-
features: featuresArr,
|
|
3041
|
-
exportFingerprint: fingerprint
|
|
3042
|
-
}
|
|
3043
|
-
} });
|
|
2985
|
+
await pctx.metaStore.rows.upsertRegistration({
|
|
2986
|
+
deviceId: id,
|
|
2987
|
+
addonId,
|
|
2988
|
+
stableId,
|
|
2989
|
+
type,
|
|
2990
|
+
name: resolvedName,
|
|
2991
|
+
userNamed: wasUserNamed,
|
|
2992
|
+
location: existingMeta?.location ?? null,
|
|
2993
|
+
disabled: existingMeta?.disabled ?? false,
|
|
2994
|
+
parentDeviceId: parentDeviceId ?? null,
|
|
2995
|
+
registered: true,
|
|
2996
|
+
features: featuresArr,
|
|
2997
|
+
exportFingerprint: fingerprint
|
|
2998
|
+
});
|
|
3044
2999
|
return {
|
|
3045
3000
|
isFirstRegistration: isFirst,
|
|
3046
3001
|
exportFingerprint: fingerprint,
|
|
@@ -3100,26 +3055,9 @@ async function removeDevice(pctx, input) {
|
|
|
3100
3055
|
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
3101
3056
|
if (!persisted) return;
|
|
3102
3057
|
const { addonId, stableId, meta: persistedMeta } = persisted;
|
|
3103
|
-
const key = String(deviceId);
|
|
3104
3058
|
const deviceName = persistedMeta.name;
|
|
3105
3059
|
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3106
|
-
|
|
3107
|
-
const remaining = (index[addonId] ?? []).filter((sid) => sid !== stableId);
|
|
3108
|
-
const updatedIndex = remaining.length > 0 ? {
|
|
3109
|
-
...index,
|
|
3110
|
-
[addonId]: remaining
|
|
3111
|
-
} : (() => {
|
|
3112
|
-
const { [addonId]: _removed, ...rest } = index;
|
|
3113
|
-
return rest;
|
|
3114
|
-
})();
|
|
3115
|
-
await pctx.settings.writeAddonStore({ deviceIndex: updatedIndex });
|
|
3116
|
-
const { [key]: _removedMeta, ...restMeta } = await pctx.metaStore.readMeta();
|
|
3117
|
-
await pctx.settings.writeAddonStore({ deviceMeta: restMeta });
|
|
3118
|
-
const map = await pctx.metaStore.readMetadataMap();
|
|
3119
|
-
if (key in map) {
|
|
3120
|
-
const { [key]: _removedMetadata, ...restMap } = map;
|
|
3121
|
-
await pctx.settings.writeAddonStore({ deviceMetadata: restMap });
|
|
3122
|
-
}
|
|
3060
|
+
await pctx.metaStore.rows.remove(deviceId);
|
|
3123
3061
|
});
|
|
3124
3062
|
await pctx.settings.clearDeviceStore(deviceId);
|
|
3125
3063
|
await pctx.settings.clearDeviceRuntimeState(deviceId);
|
|
@@ -3175,11 +3113,10 @@ async function loadConfig(pctx, input) {
|
|
|
3175
3113
|
*/
|
|
3176
3114
|
async function loadMeta(pctx, input) {
|
|
3177
3115
|
const { deviceId } = input;
|
|
3178
|
-
const
|
|
3179
|
-
if (
|
|
3180
|
-
const {
|
|
3181
|
-
const
|
|
3182
|
-
const metadata = (await pctx.metaStore.readMetadataMap())[key] ?? null;
|
|
3116
|
+
const row = await pctx.metaStore.getRow(deviceId);
|
|
3117
|
+
if (row === null) return null;
|
|
3118
|
+
const { meta: m, metadata } = row;
|
|
3119
|
+
const { addonId, stableId } = m;
|
|
3183
3120
|
return {
|
|
3184
3121
|
id: m.id,
|
|
3185
3122
|
stableId,
|
|
@@ -3209,32 +3146,26 @@ async function setName(pctx, input) {
|
|
|
3209
3146
|
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
3210
3147
|
if (!persisted) throw new Error(`[device-manager] setName: unknown device id=${deviceId}`);
|
|
3211
3148
|
const { meta: m } = persisted;
|
|
3212
|
-
const key = String(deviceId);
|
|
3213
3149
|
const oldName = m.name;
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3150
|
+
await pctx.metaStore.rows.patch(deviceId, {
|
|
3151
|
+
name,
|
|
3152
|
+
userNamed: true
|
|
3153
|
+
});
|
|
3154
|
+
if (oldName.length > 0 && oldName !== name) {
|
|
3155
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
3156
|
+
for (const row of await pctx.metaStore.rows.listByParent(deviceId)) candidates.set(row.meta.id, row.meta);
|
|
3157
|
+
for (const row of await pctx.metaStore.rows.listAll()) if (row.meta.linkDeviceId === deviceId) candidates.set(row.meta.id, row.meta);
|
|
3158
|
+
candidates.delete(deviceId);
|
|
3159
|
+
for (const childMeta of candidates.values()) {
|
|
3160
|
+
if (childMeta.name !== oldName && !childMeta.name.startsWith(`${oldName} `)) continue;
|
|
3161
|
+
const childName = childMeta.name === oldName ? name : `${name}${childMeta.name.slice(oldName.length)}`;
|
|
3162
|
+
await pctx.metaStore.rows.patch(childMeta.id, { name: childName });
|
|
3163
|
+
cascaded.push({
|
|
3164
|
+
id: childMeta.id,
|
|
3165
|
+
name: childName
|
|
3166
|
+
});
|
|
3221
3167
|
}
|
|
3222
|
-
};
|
|
3223
|
-
if (oldName.length > 0 && oldName !== name) for (const [childKey, childMeta] of Object.entries(allMeta)) {
|
|
3224
|
-
if (childKey === key) continue;
|
|
3225
|
-
if (!(childMeta.parentDeviceId === deviceId || childMeta.linkDeviceId === deviceId)) continue;
|
|
3226
|
-
if (childMeta.name !== oldName && !childMeta.name.startsWith(`${oldName} `)) continue;
|
|
3227
|
-
const childName = childMeta.name === oldName ? name : `${name}${childMeta.name.slice(oldName.length)}`;
|
|
3228
|
-
nextMeta[childKey] = {
|
|
3229
|
-
...childMeta,
|
|
3230
|
-
name: childName
|
|
3231
|
-
};
|
|
3232
|
-
cascaded.push({
|
|
3233
|
-
id: Number(childKey),
|
|
3234
|
-
name: childName
|
|
3235
|
-
});
|
|
3236
3168
|
}
|
|
3237
|
-
await pctx.settings.writeAddonStore({ deviceMeta: nextMeta });
|
|
3238
3169
|
});
|
|
3239
3170
|
pctx.host.ctx.eventBus.emit({
|
|
3240
3171
|
id: randomUUID(),
|
|
@@ -3274,18 +3205,8 @@ async function setName(pctx, input) {
|
|
|
3274
3205
|
async function setLocation(pctx, input) {
|
|
3275
3206
|
const { deviceId, location } = input;
|
|
3276
3207
|
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
const { meta: m } = persisted;
|
|
3280
|
-
const key = String(deviceId);
|
|
3281
|
-
const allMeta = await pctx.metaStore.readMeta();
|
|
3282
|
-
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
3283
|
-
...allMeta,
|
|
3284
|
-
[key]: {
|
|
3285
|
-
...m,
|
|
3286
|
-
location
|
|
3287
|
-
}
|
|
3288
|
-
} });
|
|
3208
|
+
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setLocation: unknown device id=${deviceId}`);
|
|
3209
|
+
await pctx.metaStore.rows.patch(deviceId, { location });
|
|
3289
3210
|
});
|
|
3290
3211
|
pctx.host.ctx.eventBus.emit({
|
|
3291
3212
|
id: randomUUID(),
|
|
@@ -3312,18 +3233,8 @@ async function setLocation(pctx, input) {
|
|
|
3312
3233
|
async function setType(pctx, input) {
|
|
3313
3234
|
const { deviceId, type } = input;
|
|
3314
3235
|
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
const { meta: m } = persisted;
|
|
3318
|
-
const key = String(deviceId);
|
|
3319
|
-
const allMeta = await pctx.metaStore.readMeta();
|
|
3320
|
-
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
3321
|
-
...allMeta,
|
|
3322
|
-
[key]: {
|
|
3323
|
-
...m,
|
|
3324
|
-
type
|
|
3325
|
-
}
|
|
3326
|
-
} });
|
|
3236
|
+
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setType: unknown device id=${deviceId}`);
|
|
3237
|
+
await pctx.metaStore.rows.patch(deviceId, { type });
|
|
3327
3238
|
});
|
|
3328
3239
|
pctx.host.ctx.eventBus.emit({
|
|
3329
3240
|
id: randomUUID(),
|
|
@@ -3358,18 +3269,8 @@ async function setIntegrationId(pctx, input) {
|
|
|
3358
3269
|
async function setLinkDeviceId(pctx, input) {
|
|
3359
3270
|
const { deviceId, linkDeviceId } = input;
|
|
3360
3271
|
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
const { meta: m } = persisted;
|
|
3364
|
-
const key = String(deviceId);
|
|
3365
|
-
const allMeta = await pctx.metaStore.readMeta();
|
|
3366
|
-
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
3367
|
-
...allMeta,
|
|
3368
|
-
[key]: {
|
|
3369
|
-
...m,
|
|
3370
|
-
linkDeviceId
|
|
3371
|
-
}
|
|
3372
|
-
} });
|
|
3272
|
+
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setLinkDeviceId: unknown device id=${deviceId}`);
|
|
3273
|
+
await pctx.metaStore.rows.patch(deviceId, { linkDeviceId });
|
|
3373
3274
|
});
|
|
3374
3275
|
pctx.host.ctx.eventBus.emit({
|
|
3375
3276
|
id: randomUUID(),
|
|
@@ -3396,18 +3297,8 @@ async function setLinkDeviceId(pctx, input) {
|
|
|
3396
3297
|
async function setPrimaryChildEntityId(pctx, input) {
|
|
3397
3298
|
const { deviceId, primaryChildEntityId } = input;
|
|
3398
3299
|
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
const { meta: m } = persisted;
|
|
3402
|
-
const key = String(deviceId);
|
|
3403
|
-
const allMeta = await pctx.metaStore.readMeta();
|
|
3404
|
-
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
3405
|
-
...allMeta,
|
|
3406
|
-
[key]: {
|
|
3407
|
-
...m,
|
|
3408
|
-
primaryChildEntityId
|
|
3409
|
-
}
|
|
3410
|
-
} });
|
|
3300
|
+
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setPrimaryChildEntityId: unknown device id=${deviceId}`);
|
|
3301
|
+
await pctx.metaStore.rows.patch(deviceId, { primaryChildEntityId });
|
|
3411
3302
|
});
|
|
3412
3303
|
pctx.host.ctx.eventBus.emit({
|
|
3413
3304
|
id: randomUUID(),
|
|
@@ -3433,18 +3324,8 @@ async function setPrimaryChildEntityId(pctx, input) {
|
|
|
3433
3324
|
async function setChildLayout(pctx, input) {
|
|
3434
3325
|
const { deviceId, childLayout } = input;
|
|
3435
3326
|
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
const { meta: m } = persisted;
|
|
3439
|
-
const key = String(deviceId);
|
|
3440
|
-
const allMeta = await pctx.metaStore.readMeta();
|
|
3441
|
-
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
3442
|
-
...allMeta,
|
|
3443
|
-
[key]: {
|
|
3444
|
-
...m,
|
|
3445
|
-
childLayout
|
|
3446
|
-
}
|
|
3447
|
-
} });
|
|
3327
|
+
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setChildLayout: unknown device id=${deviceId}`);
|
|
3328
|
+
await pctx.metaStore.rows.patch(deviceId, { childLayout });
|
|
3448
3329
|
});
|
|
3449
3330
|
pctx.host.ctx.eventBus.emit({
|
|
3450
3331
|
id: randomUUID(),
|
|
@@ -3470,18 +3351,8 @@ async function setChildLayout(pctx, input) {
|
|
|
3470
3351
|
async function setRole(pctx, input) {
|
|
3471
3352
|
const { deviceId, role } = input;
|
|
3472
3353
|
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
const { meta: m } = persisted;
|
|
3476
|
-
const key = String(deviceId);
|
|
3477
|
-
const allMeta = await pctx.metaStore.readMeta();
|
|
3478
|
-
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
3479
|
-
...allMeta,
|
|
3480
|
-
[key]: {
|
|
3481
|
-
...m,
|
|
3482
|
-
role
|
|
3483
|
-
}
|
|
3484
|
-
} });
|
|
3354
|
+
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setRole: unknown device id=${deviceId}`);
|
|
3355
|
+
await pctx.metaStore.rows.patch(deviceId, { role });
|
|
3485
3356
|
});
|
|
3486
3357
|
pctx.host.ctx.eventBus.emit({
|
|
3487
3358
|
id: randomUUID(),
|
|
@@ -3526,19 +3397,8 @@ async function setDisplay(pctx, input) {
|
|
|
3526
3397
|
const { deviceId, display } = input;
|
|
3527
3398
|
const normalized = display === null ? null : normalizeDisplayOverride(display);
|
|
3528
3399
|
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
const { meta: m } = persisted;
|
|
3532
|
-
const key = String(deviceId);
|
|
3533
|
-
const allMeta = await pctx.metaStore.readMeta();
|
|
3534
|
-
const nextRow = normalized === null ? (({ display: _drop, ...rest }) => rest)(m) : {
|
|
3535
|
-
...m,
|
|
3536
|
-
display: normalized
|
|
3537
|
-
};
|
|
3538
|
-
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
3539
|
-
...allMeta,
|
|
3540
|
-
[key]: nextRow
|
|
3541
|
-
} });
|
|
3400
|
+
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] setDisplay: unknown device id=${deviceId}`);
|
|
3401
|
+
await pctx.metaStore.rows.patch(deviceId, { display: normalized });
|
|
3542
3402
|
});
|
|
3543
3403
|
pctx.host.ctx.eventBus.emit({
|
|
3544
3404
|
id: randomUUID(),
|
|
@@ -3566,7 +3426,7 @@ async function getRoleDisplayDefaults(pctx, _input) {
|
|
|
3566
3426
|
* Replace the per-role display defaults whole-record (full replace). Override
|
|
3567
3427
|
* units are normalized (`normalizeUnit`) at write so the render path always
|
|
3568
3428
|
* looks up canonical spellings. Single-writer top-level key — no lock, no RMW,
|
|
3569
|
-
* no interaction with the
|
|
3429
|
+
* no interaction with the device write lock. Not per-device, so no event
|
|
3570
3430
|
* is emitted; the UI invalidates its own query on mutate.
|
|
3571
3431
|
*/
|
|
3572
3432
|
async function setRoleDisplayDefaults(pctx, input) {
|
|
@@ -3579,7 +3439,7 @@ async function setRoleDisplayDefaults(pctx, input) {
|
|
|
3579
3439
|
/**
|
|
3580
3440
|
* Batched meta pre-seed. Applies every provided field to the
|
|
3581
3441
|
* device's meta row in ONE read-modify-write under a single
|
|
3582
|
-
* `withMetaWriteLock` acquisition (one
|
|
3442
|
+
* `withMetaWriteLock` acquisition (one row write),
|
|
3583
3443
|
* then emits one `DeviceMetaChanged` event per field that was
|
|
3584
3444
|
* supplied — preserving the exact semantics of the individual
|
|
3585
3445
|
* setters (`setName` / `setLocation` / `setType` /
|
|
@@ -3595,24 +3455,15 @@ async function setRoleDisplayDefaults(pctx, input) {
|
|
|
3595
3455
|
async function applyInitialMeta(pctx, input) {
|
|
3596
3456
|
const { deviceId, name, location, type, integrationId, linkDeviceId, role } = input;
|
|
3597
3457
|
await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
const { meta: m } = persisted;
|
|
3601
|
-
const key = String(deviceId);
|
|
3602
|
-
const allMeta = await pctx.metaStore.readMeta();
|
|
3603
|
-
const merged = {
|
|
3604
|
-
...m,
|
|
3458
|
+
if (!await pctx.metaStore.resolvePersistedById(deviceId)) throw new Error(`[device-manager] applyInitialMeta: unknown device id=${deviceId}`);
|
|
3459
|
+
await pctx.metaStore.rows.patch(deviceId, {
|
|
3605
3460
|
...name !== void 0 ? { name } : {},
|
|
3606
3461
|
...location !== void 0 ? { location } : {},
|
|
3607
3462
|
...type !== void 0 ? { type } : {},
|
|
3608
3463
|
...integrationId !== void 0 ? { integrationId } : {},
|
|
3609
3464
|
...linkDeviceId !== void 0 ? { linkDeviceId } : {},
|
|
3610
3465
|
...role !== void 0 ? { role } : {}
|
|
3611
|
-
};
|
|
3612
|
-
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
3613
|
-
...allMeta,
|
|
3614
|
-
[key]: merged
|
|
3615
|
-
} });
|
|
3466
|
+
});
|
|
3616
3467
|
});
|
|
3617
3468
|
const emitMetaChanged = (field, value) => {
|
|
3618
3469
|
pctx.host.ctx.eventBus.emit({
|
|
@@ -3648,10 +3499,9 @@ async function applyInitialMeta(pctx, input) {
|
|
|
3648
3499
|
async function setMetadata(pctx, input) {
|
|
3649
3500
|
const { deviceId, patch } = input;
|
|
3650
3501
|
const result = await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
const
|
|
3654
|
-
const next = { ...map[key] ?? {} };
|
|
3502
|
+
const row = await pctx.metaStore.rows.get(deviceId);
|
|
3503
|
+
if (row === null) throw new Error(`[device-manager] setMetadata: unknown device id=${deviceId}`);
|
|
3504
|
+
const next = { ...row.metadata ?? {} };
|
|
3655
3505
|
let changed = false;
|
|
3656
3506
|
for (const [k, v] of Object.entries(patch)) if (v === null) {
|
|
3657
3507
|
if (k in next) {
|
|
@@ -3664,10 +3514,7 @@ async function setMetadata(pctx, input) {
|
|
|
3664
3514
|
}
|
|
3665
3515
|
if (!changed) return { changed: false };
|
|
3666
3516
|
const hasFields = Object.keys(next).length > 0;
|
|
3667
|
-
|
|
3668
|
-
if (hasFields) updatedMap[key] = next;
|
|
3669
|
-
else delete updatedMap[key];
|
|
3670
|
-
await pctx.settings.writeAddonStore({ deviceMetadata: updatedMap });
|
|
3517
|
+
await pctx.metaStore.rows.patch(deviceId, { metadata: hasFields ? next : null });
|
|
3671
3518
|
return {
|
|
3672
3519
|
changed: true,
|
|
3673
3520
|
finalMeta: hasFields ? next : null
|
|
@@ -3713,15 +3560,7 @@ async function setDisabled(pctx, input) {
|
|
|
3713
3560
|
const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
|
|
3714
3561
|
if (!persisted) throw new Error(`[device-manager] setDisabled: unknown device id=${deviceId}`);
|
|
3715
3562
|
const { meta: m } = persisted;
|
|
3716
|
-
|
|
3717
|
-
const allMeta = await pctx.metaStore.readMeta();
|
|
3718
|
-
await pctx.settings.writeAddonStore({ deviceMeta: {
|
|
3719
|
-
...allMeta,
|
|
3720
|
-
[key]: {
|
|
3721
|
-
...m,
|
|
3722
|
-
disabled
|
|
3723
|
-
}
|
|
3724
|
-
} });
|
|
3563
|
+
await pctx.metaStore.rows.patch(deviceId, { disabled });
|
|
3725
3564
|
return {
|
|
3726
3565
|
changed: (m.disabled ?? false) !== disabled,
|
|
3727
3566
|
integrationId: m.integrationId ?? ""
|
|
@@ -3771,9 +3610,7 @@ async function loadRuntimeState(pctx, input) {
|
|
|
3771
3610
|
* location autocomplete.
|
|
3772
3611
|
*/
|
|
3773
3612
|
async function listLocations(pctx) {
|
|
3774
|
-
const
|
|
3775
|
-
const meta = store.deviceMeta ?? {};
|
|
3776
|
-
const locations = store.locations ?? [];
|
|
3613
|
+
const locations = (await pctx.metaStore.readStore()).locations ?? [];
|
|
3777
3614
|
const seen = /* @__PURE__ */ new Map();
|
|
3778
3615
|
const consider = (raw) => {
|
|
3779
3616
|
if (typeof raw !== "string") return;
|
|
@@ -3783,7 +3620,7 @@ async function listLocations(pctx) {
|
|
|
3783
3620
|
if (!seen.has(key)) seen.set(key, trimmed);
|
|
3784
3621
|
};
|
|
3785
3622
|
for (const label of locations) consider(label);
|
|
3786
|
-
for (const
|
|
3623
|
+
for (const row of await pctx.metaStore.rows.listAll()) consider(row.meta.location);
|
|
3787
3624
|
return [...seen.values()].toSorted((a, b) => a.localeCompare(b, void 0, { sensitivity: "base" }));
|
|
3788
3625
|
}
|
|
3789
3626
|
/**
|
|
@@ -3795,7 +3632,7 @@ async function listLocations(pctx) {
|
|
|
3795
3632
|
async function addLocation(pctx, input) {
|
|
3796
3633
|
const trimmed = input.name.trim();
|
|
3797
3634
|
if (trimmed.length === 0) throw new Error("[device-manager] addLocation: name must be non-empty");
|
|
3798
|
-
const current = (await pctx.
|
|
3635
|
+
const current = (await pctx.metaStore.readStore()).locations ?? [];
|
|
3799
3636
|
if (current.some((l) => l.toLowerCase() === trimmed.toLowerCase())) return;
|
|
3800
3637
|
await pctx.settings.writeAddonStore({ locations: [...current, trimmed] });
|
|
3801
3638
|
}
|
|
@@ -3811,25 +3648,21 @@ async function addLocation(pctx, input) {
|
|
|
3811
3648
|
async function removeLocation(pctx, input) {
|
|
3812
3649
|
const trimmed = input.name.trim();
|
|
3813
3650
|
if (trimmed.length === 0) return;
|
|
3814
|
-
const
|
|
3815
|
-
const current = store.locations ?? [];
|
|
3651
|
+
const current = (await pctx.metaStore.readStore()).locations ?? [];
|
|
3816
3652
|
const remaining = current.filter((l) => l.toLowerCase() !== trimmed.toLowerCase());
|
|
3817
3653
|
if (remaining.length !== current.length) await pctx.settings.writeAddonStore({ locations: remaining });
|
|
3818
3654
|
if (input.cascade !== true) return;
|
|
3819
|
-
const
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
}
|
|
3831
|
-
if (cleared.length === 0) return;
|
|
3832
|
-
await pctx.settings.writeAddonStore({ deviceMeta: updates });
|
|
3655
|
+
const cleared = await pctx.metaStore.withMetaWriteLock(async () => {
|
|
3656
|
+
const out = [];
|
|
3657
|
+
for (const row of await pctx.metaStore.rows.listAll()) {
|
|
3658
|
+
const location = row.meta.location;
|
|
3659
|
+
if (typeof location !== "string") continue;
|
|
3660
|
+
if (location.trim().toLowerCase() !== trimmed.toLowerCase()) continue;
|
|
3661
|
+
await pctx.metaStore.rows.patch(row.meta.id, { location: null });
|
|
3662
|
+
out.push(row.meta.id);
|
|
3663
|
+
}
|
|
3664
|
+
return out;
|
|
3665
|
+
});
|
|
3833
3666
|
for (const deviceId of cleared) pctx.host.ctx.eventBus.emit({
|
|
3834
3667
|
id: randomUUID(),
|
|
3835
3668
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -3846,42 +3679,58 @@ async function removeLocation(pctx, input) {
|
|
|
3846
3679
|
});
|
|
3847
3680
|
}
|
|
3848
3681
|
//#endregion
|
|
3682
|
+
//#region src/builtins/device-manager/device-meta-types.ts
|
|
3683
|
+
/**
|
|
3684
|
+
* Decode the raw `ctx.settings.readAddonStore()` record into {@link AddonStore}.
|
|
3685
|
+
*
|
|
3686
|
+
* A field of the wrong shape reads as ABSENT rather than being trusted through
|
|
3687
|
+
* a cast: the store is JSON on disk and a hand-edit or a partial restore must
|
|
3688
|
+
* cost the caller its default, never a downstream `TypeError`.
|
|
3689
|
+
*/
|
|
3690
|
+
function decodeAddonStore(raw) {
|
|
3691
|
+
const nextDeviceId = raw["nextDeviceId"];
|
|
3692
|
+
const locations = raw["locations"];
|
|
3693
|
+
const roleDisplayDefaults = raw["roleDisplayDefaults"];
|
|
3694
|
+
return {
|
|
3695
|
+
...typeof nextDeviceId === "number" && Number.isFinite(nextDeviceId) ? { nextDeviceId } : {},
|
|
3696
|
+
...Array.isArray(locations) ? { locations: locations.filter((l) => typeof l === "string") } : {},
|
|
3697
|
+
...isRoleDisplayDefaults(roleDisplayDefaults) ? { roleDisplayDefaults } : {}
|
|
3698
|
+
};
|
|
3699
|
+
}
|
|
3700
|
+
function isRoleDisplayDefaults(value) {
|
|
3701
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3702
|
+
}
|
|
3703
|
+
//#endregion
|
|
3849
3704
|
//#region src/builtins/device-manager/device-meta-store.ts
|
|
3850
3705
|
var DeviceMetaStore = class {
|
|
3851
3706
|
settings;
|
|
3852
3707
|
registry;
|
|
3708
|
+
rows;
|
|
3853
3709
|
/** Synchronous ownership cache, keyed by NUMERIC deviceId → owning addonId.
|
|
3854
|
-
* The persisted
|
|
3710
|
+
* The persisted row store is authoritative but reads are async; hub-side
|
|
3855
3711
|
* callers (e.g. `CapabilityRegistry.getNativeProvider` fallback) need
|
|
3856
3712
|
* ownership without awaiting. Kept in sync with every register/remove and
|
|
3857
3713
|
* warmed from persistence on boot. */
|
|
3858
3714
|
idToAddonId = /* @__PURE__ */ new Map();
|
|
3859
|
-
/** Serialises every read-modify-write of
|
|
3860
|
-
*
|
|
3861
|
-
* identical to the former `onInitialize` closure variable. */
|
|
3715
|
+
/** Serialises every read-modify-write of a device row through one promise
|
|
3716
|
+
* chain (see `withMetaWriteLock`). Per-instance state. */
|
|
3862
3717
|
metaWriteChain = Promise.resolve();
|
|
3863
|
-
constructor(settings, registry) {
|
|
3718
|
+
constructor(settings, registry, rows) {
|
|
3864
3719
|
this.settings = settings;
|
|
3865
3720
|
this.registry = registry;
|
|
3721
|
+
this.rows = rows;
|
|
3866
3722
|
}
|
|
3867
3723
|
/** The read currently in flight, or null. Never a settled value — see
|
|
3868
3724
|
* {@link readStore}. */
|
|
3869
3725
|
inFlightRead = null;
|
|
3870
3726
|
/**
|
|
3871
|
-
* The
|
|
3727
|
+
* The addon's own settings row set — `nextDeviceId`, `roleDisplayDefaults`,
|
|
3728
|
+
* `locations`. NOT the fleet: no device has lived here since the flatten.
|
|
3872
3729
|
*
|
|
3873
3730
|
* **Concurrent callers join the read already in flight.** This is not a
|
|
3874
3731
|
* cache and nothing survives settlement: a caller that awaited the running
|
|
3875
3732
|
* promise could not have observed anything older than its result, so the
|
|
3876
|
-
* only thing that changes is cost.
|
|
3877
|
-
* 2026-08-19 boot: `readAddonStore` lands in `SqliteSettingsBackend
|
|
3878
|
-
* .getAllAddon`, which reads and `JSON.parse`s this addon's rows —
|
|
3879
|
-
* 625 KB on the live hub (`deviceMeta` 467 KB + `deviceMetadata` 87 KB +
|
|
3880
|
-
* `deviceIndex` 70 KB) — synchronously, on the hub's event loop. A V8
|
|
3881
|
-
* profile of that boot had hub-main's JS thread 99.9% busy with 64% of it
|
|
3882
|
-
* inside `getAllAddon`, ~89% of that entered here, and every runner's first
|
|
3883
|
-
* store read queued behind it (the notification centre's six parallel reads
|
|
3884
|
-
* all resolved together at t+44.7 s).
|
|
3733
|
+
* only thing that changes is cost.
|
|
3885
3734
|
*
|
|
3886
3735
|
* A rejection is NOT latched: the slot is cleared before the promise
|
|
3887
3736
|
* settles either way, so a failed read costs the joiners that one failure
|
|
@@ -3892,7 +3741,7 @@ var DeviceMetaStore = class {
|
|
|
3892
3741
|
if (existing !== null) return existing;
|
|
3893
3742
|
const read = (async () => {
|
|
3894
3743
|
try {
|
|
3895
|
-
return await this.settings.readAddonStore();
|
|
3744
|
+
return decodeAddonStore(await this.settings.readAddonStore());
|
|
3896
3745
|
} finally {
|
|
3897
3746
|
this.inFlightRead = null;
|
|
3898
3747
|
}
|
|
@@ -3900,41 +3749,6 @@ var DeviceMetaStore = class {
|
|
|
3900
3749
|
this.inFlightRead = read;
|
|
3901
3750
|
return read;
|
|
3902
3751
|
};
|
|
3903
|
-
/**
|
|
3904
|
-
* The three fleet projections from ONE read.
|
|
3905
|
-
*
|
|
3906
|
-
* `listAll` asked for `deviceMeta`, then `deviceMetadata`, then
|
|
3907
|
-
* `deviceIndex` — three SEQUENTIAL awaits, which {@link readStore}'s
|
|
3908
|
-
* in-flight join cannot collapse because each starts after the previous one
|
|
3909
|
-
* settled. Three full 625 KB parses per call, on a call made once per device
|
|
3910
|
-
* lifecycle event during a 974-device boot: 23% of hub-main's CPU.
|
|
3911
|
-
*
|
|
3912
|
-
* It is also ONE snapshot. Three separate reads could straddle a write and
|
|
3913
|
-
* hand back an index that names a device the meta map no longer has.
|
|
3914
|
-
*/
|
|
3915
|
-
readAll = async () => {
|
|
3916
|
-
const store = await this.readStore();
|
|
3917
|
-
return {
|
|
3918
|
-
index: store.deviceIndex ?? {},
|
|
3919
|
-
meta: store.deviceMeta ?? {},
|
|
3920
|
-
metadata: store.deviceMetadata ?? {}
|
|
3921
|
-
};
|
|
3922
|
-
};
|
|
3923
|
-
readIndex = async () => {
|
|
3924
|
-
return (await this.readStore()).deviceIndex ?? {};
|
|
3925
|
-
};
|
|
3926
|
-
readMeta = async () => {
|
|
3927
|
-
return (await this.readStore()).deviceMeta ?? {};
|
|
3928
|
-
};
|
|
3929
|
-
/** Hardware-identity metadata map. Lives in a sibling key on the
|
|
3930
|
-
* device-manager addon store so its writers (`setMetadata`) never
|
|
3931
|
-
* collide with the lifecycle writers on `deviceMeta`
|
|
3932
|
-
* (`registerDevice` / `setName` / `setLocation` / `setDisabled`).
|
|
3933
|
-
* Single-writer per row eliminates the "writer X clobbers writer
|
|
3934
|
-
* Y's field" bug class — `setMetadata` is the only producer. */
|
|
3935
|
-
readMetadataMap = async () => {
|
|
3936
|
-
return (await this.readStore()).deviceMetadata ?? {};
|
|
3937
|
-
};
|
|
3938
3752
|
withMetaWriteLock = async (fn) => {
|
|
3939
3753
|
const previous = this.metaWriteChain;
|
|
3940
3754
|
let release = () => {};
|
|
@@ -3949,31 +3763,36 @@ var DeviceMetaStore = class {
|
|
|
3949
3763
|
release();
|
|
3950
3764
|
}
|
|
3951
3765
|
};
|
|
3766
|
+
/** The whole persisted row for one device, or `null`. */
|
|
3767
|
+
getRow = async (deviceId) => this.rows.get(deviceId);
|
|
3952
3768
|
/**
|
|
3953
3769
|
* Resolve a numeric deviceId to the owning `(addonId, stableId)` pair.
|
|
3954
|
-
*
|
|
3770
|
+
* Reads the device's own row — live IDevice lookup (hub registry) is handled
|
|
3955
3771
|
* separately per call site so callers can decide whether to route to
|
|
3956
3772
|
* an in-process driver or to the cross-process `device-ops` bridge.
|
|
3957
3773
|
* Returns null when no device with that id is known to the hub.
|
|
3958
3774
|
*/
|
|
3959
3775
|
resolvePersistedById = async (deviceId) => {
|
|
3960
|
-
const
|
|
3961
|
-
if (
|
|
3776
|
+
const row = await this.rows.get(deviceId);
|
|
3777
|
+
if (row === null) return null;
|
|
3962
3778
|
return {
|
|
3963
|
-
addonId:
|
|
3964
|
-
stableId:
|
|
3965
|
-
meta:
|
|
3779
|
+
addonId: row.meta.addonId,
|
|
3780
|
+
stableId: row.meta.stableId,
|
|
3781
|
+
meta: row.meta
|
|
3966
3782
|
};
|
|
3967
3783
|
};
|
|
3784
|
+
/** The device's hardware-identity metadata blob, or `null`. */
|
|
3785
|
+
readMetadata = async (deviceId) => {
|
|
3786
|
+
return (await this.rows.get(deviceId))?.metadata ?? null;
|
|
3787
|
+
};
|
|
3968
3788
|
/** Direct children of a device: the union of the live registry's children
|
|
3969
|
-
* and the persisted
|
|
3789
|
+
* and the persisted rows whose `parentDeviceId` is `parentId`, deduplicated
|
|
3970
3790
|
* and excluding self. Shared by the `remove` cascade and the `resetToSource`
|
|
3971
3791
|
* resync purge (#19). */
|
|
3972
3792
|
directChildIds = async (parentId) => {
|
|
3973
3793
|
const ids = /* @__PURE__ */ new Set();
|
|
3974
3794
|
if (this.registry) for (const c of this.registry.getChildren(parentId)) ids.add(c.id);
|
|
3975
|
-
const
|
|
3976
|
-
for (const m of Object.values(meta)) if (m.parentDeviceId === parentId) ids.add(m.id);
|
|
3795
|
+
for (const row of await this.rows.listByParent(parentId)) ids.add(row.meta.id);
|
|
3977
3796
|
ids.delete(parentId);
|
|
3978
3797
|
return [...ids];
|
|
3979
3798
|
};
|
|
@@ -3984,6 +3803,525 @@ var DeviceMetaStore = class {
|
|
|
3984
3803
|
};
|
|
3985
3804
|
};
|
|
3986
3805
|
//#endregion
|
|
3806
|
+
//#region src/builtins/device-manager/device-row-store.ts
|
|
3807
|
+
/**
|
|
3808
|
+
* @durable class=registry owner=device-manager
|
|
3809
|
+
* write="one row per device, written by `allocateDeviceId` (identity placeholder),
|
|
3810
|
+
* `registerDevice` (full reconcile) and every meta setter (single-column patch);
|
|
3811
|
+
* `setMetadata` patches the `metadata` column of the same row"
|
|
3812
|
+
* retention="none — a row goes only when the operator removes the device
|
|
3813
|
+
* (`removeDevice`), or when an integration is deleted and cascades. Bounded by the
|
|
3814
|
+
* fleet an operator configures (974 rows on the reference hub)."
|
|
3815
|
+
*/
|
|
3816
|
+
var DEVICE_ROWS_COLLECTION = "device-manager:devices";
|
|
3817
|
+
/**
|
|
3818
|
+
* Fleet reads pass this explicitly.
|
|
3819
|
+
*
|
|
3820
|
+
* `settings-store.query` applies a DEFAULT row cap of 2 000 when the caller
|
|
3821
|
+
* names no `limit` (`query-bounds.ts`), and truncation there is silent to the
|
|
3822
|
+
* caller — it warns in the engine's log and returns a short list that looks
|
|
3823
|
+
* complete. The reference hub is already at 974 devices; half the default cap
|
|
3824
|
+
* is not a margin worth betting the fleet listing on. 20 000 is the engine's
|
|
3825
|
+
* hard ceiling, so this asks for everything the engine will ever serve in one
|
|
3826
|
+
* call and any future need to page is a loud failure rather than a quiet one.
|
|
3827
|
+
*/
|
|
3828
|
+
var DEVICE_ROWS_FLEET_LIMIT = 2e4;
|
|
3829
|
+
var DEVICE_ROWS_COLUMNS = [
|
|
3830
|
+
{
|
|
3831
|
+
name: "id",
|
|
3832
|
+
type: "TEXT",
|
|
3833
|
+
primaryKey: true,
|
|
3834
|
+
notNull: true
|
|
3835
|
+
},
|
|
3836
|
+
{
|
|
3837
|
+
name: "deviceId",
|
|
3838
|
+
type: "INTEGER",
|
|
3839
|
+
notNull: true
|
|
3840
|
+
},
|
|
3841
|
+
{
|
|
3842
|
+
name: "addonId",
|
|
3843
|
+
type: "TEXT",
|
|
3844
|
+
notNull: true
|
|
3845
|
+
},
|
|
3846
|
+
{
|
|
3847
|
+
name: "stableId",
|
|
3848
|
+
type: "TEXT",
|
|
3849
|
+
notNull: true
|
|
3850
|
+
},
|
|
3851
|
+
{
|
|
3852
|
+
name: "type",
|
|
3853
|
+
type: "TEXT",
|
|
3854
|
+
notNull: true
|
|
3855
|
+
},
|
|
3856
|
+
{
|
|
3857
|
+
name: "name",
|
|
3858
|
+
type: "TEXT",
|
|
3859
|
+
notNull: true
|
|
3860
|
+
},
|
|
3861
|
+
{
|
|
3862
|
+
name: "userNamed",
|
|
3863
|
+
type: "BOOLEAN"
|
|
3864
|
+
},
|
|
3865
|
+
{
|
|
3866
|
+
name: "location",
|
|
3867
|
+
type: "TEXT"
|
|
3868
|
+
},
|
|
3869
|
+
{
|
|
3870
|
+
name: "disabled",
|
|
3871
|
+
type: "BOOLEAN",
|
|
3872
|
+
notNull: true,
|
|
3873
|
+
defaultValue: false
|
|
3874
|
+
},
|
|
3875
|
+
{
|
|
3876
|
+
name: "parentDeviceId",
|
|
3877
|
+
type: "INTEGER"
|
|
3878
|
+
},
|
|
3879
|
+
{
|
|
3880
|
+
name: "registered",
|
|
3881
|
+
type: "BOOLEAN",
|
|
3882
|
+
notNull: true,
|
|
3883
|
+
defaultValue: false
|
|
3884
|
+
},
|
|
3885
|
+
{
|
|
3886
|
+
name: "features",
|
|
3887
|
+
type: "JSON"
|
|
3888
|
+
},
|
|
3889
|
+
{
|
|
3890
|
+
name: "exportFingerprint",
|
|
3891
|
+
type: "TEXT"
|
|
3892
|
+
},
|
|
3893
|
+
{
|
|
3894
|
+
name: "integrationId",
|
|
3895
|
+
type: "TEXT"
|
|
3896
|
+
},
|
|
3897
|
+
{
|
|
3898
|
+
name: "linkDeviceId",
|
|
3899
|
+
type: "INTEGER"
|
|
3900
|
+
},
|
|
3901
|
+
{
|
|
3902
|
+
name: "primaryChildEntityId",
|
|
3903
|
+
type: "TEXT"
|
|
3904
|
+
},
|
|
3905
|
+
{
|
|
3906
|
+
name: "childLayout",
|
|
3907
|
+
type: "JSON"
|
|
3908
|
+
},
|
|
3909
|
+
{
|
|
3910
|
+
name: "role",
|
|
3911
|
+
type: "TEXT"
|
|
3912
|
+
},
|
|
3913
|
+
{
|
|
3914
|
+
name: "display",
|
|
3915
|
+
type: "JSON"
|
|
3916
|
+
},
|
|
3917
|
+
{
|
|
3918
|
+
name: "metadata",
|
|
3919
|
+
type: "JSON"
|
|
3920
|
+
}
|
|
3921
|
+
];
|
|
3922
|
+
var DEVICE_ROWS_INDEXES = [
|
|
3923
|
+
{
|
|
3924
|
+
name: "idx_dm_devices_addon_stable",
|
|
3925
|
+
columns: ["addonId", "stableId"]
|
|
3926
|
+
},
|
|
3927
|
+
{
|
|
3928
|
+
name: "idx_dm_devices_parent",
|
|
3929
|
+
columns: ["parentDeviceId"]
|
|
3930
|
+
},
|
|
3931
|
+
{
|
|
3932
|
+
name: "idx_dm_devices_integration",
|
|
3933
|
+
columns: ["integrationId"]
|
|
3934
|
+
}
|
|
3935
|
+
];
|
|
3936
|
+
/** Adapt `ctx.api.settingsStore` (tRPC namespace) to {@link DeviceRowBackend}. */
|
|
3937
|
+
function deviceRowBackendOf(client) {
|
|
3938
|
+
return {
|
|
3939
|
+
declareCollection: async (input) => {
|
|
3940
|
+
await client.declareCollection.mutate(input);
|
|
3941
|
+
},
|
|
3942
|
+
get: (input) => client.get.query(input),
|
|
3943
|
+
set: async (input) => {
|
|
3944
|
+
await client.set.mutate(input);
|
|
3945
|
+
},
|
|
3946
|
+
query: (input) => client.query.query(input),
|
|
3947
|
+
updateWhere: (input) => client.updateWhere.mutate(input),
|
|
3948
|
+
delete: async (input) => {
|
|
3949
|
+
await client.delete.mutate(input);
|
|
3950
|
+
},
|
|
3951
|
+
count: (input) => client.count.query(input)
|
|
3952
|
+
};
|
|
3953
|
+
}
|
|
3954
|
+
function isPlainObject(value) {
|
|
3955
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3956
|
+
}
|
|
3957
|
+
function readString(value) {
|
|
3958
|
+
return typeof value === "string" ? value : void 0;
|
|
3959
|
+
}
|
|
3960
|
+
function readNumber(value) {
|
|
3961
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
3962
|
+
}
|
|
3963
|
+
function readBoolean(value) {
|
|
3964
|
+
if (typeof value === "boolean") return value;
|
|
3965
|
+
if (value === 1) return true;
|
|
3966
|
+
if (value === 0) return false;
|
|
3967
|
+
}
|
|
3968
|
+
function readStringArray(value) {
|
|
3969
|
+
if (!Array.isArray(value)) return void 0;
|
|
3970
|
+
const out = [];
|
|
3971
|
+
for (const entry of value) if (typeof entry === "string") out.push(entry);
|
|
3972
|
+
return out;
|
|
3973
|
+
}
|
|
3974
|
+
/**
|
|
3975
|
+
* Decode one stored row.
|
|
3976
|
+
*
|
|
3977
|
+
* Returns `null` for a row missing an identity field the rest of the system
|
|
3978
|
+
* treats as an invariant (`deviceId` / `addonId` / `stableId` / `type` /
|
|
3979
|
+
* `name`). A row like that cannot be projected into a `DeviceInfo` and taking
|
|
3980
|
+
* the whole fleet read down for it would be worse — the caller logs the skip.
|
|
3981
|
+
*/
|
|
3982
|
+
function decodeDeviceRow(data) {
|
|
3983
|
+
const deviceId = readNumber(data["deviceId"]);
|
|
3984
|
+
const addonId = readString(data["addonId"]);
|
|
3985
|
+
const stableId = readString(data["stableId"]);
|
|
3986
|
+
const type = readString(data["type"]);
|
|
3987
|
+
const name = readString(data["name"]);
|
|
3988
|
+
if (deviceId === void 0 || addonId === void 0 || stableId === void 0 || type === void 0 || name === void 0) return null;
|
|
3989
|
+
const location = readString(data["location"]);
|
|
3990
|
+
const parentDeviceId = readNumber(data["parentDeviceId"]);
|
|
3991
|
+
const userNamed = readBoolean(data["userNamed"]);
|
|
3992
|
+
const features = readStringArray(data["features"]);
|
|
3993
|
+
const exportFingerprint = readString(data["exportFingerprint"]);
|
|
3994
|
+
const integrationId = readString(data["integrationId"]);
|
|
3995
|
+
const linkDeviceId = readNumber(data["linkDeviceId"]);
|
|
3996
|
+
const primaryChildEntityId = readString(data["primaryChildEntityId"]);
|
|
3997
|
+
const role = readString(data["role"]);
|
|
3998
|
+
const rawChildLayout = data["childLayout"];
|
|
3999
|
+
const rawDisplay = data["display"];
|
|
4000
|
+
const rawMetadata = data["metadata"];
|
|
4001
|
+
return {
|
|
4002
|
+
meta: {
|
|
4003
|
+
id: deviceId,
|
|
4004
|
+
addonId,
|
|
4005
|
+
stableId,
|
|
4006
|
+
type,
|
|
4007
|
+
name,
|
|
4008
|
+
location: location ?? null,
|
|
4009
|
+
disabled: readBoolean(data["disabled"]) ?? false,
|
|
4010
|
+
parentDeviceId: parentDeviceId ?? null,
|
|
4011
|
+
...userNamed !== void 0 ? { userNamed } : {},
|
|
4012
|
+
...features !== void 0 ? { features } : {},
|
|
4013
|
+
...exportFingerprint !== void 0 ? { exportFingerprint } : {},
|
|
4014
|
+
...integrationId !== void 0 ? { integrationId } : {},
|
|
4015
|
+
...linkDeviceId !== void 0 ? { linkDeviceId } : {},
|
|
4016
|
+
...primaryChildEntityId !== void 0 ? { primaryChildEntityId } : {},
|
|
4017
|
+
...isChildLayout(rawChildLayout) ? { childLayout: rawChildLayout } : {},
|
|
4018
|
+
...role !== void 0 ? { role } : {},
|
|
4019
|
+
...isPlainObject(rawDisplay) ? { display: toDisplayOverride(rawDisplay) } : {}
|
|
4020
|
+
},
|
|
4021
|
+
metadata: isPlainObject(rawMetadata) ? rawMetadata : null,
|
|
4022
|
+
registered: readBoolean(data["registered"]) ?? false
|
|
4023
|
+
};
|
|
4024
|
+
}
|
|
4025
|
+
/**
|
|
4026
|
+
* `ChildLayout` is a JSON column: what comes back is whatever was written.
|
|
4027
|
+
* The projection hands it to the UI untouched, so the only thing worth
|
|
4028
|
+
* checking is that it is still the array shape the writer put in.
|
|
4029
|
+
*/
|
|
4030
|
+
function isChildLayout(value) {
|
|
4031
|
+
return Array.isArray(value);
|
|
4032
|
+
}
|
|
4033
|
+
/**
|
|
4034
|
+
* `DeviceDisplayOverride` is likewise stored verbatim in a JSON column. It is
|
|
4035
|
+
* an all-optional record, so any object round-trips; the write path
|
|
4036
|
+
* (`normalizeDisplayOverride`) is what gives it shape.
|
|
4037
|
+
*/
|
|
4038
|
+
function toDisplayOverride(value) {
|
|
4039
|
+
return value;
|
|
4040
|
+
}
|
|
4041
|
+
/** Full-row value for an upsert: identity + every field of the meta record. */
|
|
4042
|
+
function encodeDeviceRow(meta, extra = {}) {
|
|
4043
|
+
const row = {
|
|
4044
|
+
id: String(meta.id),
|
|
4045
|
+
deviceId: meta.id,
|
|
4046
|
+
addonId: meta.addonId,
|
|
4047
|
+
stableId: meta.stableId,
|
|
4048
|
+
type: meta.type,
|
|
4049
|
+
name: meta.name,
|
|
4050
|
+
userNamed: meta.userNamed ?? null,
|
|
4051
|
+
location: meta.location,
|
|
4052
|
+
disabled: meta.disabled,
|
|
4053
|
+
parentDeviceId: meta.parentDeviceId,
|
|
4054
|
+
registered: extra.registered ?? false,
|
|
4055
|
+
features: meta.features === void 0 ? null : [...meta.features],
|
|
4056
|
+
exportFingerprint: meta.exportFingerprint ?? null,
|
|
4057
|
+
integrationId: meta.integrationId ?? null,
|
|
4058
|
+
linkDeviceId: meta.linkDeviceId ?? null,
|
|
4059
|
+
primaryChildEntityId: meta.primaryChildEntityId ?? null,
|
|
4060
|
+
childLayout: meta.childLayout ?? null,
|
|
4061
|
+
role: meta.role ?? null,
|
|
4062
|
+
display: meta.display ?? null
|
|
4063
|
+
};
|
|
4064
|
+
if (extra.metadata !== void 0) row["metadata"] = extra.metadata;
|
|
4065
|
+
return row;
|
|
4066
|
+
}
|
|
4067
|
+
/** Column map for a partial write — only the keys the caller actually named. */
|
|
4068
|
+
function encodeDeviceRowPatch(patch) {
|
|
4069
|
+
const row = {};
|
|
4070
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
4071
|
+
if (value === void 0) continue;
|
|
4072
|
+
row[key] = value;
|
|
4073
|
+
}
|
|
4074
|
+
return row;
|
|
4075
|
+
}
|
|
4076
|
+
/**
|
|
4077
|
+
* Row access for the device fleet. Every method is a single statement against
|
|
4078
|
+
* `device-manager:devices` — there is no in-memory copy of the fleet here and
|
|
4079
|
+
* no cache: a per-device question is a primary-key lookup, a fleet question is
|
|
4080
|
+
* one indexed scan.
|
|
4081
|
+
*/
|
|
4082
|
+
var DeviceRowStore = class {
|
|
4083
|
+
backend;
|
|
4084
|
+
logger;
|
|
4085
|
+
declared = null;
|
|
4086
|
+
constructor(backend, logger) {
|
|
4087
|
+
this.backend = backend;
|
|
4088
|
+
this.logger = logger;
|
|
4089
|
+
}
|
|
4090
|
+
/**
|
|
4091
|
+
* Lazy, idempotent `declareCollection`, memoised on the PROMISE so N
|
|
4092
|
+
* concurrent first-callers issue one declaration rather than N. A rejection
|
|
4093
|
+
* is not latched — the slot is cleared so the next caller retries instead of
|
|
4094
|
+
* inheriting a dead collection forever.
|
|
4095
|
+
*/
|
|
4096
|
+
async declare() {
|
|
4097
|
+
const existing = this.declared;
|
|
4098
|
+
if (existing !== null) return existing;
|
|
4099
|
+
const run = (async () => {
|
|
4100
|
+
try {
|
|
4101
|
+
await this.backend.declareCollection({
|
|
4102
|
+
collection: DEVICE_ROWS_COLLECTION,
|
|
4103
|
+
columns: DEVICE_ROWS_COLUMNS,
|
|
4104
|
+
indexes: DEVICE_ROWS_INDEXES
|
|
4105
|
+
});
|
|
4106
|
+
} catch (err) {
|
|
4107
|
+
this.declared = null;
|
|
4108
|
+
throw err;
|
|
4109
|
+
}
|
|
4110
|
+
})();
|
|
4111
|
+
this.declared = run;
|
|
4112
|
+
return run;
|
|
4113
|
+
}
|
|
4114
|
+
/** One device, by numeric id. `null` when the fleet does not know it. */
|
|
4115
|
+
async get(deviceId) {
|
|
4116
|
+
await this.declare();
|
|
4117
|
+
const raw = await this.backend.get({
|
|
4118
|
+
collection: DEVICE_ROWS_COLLECTION,
|
|
4119
|
+
key: String(deviceId)
|
|
4120
|
+
});
|
|
4121
|
+
if (!isPlainObject(raw)) return null;
|
|
4122
|
+
const decoded = decodeDeviceRow(raw);
|
|
4123
|
+
if (decoded === null) {
|
|
4124
|
+
this.logger.warn("device row skipped — identity fields missing", { tags: { deviceId } });
|
|
4125
|
+
return null;
|
|
4126
|
+
}
|
|
4127
|
+
return decoded;
|
|
4128
|
+
}
|
|
4129
|
+
/** Every device, ordered by numeric id. */
|
|
4130
|
+
async listAll() {
|
|
4131
|
+
return this.list({
|
|
4132
|
+
orderBy: {
|
|
4133
|
+
field: "deviceId",
|
|
4134
|
+
direction: "asc"
|
|
4135
|
+
},
|
|
4136
|
+
limit: DEVICE_ROWS_FLEET_LIMIT
|
|
4137
|
+
});
|
|
4138
|
+
}
|
|
4139
|
+
/**
|
|
4140
|
+
* The device an addon knows as `stableId`, or `null`.
|
|
4141
|
+
*
|
|
4142
|
+
* `(addonId, stableId)` is the addon-facing identity — unique by
|
|
4143
|
+
* construction, since `allocateDeviceId` is the only thing that mints a row
|
|
4144
|
+
* and it returns the existing id for a pair it already knows. A second row
|
|
4145
|
+
* for the pair would be a corruption, so this takes the LOWEST id and says
|
|
4146
|
+
* so rather than picking arbitrarily.
|
|
4147
|
+
*/
|
|
4148
|
+
async findByStableId(addonId, stableId) {
|
|
4149
|
+
const rows = await this.list({
|
|
4150
|
+
where: {
|
|
4151
|
+
addonId,
|
|
4152
|
+
stableId
|
|
4153
|
+
},
|
|
4154
|
+
orderBy: {
|
|
4155
|
+
field: "deviceId",
|
|
4156
|
+
direction: "asc"
|
|
4157
|
+
},
|
|
4158
|
+
limit: 2
|
|
4159
|
+
});
|
|
4160
|
+
const first = rows[0];
|
|
4161
|
+
if (first === void 0) return null;
|
|
4162
|
+
if (rows.length > 1) this.logger.warn("duplicate device rows for one (addonId, stableId) — using the lowest id", {
|
|
4163
|
+
tags: { deviceId: first.meta.id },
|
|
4164
|
+
meta: {
|
|
4165
|
+
addonId,
|
|
4166
|
+
stableId,
|
|
4167
|
+
ids: rows.map((r) => r.meta.id)
|
|
4168
|
+
}
|
|
4169
|
+
});
|
|
4170
|
+
return first;
|
|
4171
|
+
}
|
|
4172
|
+
/** Every device owned by one addon, ordered by numeric id. */
|
|
4173
|
+
async listByAddon(addonId) {
|
|
4174
|
+
return this.list({
|
|
4175
|
+
where: { addonId },
|
|
4176
|
+
orderBy: {
|
|
4177
|
+
field: "deviceId",
|
|
4178
|
+
direction: "asc"
|
|
4179
|
+
},
|
|
4180
|
+
limit: DEVICE_ROWS_FLEET_LIMIT
|
|
4181
|
+
});
|
|
4182
|
+
}
|
|
4183
|
+
/** Every device an integration owns, ordered by numeric id. */
|
|
4184
|
+
async listByIntegration(integrationId) {
|
|
4185
|
+
return this.list({
|
|
4186
|
+
where: { integrationId },
|
|
4187
|
+
orderBy: {
|
|
4188
|
+
field: "deviceId",
|
|
4189
|
+
direction: "asc"
|
|
4190
|
+
},
|
|
4191
|
+
limit: DEVICE_ROWS_FLEET_LIMIT
|
|
4192
|
+
});
|
|
4193
|
+
}
|
|
4194
|
+
/** Direct children of one device, ordered by numeric id. */
|
|
4195
|
+
async listByParent(parentDeviceId) {
|
|
4196
|
+
return this.list({
|
|
4197
|
+
where: { parentDeviceId },
|
|
4198
|
+
orderBy: {
|
|
4199
|
+
field: "deviceId",
|
|
4200
|
+
direction: "asc"
|
|
4201
|
+
},
|
|
4202
|
+
limit: DEVICE_ROWS_FLEET_LIMIT
|
|
4203
|
+
});
|
|
4204
|
+
}
|
|
4205
|
+
/** How many devices the fleet holds. Used to tell "empty store" from "gone device". */
|
|
4206
|
+
async count() {
|
|
4207
|
+
await this.declare();
|
|
4208
|
+
return this.backend.count({ collection: DEVICE_ROWS_COLLECTION });
|
|
4209
|
+
}
|
|
4210
|
+
async list(filter) {
|
|
4211
|
+
await this.declare();
|
|
4212
|
+
const records = await this.backend.query({
|
|
4213
|
+
collection: DEVICE_ROWS_COLLECTION,
|
|
4214
|
+
filter
|
|
4215
|
+
});
|
|
4216
|
+
const out = [];
|
|
4217
|
+
for (const record of records) {
|
|
4218
|
+
const decoded = decodeDeviceRow(record.data);
|
|
4219
|
+
if (decoded === null) {
|
|
4220
|
+
this.logger.warn("device row skipped — identity fields missing", { meta: { rowId: record.id } });
|
|
4221
|
+
continue;
|
|
4222
|
+
}
|
|
4223
|
+
out.push(decoded);
|
|
4224
|
+
}
|
|
4225
|
+
return out;
|
|
4226
|
+
}
|
|
4227
|
+
/** Insert or replace the whole identity row. */
|
|
4228
|
+
async upsert(meta, extra = {}) {
|
|
4229
|
+
await this.declare();
|
|
4230
|
+
await this.backend.set({
|
|
4231
|
+
collection: DEVICE_ROWS_COLLECTION,
|
|
4232
|
+
key: String(meta.id),
|
|
4233
|
+
value: encodeDeviceRow(meta, extra)
|
|
4234
|
+
});
|
|
4235
|
+
}
|
|
4236
|
+
/**
|
|
4237
|
+
* Write the named columns of an EXISTING row and nothing else.
|
|
4238
|
+
*
|
|
4239
|
+
* An UPDATE, deliberately not an upsert: a partial upsert would try to INSERT
|
|
4240
|
+
* a row carrying only the patched columns, and SQLite rejects that on the
|
|
4241
|
+
* `NOT NULL` identity columns before the primary-key conflict can turn it
|
|
4242
|
+
* into an update. Every caller resolves the row under the write lock and
|
|
4243
|
+
* throws when it is gone, so matching zero rows means a device was removed
|
|
4244
|
+
* between the resolve and the write — the patch is lost, and a lost write
|
|
4245
|
+
* that says nothing reads as a write that happened.
|
|
4246
|
+
*/
|
|
4247
|
+
async patch(deviceId, patch) {
|
|
4248
|
+
const columns = encodeDeviceRowPatch(patch);
|
|
4249
|
+
if (Object.keys(columns).length === 0) return;
|
|
4250
|
+
await this.declare();
|
|
4251
|
+
const { updated } = await this.backend.updateWhere({
|
|
4252
|
+
collection: DEVICE_ROWS_COLLECTION,
|
|
4253
|
+
filter: { where: { id: String(deviceId) } },
|
|
4254
|
+
data: columns
|
|
4255
|
+
});
|
|
4256
|
+
if (updated === 0) this.logger.warn("device row patch matched no row — the device was removed under it", {
|
|
4257
|
+
tags: { deviceId },
|
|
4258
|
+
meta: { columns: Object.keys(columns) }
|
|
4259
|
+
});
|
|
4260
|
+
}
|
|
4261
|
+
/**
|
|
4262
|
+
* Insert-or-update the registration columns. See {@link DeviceRegistrationRow}
|
|
4263
|
+
* for why this is a separate, fully-specified statement rather than a patch.
|
|
4264
|
+
*/
|
|
4265
|
+
async upsertRegistration(row) {
|
|
4266
|
+
await this.declare();
|
|
4267
|
+
await this.backend.set({
|
|
4268
|
+
collection: DEVICE_ROWS_COLLECTION,
|
|
4269
|
+
key: String(row.deviceId),
|
|
4270
|
+
value: {
|
|
4271
|
+
deviceId: row.deviceId,
|
|
4272
|
+
addonId: row.addonId,
|
|
4273
|
+
stableId: row.stableId,
|
|
4274
|
+
type: row.type,
|
|
4275
|
+
name: row.name,
|
|
4276
|
+
userNamed: row.userNamed,
|
|
4277
|
+
location: row.location,
|
|
4278
|
+
disabled: row.disabled,
|
|
4279
|
+
parentDeviceId: row.parentDeviceId,
|
|
4280
|
+
registered: row.registered,
|
|
4281
|
+
features: [...row.features],
|
|
4282
|
+
exportFingerprint: row.exportFingerprint
|
|
4283
|
+
}
|
|
4284
|
+
});
|
|
4285
|
+
}
|
|
4286
|
+
/** Drop the device's row. Idempotent. */
|
|
4287
|
+
async remove(deviceId) {
|
|
4288
|
+
await this.declare();
|
|
4289
|
+
await this.backend.delete({
|
|
4290
|
+
collection: DEVICE_ROWS_COLLECTION,
|
|
4291
|
+
key: String(deviceId)
|
|
4292
|
+
});
|
|
4293
|
+
}
|
|
4294
|
+
/**
|
|
4295
|
+
* The retirement door for the three blobs this collection replaced.
|
|
4296
|
+
*
|
|
4297
|
+
* The purge is gated on THIS collection being non-empty, and the count has to
|
|
4298
|
+
* be taken after `declare()` — which is why the owning addon runs it and the
|
|
4299
|
+
* settings engine cannot: at the engine's own boot no addon has declared
|
|
4300
|
+
* anything, so every successor would read as unreadable, forever.
|
|
4301
|
+
*/
|
|
4302
|
+
retiredRowStore() {
|
|
4303
|
+
return {
|
|
4304
|
+
hasRow: async (spec) => {
|
|
4305
|
+
const raw = await this.backend.get({
|
|
4306
|
+
collection: spec.collection,
|
|
4307
|
+
key: spec.row
|
|
4308
|
+
});
|
|
4309
|
+
return raw !== void 0 && raw !== null;
|
|
4310
|
+
},
|
|
4311
|
+
deleteRow: async (spec) => {
|
|
4312
|
+
await this.backend.delete({
|
|
4313
|
+
collection: spec.collection,
|
|
4314
|
+
key: spec.row
|
|
4315
|
+
});
|
|
4316
|
+
},
|
|
4317
|
+
countRows: async (collection) => {
|
|
4318
|
+
await this.declare();
|
|
4319
|
+
return this.backend.count({ collection });
|
|
4320
|
+
}
|
|
4321
|
+
};
|
|
4322
|
+
}
|
|
4323
|
+
};
|
|
4324
|
+
//#endregion
|
|
3987
4325
|
//#region src/builtins/device-manager/runtime-state-persist-gate.ts
|
|
3988
4326
|
/**
|
|
3989
4327
|
* The subset of `blob` that is allowed on disk: slices whose capability
|
|
@@ -4548,12 +4886,21 @@ var DeviceManagerAddon = class extends BaseAddon {
|
|
|
4548
4886
|
}
|
|
4549
4887
|
}))).filter((id) => id !== null);
|
|
4550
4888
|
}
|
|
4889
|
+
/**
|
|
4890
|
+
* Row access for `device-manager:devices`. Built once in `onInitialize`
|
|
4891
|
+
* (which is also where the collection is declared) — every consumer reaches
|
|
4892
|
+
* it through {@link bindingsDeps} or the `ProviderContext`.
|
|
4893
|
+
*/
|
|
4894
|
+
deviceRows = null;
|
|
4551
4895
|
/** Build the dependency context the extracted binding resolvers consume. */
|
|
4552
4896
|
get bindingsDeps() {
|
|
4897
|
+
const rows = this.deviceRows;
|
|
4898
|
+
if (rows === null) throw new Error("[device-manager] device row store not initialized");
|
|
4553
4899
|
return {
|
|
4554
4900
|
ctx: this.ctx,
|
|
4555
4901
|
capabilityRegistry: this.capabilityRegistry,
|
|
4556
|
-
remoteNativeCaps: this.remoteNativeCaps
|
|
4902
|
+
remoteNativeCaps: this.remoteNativeCaps,
|
|
4903
|
+
rows
|
|
4557
4904
|
};
|
|
4558
4905
|
}
|
|
4559
4906
|
async getBindings(input) {
|
|
@@ -4609,17 +4956,23 @@ var DeviceManagerAddon = class extends BaseAddon {
|
|
|
4609
4956
|
if (!ops) throw new Error(`[device-manager] device-ops native provider not found for '${deviceId}'`);
|
|
4610
4957
|
return ops;
|
|
4611
4958
|
};
|
|
4612
|
-
const
|
|
4959
|
+
const settingsStoreApi = this.ctx.api?.settingsStore;
|
|
4960
|
+
if (!settingsStoreApi) throw new Error("[device-manager] settings-store API not available — refusing to serve a fleet it cannot persist");
|
|
4961
|
+
const deviceRows = new DeviceRowStore(deviceRowBackendOf(settingsStoreApi), this.ctx.logger.child("rows"));
|
|
4962
|
+
await deviceRows.declare();
|
|
4963
|
+
this.deviceRows = deviceRows;
|
|
4964
|
+
try {
|
|
4965
|
+
await purgeRetiredSettingsRows(deviceRows.retiredRowStore(), this.ctx.logger.child("RetiredRows"));
|
|
4966
|
+
} catch (err) {
|
|
4967
|
+
this.ctx.logger.warn("retired-row purge failed", { meta: { error: errMsg(err) } });
|
|
4968
|
+
}
|
|
4969
|
+
const metaStore = new DeviceMetaStore(settings, registry, deviceRows);
|
|
4613
4970
|
this.stateMirrorImpl = new DeviceStateMirror(this.ctx);
|
|
4614
4971
|
const stateMirror = this.stateMirrorImpl;
|
|
4615
|
-
const readMeta = metaStore.readMeta;
|
|
4616
4972
|
const resolvePersistedById = metaStore.resolvePersistedById;
|
|
4617
4973
|
const idToAddonId = metaStore.idToAddonId;
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
for (const m of Object.values(meta)) idToAddonId.set(m.id, m.addonId);
|
|
4621
|
-
}
|
|
4622
|
-
const stampIntegrationId$1 = (deviceId, integrationId) => stampIntegrationId(metaStore, settings, this.ctx, deviceId, integrationId);
|
|
4974
|
+
for (const row of await deviceRows.listAll()) idToAddonId.set(row.meta.id, row.meta.addonId);
|
|
4975
|
+
const stampIntegrationId$1 = (deviceId, integrationId) => stampIntegrationId(metaStore, this.ctx, deviceId, integrationId);
|
|
4623
4976
|
const pctx = {
|
|
4624
4977
|
host: this.providerHost,
|
|
4625
4978
|
metaStore,
|