@camstack/addon-export-alexa 1.2.22 → 1.2.23
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 +95 -24
- package/dist/export-alexa.addon.mjs +95 -24
- package/package.json +1 -1
|
@@ -19376,7 +19376,16 @@ targets: array(object({
|
|
|
19376
19376
|
/** A sleeping battery camera: the frame is deliberately stale and will
|
|
19377
19377
|
* NOT refresh in the background. A surface should say so rather than
|
|
19378
19378
|
* present it as current. */
|
|
19379
|
-
sleeping: boolean()
|
|
19379
|
+
sleeping: boolean(),
|
|
19380
|
+
/** Current device state rendered over the cached frame. State images
|
|
19381
|
+
* remain authoritative even when their photographic background is
|
|
19382
|
+
* old; null means the link must carry a current camera frame. */
|
|
19383
|
+
stateReason: _enum([
|
|
19384
|
+
"disabled",
|
|
19385
|
+
"sleeping",
|
|
19386
|
+
"unreachable",
|
|
19387
|
+
"waking"
|
|
19388
|
+
]).nullable()
|
|
19380
19389
|
})));
|
|
19381
19390
|
/**
|
|
19382
19391
|
* `sso-bridge` — internal hub-only cap that lets SSO-style auth
|
|
@@ -34340,6 +34349,9 @@ function errorEnvelope(header, type, message) {
|
|
|
34340
34349
|
* route to the right cap on the right device.
|
|
34341
34350
|
*/
|
|
34342
34351
|
var discoveryPayloadSchema = DiscoveryDirectiveSchema.shape.payload;
|
|
34352
|
+
/** Devices built at once in `handleDiscovery` — latency-bound tRPC reads, so
|
|
34353
|
+
* six lanes hide the round-trips without stampeding a booting hub. */
|
|
34354
|
+
var DISCOVERY_BUILD_CONCURRENCY = 6;
|
|
34343
34355
|
var CAP_DOORBELL = "doorbell";
|
|
34344
34356
|
var CAP_INTERCOM = "intercom";
|
|
34345
34357
|
var CAP_MOTION = "motion";
|
|
@@ -34352,16 +34364,28 @@ var CAP_PRIVACY_MASK = "privacy-mask";
|
|
|
34352
34364
|
async function handleDiscovery(header, payloadRaw, deps, callerIdentity) {
|
|
34353
34365
|
if (!discoveryPayloadSchema.safeParse(payloadRaw).success) return errorEnvelope(header, "INVALID_DIRECTIVE", "Discovery payload shape unexpected");
|
|
34354
34366
|
const exposed = await deps.listExposed();
|
|
34355
|
-
const
|
|
34356
|
-
|
|
34357
|
-
|
|
34358
|
-
|
|
34359
|
-
}
|
|
34360
|
-
|
|
34361
|
-
|
|
34362
|
-
|
|
34363
|
-
|
|
34364
|
-
|
|
34367
|
+
const built = new Array(exposed.length).fill(null);
|
|
34368
|
+
const queue = exposed.map((entry, index) => ({
|
|
34369
|
+
entry,
|
|
34370
|
+
index
|
|
34371
|
+
}));
|
|
34372
|
+
const lane = async () => {
|
|
34373
|
+
for (;;) {
|
|
34374
|
+
const item = queue.shift();
|
|
34375
|
+
if (item === void 0) return;
|
|
34376
|
+
try {
|
|
34377
|
+
built[item.index] = await buildEndpointsForExposedDevice(item.entry.deviceId, deps);
|
|
34378
|
+
} catch (err) {
|
|
34379
|
+
deps.logger.warn("alexa discovery (on-demand): skipping device — build failed", {
|
|
34380
|
+
tags: { deviceId: item.entry.deviceId },
|
|
34381
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
34382
|
+
});
|
|
34383
|
+
}
|
|
34384
|
+
}
|
|
34385
|
+
};
|
|
34386
|
+
const lanes = Math.min(DISCOVERY_BUILD_CONCURRENCY, Math.max(1, queue.length));
|
|
34387
|
+
await Promise.all(Array.from({ length: lanes }, lane));
|
|
34388
|
+
const endpoints = built.flatMap((b) => b ?? []);
|
|
34365
34389
|
deps.logger.info("alexa discovery returned endpoints", { meta: {
|
|
34366
34390
|
count: endpoints.length,
|
|
34367
34391
|
userId: callerIdentity.userId
|
|
@@ -36726,8 +36750,30 @@ function endpointsSignature(endpoints) {
|
|
|
36726
36750
|
}
|
|
36727
36751
|
//#endregion
|
|
36728
36752
|
//#region src/sync-engine.ts
|
|
36753
|
+
/**
|
|
36754
|
+
* Devices reconciled at once in {@link SyncEngine.reconcileAllExposed}. Each
|
|
36755
|
+
* lane's device build is a handful of cross-process tRPC calls — latency, not
|
|
36756
|
+
* CPU — so four lanes hide most of it without turning a boot-time cap
|
|
36757
|
+
* registration burst into a second stampede.
|
|
36758
|
+
*/
|
|
36759
|
+
var RECONCILE_ALL_CONCURRENCY = 4;
|
|
36729
36760
|
var SyncEngine = class {
|
|
36730
36761
|
deps;
|
|
36762
|
+
/**
|
|
36763
|
+
* Serializes the `recordEndpoints` read-modify-write. The build/push half of
|
|
36764
|
+
* a reconcile runs parallel across devices, but the persisted maps are
|
|
36765
|
+
* whole-object snapshots: two concurrent `get → mutate → set` cycles would
|
|
36766
|
+
* silently drop one device's ids/signature (last writer wins).
|
|
36767
|
+
*/
|
|
36768
|
+
recordChain = Promise.resolve();
|
|
36769
|
+
/**
|
|
36770
|
+
* One in-flight reconcile per device. At boot the 60 s full pass and the
|
|
36771
|
+
* debounced bindings-change reconciles overlap, and each one rebuilt the
|
|
36772
|
+
* same device serially — the live hub built camera 615's endpoint shape
|
|
36773
|
+
* four times inside one boot (2026-08-15). Joining the running pass costs
|
|
36774
|
+
* the duplicate nothing.
|
|
36775
|
+
*/
|
|
36776
|
+
reconcileFlights = /* @__PURE__ */ new Map();
|
|
36731
36777
|
constructor(deps) {
|
|
36732
36778
|
this.deps = deps;
|
|
36733
36779
|
}
|
|
@@ -36838,6 +36884,15 @@ var SyncEngine = class {
|
|
|
36838
36884
|
* re-push a shape Alexa already has.
|
|
36839
36885
|
*/
|
|
36840
36886
|
async reconcileDevice(deviceId) {
|
|
36887
|
+
const inFlight = this.reconcileFlights.get(deviceId);
|
|
36888
|
+
if (inFlight) return inFlight;
|
|
36889
|
+
const flight = this.reconcileDeviceNow(deviceId).finally(() => {
|
|
36890
|
+
if (this.reconcileFlights.get(deviceId) === flight) this.reconcileFlights.delete(deviceId);
|
|
36891
|
+
});
|
|
36892
|
+
this.reconcileFlights.set(deviceId, flight);
|
|
36893
|
+
return flight;
|
|
36894
|
+
}
|
|
36895
|
+
async reconcileDeviceNow(deviceId) {
|
|
36841
36896
|
if (!this.deps.listExposed().some((e) => e.deviceId === deviceId)) return {
|
|
36842
36897
|
deviceId,
|
|
36843
36898
|
pushed: false
|
|
@@ -36878,26 +36933,42 @@ var SyncEngine = class {
|
|
|
36878
36933
|
async reconcileAllExposed() {
|
|
36879
36934
|
const checkedDeviceIds = [];
|
|
36880
36935
|
const pushedDeviceIds = [];
|
|
36881
|
-
|
|
36882
|
-
|
|
36883
|
-
|
|
36884
|
-
|
|
36936
|
+
const queue = [...this.deps.listExposed()];
|
|
36937
|
+
const lane = async () => {
|
|
36938
|
+
for (;;) {
|
|
36939
|
+
const entry = queue.shift();
|
|
36940
|
+
if (entry === void 0) return;
|
|
36941
|
+
checkedDeviceIds.push(entry.deviceId);
|
|
36942
|
+
if ((await this.reconcileDevice(entry.deviceId)).pushed) pushedDeviceIds.push(entry.deviceId);
|
|
36943
|
+
}
|
|
36944
|
+
};
|
|
36945
|
+
const lanes = Math.min(RECONCILE_ALL_CONCURRENCY, Math.max(1, queue.length));
|
|
36946
|
+
await Promise.all(Array.from({ length: lanes }, lane));
|
|
36885
36947
|
return {
|
|
36886
36948
|
checkedDeviceIds,
|
|
36887
36949
|
pushedDeviceIds
|
|
36888
36950
|
};
|
|
36889
36951
|
}
|
|
36890
36952
|
/** Persist the endpoint ids AND the shape signature for a device.
|
|
36891
|
-
* `signature === null` forgets both (delete / empty build).
|
|
36953
|
+
* `signature === null` forgets both (delete / empty build).
|
|
36954
|
+
*
|
|
36955
|
+
* Serialized through `recordChain`: the maps are whole-object snapshots
|
|
36956
|
+
* (`get` → spread → `set`), so parallel reconciles would otherwise race —
|
|
36957
|
+
* two devices' writes computed off the same stale snapshot, the second
|
|
36958
|
+
* `set` erasing the first's entry. */
|
|
36892
36959
|
async recordEndpoints(deviceId, ids, signature) {
|
|
36893
|
-
const
|
|
36894
|
-
|
|
36895
|
-
|
|
36896
|
-
|
|
36897
|
-
|
|
36898
|
-
|
|
36899
|
-
|
|
36900
|
-
|
|
36960
|
+
const write = this.recordChain.then(async () => {
|
|
36961
|
+
const next = { ...this.deps.getExportedEndpoints() };
|
|
36962
|
+
if (ids.length > 0) next[deviceId] = ids;
|
|
36963
|
+
else delete next[deviceId];
|
|
36964
|
+
await this.deps.setExportedEndpoints(next);
|
|
36965
|
+
const nextSignatures = { ...this.deps.getExportedSignatures() };
|
|
36966
|
+
if (signature !== null) nextSignatures[deviceId] = signature;
|
|
36967
|
+
else delete nextSignatures[deviceId];
|
|
36968
|
+
await this.deps.setExportedSignatures(nextSignatures);
|
|
36969
|
+
});
|
|
36970
|
+
this.recordChain = write.then(() => void 0, () => void 0);
|
|
36971
|
+
return write;
|
|
36901
36972
|
}
|
|
36902
36973
|
};
|
|
36903
36974
|
//#endregion
|
|
@@ -19349,7 +19349,16 @@ targets: array(object({
|
|
|
19349
19349
|
/** A sleeping battery camera: the frame is deliberately stale and will
|
|
19350
19350
|
* NOT refresh in the background. A surface should say so rather than
|
|
19351
19351
|
* present it as current. */
|
|
19352
|
-
sleeping: boolean()
|
|
19352
|
+
sleeping: boolean(),
|
|
19353
|
+
/** Current device state rendered over the cached frame. State images
|
|
19354
|
+
* remain authoritative even when their photographic background is
|
|
19355
|
+
* old; null means the link must carry a current camera frame. */
|
|
19356
|
+
stateReason: _enum([
|
|
19357
|
+
"disabled",
|
|
19358
|
+
"sleeping",
|
|
19359
|
+
"unreachable",
|
|
19360
|
+
"waking"
|
|
19361
|
+
]).nullable()
|
|
19353
19362
|
})));
|
|
19354
19363
|
/**
|
|
19355
19364
|
* `sso-bridge` — internal hub-only cap that lets SSO-style auth
|
|
@@ -34313,6 +34322,9 @@ function errorEnvelope(header, type, message) {
|
|
|
34313
34322
|
* route to the right cap on the right device.
|
|
34314
34323
|
*/
|
|
34315
34324
|
var discoveryPayloadSchema = DiscoveryDirectiveSchema.shape.payload;
|
|
34325
|
+
/** Devices built at once in `handleDiscovery` — latency-bound tRPC reads, so
|
|
34326
|
+
* six lanes hide the round-trips without stampeding a booting hub. */
|
|
34327
|
+
var DISCOVERY_BUILD_CONCURRENCY = 6;
|
|
34316
34328
|
var CAP_DOORBELL = "doorbell";
|
|
34317
34329
|
var CAP_INTERCOM = "intercom";
|
|
34318
34330
|
var CAP_MOTION = "motion";
|
|
@@ -34325,16 +34337,28 @@ var CAP_PRIVACY_MASK = "privacy-mask";
|
|
|
34325
34337
|
async function handleDiscovery(header, payloadRaw, deps, callerIdentity) {
|
|
34326
34338
|
if (!discoveryPayloadSchema.safeParse(payloadRaw).success) return errorEnvelope(header, "INVALID_DIRECTIVE", "Discovery payload shape unexpected");
|
|
34327
34339
|
const exposed = await deps.listExposed();
|
|
34328
|
-
const
|
|
34329
|
-
|
|
34330
|
-
|
|
34331
|
-
|
|
34332
|
-
}
|
|
34333
|
-
|
|
34334
|
-
|
|
34335
|
-
|
|
34336
|
-
|
|
34337
|
-
|
|
34340
|
+
const built = new Array(exposed.length).fill(null);
|
|
34341
|
+
const queue = exposed.map((entry, index) => ({
|
|
34342
|
+
entry,
|
|
34343
|
+
index
|
|
34344
|
+
}));
|
|
34345
|
+
const lane = async () => {
|
|
34346
|
+
for (;;) {
|
|
34347
|
+
const item = queue.shift();
|
|
34348
|
+
if (item === void 0) return;
|
|
34349
|
+
try {
|
|
34350
|
+
built[item.index] = await buildEndpointsForExposedDevice(item.entry.deviceId, deps);
|
|
34351
|
+
} catch (err) {
|
|
34352
|
+
deps.logger.warn("alexa discovery (on-demand): skipping device — build failed", {
|
|
34353
|
+
tags: { deviceId: item.entry.deviceId },
|
|
34354
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
34355
|
+
});
|
|
34356
|
+
}
|
|
34357
|
+
}
|
|
34358
|
+
};
|
|
34359
|
+
const lanes = Math.min(DISCOVERY_BUILD_CONCURRENCY, Math.max(1, queue.length));
|
|
34360
|
+
await Promise.all(Array.from({ length: lanes }, lane));
|
|
34361
|
+
const endpoints = built.flatMap((b) => b ?? []);
|
|
34338
34362
|
deps.logger.info("alexa discovery returned endpoints", { meta: {
|
|
34339
34363
|
count: endpoints.length,
|
|
34340
34364
|
userId: callerIdentity.userId
|
|
@@ -36699,8 +36723,30 @@ function endpointsSignature(endpoints) {
|
|
|
36699
36723
|
}
|
|
36700
36724
|
//#endregion
|
|
36701
36725
|
//#region src/sync-engine.ts
|
|
36726
|
+
/**
|
|
36727
|
+
* Devices reconciled at once in {@link SyncEngine.reconcileAllExposed}. Each
|
|
36728
|
+
* lane's device build is a handful of cross-process tRPC calls — latency, not
|
|
36729
|
+
* CPU — so four lanes hide most of it without turning a boot-time cap
|
|
36730
|
+
* registration burst into a second stampede.
|
|
36731
|
+
*/
|
|
36732
|
+
var RECONCILE_ALL_CONCURRENCY = 4;
|
|
36702
36733
|
var SyncEngine = class {
|
|
36703
36734
|
deps;
|
|
36735
|
+
/**
|
|
36736
|
+
* Serializes the `recordEndpoints` read-modify-write. The build/push half of
|
|
36737
|
+
* a reconcile runs parallel across devices, but the persisted maps are
|
|
36738
|
+
* whole-object snapshots: two concurrent `get → mutate → set` cycles would
|
|
36739
|
+
* silently drop one device's ids/signature (last writer wins).
|
|
36740
|
+
*/
|
|
36741
|
+
recordChain = Promise.resolve();
|
|
36742
|
+
/**
|
|
36743
|
+
* One in-flight reconcile per device. At boot the 60 s full pass and the
|
|
36744
|
+
* debounced bindings-change reconciles overlap, and each one rebuilt the
|
|
36745
|
+
* same device serially — the live hub built camera 615's endpoint shape
|
|
36746
|
+
* four times inside one boot (2026-08-15). Joining the running pass costs
|
|
36747
|
+
* the duplicate nothing.
|
|
36748
|
+
*/
|
|
36749
|
+
reconcileFlights = /* @__PURE__ */ new Map();
|
|
36704
36750
|
constructor(deps) {
|
|
36705
36751
|
this.deps = deps;
|
|
36706
36752
|
}
|
|
@@ -36811,6 +36857,15 @@ var SyncEngine = class {
|
|
|
36811
36857
|
* re-push a shape Alexa already has.
|
|
36812
36858
|
*/
|
|
36813
36859
|
async reconcileDevice(deviceId) {
|
|
36860
|
+
const inFlight = this.reconcileFlights.get(deviceId);
|
|
36861
|
+
if (inFlight) return inFlight;
|
|
36862
|
+
const flight = this.reconcileDeviceNow(deviceId).finally(() => {
|
|
36863
|
+
if (this.reconcileFlights.get(deviceId) === flight) this.reconcileFlights.delete(deviceId);
|
|
36864
|
+
});
|
|
36865
|
+
this.reconcileFlights.set(deviceId, flight);
|
|
36866
|
+
return flight;
|
|
36867
|
+
}
|
|
36868
|
+
async reconcileDeviceNow(deviceId) {
|
|
36814
36869
|
if (!this.deps.listExposed().some((e) => e.deviceId === deviceId)) return {
|
|
36815
36870
|
deviceId,
|
|
36816
36871
|
pushed: false
|
|
@@ -36851,26 +36906,42 @@ var SyncEngine = class {
|
|
|
36851
36906
|
async reconcileAllExposed() {
|
|
36852
36907
|
const checkedDeviceIds = [];
|
|
36853
36908
|
const pushedDeviceIds = [];
|
|
36854
|
-
|
|
36855
|
-
|
|
36856
|
-
|
|
36857
|
-
|
|
36909
|
+
const queue = [...this.deps.listExposed()];
|
|
36910
|
+
const lane = async () => {
|
|
36911
|
+
for (;;) {
|
|
36912
|
+
const entry = queue.shift();
|
|
36913
|
+
if (entry === void 0) return;
|
|
36914
|
+
checkedDeviceIds.push(entry.deviceId);
|
|
36915
|
+
if ((await this.reconcileDevice(entry.deviceId)).pushed) pushedDeviceIds.push(entry.deviceId);
|
|
36916
|
+
}
|
|
36917
|
+
};
|
|
36918
|
+
const lanes = Math.min(RECONCILE_ALL_CONCURRENCY, Math.max(1, queue.length));
|
|
36919
|
+
await Promise.all(Array.from({ length: lanes }, lane));
|
|
36858
36920
|
return {
|
|
36859
36921
|
checkedDeviceIds,
|
|
36860
36922
|
pushedDeviceIds
|
|
36861
36923
|
};
|
|
36862
36924
|
}
|
|
36863
36925
|
/** Persist the endpoint ids AND the shape signature for a device.
|
|
36864
|
-
* `signature === null` forgets both (delete / empty build).
|
|
36926
|
+
* `signature === null` forgets both (delete / empty build).
|
|
36927
|
+
*
|
|
36928
|
+
* Serialized through `recordChain`: the maps are whole-object snapshots
|
|
36929
|
+
* (`get` → spread → `set`), so parallel reconciles would otherwise race —
|
|
36930
|
+
* two devices' writes computed off the same stale snapshot, the second
|
|
36931
|
+
* `set` erasing the first's entry. */
|
|
36865
36932
|
async recordEndpoints(deviceId, ids, signature) {
|
|
36866
|
-
const
|
|
36867
|
-
|
|
36868
|
-
|
|
36869
|
-
|
|
36870
|
-
|
|
36871
|
-
|
|
36872
|
-
|
|
36873
|
-
|
|
36933
|
+
const write = this.recordChain.then(async () => {
|
|
36934
|
+
const next = { ...this.deps.getExportedEndpoints() };
|
|
36935
|
+
if (ids.length > 0) next[deviceId] = ids;
|
|
36936
|
+
else delete next[deviceId];
|
|
36937
|
+
await this.deps.setExportedEndpoints(next);
|
|
36938
|
+
const nextSignatures = { ...this.deps.getExportedSignatures() };
|
|
36939
|
+
if (signature !== null) nextSignatures[deviceId] = signature;
|
|
36940
|
+
else delete nextSignatures[deviceId];
|
|
36941
|
+
await this.deps.setExportedSignatures(nextSignatures);
|
|
36942
|
+
});
|
|
36943
|
+
this.recordChain = write.then(() => void 0, () => void 0);
|
|
36944
|
+
return write;
|
|
36874
36945
|
}
|
|
36875
36946
|
};
|
|
36876
36947
|
//#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.23",
|
|
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",
|