@camstack/addon-export-alexa 1.2.22 → 1.2.24
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/export-alexa.addon.js +137 -30
- package/dist/export-alexa.addon.mjs +137 -30
- package/package.json +1 -1
|
@@ -11085,6 +11085,8 @@ var QueryFilterSchema = object({
|
|
|
11085
11085
|
where: record(string(), unknown()).optional(),
|
|
11086
11086
|
whereIn: record(string(), array(unknown())).optional(),
|
|
11087
11087
|
whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
|
|
11088
|
+
/** NULL-safe exclusion: matches rows whose field is NULL OR != the value. */
|
|
11089
|
+
whereNot: record(string(), unknown()).optional(),
|
|
11088
11090
|
orderBy: object({
|
|
11089
11091
|
field: string(),
|
|
11090
11092
|
direction: _enum(["asc", "desc"])
|
|
@@ -11104,7 +11106,8 @@ var QueryFilterSchema = object({
|
|
|
11104
11106
|
var MutationFilterSchema = object({
|
|
11105
11107
|
where: record(string(), unknown()).optional(),
|
|
11106
11108
|
whereIn: record(string(), array(unknown())).optional(),
|
|
11107
|
-
whereBetween: record(string(), tuple([unknown(), unknown()])).optional()
|
|
11109
|
+
whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
|
|
11110
|
+
whereNot: record(string(), unknown()).optional()
|
|
11108
11111
|
});
|
|
11109
11112
|
/** A single stored record: `{ id, data }`. */
|
|
11110
11113
|
var SettingsRecordSchema = object({
|
|
@@ -17011,7 +17014,10 @@ var RecentTracksQueryInput = object({
|
|
|
17011
17014
|
* Encodes the (lastSeen, trackId) sort position — treat as opaque. */
|
|
17012
17015
|
cursor: string().optional(),
|
|
17013
17016
|
/** See {@link TrackProjectionSchema}. Default `full`. */
|
|
17014
|
-
projection: TrackProjectionSchema.optional()
|
|
17017
|
+
projection: TrackProjectionSchema.optional(),
|
|
17018
|
+
/** Include stationary-promoted rows (parked objects). Default false: the
|
|
17019
|
+
* feed lists passages; parking records live on the stationary registry. */
|
|
17020
|
+
includeStationary: boolean().optional()
|
|
17015
17021
|
});
|
|
17016
17022
|
var RecentTracksPageSchema = object({
|
|
17017
17023
|
/** Merged page, ordered by (`lastSeen` DESC, `trackId` DESC). */
|
|
@@ -17229,7 +17235,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
17229
17235
|
zone: TrackZoneFilterSchema.optional(),
|
|
17230
17236
|
/** See {@link TrackProjectionSchema}. Default `full` (backward
|
|
17231
17237
|
* compatible — omitting the field keeps today's exact behaviour). */
|
|
17232
|
-
projection: TrackProjectionSchema.optional()
|
|
17238
|
+
projection: TrackProjectionSchema.optional(),
|
|
17239
|
+
/** Include stationary-promoted rows (parked objects handed to the
|
|
17240
|
+
* stationary registry). Default false: the timeline lists passages,
|
|
17241
|
+
* not parking records (operator decision, 2026-08-15). */
|
|
17242
|
+
includeStationary: boolean().optional()
|
|
17233
17243
|
}), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
|
|
17234
17244
|
kind: "mutation",
|
|
17235
17245
|
auth: "admin"
|
|
@@ -17393,11 +17403,16 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
17393
17403
|
auth: "admin"
|
|
17394
17404
|
}), method(object({
|
|
17395
17405
|
eventId: string(),
|
|
17396
|
-
kind: MediaFileKindEnum.optional()
|
|
17406
|
+
kind: MediaFileKindEnum.optional(),
|
|
17407
|
+
deviceId: number()
|
|
17408
|
+
}), array(MediaFileSchema).readonly()), method(object({
|
|
17409
|
+
trackId: string(),
|
|
17410
|
+
kinds: array(MediaFileKindEnum).optional(),
|
|
17411
|
+
deviceId: number()
|
|
17397
17412
|
}), array(MediaFileSchema).readonly()), method(object({
|
|
17398
17413
|
trackId: string(),
|
|
17399
|
-
|
|
17400
|
-
}), array(
|
|
17414
|
+
deviceId: number()
|
|
17415
|
+
}), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
|
|
17401
17416
|
kind: "mutation",
|
|
17402
17417
|
auth: "admin"
|
|
17403
17418
|
}), method(RebuildObjectEmbeddingsInput, RebuildObjectEmbeddingsResultSchema, {
|
|
@@ -19376,7 +19391,16 @@ targets: array(object({
|
|
|
19376
19391
|
/** A sleeping battery camera: the frame is deliberately stale and will
|
|
19377
19392
|
* NOT refresh in the background. A surface should say so rather than
|
|
19378
19393
|
* present it as current. */
|
|
19379
|
-
sleeping: boolean()
|
|
19394
|
+
sleeping: boolean(),
|
|
19395
|
+
/** Current device state rendered over the cached frame. State images
|
|
19396
|
+
* remain authoritative even when their photographic background is
|
|
19397
|
+
* old; null means the link must carry a current camera frame. */
|
|
19398
|
+
stateReason: _enum([
|
|
19399
|
+
"disabled",
|
|
19400
|
+
"sleeping",
|
|
19401
|
+
"unreachable",
|
|
19402
|
+
"waking"
|
|
19403
|
+
]).nullable()
|
|
19380
19404
|
})));
|
|
19381
19405
|
/**
|
|
19382
19406
|
* `sso-bridge` — internal hub-only cap that lets SSO-style auth
|
|
@@ -32634,6 +32658,11 @@ Object.freeze({
|
|
|
32634
32658
|
form: "single",
|
|
32635
32659
|
optional: false
|
|
32636
32660
|
}],
|
|
32661
|
+
"pipelineAnalytics.getEventMedia": [{
|
|
32662
|
+
name: "deviceId",
|
|
32663
|
+
form: "single",
|
|
32664
|
+
optional: false
|
|
32665
|
+
}],
|
|
32637
32666
|
"pipelineAnalytics.getKeyEvents": [{
|
|
32638
32667
|
name: "deviceId",
|
|
32639
32668
|
form: "single",
|
|
@@ -32664,6 +32693,11 @@ Object.freeze({
|
|
|
32664
32693
|
form: "single",
|
|
32665
32694
|
optional: false
|
|
32666
32695
|
}],
|
|
32696
|
+
"pipelineAnalytics.getTrackMedia": [{
|
|
32697
|
+
name: "deviceId",
|
|
32698
|
+
form: "single",
|
|
32699
|
+
optional: false
|
|
32700
|
+
}],
|
|
32667
32701
|
"pipelineAnalytics.getTrainingExportSummary": [{
|
|
32668
32702
|
name: "deviceIds",
|
|
32669
32703
|
form: "array",
|
|
@@ -32699,6 +32733,11 @@ Object.freeze({
|
|
|
32699
32733
|
form: "array",
|
|
32700
32734
|
optional: true
|
|
32701
32735
|
}],
|
|
32736
|
+
"pipelineAnalytics.listTrackMedia": [{
|
|
32737
|
+
name: "deviceId",
|
|
32738
|
+
form: "single",
|
|
32739
|
+
optional: false
|
|
32740
|
+
}],
|
|
32702
32741
|
"pipelineAnalytics.listTracks": [{
|
|
32703
32742
|
name: "deviceId",
|
|
32704
32743
|
form: "single",
|
|
@@ -33139,6 +33178,12 @@ Object.freeze({
|
|
|
33139
33178
|
form: "single",
|
|
33140
33179
|
optional: false
|
|
33141
33180
|
}],
|
|
33181
|
+
"snapshot.getSnapshotLinks": [{
|
|
33182
|
+
name: "targets",
|
|
33183
|
+
form: "object-array",
|
|
33184
|
+
optional: false,
|
|
33185
|
+
itemField: "deviceId"
|
|
33186
|
+
}],
|
|
33142
33187
|
"snapshot.getSnapshotOverview": [{
|
|
33143
33188
|
name: "deviceIds",
|
|
33144
33189
|
form: "array",
|
|
@@ -34340,6 +34385,9 @@ function errorEnvelope(header, type, message) {
|
|
|
34340
34385
|
* route to the right cap on the right device.
|
|
34341
34386
|
*/
|
|
34342
34387
|
var discoveryPayloadSchema = DiscoveryDirectiveSchema.shape.payload;
|
|
34388
|
+
/** Devices built at once in `handleDiscovery` — latency-bound tRPC reads, so
|
|
34389
|
+
* six lanes hide the round-trips without stampeding a booting hub. */
|
|
34390
|
+
var DISCOVERY_BUILD_CONCURRENCY = 6;
|
|
34343
34391
|
var CAP_DOORBELL = "doorbell";
|
|
34344
34392
|
var CAP_INTERCOM = "intercom";
|
|
34345
34393
|
var CAP_MOTION = "motion";
|
|
@@ -34352,16 +34400,28 @@ var CAP_PRIVACY_MASK = "privacy-mask";
|
|
|
34352
34400
|
async function handleDiscovery(header, payloadRaw, deps, callerIdentity) {
|
|
34353
34401
|
if (!discoveryPayloadSchema.safeParse(payloadRaw).success) return errorEnvelope(header, "INVALID_DIRECTIVE", "Discovery payload shape unexpected");
|
|
34354
34402
|
const exposed = await deps.listExposed();
|
|
34355
|
-
const
|
|
34356
|
-
|
|
34357
|
-
|
|
34358
|
-
|
|
34359
|
-
}
|
|
34360
|
-
|
|
34361
|
-
|
|
34362
|
-
|
|
34363
|
-
|
|
34364
|
-
|
|
34403
|
+
const built = new Array(exposed.length).fill(null);
|
|
34404
|
+
const queue = exposed.map((entry, index) => ({
|
|
34405
|
+
entry,
|
|
34406
|
+
index
|
|
34407
|
+
}));
|
|
34408
|
+
const lane = async () => {
|
|
34409
|
+
for (;;) {
|
|
34410
|
+
const item = queue.shift();
|
|
34411
|
+
if (item === void 0) return;
|
|
34412
|
+
try {
|
|
34413
|
+
built[item.index] = await buildEndpointsForExposedDevice(item.entry.deviceId, deps);
|
|
34414
|
+
} catch (err) {
|
|
34415
|
+
deps.logger.warn("alexa discovery (on-demand): skipping device — build failed", {
|
|
34416
|
+
tags: { deviceId: item.entry.deviceId },
|
|
34417
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
34418
|
+
});
|
|
34419
|
+
}
|
|
34420
|
+
}
|
|
34421
|
+
};
|
|
34422
|
+
const lanes = Math.min(DISCOVERY_BUILD_CONCURRENCY, Math.max(1, queue.length));
|
|
34423
|
+
await Promise.all(Array.from({ length: lanes }, lane));
|
|
34424
|
+
const endpoints = built.flatMap((b) => b ?? []);
|
|
34365
34425
|
deps.logger.info("alexa discovery returned endpoints", { meta: {
|
|
34366
34426
|
count: endpoints.length,
|
|
34367
34427
|
userId: callerIdentity.userId
|
|
@@ -36726,8 +36786,30 @@ function endpointsSignature(endpoints) {
|
|
|
36726
36786
|
}
|
|
36727
36787
|
//#endregion
|
|
36728
36788
|
//#region src/sync-engine.ts
|
|
36789
|
+
/**
|
|
36790
|
+
* Devices reconciled at once in {@link SyncEngine.reconcileAllExposed}. Each
|
|
36791
|
+
* lane's device build is a handful of cross-process tRPC calls — latency, not
|
|
36792
|
+
* CPU — so four lanes hide most of it without turning a boot-time cap
|
|
36793
|
+
* registration burst into a second stampede.
|
|
36794
|
+
*/
|
|
36795
|
+
var RECONCILE_ALL_CONCURRENCY = 4;
|
|
36729
36796
|
var SyncEngine = class {
|
|
36730
36797
|
deps;
|
|
36798
|
+
/**
|
|
36799
|
+
* Serializes the `recordEndpoints` read-modify-write. The build/push half of
|
|
36800
|
+
* a reconcile runs parallel across devices, but the persisted maps are
|
|
36801
|
+
* whole-object snapshots: two concurrent `get → mutate → set` cycles would
|
|
36802
|
+
* silently drop one device's ids/signature (last writer wins).
|
|
36803
|
+
*/
|
|
36804
|
+
recordChain = Promise.resolve();
|
|
36805
|
+
/**
|
|
36806
|
+
* One in-flight reconcile per device. At boot the 60 s full pass and the
|
|
36807
|
+
* debounced bindings-change reconciles overlap, and each one rebuilt the
|
|
36808
|
+
* same device serially — the live hub built camera 615's endpoint shape
|
|
36809
|
+
* four times inside one boot (2026-08-15). Joining the running pass costs
|
|
36810
|
+
* the duplicate nothing.
|
|
36811
|
+
*/
|
|
36812
|
+
reconcileFlights = /* @__PURE__ */ new Map();
|
|
36731
36813
|
constructor(deps) {
|
|
36732
36814
|
this.deps = deps;
|
|
36733
36815
|
}
|
|
@@ -36838,6 +36920,15 @@ var SyncEngine = class {
|
|
|
36838
36920
|
* re-push a shape Alexa already has.
|
|
36839
36921
|
*/
|
|
36840
36922
|
async reconcileDevice(deviceId) {
|
|
36923
|
+
const inFlight = this.reconcileFlights.get(deviceId);
|
|
36924
|
+
if (inFlight) return inFlight;
|
|
36925
|
+
const flight = this.reconcileDeviceNow(deviceId).finally(() => {
|
|
36926
|
+
if (this.reconcileFlights.get(deviceId) === flight) this.reconcileFlights.delete(deviceId);
|
|
36927
|
+
});
|
|
36928
|
+
this.reconcileFlights.set(deviceId, flight);
|
|
36929
|
+
return flight;
|
|
36930
|
+
}
|
|
36931
|
+
async reconcileDeviceNow(deviceId) {
|
|
36841
36932
|
if (!this.deps.listExposed().some((e) => e.deviceId === deviceId)) return {
|
|
36842
36933
|
deviceId,
|
|
36843
36934
|
pushed: false
|
|
@@ -36878,26 +36969,42 @@ var SyncEngine = class {
|
|
|
36878
36969
|
async reconcileAllExposed() {
|
|
36879
36970
|
const checkedDeviceIds = [];
|
|
36880
36971
|
const pushedDeviceIds = [];
|
|
36881
|
-
|
|
36882
|
-
|
|
36883
|
-
|
|
36884
|
-
|
|
36972
|
+
const queue = [...this.deps.listExposed()];
|
|
36973
|
+
const lane = async () => {
|
|
36974
|
+
for (;;) {
|
|
36975
|
+
const entry = queue.shift();
|
|
36976
|
+
if (entry === void 0) return;
|
|
36977
|
+
checkedDeviceIds.push(entry.deviceId);
|
|
36978
|
+
if ((await this.reconcileDevice(entry.deviceId)).pushed) pushedDeviceIds.push(entry.deviceId);
|
|
36979
|
+
}
|
|
36980
|
+
};
|
|
36981
|
+
const lanes = Math.min(RECONCILE_ALL_CONCURRENCY, Math.max(1, queue.length));
|
|
36982
|
+
await Promise.all(Array.from({ length: lanes }, lane));
|
|
36885
36983
|
return {
|
|
36886
36984
|
checkedDeviceIds,
|
|
36887
36985
|
pushedDeviceIds
|
|
36888
36986
|
};
|
|
36889
36987
|
}
|
|
36890
36988
|
/** Persist the endpoint ids AND the shape signature for a device.
|
|
36891
|
-
* `signature === null` forgets both (delete / empty build).
|
|
36989
|
+
* `signature === null` forgets both (delete / empty build).
|
|
36990
|
+
*
|
|
36991
|
+
* Serialized through `recordChain`: the maps are whole-object snapshots
|
|
36992
|
+
* (`get` → spread → `set`), so parallel reconciles would otherwise race —
|
|
36993
|
+
* two devices' writes computed off the same stale snapshot, the second
|
|
36994
|
+
* `set` erasing the first's entry. */
|
|
36892
36995
|
async recordEndpoints(deviceId, ids, signature) {
|
|
36893
|
-
const
|
|
36894
|
-
|
|
36895
|
-
|
|
36896
|
-
|
|
36897
|
-
|
|
36898
|
-
|
|
36899
|
-
|
|
36900
|
-
|
|
36996
|
+
const write = this.recordChain.then(async () => {
|
|
36997
|
+
const next = { ...this.deps.getExportedEndpoints() };
|
|
36998
|
+
if (ids.length > 0) next[deviceId] = ids;
|
|
36999
|
+
else delete next[deviceId];
|
|
37000
|
+
await this.deps.setExportedEndpoints(next);
|
|
37001
|
+
const nextSignatures = { ...this.deps.getExportedSignatures() };
|
|
37002
|
+
if (signature !== null) nextSignatures[deviceId] = signature;
|
|
37003
|
+
else delete nextSignatures[deviceId];
|
|
37004
|
+
await this.deps.setExportedSignatures(nextSignatures);
|
|
37005
|
+
});
|
|
37006
|
+
this.recordChain = write.then(() => void 0, () => void 0);
|
|
37007
|
+
return write;
|
|
36901
37008
|
}
|
|
36902
37009
|
};
|
|
36903
37010
|
//#endregion
|
|
@@ -11058,6 +11058,8 @@ var QueryFilterSchema = object({
|
|
|
11058
11058
|
where: record(string(), unknown()).optional(),
|
|
11059
11059
|
whereIn: record(string(), array(unknown())).optional(),
|
|
11060
11060
|
whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
|
|
11061
|
+
/** NULL-safe exclusion: matches rows whose field is NULL OR != the value. */
|
|
11062
|
+
whereNot: record(string(), unknown()).optional(),
|
|
11061
11063
|
orderBy: object({
|
|
11062
11064
|
field: string(),
|
|
11063
11065
|
direction: _enum(["asc", "desc"])
|
|
@@ -11077,7 +11079,8 @@ var QueryFilterSchema = object({
|
|
|
11077
11079
|
var MutationFilterSchema = object({
|
|
11078
11080
|
where: record(string(), unknown()).optional(),
|
|
11079
11081
|
whereIn: record(string(), array(unknown())).optional(),
|
|
11080
|
-
whereBetween: record(string(), tuple([unknown(), unknown()])).optional()
|
|
11082
|
+
whereBetween: record(string(), tuple([unknown(), unknown()])).optional(),
|
|
11083
|
+
whereNot: record(string(), unknown()).optional()
|
|
11081
11084
|
});
|
|
11082
11085
|
/** A single stored record: `{ id, data }`. */
|
|
11083
11086
|
var SettingsRecordSchema = object({
|
|
@@ -16984,7 +16987,10 @@ var RecentTracksQueryInput = object({
|
|
|
16984
16987
|
* Encodes the (lastSeen, trackId) sort position — treat as opaque. */
|
|
16985
16988
|
cursor: string().optional(),
|
|
16986
16989
|
/** See {@link TrackProjectionSchema}. Default `full`. */
|
|
16987
|
-
projection: TrackProjectionSchema.optional()
|
|
16990
|
+
projection: TrackProjectionSchema.optional(),
|
|
16991
|
+
/** Include stationary-promoted rows (parked objects). Default false: the
|
|
16992
|
+
* feed lists passages; parking records live on the stationary registry. */
|
|
16993
|
+
includeStationary: boolean().optional()
|
|
16988
16994
|
});
|
|
16989
16995
|
var RecentTracksPageSchema = object({
|
|
16990
16996
|
/** Merged page, ordered by (`lastSeen` DESC, `trackId` DESC). */
|
|
@@ -17202,7 +17208,11 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
17202
17208
|
zone: TrackZoneFilterSchema.optional(),
|
|
17203
17209
|
/** See {@link TrackProjectionSchema}. Default `full` (backward
|
|
17204
17210
|
* compatible — omitting the field keeps today's exact behaviour). */
|
|
17205
|
-
projection: TrackProjectionSchema.optional()
|
|
17211
|
+
projection: TrackProjectionSchema.optional(),
|
|
17212
|
+
/** Include stationary-promoted rows (parked objects handed to the
|
|
17213
|
+
* stationary registry). Default false: the timeline lists passages,
|
|
17214
|
+
* not parking records (operator decision, 2026-08-15). */
|
|
17215
|
+
includeStationary: boolean().optional()
|
|
17206
17216
|
}), array(TrackSchema).readonly()), method(RecentTracksQueryInput, RecentTracksPageSchema), method(object({ deviceId: number() }), _void(), {
|
|
17207
17217
|
kind: "mutation",
|
|
17208
17218
|
auth: "admin"
|
|
@@ -17366,11 +17376,16 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
17366
17376
|
auth: "admin"
|
|
17367
17377
|
}), method(object({
|
|
17368
17378
|
eventId: string(),
|
|
17369
|
-
kind: MediaFileKindEnum.optional()
|
|
17379
|
+
kind: MediaFileKindEnum.optional(),
|
|
17380
|
+
deviceId: number()
|
|
17381
|
+
}), array(MediaFileSchema).readonly()), method(object({
|
|
17382
|
+
trackId: string(),
|
|
17383
|
+
kinds: array(MediaFileKindEnum).optional(),
|
|
17384
|
+
deviceId: number()
|
|
17370
17385
|
}), array(MediaFileSchema).readonly()), method(object({
|
|
17371
17386
|
trackId: string(),
|
|
17372
|
-
|
|
17373
|
-
}), array(
|
|
17387
|
+
deviceId: number()
|
|
17388
|
+
}), array(MediaFileInfoSchema).readonly()), method(SearchObjectEventsInput, array(ScoredObjectEventSchema).readonly()), method(object({}), WipeObjectEmbeddingsResultSchema, {
|
|
17374
17389
|
kind: "mutation",
|
|
17375
17390
|
auth: "admin"
|
|
17376
17391
|
}), method(RebuildObjectEmbeddingsInput, RebuildObjectEmbeddingsResultSchema, {
|
|
@@ -19349,7 +19364,16 @@ targets: array(object({
|
|
|
19349
19364
|
/** A sleeping battery camera: the frame is deliberately stale and will
|
|
19350
19365
|
* NOT refresh in the background. A surface should say so rather than
|
|
19351
19366
|
* present it as current. */
|
|
19352
|
-
sleeping: boolean()
|
|
19367
|
+
sleeping: boolean(),
|
|
19368
|
+
/** Current device state rendered over the cached frame. State images
|
|
19369
|
+
* remain authoritative even when their photographic background is
|
|
19370
|
+
* old; null means the link must carry a current camera frame. */
|
|
19371
|
+
stateReason: _enum([
|
|
19372
|
+
"disabled",
|
|
19373
|
+
"sleeping",
|
|
19374
|
+
"unreachable",
|
|
19375
|
+
"waking"
|
|
19376
|
+
]).nullable()
|
|
19353
19377
|
})));
|
|
19354
19378
|
/**
|
|
19355
19379
|
* `sso-bridge` — internal hub-only cap that lets SSO-style auth
|
|
@@ -32607,6 +32631,11 @@ Object.freeze({
|
|
|
32607
32631
|
form: "single",
|
|
32608
32632
|
optional: false
|
|
32609
32633
|
}],
|
|
32634
|
+
"pipelineAnalytics.getEventMedia": [{
|
|
32635
|
+
name: "deviceId",
|
|
32636
|
+
form: "single",
|
|
32637
|
+
optional: false
|
|
32638
|
+
}],
|
|
32610
32639
|
"pipelineAnalytics.getKeyEvents": [{
|
|
32611
32640
|
name: "deviceId",
|
|
32612
32641
|
form: "single",
|
|
@@ -32637,6 +32666,11 @@ Object.freeze({
|
|
|
32637
32666
|
form: "single",
|
|
32638
32667
|
optional: false
|
|
32639
32668
|
}],
|
|
32669
|
+
"pipelineAnalytics.getTrackMedia": [{
|
|
32670
|
+
name: "deviceId",
|
|
32671
|
+
form: "single",
|
|
32672
|
+
optional: false
|
|
32673
|
+
}],
|
|
32640
32674
|
"pipelineAnalytics.getTrainingExportSummary": [{
|
|
32641
32675
|
name: "deviceIds",
|
|
32642
32676
|
form: "array",
|
|
@@ -32672,6 +32706,11 @@ Object.freeze({
|
|
|
32672
32706
|
form: "array",
|
|
32673
32707
|
optional: true
|
|
32674
32708
|
}],
|
|
32709
|
+
"pipelineAnalytics.listTrackMedia": [{
|
|
32710
|
+
name: "deviceId",
|
|
32711
|
+
form: "single",
|
|
32712
|
+
optional: false
|
|
32713
|
+
}],
|
|
32675
32714
|
"pipelineAnalytics.listTracks": [{
|
|
32676
32715
|
name: "deviceId",
|
|
32677
32716
|
form: "single",
|
|
@@ -33112,6 +33151,12 @@ Object.freeze({
|
|
|
33112
33151
|
form: "single",
|
|
33113
33152
|
optional: false
|
|
33114
33153
|
}],
|
|
33154
|
+
"snapshot.getSnapshotLinks": [{
|
|
33155
|
+
name: "targets",
|
|
33156
|
+
form: "object-array",
|
|
33157
|
+
optional: false,
|
|
33158
|
+
itemField: "deviceId"
|
|
33159
|
+
}],
|
|
33115
33160
|
"snapshot.getSnapshotOverview": [{
|
|
33116
33161
|
name: "deviceIds",
|
|
33117
33162
|
form: "array",
|
|
@@ -34313,6 +34358,9 @@ function errorEnvelope(header, type, message) {
|
|
|
34313
34358
|
* route to the right cap on the right device.
|
|
34314
34359
|
*/
|
|
34315
34360
|
var discoveryPayloadSchema = DiscoveryDirectiveSchema.shape.payload;
|
|
34361
|
+
/** Devices built at once in `handleDiscovery` — latency-bound tRPC reads, so
|
|
34362
|
+
* six lanes hide the round-trips without stampeding a booting hub. */
|
|
34363
|
+
var DISCOVERY_BUILD_CONCURRENCY = 6;
|
|
34316
34364
|
var CAP_DOORBELL = "doorbell";
|
|
34317
34365
|
var CAP_INTERCOM = "intercom";
|
|
34318
34366
|
var CAP_MOTION = "motion";
|
|
@@ -34325,16 +34373,28 @@ var CAP_PRIVACY_MASK = "privacy-mask";
|
|
|
34325
34373
|
async function handleDiscovery(header, payloadRaw, deps, callerIdentity) {
|
|
34326
34374
|
if (!discoveryPayloadSchema.safeParse(payloadRaw).success) return errorEnvelope(header, "INVALID_DIRECTIVE", "Discovery payload shape unexpected");
|
|
34327
34375
|
const exposed = await deps.listExposed();
|
|
34328
|
-
const
|
|
34329
|
-
|
|
34330
|
-
|
|
34331
|
-
|
|
34332
|
-
}
|
|
34333
|
-
|
|
34334
|
-
|
|
34335
|
-
|
|
34336
|
-
|
|
34337
|
-
|
|
34376
|
+
const built = new Array(exposed.length).fill(null);
|
|
34377
|
+
const queue = exposed.map((entry, index) => ({
|
|
34378
|
+
entry,
|
|
34379
|
+
index
|
|
34380
|
+
}));
|
|
34381
|
+
const lane = async () => {
|
|
34382
|
+
for (;;) {
|
|
34383
|
+
const item = queue.shift();
|
|
34384
|
+
if (item === void 0) return;
|
|
34385
|
+
try {
|
|
34386
|
+
built[item.index] = await buildEndpointsForExposedDevice(item.entry.deviceId, deps);
|
|
34387
|
+
} catch (err) {
|
|
34388
|
+
deps.logger.warn("alexa discovery (on-demand): skipping device — build failed", {
|
|
34389
|
+
tags: { deviceId: item.entry.deviceId },
|
|
34390
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
34391
|
+
});
|
|
34392
|
+
}
|
|
34393
|
+
}
|
|
34394
|
+
};
|
|
34395
|
+
const lanes = Math.min(DISCOVERY_BUILD_CONCURRENCY, Math.max(1, queue.length));
|
|
34396
|
+
await Promise.all(Array.from({ length: lanes }, lane));
|
|
34397
|
+
const endpoints = built.flatMap((b) => b ?? []);
|
|
34338
34398
|
deps.logger.info("alexa discovery returned endpoints", { meta: {
|
|
34339
34399
|
count: endpoints.length,
|
|
34340
34400
|
userId: callerIdentity.userId
|
|
@@ -36699,8 +36759,30 @@ function endpointsSignature(endpoints) {
|
|
|
36699
36759
|
}
|
|
36700
36760
|
//#endregion
|
|
36701
36761
|
//#region src/sync-engine.ts
|
|
36762
|
+
/**
|
|
36763
|
+
* Devices reconciled at once in {@link SyncEngine.reconcileAllExposed}. Each
|
|
36764
|
+
* lane's device build is a handful of cross-process tRPC calls — latency, not
|
|
36765
|
+
* CPU — so four lanes hide most of it without turning a boot-time cap
|
|
36766
|
+
* registration burst into a second stampede.
|
|
36767
|
+
*/
|
|
36768
|
+
var RECONCILE_ALL_CONCURRENCY = 4;
|
|
36702
36769
|
var SyncEngine = class {
|
|
36703
36770
|
deps;
|
|
36771
|
+
/**
|
|
36772
|
+
* Serializes the `recordEndpoints` read-modify-write. The build/push half of
|
|
36773
|
+
* a reconcile runs parallel across devices, but the persisted maps are
|
|
36774
|
+
* whole-object snapshots: two concurrent `get → mutate → set` cycles would
|
|
36775
|
+
* silently drop one device's ids/signature (last writer wins).
|
|
36776
|
+
*/
|
|
36777
|
+
recordChain = Promise.resolve();
|
|
36778
|
+
/**
|
|
36779
|
+
* One in-flight reconcile per device. At boot the 60 s full pass and the
|
|
36780
|
+
* debounced bindings-change reconciles overlap, and each one rebuilt the
|
|
36781
|
+
* same device serially — the live hub built camera 615's endpoint shape
|
|
36782
|
+
* four times inside one boot (2026-08-15). Joining the running pass costs
|
|
36783
|
+
* the duplicate nothing.
|
|
36784
|
+
*/
|
|
36785
|
+
reconcileFlights = /* @__PURE__ */ new Map();
|
|
36704
36786
|
constructor(deps) {
|
|
36705
36787
|
this.deps = deps;
|
|
36706
36788
|
}
|
|
@@ -36811,6 +36893,15 @@ var SyncEngine = class {
|
|
|
36811
36893
|
* re-push a shape Alexa already has.
|
|
36812
36894
|
*/
|
|
36813
36895
|
async reconcileDevice(deviceId) {
|
|
36896
|
+
const inFlight = this.reconcileFlights.get(deviceId);
|
|
36897
|
+
if (inFlight) return inFlight;
|
|
36898
|
+
const flight = this.reconcileDeviceNow(deviceId).finally(() => {
|
|
36899
|
+
if (this.reconcileFlights.get(deviceId) === flight) this.reconcileFlights.delete(deviceId);
|
|
36900
|
+
});
|
|
36901
|
+
this.reconcileFlights.set(deviceId, flight);
|
|
36902
|
+
return flight;
|
|
36903
|
+
}
|
|
36904
|
+
async reconcileDeviceNow(deviceId) {
|
|
36814
36905
|
if (!this.deps.listExposed().some((e) => e.deviceId === deviceId)) return {
|
|
36815
36906
|
deviceId,
|
|
36816
36907
|
pushed: false
|
|
@@ -36851,26 +36942,42 @@ var SyncEngine = class {
|
|
|
36851
36942
|
async reconcileAllExposed() {
|
|
36852
36943
|
const checkedDeviceIds = [];
|
|
36853
36944
|
const pushedDeviceIds = [];
|
|
36854
|
-
|
|
36855
|
-
|
|
36856
|
-
|
|
36857
|
-
|
|
36945
|
+
const queue = [...this.deps.listExposed()];
|
|
36946
|
+
const lane = async () => {
|
|
36947
|
+
for (;;) {
|
|
36948
|
+
const entry = queue.shift();
|
|
36949
|
+
if (entry === void 0) return;
|
|
36950
|
+
checkedDeviceIds.push(entry.deviceId);
|
|
36951
|
+
if ((await this.reconcileDevice(entry.deviceId)).pushed) pushedDeviceIds.push(entry.deviceId);
|
|
36952
|
+
}
|
|
36953
|
+
};
|
|
36954
|
+
const lanes = Math.min(RECONCILE_ALL_CONCURRENCY, Math.max(1, queue.length));
|
|
36955
|
+
await Promise.all(Array.from({ length: lanes }, lane));
|
|
36858
36956
|
return {
|
|
36859
36957
|
checkedDeviceIds,
|
|
36860
36958
|
pushedDeviceIds
|
|
36861
36959
|
};
|
|
36862
36960
|
}
|
|
36863
36961
|
/** Persist the endpoint ids AND the shape signature for a device.
|
|
36864
|
-
* `signature === null` forgets both (delete / empty build).
|
|
36962
|
+
* `signature === null` forgets both (delete / empty build).
|
|
36963
|
+
*
|
|
36964
|
+
* Serialized through `recordChain`: the maps are whole-object snapshots
|
|
36965
|
+
* (`get` → spread → `set`), so parallel reconciles would otherwise race —
|
|
36966
|
+
* two devices' writes computed off the same stale snapshot, the second
|
|
36967
|
+
* `set` erasing the first's entry. */
|
|
36865
36968
|
async recordEndpoints(deviceId, ids, signature) {
|
|
36866
|
-
const
|
|
36867
|
-
|
|
36868
|
-
|
|
36869
|
-
|
|
36870
|
-
|
|
36871
|
-
|
|
36872
|
-
|
|
36873
|
-
|
|
36969
|
+
const write = this.recordChain.then(async () => {
|
|
36970
|
+
const next = { ...this.deps.getExportedEndpoints() };
|
|
36971
|
+
if (ids.length > 0) next[deviceId] = ids;
|
|
36972
|
+
else delete next[deviceId];
|
|
36973
|
+
await this.deps.setExportedEndpoints(next);
|
|
36974
|
+
const nextSignatures = { ...this.deps.getExportedSignatures() };
|
|
36975
|
+
if (signature !== null) nextSignatures[deviceId] = signature;
|
|
36976
|
+
else delete nextSignatures[deviceId];
|
|
36977
|
+
await this.deps.setExportedSignatures(nextSignatures);
|
|
36978
|
+
});
|
|
36979
|
+
this.recordChain = write.then(() => void 0, () => void 0);
|
|
36980
|
+
return write;
|
|
36874
36981
|
}
|
|
36875
36982
|
};
|
|
36876
36983
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-export-alexa",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.24",
|
|
4
4
|
"description": "Alexa Smart Home Skill export — hub-side OAuth provider + directive handler. The companion AWS Lambda forwards Alexa directives to this addon's /addon/export-alexa/directive endpoint.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|