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