@camstack/system 1.2.303 → 1.2.305

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/addon-utils.d.ts +1 -1
  4. package/dist/addon-utils.js +3 -1
  5. package/dist/addon-utils.mjs +2 -2
  6. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  7. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  8. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  9. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  10. package/dist/builtins/alerts/alerts.addon.js +1 -1
  11. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  12. package/dist/builtins/autotrack/index.js +1 -1
  13. package/dist/builtins/autotrack/index.mjs +1 -1
  14. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  15. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  16. package/dist/builtins/camera-grid/index.js +1 -1
  17. package/dist/builtins/camera-grid/index.mjs +1 -1
  18. package/dist/builtins/console-logging/index.js +1 -1
  19. package/dist/builtins/console-logging/index.mjs +1 -1
  20. package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
  21. package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
  22. package/dist/builtins/device-manager/device-extension-linked-slot.d.ts +8 -0
  23. package/dist/builtins/device-manager/device-linked-devices.d.ts +34 -0
  24. package/dist/builtins/device-manager/device-manager.addon.js +188 -22
  25. package/dist/builtins/device-manager/device-manager.addon.mjs +188 -22
  26. package/dist/builtins/doorbell/binding-mirror.d.ts +24 -0
  27. package/dist/builtins/doorbell/doorbell-extension-slot.d.ts +66 -0
  28. package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +14 -0
  29. package/dist/builtins/doorbell/virtual-doorbell.addon.js +307 -38
  30. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +307 -38
  31. package/dist/builtins/hub-forwarder/index.js +1 -1
  32. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  33. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  34. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  35. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  36. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  37. package/dist/builtins/local-network/local-network.addon.js +1 -1
  38. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  39. package/dist/builtins/loki-logging/index.js +1 -1
  40. package/dist/builtins/loki-logging/index.mjs +1 -1
  41. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  42. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  43. package/dist/builtins/platform-probe/index.js +3 -3
  44. package/dist/builtins/platform-probe/index.mjs +3 -3
  45. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  46. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  47. package/dist/builtins/snapshot/index.js +1 -1
  48. package/dist/builtins/snapshot/index.mjs +1 -1
  49. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  50. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  51. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  52. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  53. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  54. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  55. package/dist/builtins/system-config/system-config.addon.js +1 -1
  56. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  57. package/dist/builtins/winston-logging/index.js +1 -1
  58. package/dist/builtins/winston-logging/index.mjs +1 -1
  59. package/dist/{child-cap-dispatch-C1c3rWaX.mjs → child-cap-dispatch-BCJjUtXV.mjs} +9 -1
  60. package/dist/{child-cap-dispatch-De5_HcvR.js → child-cap-dispatch-Be8e2cBP.js} +9 -1
  61. package/dist/{dist-DzHjDpU7.js → dist-BK4iP6ab.js} +2700 -1583
  62. package/dist/{dist-FwpS07w3.mjs → dist-Df1NhKxm.mjs} +2677 -1584
  63. package/dist/download/model-downloader.d.ts +15 -0
  64. package/dist/{event-loop-stall-monitor-GPrVvx14.mjs → event-loop-stall-monitor-EXjzIxDz.mjs} +25 -3
  65. package/dist/{event-loop-stall-monitor-CAsHdMwP.js → event-loop-stall-monitor-ey4mbofz.js} +36 -2
  66. package/dist/index.js +21 -3
  67. package/dist/index.mjs +21 -3
  68. package/dist/kernel/config-manager.d.ts +3 -1
  69. package/dist/{retired-settings-keys-CNcKBSQS.js → retired-settings-keys-DwWMph6F.js} +1 -1
  70. package/dist/{retired-settings-keys-rDPn2MMp.mjs → retired-settings-keys-dC4ckbXJ.mjs} +1 -1
  71. package/package.json +7 -1
@@ -1,5 +1,5 @@
1
- import { $t as siteZoneKey, A as STREAM_PROFILE_META, Bn as EventCategory, Bt as parseStreamParamsFormPatch, G as buildStreamParamsConfigSchema, Jt as runtimeStatePolicyFor, Lt as normalizeUnit, Ot as locationIconKey, Sn as WELL_KNOWN_TAB_MAP, _n as DeviceFeature, d as DAY_NAMES, dn as errMsg, h as DeviceStatusSchema, jn as isDeviceConfigCap, mt as formatMinutes, nt as deviceStateCapability, o as CAMERA_SWITCH_ORDER, ot as enumerateItemArrayFields, pn as BaseAddon, rt as deviceStatusCapability, s as CAP_NAMES_WITH_STATUS, st as enumerateSchemaFields, t as ALL_CAPABILITY_DEFINITIONS, tt as deviceManagerCapability, ut as expandContainer, vn as DeviceRole, wt as legacyAutoLinkIds, x as MINUTES_PER_DAY, y as LocationIconIdSchema, yn as DeviceType, zn as sleep$1 } from "../../dist-FwpS07w3.mjs";
2
- import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-rDPn2MMp.mjs";
1
+ import { A as STREAM_PROFILE_META, Bt as normalizeUnit, Cn as DeviceType, Dt as legacyAutoLinkIds, En as WELL_KNOWN_TAB_MAP, Fn as isDeviceConfigCap, G as buildStreamParamsConfigSchema, Sn as DeviceRole, Un as sleep$1, Ut as parseStreamParamsFormPatch, Wn as EventCategory, Z as createDeviceExtensionProvider, Zt as runtimeStatePolicyFor, _n as BaseAddon, _t as formatMinutes, at as deviceStateCapability, d as DAY_NAMES, h as DeviceStatusSchema, hn as errMsg, it as deviceManagerCapability, jt as locationIconKey, lt as enumerateItemArrayFields, nn as siteZoneKey, o as CAMERA_SWITCH_ORDER, ot as deviceStatusCapability, pt as expandContainer, rt as deviceExtensionCapability, s as CAP_NAMES_WITH_STATUS, t as ALL_CAPABILITY_DEFINITIONS, tt as defineExtensionSlotProvider, ut as enumerateSchemaFields, x as MINUTES_PER_DAY, xn as DeviceFeature, y as LocationIconIdSchema } from "../../dist-Df1NhKxm.mjs";
2
+ import { n as purgeRetiredSettingsRows } from "../../retired-settings-keys-dC4ckbXJ.mjs";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { canonicalDeviceFingerprint } from "@camstack/types/node";
5
5
  //#region src/builtins/device-manager/adoption-job-engine.ts
@@ -2937,6 +2937,47 @@ async function applyLinkedDevicesPatch(pctx, deviceId, patch) {
2937
2937
  await pctx.settings.writeDeviceStore(deviceId, next);
2938
2938
  return { success: true };
2939
2939
  }
2940
+ /**
2941
+ * Which NEW picks a linked-devices write must refuse: the camera itself, a
2942
+ * container (never a pick — its children are, D356 and the file header), and
2943
+ * an id no device has.
2944
+ *
2945
+ * Only ids that are not ALREADY persisted are judged. A list written before
2946
+ * this rule existed may hold a container or a device that has since been
2947
+ * deleted; the editor shows those as unavailable / missing, and keeping them
2948
+ * is not a new link. Removing them is the operator's call, never this
2949
+ * function's (a missing source is shown, never silently dropped).
2950
+ *
2951
+ * Deliberately NOT applied to the settings-section save
2952
+ * (`applyLinkedDevicesPatch` via `DEVICE_STORE_SECTIONS`): that surface has
2953
+ * never refused, its picker never offers a container, and the Locations page
2954
+ * writes the list through it. Slice 1 changes no behaviour on the old path;
2955
+ * both paths persist through `applyLinkedDevicesPatch`.
2956
+ */
2957
+ function refusedLinkedPicks(params) {
2958
+ const kept = new Set(params.persisted);
2959
+ const out = [];
2960
+ for (const deviceId of params.picked) {
2961
+ if (kept.has(deviceId)) continue;
2962
+ if (deviceId === params.selfId) {
2963
+ out.push({
2964
+ deviceId,
2965
+ reason: "self"
2966
+ });
2967
+ continue;
2968
+ }
2969
+ const device = params.fleet.get(deviceId);
2970
+ if (device === void 0) out.push({
2971
+ deviceId,
2972
+ reason: "unknown"
2973
+ });
2974
+ else if (device.type === DeviceType.Container) out.push({
2975
+ deviceId,
2976
+ reason: "container"
2977
+ });
2978
+ }
2979
+ return out;
2980
+ }
2940
2981
  //#endregion
2941
2982
  //#region src/builtins/device-manager/device-store-sections.ts
2942
2983
  /** The device-manager's device-store-backed sections. Add an entry to extend. */
@@ -3926,6 +3967,124 @@ var DeviceEventPropagator = class {
3926
3967
  return chain;
3927
3968
  }
3928
3969
  };
3970
+ //#endregion
3971
+ //#region src/builtins/device-manager/device-extension-linked-slot.ts
3972
+ /** The routing prefix of this owner's `slotRef`s. */
3973
+ var DEVICE_MANAGER_EXTENSION_SOURCE = "device-manager";
3974
+ /** The store the slot reads and writes, named (D62). */
3975
+ var LINKED_DEVICES_AUTHORITY = `device-manager:${LINKED_IDS_CONFIG_KEY}`;
3976
+ /**
3977
+ * The picked ids' ledger rows, in ONE indexed read of the device manager's own
3978
+ * store (`rows.getMany`) — never the whole fleet to label a handful of ids.
3979
+ * An id no device has is absent from the map.
3980
+ */
3981
+ async function pickedDevices(pctx, ids) {
3982
+ const rows = await pctx.metaStore.rows.getMany(ids);
3983
+ const out = /* @__PURE__ */ new Map();
3984
+ for (const [id, row] of rows) out.set(id, {
3985
+ name: row.meta.name,
3986
+ type: row.meta.type
3987
+ });
3988
+ return out;
3989
+ }
3990
+ /** What a camera's picked id is, said once for the editor. */
3991
+ function sourceOf(deviceId, byId) {
3992
+ const device = byId.get(deviceId);
3993
+ if (device === void 0) return {
3994
+ deviceId,
3995
+ name: null,
3996
+ health: "missing",
3997
+ detail: "No device has this id any more — it links nothing until removed."
3998
+ };
3999
+ if (device.type === DeviceType.Container) return {
4000
+ deviceId,
4001
+ name: device.name,
4002
+ health: "unavailable",
4003
+ detail: "A container is never a pick (D356): its children are linked in its place. Pick the children instead."
4004
+ };
4005
+ return {
4006
+ deviceId,
4007
+ name: device.name,
4008
+ health: "ok"
4009
+ };
4010
+ }
4011
+ function describeRefusal(deviceId, reason) {
4012
+ if (reason === "self") return `${deviceId} is the camera itself`;
4013
+ if (reason === "container") return `${deviceId} is a container — pick its children`;
4014
+ return `${deviceId} is not a device`;
4015
+ }
4016
+ async function fleetOf(pctx) {
4017
+ const all = await listAll(pctx, LINK_CANDIDATE_PROJECTION);
4018
+ return new Map(all.map((d) => [d.id, d]));
4019
+ }
4020
+ function candidatesFor(deviceId, byId) {
4021
+ const location = byId.get(deviceId)?.location ?? null;
4022
+ const out = [];
4023
+ for (const device of byId.values()) {
4024
+ if (device.id === deviceId) continue;
4025
+ if (device.parentDeviceId === deviceId) continue;
4026
+ if (device.type === DeviceType.Container) continue;
4027
+ const suggested = location !== null && device.location === location;
4028
+ const parent = device.parentDeviceId === null ? void 0 : byId.get(device.parentDeviceId);
4029
+ out.push({
4030
+ deviceId: device.id,
4031
+ name: device.name,
4032
+ type: device.type,
4033
+ location: device.location,
4034
+ parentName: parent?.name ?? null,
4035
+ suggested,
4036
+ reason: suggested ? "same location" : null
4037
+ });
4038
+ }
4039
+ return out.toSorted((a, b) => Number(b.suggested) - Number(a.suggested) || a.name.localeCompare(b.name));
4040
+ }
4041
+ /** Build the `device-extension` provider the device manager registers. */
4042
+ function createDeviceManagerExtensionProvider(pctx) {
4043
+ return createDeviceExtensionProvider({
4044
+ source: DEVICE_MANAGER_EXTENSION_SOURCE,
4045
+ label: "Device manager",
4046
+ slots: [defineExtensionSlotProvider("linked-devices", {
4047
+ list: async (deviceId) => {
4048
+ const { manualIds } = parseLinkedDevicesConfig(await pctx.settings.readDeviceStore(deviceId));
4049
+ const byId = await pickedDevices(pctx, manualIds);
4050
+ return {
4051
+ authority: LINKED_DEVICES_AUTHORITY,
4052
+ enabled: null,
4053
+ activation: null,
4054
+ value: {
4055
+ slot: "linked-devices",
4056
+ deviceIds: [...manualIds]
4057
+ },
4058
+ sources: manualIds.map((id) => sourceOf(id, byId))
4059
+ };
4060
+ },
4061
+ set: async (deviceId, value) => {
4062
+ const [byId, blob] = await Promise.all([pickedDevices(pctx, value.deviceIds), pctx.settings.readDeviceStore(deviceId)]);
4063
+ const refused = refusedLinkedPicks({
4064
+ selfId: deviceId,
4065
+ picked: value.deviceIds,
4066
+ persisted: parseLinkedDevicesConfig(blob).manualIds,
4067
+ fleet: byId
4068
+ });
4069
+ if (refused.length > 0) {
4070
+ pctx.host.ctx.logger.warn("linked-devices: write refused — nothing persisted", {
4071
+ tags: { deviceId },
4072
+ meta: { refused }
4073
+ });
4074
+ throw new Error(`linked-devices on ${deviceId}: refused — ` + refused.map((r) => describeRefusal(r.deviceId, r.reason)).join("; "));
4075
+ }
4076
+ await applyLinkedDevicesPatch(pctx, deviceId, { [LINKED_IDS_CONFIG_KEY]: value.deviceIds });
4077
+ return {
4078
+ applied: true,
4079
+ reload: "none"
4080
+ };
4081
+ },
4082
+ candidates: async (deviceId) => candidatesFor(deviceId, await fleetOf(pctx))
4083
+ })],
4084
+ hostTypeOf: async (deviceId) => (await pctx.metaStore.resolvePersistedById(deviceId))?.meta.type ?? null,
4085
+ logger: pctx.host.ctx.logger
4086
+ });
4087
+ }
3929
4088
  /** A refusal that names a switch the camera does not offer, rather than one it
3930
4089
  * could not be asked about. The cap documents rejecting the former; anything
3931
4090
  * else is treated as the camera being out of reach, which is the safe reading:
@@ -8044,28 +8203,35 @@ var DeviceManagerAddon = class extends BaseAddon {
8044
8203
  this.propagator.start();
8045
8204
  this.ctx.logger.info("device-event-propagator started");
8046
8205
  }
8047
- return [{
8048
- capability: deviceManagerCapability,
8049
- provider
8050
- }, {
8051
- capability: deviceStateCapability,
8052
- provider: {
8053
- getSnapshot: async (input) => {
8054
- return stateMirror.snapshotForDevice(input.deviceId);
8055
- },
8056
- getCapSlice: async (input) => {
8057
- return stateMirror.capSlice(input.deviceId, input.capName);
8058
- },
8059
- getAllSnapshots: async () => {
8060
- return stateMirror.allSnapshots();
8061
- },
8062
- setCapSlice: async (input) => {
8063
- const { deviceId, capName, slice } = input;
8064
- if (!await resolvePersistedById(deviceId)) throw new Error(`[device-manager] setCapSlice: unknown device id=${deviceId}`);
8065
- if (stateMirror.applySingleCapUpdate(deviceId, capName, slice)) stateMirror.scheduleRuntimeStateDiskWrite(deviceId, settings, capName);
8206
+ return [
8207
+ {
8208
+ capability: deviceManagerCapability,
8209
+ provider
8210
+ },
8211
+ {
8212
+ capability: deviceStateCapability,
8213
+ provider: {
8214
+ getSnapshot: async (input) => {
8215
+ return stateMirror.snapshotForDevice(input.deviceId);
8216
+ },
8217
+ getCapSlice: async (input) => {
8218
+ return stateMirror.capSlice(input.deviceId, input.capName);
8219
+ },
8220
+ getAllSnapshots: async () => {
8221
+ return stateMirror.allSnapshots();
8222
+ },
8223
+ setCapSlice: async (input) => {
8224
+ const { deviceId, capName, slice } = input;
8225
+ if (!await resolvePersistedById(deviceId)) throw new Error(`[device-manager] setCapSlice: unknown device id=${deviceId}`);
8226
+ if (stateMirror.applySingleCapUpdate(deviceId, capName, slice)) stateMirror.scheduleRuntimeStateDiskWrite(deviceId, settings, capName);
8227
+ }
8066
8228
  }
8229
+ },
8230
+ {
8231
+ capability: deviceExtensionCapability,
8232
+ provider: createDeviceManagerExtensionProvider(pctx)
8067
8233
  }
8068
- }];
8234
+ ];
8069
8235
  }
8070
8236
  async onShutdown() {
8071
8237
  this.propagator?.stop();
@@ -113,3 +113,27 @@ export type DurableBindingVerdict = 'bound' | 'unbound' | 'unreadable';
113
113
  * from an empty snapshot, whereas a genuine unbind leaves the key present.
114
114
  */
115
115
  export declare function readDurableWrapperBinding(record: unknown, deviceId: number, capName: string, ownAddonId: string): DurableBindingVerdict;
116
+ /**
117
+ * What a press on this camera would do right now.
118
+ *
119
+ * `rings` — the binding is confirmed (mirror `bound`, or the durable store
120
+ * rescued an `unbound` mirror). `rings-pending` — one negative read, not yet
121
+ * believed. `rings-unconfirmed` — no read has happened yet; the ring path errs
122
+ * open. `unknown` — the mirror says unbound but the durable store could not
123
+ * be read: a press RINGS (an unanswerable store never destroys a one-shot
124
+ * press), and nobody can say whether that is right. `silent` — the durable
125
+ * store agrees with an unbound mirror: the only verdict that drops a press.
126
+ */
127
+ export type RingVerdict = 'rings' | 'rings-pending' | 'rings-unconfirmed' | 'unknown' | 'silent';
128
+ /** Does this mirror state need the durable authority before a verdict? Only `unbound` does. */
129
+ export declare function needsDurableRead(state: DoorbellBindingState | undefined): boolean;
130
+ /**
131
+ * THE decision — the ring gate (`fireDoorbell` / `ringsDespiteUnboundMirror`)
132
+ * and the Extensions card's activation both call it, so what the card says
133
+ * and what a press does cannot disagree. `durable` is `null` when it was not
134
+ * read, which {@link needsDurableRead} allows for every state but `unbound`.
135
+ * Pure: it reads nothing and writes nothing.
136
+ */
137
+ export declare function ringVerdict(state: DoorbellBindingState | undefined, durable: DurableBindingVerdict | null): RingVerdict;
138
+ /** Whether a press proceeds under this verdict. */
139
+ export declare function verdictRings(verdict: RingVerdict): boolean;
@@ -0,0 +1,66 @@
1
+ import { ExtensionSlotActivation, IDeviceExtensionProvider, IScopedLogger } from '@camstack/types';
2
+ import { DoorbellBindingState, DurableBindingVerdict, RingVerdict } from './binding-mirror.js';
3
+ import { DoorbellSource } from './doorbell-settings.js';
4
+ /** The routing prefix of this owner's `slotRef`s. */
5
+ export declare const VIRTUAL_DOORBELL_EXTENSION_SOURCE = "virtual-doorbell";
6
+ /** The store the slot reads and writes, named (D62). */
7
+ export declare const DOORBELL_SOURCES_AUTHORITY = "virtual-doorbell:doorbellSources";
8
+ /** The authority that decides whether the wrapper serves a camera at all. */
9
+ export declare const WRAPPER_ACTIVATION_AUTHORITY = "device-manager:setWrapperActive";
10
+ /** The compact device row this slot needs from the device manager. */
11
+ export interface DoorbellSlotDevice {
12
+ readonly id: number;
13
+ readonly name: string;
14
+ readonly type: string;
15
+ readonly location: string | null;
16
+ readonly parentDeviceId: number | null;
17
+ }
18
+ /** What the slot needs from the addon — its store, its mirror, and the fleet's reads. */
19
+ export interface DoorbellSlotDeps {
20
+ readonly ownAddonId: string;
21
+ readonly logger: IScopedLogger;
22
+ readonly readSources: (deviceId: number) => Promise<readonly DoorbellSource[]>;
23
+ /** The addon's own settings-save path (normalise, store, invalidate). */
24
+ readonly persistSources: (deviceId: number, deviceIds: readonly number[]) => Promise<void>;
25
+ /** The host's own row — ONE read, for its type. */
26
+ readonly getDevice: (deviceId: number) => Promise<DoorbellSlotDevice | null>;
27
+ /** Several devices in ONE read. Ids no device has are simply absent. */
28
+ readonly listDevices: (deviceIds: readonly number[]) => Promise<readonly DoorbellSlotDevice[]>;
29
+ /** The D49 mirror's verdict for this camera, or undefined when it has none. */
30
+ readonly readBindingMirror: (deviceId: number) => DoorbellBindingState | undefined;
31
+ /**
32
+ * The DURABLE authority, read-only — the same read the ring gate makes
33
+ * before it drops a press. Never throws: a store that cannot answer is
34
+ * `unreadable`, which carries no information.
35
+ */
36
+ readonly readDurableBinding: (deviceId: number) => Promise<DurableBindingVerdict>;
37
+ readonly sourceCapableDeviceIds: () => Promise<readonly number[]>;
38
+ readonly listFleet: () => Promise<readonly DoorbellSlotDevice[]>;
39
+ }
40
+ /** How the card SAYS a verdict. The verdict itself is `ringVerdict`'s, never the card's. */
41
+ export declare function activationFromVerdict(verdict: RingVerdict): ExtensionSlotActivation;
42
+ /**
43
+ * The card's activation — READ-ONLY (D648 fix round 3).
44
+ *
45
+ * The verdict is `ringVerdict`, the function the ring gate decides with, fed
46
+ * the addon's mirror as it stands. The card never refreshes the mirror and
47
+ * never writes it: the mirror gates the ring, and a view that could move it
48
+ * would let a page refetch during a store blip silence a doorbell. The only
49
+ * read the card may make is the durable authority, and only where the ring
50
+ * gate would make it too (`needsDurableRead`: an `unbound` mirror). A camera
51
+ * the mirror has never seen costs nothing — the ring path errs open there and
52
+ * so does the card.
53
+ */
54
+ /** The card's durable read for one camera, shared by every view asking at once. */
55
+ type DurableReader = (deviceId: number) => Promise<DurableBindingVerdict>;
56
+ /**
57
+ * Concurrent card views of the same `unbound` camera share ONE durable read,
58
+ * per camera, for as long as it is in flight — a page with several viewers
59
+ * (or a refetch storm) must not multiply the store round trip. Only the
60
+ * CARD's reads are shared: the ring gate calls the addon's
61
+ * `readDurableAuthority` on its own path and never waits behind a card.
62
+ */
63
+ export declare function shareDurableReads(read: DurableReader): DurableReader;
64
+ /** Build the `device-extension` provider the virtual doorbell registers. */
65
+ export declare function createDoorbellExtensionProvider(deps: DoorbellSlotDeps): IDeviceExtensionProvider;
66
+ export {};
@@ -53,6 +53,11 @@ export declare class VirtualDoorbellAddon extends BaseAddon<VirtualDoorbellAddon
53
53
  private readonly bindingMirror;
54
54
  constructor();
55
55
  protected onInitialize(): Promise<ProviderRegistration[]>;
56
+ /**
57
+ * The `device-extension` provider: the same store and the same save path as
58
+ * the settings section below (see `doorbell-extension-slot.ts`).
59
+ */
60
+ private buildExtensionProvider;
56
61
  private handleStateChanged;
57
62
  /**
58
63
  * Say out loud every case where a rise did NOT become a ring. Silence
@@ -188,5 +193,14 @@ export declare class VirtualDoorbellAddon extends BaseAddon<VirtualDoorbellAddon
188
193
  */
189
194
  private buildDeviceSettingsContribution;
190
195
  private saveDeviceSettingsPatch;
196
+ /**
197
+ * THE write path for a camera's trigger sources — the settings save above
198
+ * and the `doorbell-source` extension slot both land here (D648), so the
199
+ * two surfaces cannot store different shapes: normalise to
200
+ * `doorbellSources: [{deviceId}]`, strip the legacy keys, and invalidate the
201
+ * assignment cache so the next rise (and the binding mirror's next
202
+ * refresh, off the ring path) sees the new wiring.
203
+ */
204
+ private persistDoorbellSources;
191
205
  }
192
206
  export default VirtualDoorbellAddon;