@camstack/server 1.2.354 → 1.2.356
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/api/capabilities.router.js +10 -20
- package/dist/api/core/capabilities.router.js +10 -20
- package/dist/api/trpc/cap-router-runtime.js +42 -3
- package/dist/api/trpc/device-collection-dispatch.js +513 -0
- package/dist/api/trpc/generated-cap-mounts.js +8 -9
- package/dist/api/trpc/generated-cap-routers.js +11 -2
- package/package.json +8 -8
|
@@ -205,25 +205,15 @@ function createCapabilitiesRouter(addonRegistry, configService, loggingService)
|
|
|
205
205
|
const overrides = registry.getDeviceOverrides(input.deviceId);
|
|
206
206
|
return Object.fromEntries(overrides);
|
|
207
207
|
}),
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
clearDeviceCollectionFilter: trpc_middleware_1.adminProcedure
|
|
219
|
-
.input(zod_1.z.object({ deviceId: zod_1.z.string(), capability: zod_1.z.string() }))
|
|
220
|
-
.mutation(({ input }) => {
|
|
221
|
-
const registry = addonRegistry.getCapabilityRegistry();
|
|
222
|
-
registry.clearDeviceCollectionFilter(input.deviceId, input.capability);
|
|
223
|
-
logger.info('Device collection filter cleared', {
|
|
224
|
-
tags: { deviceId: Number(input.deviceId) },
|
|
225
|
-
meta: { capability: input.capability },
|
|
226
|
-
});
|
|
227
|
-
}),
|
|
208
|
+
/*
|
|
209
|
+
* `setDeviceCollectionFilter` / `clearDeviceCollectionFilter` were REMOVED
|
|
210
|
+
* here (D556). They wrote an in-memory per-device allow list that nothing
|
|
211
|
+
* persisted, so an operator's pick survived until the next hub restart and
|
|
212
|
+
* then silently reverted — a control that looks like it works, which is the
|
|
213
|
+
* defect D62 exists for. Which sources a camera has is now asked of the ONE
|
|
214
|
+
* authority that already owned a per-camera function switch:
|
|
215
|
+
* `deviceManager.setWrapperActive`. The registry keeps only its projection,
|
|
216
|
+
* written by `device-source-activation.ts` and by nothing else.
|
|
217
|
+
*/
|
|
228
218
|
});
|
|
229
219
|
}
|
|
@@ -114,25 +114,15 @@ function createCapabilitiesRouter(registry, config) {
|
|
|
114
114
|
}
|
|
115
115
|
return result;
|
|
116
116
|
}),
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
registry.setDeviceCollectionFilter(input.deviceId, input.capability, input.addonIds);
|
|
128
|
-
}),
|
|
129
|
-
clearDeviceCollectionFilter: trpc_middleware_js_1.adminProcedure
|
|
130
|
-
.input(zod_1.z.object({ deviceId: zod_1.z.string(), capability: zod_1.z.string() }))
|
|
131
|
-
.output(zod_1.z.void())
|
|
132
|
-
.mutation(({ input }) => {
|
|
133
|
-
if (!registry)
|
|
134
|
-
throw new Error('Capability registry unavailable');
|
|
135
|
-
registry.clearDeviceCollectionFilter(input.deviceId, input.capability);
|
|
136
|
-
}),
|
|
117
|
+
/*
|
|
118
|
+
* `setDeviceCollectionFilter` / `clearDeviceCollectionFilter` were REMOVED
|
|
119
|
+
* here (D556). They wrote an in-memory per-device allow list that nothing
|
|
120
|
+
* persisted, so an operator's pick survived until the next hub restart and
|
|
121
|
+
* then silently reverted — a control that looks like it works, which is the
|
|
122
|
+
* defect D62 exists for. Which sources a camera has is now asked of the ONE
|
|
123
|
+
* authority that already owned a per-camera function switch:
|
|
124
|
+
* `deviceManager.setWrapperActive`. The registry keeps only its projection,
|
|
125
|
+
* written by `device-source-activation.ts` and by nothing else.
|
|
126
|
+
*/
|
|
137
127
|
});
|
|
138
128
|
}
|
|
@@ -28,8 +28,10 @@ exports.createCapRouterPrimitives = createCapRouterPrimitives;
|
|
|
28
28
|
exports.createCapRouterServices = createCapRouterServices;
|
|
29
29
|
exports.buildRuntimeCapRouters = buildRuntimeCapRouters;
|
|
30
30
|
const system_1 = require("@camstack/system");
|
|
31
|
+
const types_1 = require("@camstack/types");
|
|
31
32
|
const server_1 = require("@trpc/server");
|
|
32
33
|
const cap_mount_helpers_js_1 = require("./cap-mount-helpers.js");
|
|
34
|
+
const device_collection_dispatch_js_1 = require("./device-collection-dispatch.js");
|
|
33
35
|
const trpc_middleware_js_1 = require("./trpc.middleware.js");
|
|
34
36
|
/**
|
|
35
37
|
* Pre-computed set of capName → array-output method names for collection
|
|
@@ -41,7 +43,10 @@ exports.COLLECTION_ARRAY_METHODS = computeArrayMethods();
|
|
|
41
43
|
function computeArrayMethods() {
|
|
42
44
|
const out = new Map();
|
|
43
45
|
for (const def of system_1.ALL_CAPABILITY_DEFINITIONS) {
|
|
44
|
-
|
|
46
|
+
// The effective MOUNT, never `mode`: a device-scoped wrapper collection
|
|
47
|
+
// (`videoclips`, D554) fans over ONE DEVICE's sources through the
|
|
48
|
+
// per-device dispatcher, and must never be fanned globally here.
|
|
49
|
+
if ((0, types_1.resolveCapMount)(def).kind !== 'collection')
|
|
45
50
|
continue;
|
|
46
51
|
const names = [];
|
|
47
52
|
for (const [name, schema] of Object.entries(def.methods)) {
|
|
@@ -56,6 +61,22 @@ function computeArrayMethods() {
|
|
|
56
61
|
}
|
|
57
62
|
return out;
|
|
58
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Is this cap served by the GLOBAL collection mount — every registered
|
|
66
|
+
* provider, for every caller?
|
|
67
|
+
*
|
|
68
|
+
* `mode: 'collection'` alone does not answer it any more: a device-scoped
|
|
69
|
+
* wrapper collection keeps the per-device mount (D554,
|
|
70
|
+
* `device-collection-dispatch.ts`), and fanning it globally here would answer
|
|
71
|
+
* one camera with another camera's sources on the `ctx.api` path while the
|
|
72
|
+
* tRPC path resolved per device — two authorities disagreeing about the same
|
|
73
|
+
* call, which is exactly what {@link aggregateCollectionCall} exists to
|
|
74
|
+
* prevent.
|
|
75
|
+
*/
|
|
76
|
+
function isGlobalCollection(reg, capName) {
|
|
77
|
+
const def = reg.getDefinition(capName);
|
|
78
|
+
return def !== undefined && (0, types_1.resolveCapMount)(def).kind === 'collection';
|
|
79
|
+
}
|
|
59
80
|
/**
|
|
60
81
|
* Resolve a collection cap's provider exactly as the codegen-emitted
|
|
61
82
|
* mount did:
|
|
@@ -108,7 +129,7 @@ function resolveCollectionProvider(reg, capName, addonId) {
|
|
|
108
129
|
async function aggregateCollectionCall(reg, capName, method, args) {
|
|
109
130
|
if (!reg)
|
|
110
131
|
return null;
|
|
111
|
-
if (reg
|
|
132
|
+
if (!isGlobalCollection(reg, capName))
|
|
112
133
|
return null;
|
|
113
134
|
if (!exports.COLLECTION_ARRAY_METHODS.get(capName)?.includes(method))
|
|
114
135
|
return null;
|
|
@@ -141,7 +162,7 @@ async function aggregateCollectionCall(reg, capName, method, args) {
|
|
|
141
162
|
function addonPinOf(reg, capName, method, args) {
|
|
142
163
|
if (!reg)
|
|
143
164
|
return null;
|
|
144
|
-
if (reg
|
|
165
|
+
if (!isGlobalCollection(reg, capName))
|
|
145
166
|
return null;
|
|
146
167
|
if (exports.COLLECTION_ARRAY_METHODS.get(capName)?.includes(method) === true)
|
|
147
168
|
return null;
|
|
@@ -258,8 +279,26 @@ function createCapRouterServices(deps) {
|
|
|
258
279
|
const factory = deps.serverProviders[capName];
|
|
259
280
|
return factory ? factory(ctx) : null;
|
|
260
281
|
}
|
|
282
|
+
// Both per-device mounts go through the SAME dispatcher. It resolves
|
|
283
|
+
// `getNativeProvider` first (the provider IS the device) and falls
|
|
284
|
+
// through to the D13 resolver `getProviderForDevice` — which is what a
|
|
285
|
+
// `device-scoped` WRAPPER needs: the per-device binding
|
|
286
|
+
// (`setWrapperActive` → `setDeviceOverride`) when there is one, the
|
|
287
|
+
// global active provider when there is not. No second resolver.
|
|
261
288
|
case 'device-native':
|
|
289
|
+
case 'device-scoped': {
|
|
290
|
+
// A device-scoped cap whose mode is `collection` has SEVERAL providers
|
|
291
|
+
// per device — sources listed beside each other (D549 decision 2,
|
|
292
|
+
// D554). `requireDeviceScoped` resolves exactly one of them, and on a
|
|
293
|
+
// collection cap `getProviderForDevice` answers with whichever addon
|
|
294
|
+
// registered FIRST: every camera served by one source, silently. The
|
|
295
|
+
// collection twin fans the array methods over the device's sources and
|
|
296
|
+
// routes the scalar ones by the source inside the id.
|
|
297
|
+
if (reg?.getMode(capName) === 'collection') {
|
|
298
|
+
return (0, device_collection_dispatch_js_1.requireDeviceCollection)(reg, capName, deps.logger);
|
|
299
|
+
}
|
|
262
300
|
return (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, capName);
|
|
301
|
+
}
|
|
263
302
|
case 'collection':
|
|
264
303
|
return resolveCollectionProvider(reg, capName, addonId);
|
|
265
304
|
// singleton / per-node / hub-only resolve the active singleton.
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The per-device COLLECTION dispatcher — a capability whose providers are
|
|
4
|
+
* SOURCES of one device, listed beside each other.
|
|
5
|
+
*
|
|
6
|
+
* `requireDeviceScoped` (`cap-mount-helpers.ts`) resolves ONE provider for a
|
|
7
|
+
* device: the native, else the D13 `getProviderForDevice`. That is right for a
|
|
8
|
+
* cap where a device has one answer. `videoclips` is not that cap any more
|
|
9
|
+
* (D549 decision 2, D554): a camera's clips live in several places at once —
|
|
10
|
+
* the analytics visit log, the camera's own SD card, a hub's storage, HomeKit
|
|
11
|
+
* — and the operator asked for all of them, side by side, never one hidden
|
|
12
|
+
* behind another. `getProviderForDevice` on a collection cap returns the FIRST
|
|
13
|
+
* registered provider, so leaving `videoclips` on it would have answered every
|
|
14
|
+
* camera from whichever addon booted first: the same silent single-answer
|
|
15
|
+
* defect D552 closed, wearing a different hat.
|
|
16
|
+
*
|
|
17
|
+
* The three rules this module implements, and nothing else:
|
|
18
|
+
*
|
|
19
|
+
* 1. **Fan out, per device.** An ARRAY-returning method is called on every
|
|
20
|
+
* source the DEVICE has (`CapabilityRegistry.resolveCollectionForDevice` —
|
|
21
|
+
* the enabled providers, minus the ones the operator switched off for this
|
|
22
|
+
* camera through `deviceManager.setWrapperActive`, D556), and the results
|
|
23
|
+
* are concatenated. No dedup ACROSS SOURCES, no best-source pick: a clip
|
|
24
|
+
* held in two storages appears twice, once per source, and each row says
|
|
25
|
+
* which (D549 decision 11 — stated there as a consequence precisely so it
|
|
26
|
+
* is not "fixed" later). But a source is not a REGISTRATION: one addon
|
|
27
|
+
* reaches this dispatcher as the device's native provider, as the
|
|
28
|
+
* hub-local registration and once per agent node, and those are one
|
|
29
|
+
* source answering three times. Each row is held once, keyed by the
|
|
30
|
+
* identity its provider minted ({@link holdEachRowOnce}); on 592 that was
|
|
31
|
+
* three `analytics` tick-boxes for one thing, D62's defect with the
|
|
32
|
+
* polarity reversed (D554 amended 2026-09-20).
|
|
33
|
+
* 2. **One failing source never empties the others.** Every source is called
|
|
34
|
+
* independently and a rejection removes only its own rows — and says so,
|
|
35
|
+
* per source, with `tags: { deviceId }`. A fan-out where one throwing
|
|
36
|
+
* provider empties the list is worse than no fan-out. For the SOURCE
|
|
37
|
+
* listing itself the failure becomes a row (`unreachable` + the reason):
|
|
38
|
+
* an empty list from a camera nobody could reach reads as "this camera has
|
|
39
|
+
* no recordings", which is the defect this whole line of work is about.
|
|
40
|
+
* 3. **A scalar method routes by the SOURCE inside the id it carries.** Clip
|
|
41
|
+
* ids are self-contained and source-prefixed (`analytics:…`,
|
|
42
|
+
* `native:reolink:onboard:…`, `hksv:…`), so the dispatcher asks each
|
|
43
|
+
* source which ids it owns ({@link SOURCE_LISTING_METHOD}) and hands the
|
|
44
|
+
* call to the one that claims the longest matching prefix. Several
|
|
45
|
+
* registrations of one source all claim it: the FIRST serving one wins,
|
|
46
|
+
* and it is the registration the collapsed listing row names. An id nobody
|
|
47
|
+
* claims is REFUSED by name — never handed to the first provider, which is
|
|
48
|
+
* how a fabricated `addonId` once came back with another addon's answer
|
|
49
|
+
* (D552 consequences, `rejectUnresolvedAddonPin`).
|
|
50
|
+
*
|
|
51
|
+
* A device with exactly ONE source skips 1's merge and 3's probe entirely: its
|
|
52
|
+
* rows are returned verbatim in the provider's own order and its scalar calls
|
|
53
|
+
* go straight through. That is not an optimisation — it is the promise that a
|
|
54
|
+
* camera with one provider behaves exactly as it did before this mount
|
|
55
|
+
* existed, and it is pinned by spec.
|
|
56
|
+
*/
|
|
57
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
58
|
+
exports.SOURCE_LISTING_METHOD = void 0;
|
|
59
|
+
exports.requireDeviceCollection = requireDeviceCollection;
|
|
60
|
+
const types_1 = require("@camstack/types");
|
|
61
|
+
const server_1 = require("@trpc/server");
|
|
62
|
+
/**
|
|
63
|
+
* The device-collection CONVENTION: a cap whose providers are sources declares
|
|
64
|
+
* this method, taking `{ deviceId }` and returning one row per source with a
|
|
65
|
+
* `source` id and an `availability` block. It is what makes rule 3 possible
|
|
66
|
+
* without a second registry of id prefixes — the provider that mints the ids
|
|
67
|
+
* is the one that names them.
|
|
68
|
+
*/
|
|
69
|
+
exports.SOURCE_LISTING_METHOD = 'listSources';
|
|
70
|
+
/** The field on a source row that carries its id / clip-id namespace. */
|
|
71
|
+
const SOURCE_ID_FIELD = 'source';
|
|
72
|
+
/** The field on a source row that names the addon serving it (D555/D557). */
|
|
73
|
+
const SOURCE_ADDON_FIELD = 'addonId';
|
|
74
|
+
/**
|
|
75
|
+
* The input field naming WHICH of a device's providers to ask. One at a time,
|
|
76
|
+
* by `addonId`, never a list of source ids: the operator settled the surface
|
|
77
|
+
* on 2026-09-20 (*"l'utilizzatore è uno solo"*). Absent means the device's
|
|
78
|
+
* bound provider.
|
|
79
|
+
*/
|
|
80
|
+
const PROVIDER_PIN_FIELD = 'provider';
|
|
81
|
+
/** The capability whose singleton answers what a device is bound to. */
|
|
82
|
+
const DEVICE_MANAGER_CAP = 'device-manager';
|
|
83
|
+
/**
|
|
84
|
+
* EVERY source a DEVICE has — read from the device's BINDINGS, which is the
|
|
85
|
+
* framework's answer to "which providers does this device have".
|
|
86
|
+
*
|
|
87
|
+
* It used to enumerate REGISTRATIONS, and that is what put `analytics` in the
|
|
88
|
+
* picker three times on the first live run (device 592, 2026-09-20): one addon
|
|
89
|
+
* reached here as the device's native provider, as the hub-local registration
|
|
90
|
+
* and again as `pipeline-analytics@little-unraid`, with a fourth on the Mac
|
|
91
|
+
* node that `resolveCollectionForDevice` had already dropped. Three tick-boxes
|
|
92
|
+
* for one thing, and `listClips` triplicating every row underneath them.
|
|
93
|
+
*
|
|
94
|
+
* A binding is one per `(device, cap, provider)` and `setWrapperActive` (D62)
|
|
95
|
+
* is the authority over it, so **three rows for one provider stop being
|
|
96
|
+
* representable** — there is nothing to de-duplicate after the fact. It also
|
|
97
|
+
* makes the list mean what the operator says it means: a source exists for a
|
|
98
|
+
* camera because a configuration elsewhere put it there, not because some
|
|
99
|
+
* addon somewhere registered the capability.
|
|
100
|
+
*
|
|
101
|
+
* The binding names an ADDON, never a shape: `DeviceBindingEntry.kind` is UI
|
|
102
|
+
* metadata and is not branched on here (D13). A named addon is looked up in the
|
|
103
|
+
* cap's collection map, and failing that against the device's native provider,
|
|
104
|
+
* which the registry cannot always NAME (a cross-process native arrives through
|
|
105
|
+
* `setNativeFallback` with no addon id of its own) — in that case the binding is
|
|
106
|
+
* the only thing that names it.
|
|
107
|
+
*
|
|
108
|
+
* **A bindings read that FAILS keeps the registration set** (D49). Answering an
|
|
109
|
+
* empty list would read as "this camera has no recordings", which is the defect
|
|
110
|
+
* this whole line of work exists to end, so a failure is logged and the old
|
|
111
|
+
* behaviour stands for that call.
|
|
112
|
+
*/
|
|
113
|
+
async function sourcesForDevice(registry, capName, deviceId, logger) {
|
|
114
|
+
const registered = new Map(registry.getCollectionEntries(capName));
|
|
115
|
+
const bound = await boundAddonIds(registry, capName, deviceId, logger);
|
|
116
|
+
if (bound === null)
|
|
117
|
+
return registrationsForDevice(registry, capName, deviceId);
|
|
118
|
+
const sources = [];
|
|
119
|
+
for (const addonId of bound) {
|
|
120
|
+
const provider = registered.get(addonId) ?? nativeNamedBy(registry, capName, deviceId, addonId);
|
|
121
|
+
if (provider === undefined) {
|
|
122
|
+
// A binding whose provider never booted is a DROPPED source, and a
|
|
123
|
+
// dropped source that says nothing is indistinguishable from a camera
|
|
124
|
+
// that has none.
|
|
125
|
+
logger?.warn(`${capName}: device is bound to "${addonId}", which has registered no provider here`, { tags: { deviceId, addonId }, meta: { capability: capName } });
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
sources.push({ addonId, provider });
|
|
129
|
+
}
|
|
130
|
+
return sources;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The addon ids the device is bound to for `capName`, or `null` when the
|
|
134
|
+
* bindings could not be read at all (never an empty list for that case — the
|
|
135
|
+
* two must not look alike).
|
|
136
|
+
*/
|
|
137
|
+
async function boundAddonIds(registry, capName, deviceId, logger) {
|
|
138
|
+
const manager = registry.getSingleton(DEVICE_MANAGER_CAP);
|
|
139
|
+
if (manager === null || typeof manager.getBindings !== 'function') {
|
|
140
|
+
logger?.warn(`${capName}: no device manager to read this device's bindings from`, {
|
|
141
|
+
tags: { deviceId },
|
|
142
|
+
meta: { capability: capName },
|
|
143
|
+
});
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const answer = await manager.getBindings({ deviceId });
|
|
148
|
+
const ids = [];
|
|
149
|
+
for (const entry of answer.entries) {
|
|
150
|
+
if (entry.capName !== capName)
|
|
151
|
+
continue;
|
|
152
|
+
const addonId = entry.providerAddonId;
|
|
153
|
+
if (addonId.length > 0 && !ids.includes(addonId))
|
|
154
|
+
ids.push(addonId);
|
|
155
|
+
}
|
|
156
|
+
return ids;
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
logger?.warn(`${capName}: could not read this device's bindings — keeping the registrations`, {
|
|
160
|
+
tags: { deviceId },
|
|
161
|
+
meta: { capability: capName, reason: errMessage(err) },
|
|
162
|
+
});
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The device's native provider when the binding's addon id is the one that
|
|
168
|
+
* registered it — or when the registry cannot name it at all, which is how a
|
|
169
|
+
* cross-process native arrives (`setNativeFallback`). The native is not in
|
|
170
|
+
* `state.providers`, so it is asked about separately (D556 decision 5).
|
|
171
|
+
*/
|
|
172
|
+
function nativeNamedBy(registry, capName, deviceId, addonId) {
|
|
173
|
+
const native = registry.getNativeProvider(capName, deviceId);
|
|
174
|
+
if (native === null)
|
|
175
|
+
return undefined;
|
|
176
|
+
const nativeAddonId = registry.getNativeAddonId(capName, deviceId);
|
|
177
|
+
if (nativeAddonId !== null && nativeAddonId !== addonId)
|
|
178
|
+
return undefined;
|
|
179
|
+
return native;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* What this dispatcher did before the bindings became its authority, kept for
|
|
183
|
+
* exactly one caller: the failure path above. Every registered provider this
|
|
184
|
+
* device may use, native first.
|
|
185
|
+
*/
|
|
186
|
+
function registrationsForDevice(registry, capName, deviceId) {
|
|
187
|
+
const admitted = new Set(registry.resolveCollectionForDevice(capName, String(deviceId)));
|
|
188
|
+
const sources = [];
|
|
189
|
+
const seen = new Set();
|
|
190
|
+
const native = registry.getNativeProvider(capName, deviceId);
|
|
191
|
+
if (native !== null) {
|
|
192
|
+
sources.push({
|
|
193
|
+
addonId: registry.getNativeAddonId(capName, deviceId) ?? 'native',
|
|
194
|
+
provider: native,
|
|
195
|
+
});
|
|
196
|
+
seen.add(native);
|
|
197
|
+
}
|
|
198
|
+
for (const [addonId, provider] of registry.getCollectionEntries(capName)) {
|
|
199
|
+
if (!admitted.has(provider) || seen.has(provider))
|
|
200
|
+
continue;
|
|
201
|
+
seen.add(provider);
|
|
202
|
+
sources.push({ addonId, provider });
|
|
203
|
+
}
|
|
204
|
+
return sources;
|
|
205
|
+
}
|
|
206
|
+
/** Is `method` one of the cap's array-returning (fan-out) methods? */
|
|
207
|
+
function isFanOutMethod(registry, capName, method) {
|
|
208
|
+
const def = registry.getDefinition(capName);
|
|
209
|
+
if (def === undefined)
|
|
210
|
+
return false;
|
|
211
|
+
const schema = (0, types_1.expandCapMethods)(def)[method];
|
|
212
|
+
if (schema === undefined || schema.kind === 'subscription')
|
|
213
|
+
return false;
|
|
214
|
+
return (0, types_1.isArrayOutputSchema)(schema.output);
|
|
215
|
+
}
|
|
216
|
+
function errMessage(err) {
|
|
217
|
+
return err instanceof Error ? err.message : String(err);
|
|
218
|
+
}
|
|
219
|
+
/** Call one source, naming the cap, the source and the method on any refusal. */
|
|
220
|
+
async function callSource(source, method, input, capName, deviceId) {
|
|
221
|
+
const fn = source.provider[method];
|
|
222
|
+
if (typeof fn !== 'function') {
|
|
223
|
+
throw new server_1.TRPCError({
|
|
224
|
+
code: 'NOT_IMPLEMENTED',
|
|
225
|
+
message: `Capability "${capName}" source "${source.addonId}" for device ${deviceId} does not implement "${method}"`,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return await fn.call(source.provider, input);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* The `startMs` a row is ordered by, or null when the row does not declare a
|
|
232
|
+
* time range. A merge across sources is newest-first (D549 decision 2's
|
|
233
|
+
* `(startMs, endMs, id)`); rows that carry no window — a source listing, say —
|
|
234
|
+
* keep the order their providers produced them in.
|
|
235
|
+
*/
|
|
236
|
+
function startMsOf(row) {
|
|
237
|
+
if (row === null || typeof row !== 'object')
|
|
238
|
+
return null;
|
|
239
|
+
const range = Reflect.get(row, 'timeRange');
|
|
240
|
+
if (range === null || typeof range !== 'object')
|
|
241
|
+
return null;
|
|
242
|
+
const startMs = Reflect.get(range, 'startMs');
|
|
243
|
+
return typeof startMs === 'number' ? startMs : null;
|
|
244
|
+
}
|
|
245
|
+
function endMsOf(row) {
|
|
246
|
+
if (row === null || typeof row !== 'object')
|
|
247
|
+
return 0;
|
|
248
|
+
const range = Reflect.get(row, 'timeRange');
|
|
249
|
+
if (range === null || typeof range !== 'object')
|
|
250
|
+
return 0;
|
|
251
|
+
const endMs = Reflect.get(range, 'endMs');
|
|
252
|
+
return typeof endMs === 'number' ? endMs : 0;
|
|
253
|
+
}
|
|
254
|
+
function idOf(row) {
|
|
255
|
+
if (row === null || typeof row !== 'object')
|
|
256
|
+
return '';
|
|
257
|
+
const id = Reflect.get(row, 'id');
|
|
258
|
+
return typeof id === 'string' ? id : '';
|
|
259
|
+
}
|
|
260
|
+
/** Newest-first by `(startMs, endMs, id)`; applied only when EVERY row has a window. */
|
|
261
|
+
function mergeRows(rows) {
|
|
262
|
+
if (rows.some((row) => startMsOf(row) === null))
|
|
263
|
+
return rows;
|
|
264
|
+
return [...rows].sort((a, b) => {
|
|
265
|
+
const byStart = (startMsOf(b) ?? 0) - (startMsOf(a) ?? 0);
|
|
266
|
+
if (byStart !== 0)
|
|
267
|
+
return byStart;
|
|
268
|
+
const byEnd = endMsOf(b) - endMsOf(a);
|
|
269
|
+
if (byEnd !== 0)
|
|
270
|
+
return byEnd;
|
|
271
|
+
return idOf(a).localeCompare(idOf(b));
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* The row a source listing gets when the source could not be reached AT ALL.
|
|
276
|
+
*
|
|
277
|
+
* The source id is the addon's, because a provider that threw could not name
|
|
278
|
+
* its own sources — saying "this provider, as a whole, did not answer" is the
|
|
279
|
+
* honest version, and the reason carries the refusal verbatim. Never composed
|
|
280
|
+
* with a provider's output: this row replaces rows that do not exist.
|
|
281
|
+
*/
|
|
282
|
+
function unreachableSourceRow(addonId, reason) {
|
|
283
|
+
return {
|
|
284
|
+
[SOURCE_ID_FIELD]: addonId,
|
|
285
|
+
label: addonId,
|
|
286
|
+
availability: { state: 'unreachable', reason },
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
async function fanOut(sources, method, input, capName, deviceId, logger) {
|
|
290
|
+
const isSourceListing = method === exports.SOURCE_LISTING_METHOD;
|
|
291
|
+
const settled = await Promise.allSettled(sources.map(async (source) => await callSource(source, method, input, capName, deviceId)));
|
|
292
|
+
const rows = [];
|
|
293
|
+
for (const [index, result] of settled.entries()) {
|
|
294
|
+
const source = sources[index];
|
|
295
|
+
if (source === undefined)
|
|
296
|
+
continue;
|
|
297
|
+
if (result.status === 'rejected') {
|
|
298
|
+
const reason = errMessage(result.reason);
|
|
299
|
+
// A branch that drops work says so — a fan-out that silently loses a
|
|
300
|
+
// source is indistinguishable from a camera that has none.
|
|
301
|
+
logger?.warn(`${capName}.${method}: source failed — its rows are missing from this answer`, {
|
|
302
|
+
tags: { deviceId, addonId: source.addonId },
|
|
303
|
+
meta: { capability: capName, method, reason },
|
|
304
|
+
});
|
|
305
|
+
if (isSourceListing)
|
|
306
|
+
rows.push(unreachableSourceRow(source.addonId, reason));
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
if (!Array.isArray(result.value)) {
|
|
310
|
+
logger?.warn(`${capName}.${method}: source answered a non-array — contributing nothing`, {
|
|
311
|
+
tags: { deviceId, addonId: source.addonId },
|
|
312
|
+
meta: { capability: capName, method },
|
|
313
|
+
});
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
rows.push(...finishRows(source, result.value, isSourceListing));
|
|
317
|
+
}
|
|
318
|
+
return mergeRows(rows);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Stamp each source-listing row with the addon that really served it.
|
|
322
|
+
*
|
|
323
|
+
* The stamp is authoritative: a surface cannot otherwise resolve a source row
|
|
324
|
+
* to who answers it, and the alternative — a `native:reolink:* →
|
|
325
|
+
* provider-reolink` prefix table inside the widget — is a second authority on
|
|
326
|
+
* provider identity living in the one package with no business knowing it
|
|
327
|
+
* (D557). The registry's id is a fact; a provider's own value is a claim, so
|
|
328
|
+
* the fact wins.
|
|
329
|
+
*
|
|
330
|
+
* It used to also DROP the rows of a source the operator had switched off, and
|
|
331
|
+
* mark the listing row that let him switch it back on. That whole half went on
|
|
332
|
+
* 2026-09-20: every provider always works, and a source exists for a camera
|
|
333
|
+
* because a binding put it there — so "off" is an unbound provider, not a
|
|
334
|
+
* second store beside the binding (D556/D557 amended). Nothing here filters
|
|
335
|
+
* any more; the filtering happened when {@link sourcesForDevice} read the
|
|
336
|
+
* bindings.
|
|
337
|
+
*/
|
|
338
|
+
function finishRows(source, rows, isSourceListing) {
|
|
339
|
+
if (!isSourceListing)
|
|
340
|
+
return rows;
|
|
341
|
+
const out = [];
|
|
342
|
+
for (const row of rows) {
|
|
343
|
+
if (row === null || typeof row !== 'object') {
|
|
344
|
+
out.push(row);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
out.push({ ...row, [SOURCE_ADDON_FIELD]: source.addonId });
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
async function claimsOf(sources, capName, deviceId, logger) {
|
|
352
|
+
const claimed = new Map();
|
|
353
|
+
const unknownClaims = [];
|
|
354
|
+
const settled = await Promise.allSettled(sources.map(async (source) => await callSource(source, exports.SOURCE_LISTING_METHOD, { deviceId }, capName, deviceId)));
|
|
355
|
+
for (const [index, result] of settled.entries()) {
|
|
356
|
+
const source = sources[index];
|
|
357
|
+
if (source === undefined)
|
|
358
|
+
continue;
|
|
359
|
+
if (result.status === 'rejected' || !Array.isArray(result.value)) {
|
|
360
|
+
// It could not name its sources, so it cannot be excluded on that basis
|
|
361
|
+
// either — a read that FAILS must not destroy work (D49).
|
|
362
|
+
logger?.warn(`${capName}.${exports.SOURCE_LISTING_METHOD}: source could not name its ids — routing falls back to it only if it is the only such source`, {
|
|
363
|
+
tags: { deviceId, addonId: source.addonId },
|
|
364
|
+
meta: {
|
|
365
|
+
capability: capName,
|
|
366
|
+
reason: result.status === 'rejected' ? errMessage(result.reason) : 'non-array answer',
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
unknownClaims.push(source);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
for (const row of result.value) {
|
|
373
|
+
if (row === null || typeof row !== 'object')
|
|
374
|
+
continue;
|
|
375
|
+
const id = Reflect.get(row, SOURCE_ID_FIELD);
|
|
376
|
+
if (typeof id === 'string' && id.length > 0 && !claimed.has(id))
|
|
377
|
+
claimed.set(id, source);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return { claimed, unknownClaims };
|
|
381
|
+
}
|
|
382
|
+
/** Every top-level string value of an object input — the candidate routing keys. */
|
|
383
|
+
function stringFieldsOf(input) {
|
|
384
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input))
|
|
385
|
+
return [];
|
|
386
|
+
const out = [];
|
|
387
|
+
for (const value of Object.values(input)) {
|
|
388
|
+
if (typeof value === 'string' && value.length > 0)
|
|
389
|
+
out.push(value);
|
|
390
|
+
}
|
|
391
|
+
return out;
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* The source that owns a source-prefixed id: `<sourceId>` itself, or anything
|
|
395
|
+
* under `<sourceId>:`. The LONGEST matching source wins, so
|
|
396
|
+
* `native:reolink:onboard` beats a hypothetical `native:reolink`.
|
|
397
|
+
*/
|
|
398
|
+
function ownerOf(claims, input) {
|
|
399
|
+
let best = null;
|
|
400
|
+
for (const value of stringFieldsOf(input)) {
|
|
401
|
+
for (const [sourceId, source] of claims.claimed) {
|
|
402
|
+
if (value !== sourceId && !value.startsWith(`${sourceId}:`))
|
|
403
|
+
continue;
|
|
404
|
+
if (best === null || sourceId.length > best.sourceId.length)
|
|
405
|
+
best = { source, sourceId };
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return best;
|
|
409
|
+
}
|
|
410
|
+
async function dispatchScalar(sources, method, input, capName, deviceId, logger) {
|
|
411
|
+
const claims = await claimsOf(sources, capName, deviceId, logger);
|
|
412
|
+
const owner = ownerOf(claims, input);
|
|
413
|
+
if (owner !== null)
|
|
414
|
+
return await callSource(owner.source, method, input, capName, deviceId);
|
|
415
|
+
const fallback = claims.unknownClaims.length === 1 ? claims.unknownClaims[0] : undefined;
|
|
416
|
+
if (fallback !== undefined) {
|
|
417
|
+
logger?.warn(`${capName}.${method}: no source claimed this id — using the one source that could not name its ids`, {
|
|
418
|
+
tags: { deviceId, addonId: fallback.addonId },
|
|
419
|
+
meta: { capability: capName, method },
|
|
420
|
+
});
|
|
421
|
+
return await callSource(fallback, method, input, capName, deviceId);
|
|
422
|
+
}
|
|
423
|
+
const known = [...claims.claimed.keys()];
|
|
424
|
+
logger?.warn(`${capName}.${method}: refused — no source of this device claims the id`, {
|
|
425
|
+
tags: { deviceId },
|
|
426
|
+
meta: { capability: capName, method, knownSources: known, input },
|
|
427
|
+
});
|
|
428
|
+
throw new server_1.TRPCError({
|
|
429
|
+
code: 'BAD_REQUEST',
|
|
430
|
+
message: `Capability "${capName}.${method}": no source of device ${deviceId} claims ` +
|
|
431
|
+
`${JSON.stringify(input)}. Sources: ${known.length > 0 ? known.join(', ') : '(none named)'}. ` +
|
|
432
|
+
`An unclaimed id is refused rather than answered by another source.`,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* The device's sources narrowed to the one the caller PINNED, if it pinned one.
|
|
437
|
+
*
|
|
438
|
+
* A pin the device is not bound to is REFUSED by name. Answering it from
|
|
439
|
+
* another provider is how a fabricated addon id once came back with somebody
|
|
440
|
+
* else's answer (D552, `rejectUnresolvedAddonPin`), and here it would hand a
|
|
441
|
+
* caller who asked for the camera's own SD card the analytics visit log with
|
|
442
|
+
* nothing saying so.
|
|
443
|
+
*/
|
|
444
|
+
function pinnedProvider(sources, input, capName, deviceId) {
|
|
445
|
+
if (input === null || typeof input !== 'object')
|
|
446
|
+
return sources;
|
|
447
|
+
const pin = Reflect.get(input, PROVIDER_PIN_FIELD);
|
|
448
|
+
if (typeof pin !== 'string' || pin.length === 0)
|
|
449
|
+
return sources;
|
|
450
|
+
const picked = sources.filter((source) => source.addonId === pin);
|
|
451
|
+
if (picked.length > 0)
|
|
452
|
+
return picked;
|
|
453
|
+
throw new server_1.TRPCError({
|
|
454
|
+
code: 'BAD_REQUEST',
|
|
455
|
+
message: `Capability "${capName}": device ${deviceId} is not bound to provider "${pin}". ` +
|
|
456
|
+
`Bound providers: ${sources.map((source) => source.addonId).join(', ') || '(none)'}. ` +
|
|
457
|
+
`A pin nobody answers is refused rather than served by another provider.`,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Build the per-device COLLECTION dispatcher for `capName` — the collection
|
|
462
|
+
* twin of `requireDeviceScoped`, and the provider object the cap router's
|
|
463
|
+
* `device-scoped` mount resolves when the cap's mode is `collection`.
|
|
464
|
+
*
|
|
465
|
+
* Returns null without a registry (the router then surfaces
|
|
466
|
+
* PRECONDITION_FAILED), exactly as the singleton dispatcher does.
|
|
467
|
+
*/
|
|
468
|
+
function requireDeviceCollection(registry, capName, logger) {
|
|
469
|
+
if (!registry)
|
|
470
|
+
return null;
|
|
471
|
+
const dispatcher = new Proxy({}, {
|
|
472
|
+
get(_target, prop) {
|
|
473
|
+
if (typeof prop !== 'string')
|
|
474
|
+
return undefined;
|
|
475
|
+
return async (input) => {
|
|
476
|
+
const deviceId = input !== null && typeof input === 'object' ? Reflect.get(input, 'deviceId') : undefined;
|
|
477
|
+
if (typeof deviceId !== 'number') {
|
|
478
|
+
throw new server_1.TRPCError({
|
|
479
|
+
code: 'BAD_REQUEST',
|
|
480
|
+
message: `${capName}.${prop}: input must carry numeric "deviceId"`,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
const bound = await sourcesForDevice(registry, capName, deviceId, logger);
|
|
484
|
+
const sources = pinnedProvider(bound, input, capName, deviceId);
|
|
485
|
+
if (sources.length === 0) {
|
|
486
|
+
throw new server_1.TRPCError({
|
|
487
|
+
code: 'PRECONDITION_FAILED',
|
|
488
|
+
message: `Capability "${capName}" not registered for device ${deviceId}`,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
const first = sources[0];
|
|
492
|
+
if (isFanOutMethod(registry, capName, prop)) {
|
|
493
|
+
// One source is not a merge: its rows travel verbatim, in its own
|
|
494
|
+
// order, so a camera with one provider answers exactly as it did
|
|
495
|
+
// before this dispatcher existed (D554 decision 6). Only the
|
|
496
|
+
// listing rows are finished, and only to carry the addon stamp.
|
|
497
|
+
if (sources.length === 1 && first !== undefined) {
|
|
498
|
+
const answered = await callSource(first, prop, input, capName, deviceId);
|
|
499
|
+
if (!Array.isArray(answered))
|
|
500
|
+
return answered;
|
|
501
|
+
return finishRows(first, answered, prop === exports.SOURCE_LISTING_METHOD);
|
|
502
|
+
}
|
|
503
|
+
return await fanOut(sources, prop, input, capName, deviceId, logger);
|
|
504
|
+
}
|
|
505
|
+
if (sources.length === 1 && first !== undefined) {
|
|
506
|
+
return await callSource(first, prop, input, capName, deviceId);
|
|
507
|
+
}
|
|
508
|
+
return await dispatchScalar(sources, prop, input, capName, deviceId, logger);
|
|
509
|
+
};
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
return dispatcher;
|
|
513
|
+
}
|
|
@@ -78,7 +78,7 @@ function mountAllCaps(services) {
|
|
|
78
78
|
alarmPanel: (0, generated_cap_routers_js_1.createCapRouter_alarmPanel)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'alarm-panel'), remoteCapProxy),
|
|
79
79
|
alerts: (0, generated_cap_routers_js_1.createCapRouter_alerts)((_ctx) => reg?.getSingleton('alerts') ?? null, remoteCapProxy),
|
|
80
80
|
ambientLightSensor: (0, generated_cap_routers_js_1.createCapRouter_ambientLightSensor)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'ambient-light-sensor'), remoteCapProxy),
|
|
81
|
-
audioAnalysis: (0, generated_cap_routers_js_1.createCapRouter_audioAnalysis)((_ctx) => reg
|
|
81
|
+
audioAnalysis: (0, generated_cap_routers_js_1.createCapRouter_audioAnalysis)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'audio-analysis'), remoteCapProxy),
|
|
82
82
|
audioAnalyzer: (0, generated_cap_routers_js_1.createCapRouter_audioAnalyzer)((_ctx) => reg?.getSingleton('audio-analyzer') ?? null, remoteCapProxy),
|
|
83
83
|
audioCodec: (0, generated_cap_routers_js_1.createCapRouter_audioCodec)((_ctx) => reg?.getSingleton('audio-codec') ?? null, remoteCapProxy),
|
|
84
84
|
audioMetrics: (0, generated_cap_routers_js_1.createCapRouter_audioMetrics)((_ctx) => reg?.getSingleton('audio-metrics') ?? null, remoteCapProxy),
|
|
@@ -107,8 +107,8 @@ function mountAllCaps(services) {
|
|
|
107
107
|
button: (0, generated_cap_routers_js_1.createCapRouter_button)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'button'), remoteCapProxy),
|
|
108
108
|
cameraCredentials: (0, generated_cap_routers_js_1.createCapRouter_cameraCredentials)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'camera-credentials'), remoteCapProxy),
|
|
109
109
|
cameraGridLayout: (0, generated_cap_routers_js_1.createCapRouter_cameraGridLayout)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'camera-grid-layout'), remoteCapProxy),
|
|
110
|
-
cameraPipelineConfig: (0, generated_cap_routers_js_1.createCapRouter_cameraPipelineConfig)((_ctx) => reg
|
|
111
|
-
cameraStreams: (0, generated_cap_routers_js_1.createCapRouter_cameraStreams)((_ctx) => reg
|
|
110
|
+
cameraPipelineConfig: (0, generated_cap_routers_js_1.createCapRouter_cameraPipelineConfig)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'camera-pipeline-config'), remoteCapProxy),
|
|
111
|
+
cameraStreams: (0, generated_cap_routers_js_1.createCapRouter_cameraStreams)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'camera-streams'), remoteCapProxy),
|
|
112
112
|
carbonMonoxide: (0, generated_cap_routers_js_1.createCapRouter_carbonMonoxide)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'carbon-monoxide'), remoteCapProxy),
|
|
113
113
|
climateControl: (0, generated_cap_routers_js_1.createCapRouter_climateControl)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'climate-control'), remoteCapProxy),
|
|
114
114
|
color: (0, generated_cap_routers_js_1.createCapRouter_color)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'color'), remoteCapProxy),
|
|
@@ -162,7 +162,7 @@ function mountAllCaps(services) {
|
|
|
162
162
|
}, remoteCapProxy),
|
|
163
163
|
dayNight: (0, generated_cap_routers_js_1.createCapRouter_dayNight)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'day-night'), remoteCapProxy),
|
|
164
164
|
decoder: (0, generated_cap_routers_js_1.createCapRouter_decoder)((_ctx) => reg?.getSingleton('decoder') ?? null, remoteCapProxy),
|
|
165
|
-
detectionPipeline: (0, generated_cap_routers_js_1.createCapRouter_detectionPipeline)((_ctx) => reg
|
|
165
|
+
detectionPipeline: (0, generated_cap_routers_js_1.createCapRouter_detectionPipeline)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'detection-pipeline'), remoteCapProxy),
|
|
166
166
|
deviceAdoption: (0, generated_cap_routers_js_1.createCapRouter_deviceAdoption)((_ctx) => reg?.getSingleton('device-adoption') ??
|
|
167
167
|
null, remoteCapProxy),
|
|
168
168
|
deviceDiscovery: (0, generated_cap_routers_js_1.createCapRouter_deviceDiscovery)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'device-discovery'), remoteCapProxy),
|
|
@@ -337,8 +337,7 @@ function mountAllCaps(services) {
|
|
|
337
337
|
modelDistributor: (0, generated_cap_routers_js_1.createCapRouter_modelDistributor)((_ctx) => reg?.getSingleton('model-distributor') ??
|
|
338
338
|
null, remoteCapProxy),
|
|
339
339
|
motion: (0, generated_cap_routers_js_1.createCapRouter_motion)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'motion'), remoteCapProxy),
|
|
340
|
-
motionDetection: (0, generated_cap_routers_js_1.createCapRouter_motionDetection)((_ctx) => reg
|
|
341
|
-
null, remoteCapProxy),
|
|
340
|
+
motionDetection: (0, generated_cap_routers_js_1.createCapRouter_motionDetection)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'motion-detection'), remoteCapProxy),
|
|
342
341
|
motionTrigger: (0, generated_cap_routers_js_1.createCapRouter_motionTrigger)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'motion-trigger'), remoteCapProxy),
|
|
343
342
|
motionZones: (0, generated_cap_routers_js_1.createCapRouter_motionZones)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'motion-zones'), remoteCapProxy),
|
|
344
343
|
mqttBroker: (0, generated_cap_routers_js_1.createCapRouter_mqttBroker)((_ctx, addonId) => {
|
|
@@ -412,7 +411,7 @@ function mountAllCaps(services) {
|
|
|
412
411
|
osd: (0, generated_cap_routers_js_1.createCapRouter_osd)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'osd'), remoteCapProxy),
|
|
413
412
|
osdManager: (0, generated_cap_routers_js_1.createCapRouter_osdManager)((_ctx) => reg?.getSingleton('osd-manager') ?? null, remoteCapProxy),
|
|
414
413
|
petFeeder: (0, generated_cap_routers_js_1.createCapRouter_petFeeder)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'pet-feeder'), remoteCapProxy),
|
|
415
|
-
pipelineAnalytics: (0, generated_cap_routers_js_1.createCapRouter_pipelineAnalytics)((_ctx) => reg
|
|
414
|
+
pipelineAnalytics: (0, generated_cap_routers_js_1.createCapRouter_pipelineAnalytics)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'pipeline-analytics'), remoteCapProxy),
|
|
416
415
|
pipelineExecutor: (0, generated_cap_routers_js_1.createCapRouter_pipelineExecutor)((_ctx) => reg?.getSingleton('pipeline-executor') ??
|
|
417
416
|
null, remoteCapProxy),
|
|
418
417
|
pipelineOrchestrator: (0, generated_cap_routers_js_1.createCapRouter_pipelineOrchestrator)((_ctx) => reg?.getSingleton('pipeline-orchestrator') ?? null, remoteCapProxy),
|
|
@@ -431,7 +430,7 @@ function mountAllCaps(services) {
|
|
|
431
430
|
recordingExport: (0, generated_cap_routers_js_1.createCapRouter_recordingExport)((_ctx) => reg?.getSingleton('recording-export') ??
|
|
432
431
|
null, remoteCapProxy),
|
|
433
432
|
recordingSignal: (0, generated_cap_routers_js_1.createCapRouter_recordingSignal)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'recording-signal'), remoteCapProxy),
|
|
434
|
-
sceneMonitor: (0, generated_cap_routers_js_1.createCapRouter_sceneMonitor)((_ctx) => reg
|
|
433
|
+
sceneMonitor: (0, generated_cap_routers_js_1.createCapRouter_sceneMonitor)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'scene-monitor'), remoteCapProxy),
|
|
435
434
|
scriptRunner: (0, generated_cap_routers_js_1.createCapRouter_scriptRunner)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'script-runner'), remoteCapProxy),
|
|
436
435
|
serverManagement: (0, generated_cap_routers_js_1.createCapRouter_serverManagement)((_ctx) => reg?.getSingleton('server-management') ??
|
|
437
436
|
null, remoteCapProxy),
|
|
@@ -542,7 +541,7 @@ function mountAllCaps(services) {
|
|
|
542
541
|
valve: (0, generated_cap_routers_js_1.createCapRouter_valve)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'valve'), remoteCapProxy),
|
|
543
542
|
vectorStore: (0, generated_cap_routers_js_1.createCapRouter_vectorStore)((_ctx) => reg?.getSingleton('vector-store') ?? null, remoteCapProxy),
|
|
544
543
|
vibration: (0, generated_cap_routers_js_1.createCapRouter_vibration)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'vibration'), remoteCapProxy),
|
|
545
|
-
videoclips: (0, generated_cap_routers_js_1.createCapRouter_videoclips)((_ctx) => reg
|
|
544
|
+
videoclips: (0, generated_cap_routers_js_1.createCapRouter_videoclips)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'videoclips'), remoteCapProxy),
|
|
546
545
|
viewerUi: (0, generated_cap_routers_js_1.createCapRouter_viewerUi)((_ctx) => reg?.getSingleton('viewer-ui') ?? null, remoteCapProxy),
|
|
547
546
|
waterHeater: (0, generated_cap_routers_js_1.createCapRouter_waterHeater)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'water-heater'), remoteCapProxy),
|
|
548
547
|
weather: (0, generated_cap_routers_js_1.createCapRouter_weather)((_ctx) => (0, cap_mount_helpers_js_1.requireDeviceScoped)(reg, 'weather'), remoteCapProxy),
|
|
@@ -10733,7 +10733,7 @@ function createCapRouter_vibration(getProvider, createRemoteProxy) {
|
|
|
10733
10733
|
}
|
|
10734
10734
|
function createCapRouter_videoclips(getProvider, createRemoteProxy) {
|
|
10735
10735
|
return (0, trpc_middleware_js_1.trpcRouter)({
|
|
10736
|
-
listClips: trpc_middleware_js_1.
|
|
10736
|
+
listClips: trpc_middleware_js_1.protectedProcedure
|
|
10737
10737
|
.input(types_126.videoclipsCapability.methods.listClips.input.loose())
|
|
10738
10738
|
.output(types_126.videoclipsCapability.methods.listClips.output)
|
|
10739
10739
|
.query(async ({ input, ctx }) => {
|
|
@@ -10742,7 +10742,16 @@ function createCapRouter_videoclips(getProvider, createRemoteProxy) {
|
|
|
10742
10742
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
10743
10743
|
return p.listClips(methodInput);
|
|
10744
10744
|
}),
|
|
10745
|
-
|
|
10745
|
+
listSources: trpc_middleware_js_1.protectedProcedure
|
|
10746
|
+
.input(types_126.videoclipsCapability.methods.listSources.input.loose())
|
|
10747
|
+
.output(types_126.videoclipsCapability.methods.listSources.output)
|
|
10748
|
+
.query(async ({ input, ctx }) => {
|
|
10749
|
+
const { nodeId, ...methodInput } = input;
|
|
10750
|
+
const p = resolveProvider('videoclips', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
10751
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
10752
|
+
return p.listSources(methodInput);
|
|
10753
|
+
}),
|
|
10754
|
+
getClipPlayback: trpc_middleware_js_1.protectedProcedure
|
|
10746
10755
|
.input(types_126.videoclipsCapability.methods.getClipPlayback.input.loose())
|
|
10747
10756
|
.output(types_126.videoclipsCapability.methods.getClipPlayback.output)
|
|
10748
10757
|
.query(async ({ input, ctx }) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.356",
|
|
4
4
|
"private": false,
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -35,18 +35,18 @@
|
|
|
35
35
|
]
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@camstack/addon-admin-ui": "1.2.
|
|
39
|
-
"@camstack/addon-agent-ui": "1.2.
|
|
38
|
+
"@camstack/addon-admin-ui": "1.2.245",
|
|
39
|
+
"@camstack/addon-agent-ui": "1.2.133",
|
|
40
40
|
"@camstack/addon-auth": "1.2.123",
|
|
41
41
|
"@camstack/addon-decoder-nodeav": "1.2.115",
|
|
42
42
|
"@camstack/addon-notifiers": "1.2.119",
|
|
43
|
-
"@camstack/addon-pipeline": "1.2.
|
|
43
|
+
"@camstack/addon-pipeline": "1.2.273",
|
|
44
44
|
"@camstack/addon-pipeline-orchestrator": "1.2.231",
|
|
45
|
-
"@camstack/addon-post-analysis": "1.2.
|
|
45
|
+
"@camstack/addon-post-analysis": "1.2.272",
|
|
46
46
|
"@camstack/sdk": "1.2.118",
|
|
47
|
-
"@camstack/system": "1.2.
|
|
48
|
-
"@camstack/types": "1.2.
|
|
49
|
-
"@camstack/ui-library": "1.2.
|
|
47
|
+
"@camstack/system": "1.2.277",
|
|
48
|
+
"@camstack/types": "1.2.235",
|
|
49
|
+
"@camstack/ui-library": "1.2.191",
|
|
50
50
|
"@fastify/compress": "^9.0.0",
|
|
51
51
|
"@fastify/cookie": "^11.0.2",
|
|
52
52
|
"@fastify/cors": "^11.2.0",
|