@camstack/addon-provider-amcrest 0.2.25 → 0.2.27
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/addon.js +161 -16
- package/dist/addon.mjs +161 -16
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -4,7 +4,7 @@ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).expor
|
|
|
4
4
|
//#endregion
|
|
5
5
|
let node_crypto = require("node:crypto");
|
|
6
6
|
let node_os = require("node:os");
|
|
7
|
-
//#region ../types/dist/event-category-
|
|
7
|
+
//#region ../types/dist/event-category-CIa_iT6b.mjs
|
|
8
8
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
9
9
|
EventCategory["SystemBoot"] = "system.boot";
|
|
10
10
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -20,6 +20,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
20
20
|
*/
|
|
21
21
|
EventCategory["SystemRestartCompleted"] = "system.restart-completed";
|
|
22
22
|
/**
|
|
23
|
+
* The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
|
|
24
|
+
* Emitted only when the material on disk actually changed, so an
|
|
25
|
+
* operator who trusted the old certificate by hand is told rather than
|
|
26
|
+
* discovering it as a browser error. Payload `TlsCertChangedPayload`.
|
|
27
|
+
*
|
|
28
|
+
* Rule: docs/decisions/adr-0227-*.md
|
|
29
|
+
*/
|
|
30
|
+
EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
|
|
31
|
+
/**
|
|
23
32
|
* A newer addon or server-root package version was found by the
|
|
24
33
|
* authoritative registry check. Emitted once when any observed
|
|
25
34
|
* `latestVersion` changes (or a package/node first appears behind);
|
|
@@ -18974,6 +18983,12 @@ var CameraStatusSchema = object({
|
|
|
18974
18983
|
/** Unix timestamp (ms) when this snapshot was composed server-side. */
|
|
18975
18984
|
fetchedAt: number()
|
|
18976
18985
|
});
|
|
18986
|
+
var InferenceDeviceExclusionReasonSchema = _enum([
|
|
18987
|
+
"disabled",
|
|
18988
|
+
"unavailable",
|
|
18989
|
+
"cannot-host-camera-root",
|
|
18990
|
+
"accelerator-preferred"
|
|
18991
|
+
]);
|
|
18977
18992
|
var NodeInferenceDeviceSchema = object({
|
|
18978
18993
|
/** Stable per-node device key, e.g. `openvino:npu`, `edgetpu:usb`, `cpu`. */
|
|
18979
18994
|
key: string(),
|
|
@@ -19004,7 +19019,17 @@ var NodeInferenceDeviceSchema = object({
|
|
|
19004
19019
|
* available per format; this is the stored selection that becomes the
|
|
19005
19020
|
* default for EVERY camera landing on this accelerator.
|
|
19006
19021
|
*/
|
|
19007
|
-
steps: record(string(), DeviceStepConfigSchema).optional()
|
|
19022
|
+
steps: record(string(), DeviceStepConfigSchema).optional(),
|
|
19023
|
+
/**
|
|
19024
|
+
* `null` when the device IS a camera-root candidate on this node; otherwise
|
|
19025
|
+
* the reason the dispatcher drops it. Computed by the SAME
|
|
19026
|
+
* `resolveInferenceDeviceEligibility` the dispatcher runs, so this view can
|
|
19027
|
+
* never disagree with the election — deriving it in the UI from
|
|
19028
|
+
* `enabled`/`available` would silently miss `cannot-host-camera-root` (needs
|
|
19029
|
+
* the node's model catalog) and `accelerator-preferred` (needs the node-wide
|
|
19030
|
+
* "an accelerator is serving" predicate).
|
|
19031
|
+
*/
|
|
19032
|
+
exclusion: InferenceDeviceExclusionReasonSchema.nullable()
|
|
19008
19033
|
});
|
|
19009
19034
|
var NodeInferenceDevicesSchema = object({
|
|
19010
19035
|
nodeId: string(),
|
|
@@ -23959,7 +23984,12 @@ var ListResultSchema = object({
|
|
|
23959
23984
|
probedAt: number()
|
|
23960
23985
|
});
|
|
23961
23986
|
var PreferredSchema = LocalInterfaceSchema.nullable();
|
|
23962
|
-
|
|
23987
|
+
/**
|
|
23988
|
+
* Candidate base URL for the SDK to race on connect. Order matters —
|
|
23989
|
+
* the SDK should attempt these top-to-bottom with a short per-candidate
|
|
23990
|
+
* timeout (e.g. 1500ms) and cache the winner for the session.
|
|
23991
|
+
*/
|
|
23992
|
+
var ConnectionEndpointSchema = object({
|
|
23963
23993
|
/** Operator-facing label (e.g. "LAN — en0", "Public tunnel"). */
|
|
23964
23994
|
label: string(),
|
|
23965
23995
|
/** Fully-formed base URL with scheme + host + port. */
|
|
@@ -24002,7 +24032,42 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
|
|
|
24002
24032
|
* ordering between polls.
|
|
24003
24033
|
*/
|
|
24004
24034
|
priority: number()
|
|
24005
|
-
})
|
|
24035
|
+
});
|
|
24036
|
+
/**
|
|
24037
|
+
* Where the advertised local port came from. Ordered most → least
|
|
24038
|
+
* authoritative, and the whole point of returning it: a client must be able to
|
|
24039
|
+
* tell a FACT about the hub's socket from an echo of its own guess.
|
|
24040
|
+
*/
|
|
24041
|
+
var LocalPortSourceEnum = _enum([
|
|
24042
|
+
"server-config",
|
|
24043
|
+
"server-env",
|
|
24044
|
+
"caller-hint",
|
|
24045
|
+
"default"
|
|
24046
|
+
]);
|
|
24047
|
+
/** The port every LAN/loopback `baseUrl` in the same result was built with. */
|
|
24048
|
+
var AdvertisedLocalPortSchema = object({
|
|
24049
|
+
port: number().int().min(1).max(65535),
|
|
24050
|
+
source: LocalPortSourceEnum
|
|
24051
|
+
});
|
|
24052
|
+
var GetConnectionEndpointsResultSchema = object({
|
|
24053
|
+
endpoints: array(ConnectionEndpointSchema).readonly(),
|
|
24054
|
+
/**
|
|
24055
|
+
* The port the hub built the LAN/loopback URLs with, and where that number
|
|
24056
|
+
* came from.
|
|
24057
|
+
*
|
|
24058
|
+
* Returned rather than merely applied, because "the URL is right" and "the
|
|
24059
|
+
* client can KNOW the URL is right" are different properties. A client that
|
|
24060
|
+
* only sees a corrected URL cannot distinguish a hub that fixed the port from
|
|
24061
|
+
* a hub that echoed the port the client sent, so it cannot decide whether to
|
|
24062
|
+
* race the candidate or discard it. With `source` it can: anything but
|
|
24063
|
+
* `caller-hint` is the hub's own socket.
|
|
24064
|
+
*
|
|
24065
|
+
* Absent on hubs predating this field — a client that finds it missing is
|
|
24066
|
+
* talking to an echoing hub and must degrade exactly as it does for
|
|
24067
|
+
* `caller-hint`.
|
|
24068
|
+
*/
|
|
24069
|
+
localPort: AdvertisedLocalPortSchema
|
|
24070
|
+
});
|
|
24006
24071
|
/**
|
|
24007
24072
|
* The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
|
|
24008
24073
|
* AUTO (resolved from the candidate ranking at send time); `resolved` reports
|
|
@@ -24023,8 +24088,13 @@ var AllowedAddressesSchema = object({
|
|
|
24023
24088
|
*/
|
|
24024
24089
|
addresses: array(string()).readonly() });
|
|
24025
24090
|
method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
|
|
24026
|
-
/**
|
|
24027
|
-
|
|
24091
|
+
/**
|
|
24092
|
+
* LEGACY HINT — do not send from new code. Kept optional so clients
|
|
24093
|
+
* written against the echoing contract keep working; the hub uses it
|
|
24094
|
+
* only when it cannot read its own port, and says so via
|
|
24095
|
+
* `localPort.source === 'caller-hint'`.
|
|
24096
|
+
*/
|
|
24097
|
+
port: number().int().min(1).max(65535).optional(),
|
|
24028
24098
|
/** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
|
|
24029
24099
|
* candidate. Default `true`. */
|
|
24030
24100
|
includeLoopback: boolean().optional(),
|
|
@@ -30183,11 +30253,51 @@ function startReachabilityPoll(options) {
|
|
|
30183
30253
|
} };
|
|
30184
30254
|
}
|
|
30185
30255
|
var LAST_FETCHED_FIELD = "lastFetchedAt";
|
|
30256
|
+
/**
|
|
30257
|
+
* The freshness window for a cap whose value changes ONLY when WE write it.
|
|
30258
|
+
*
|
|
30259
|
+
* The usual `staleMs: 10_000` is tuned for a reading that moves on its own — a
|
|
30260
|
+
* battery level, a day/night state that flips at dusk. It is the wrong number
|
|
30261
|
+
* for a cap like `privacy-mask`, whose two facts (is the video mask on, is the
|
|
30262
|
+
* microphone on) change when an operator changes them, through a `setMask` /
|
|
30263
|
+
* `setAudioEnabled` that **re-reads the camera and rewrites this very slice**.
|
|
30264
|
+
* A write is therefore its own invalidation: the window never delays an
|
|
30265
|
+
* operator's own change, however long it is.
|
|
30266
|
+
*
|
|
30267
|
+
* What the window really bounds is the ONE case we do not write: somebody
|
|
30268
|
+
* changing the mask in the vendor app or the camera's web UI. Ten minutes is
|
|
30269
|
+
* the trade — that change is visible within ten minutes, and a fleet of 29
|
|
30270
|
+
* cameras behind a page that polls every 5s costs about 3 camera round trips a
|
|
30271
|
+
* minute instead of 696
|
|
30272
|
+
* ([D221](../../../../docs/decisions/adr-0221-a-polled-list-never-dials-a-camera.md)).
|
|
30273
|
+
*
|
|
30274
|
+
* Use it ONLY with {@link RuntimeStateStaleReadPolicy} `'serve-and-revalidate'`.
|
|
30275
|
+
* On the awaiting default a window this long does not remove the stall, it
|
|
30276
|
+
* merely makes it rarer and just as long.
|
|
30277
|
+
*/
|
|
30278
|
+
var OPERATOR_WRITTEN_STALE_MS = 10 * 6e4;
|
|
30186
30279
|
function createRuntimeStateBridge(params) {
|
|
30187
30280
|
const { runtimeState, cap, ownDeviceId, refresh, staleMs, empty, logger } = params;
|
|
30188
30281
|
const missCooldownMs = params.refreshMissCooldownMs ?? 6e4;
|
|
30282
|
+
const staleRead = params.staleRead ?? "await-refresh";
|
|
30189
30283
|
/** Epoch ms until which a refresh is not re-attempted. 0 = no cooldown. */
|
|
30190
30284
|
let missCooldownUntil = 0;
|
|
30285
|
+
/**
|
|
30286
|
+
* The refresh this bridge currently has in the air, if any.
|
|
30287
|
+
*
|
|
30288
|
+
* Providers single-flight their own camera client, so this is not what stops
|
|
30289
|
+
* a second round trip. What it stops is a POLLED reader JOINING one: under
|
|
30290
|
+
* `'serve-and-revalidate'` a read that finds a refresh already outstanding is
|
|
30291
|
+
* answered from the slice at once — even a cold, empty slice, which reports
|
|
30292
|
+
* UNKNOWN, which is the truth about a camera nobody has reached.
|
|
30293
|
+
*
|
|
30294
|
+
* That distinction is the whole cost of an unreachable camera. Measured on
|
|
30295
|
+
* the live hub (device 3629, an offline battery Reolink): its refresh takes
|
|
30296
|
+
* 23.1s to give up, so without this every 5s poll landing inside those 23s
|
|
30297
|
+
* joined the wait and spent the caller's full 1.2s source budget — five
|
|
30298
|
+
* stalled polls per cooldown cycle, for one camera, forever.
|
|
30299
|
+
*/
|
|
30300
|
+
let refreshInFlight = null;
|
|
30191
30301
|
const readFetchedAt = () => {
|
|
30192
30302
|
const value = runtimeState.getCapState(cap.name)?.[LAST_FETCHED_FIELD];
|
|
30193
30303
|
return typeof value === "number" ? value : 0;
|
|
@@ -30208,14 +30318,14 @@ function createRuntimeStateBridge(params) {
|
|
|
30208
30318
|
}
|
|
30209
30319
|
});
|
|
30210
30320
|
};
|
|
30211
|
-
|
|
30212
|
-
|
|
30213
|
-
|
|
30214
|
-
|
|
30215
|
-
|
|
30216
|
-
|
|
30217
|
-
|
|
30218
|
-
|
|
30321
|
+
/**
|
|
30322
|
+
* One refresh attempt, plus the LANDED check that decides the cooldown.
|
|
30323
|
+
*
|
|
30324
|
+
* @param fetchedAt What `lastFetchedAt` was before the attempt — the only
|
|
30325
|
+
* evidence the bridge has that the refresh persisted
|
|
30326
|
+
* anything, since providers swallow their own camera errors.
|
|
30327
|
+
*/
|
|
30328
|
+
const runRefresh = async (fetchedAt) => {
|
|
30219
30329
|
try {
|
|
30220
30330
|
await refresh();
|
|
30221
30331
|
} catch (err) {
|
|
@@ -30231,6 +30341,41 @@ function createRuntimeStateBridge(params) {
|
|
|
30231
30341
|
}
|
|
30232
30342
|
openMissCooldown(void 0);
|
|
30233
30343
|
};
|
|
30344
|
+
/**
|
|
30345
|
+
* Start a refresh and remember it, at most one at a time.
|
|
30346
|
+
*
|
|
30347
|
+
* It never rejects: under `'serve-and-revalidate'` the caller is answered
|
|
30348
|
+
* from the slice either way, so a floating rejection would take the process
|
|
30349
|
+
* down for a fault the miss cooldown has already recorded and logged.
|
|
30350
|
+
*/
|
|
30351
|
+
const startRefresh = (fetchedAt) => {
|
|
30352
|
+
const existing = refreshInFlight;
|
|
30353
|
+
if (existing !== null) return existing;
|
|
30354
|
+
const started = runRefresh(fetchedAt).catch(() => void 0).finally(() => {
|
|
30355
|
+
refreshInFlight = null;
|
|
30356
|
+
});
|
|
30357
|
+
refreshInFlight = started;
|
|
30358
|
+
return started;
|
|
30359
|
+
};
|
|
30360
|
+
const ensureFresh = async () => {
|
|
30361
|
+
const slice = runtimeState.getCapState(cap.name);
|
|
30362
|
+
const fetchedAt = readFetchedAt();
|
|
30363
|
+
if (slice && Date.now() - fetchedAt <= staleMs) {
|
|
30364
|
+
missCooldownUntil = 0;
|
|
30365
|
+
return;
|
|
30366
|
+
}
|
|
30367
|
+
if (Date.now() < missCooldownUntil) return;
|
|
30368
|
+
if (staleRead === "serve-and-revalidate") {
|
|
30369
|
+
if (refreshInFlight !== null) return;
|
|
30370
|
+
if (slice && fetchedAt > 0) {
|
|
30371
|
+
startRefresh(fetchedAt);
|
|
30372
|
+
return;
|
|
30373
|
+
}
|
|
30374
|
+
await startRefresh(fetchedAt);
|
|
30375
|
+
return;
|
|
30376
|
+
}
|
|
30377
|
+
await runRefresh(fetchedAt);
|
|
30378
|
+
};
|
|
30234
30379
|
const projectStatus = () => {
|
|
30235
30380
|
const slice = runtimeState.getCapState(cap.name);
|
|
30236
30381
|
if (!slice) return empty();
|
|
@@ -39455,7 +39600,6 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
39455
39600
|
static PRIVACY_COORD_MAX = 8191;
|
|
39456
39601
|
registerPrivacyMaskCap() {
|
|
39457
39602
|
const CAP_NAME = "privacy-mask";
|
|
39458
|
-
const STALE_MS = 1e4;
|
|
39459
39603
|
const maxRegions = AmcrestCamera.PRIVACY_MASK_MAX_REGIONS;
|
|
39460
39604
|
const coordMax = AmcrestCamera.PRIVACY_COORD_MAX;
|
|
39461
39605
|
const refreshFromCamera = async () => {
|
|
@@ -39510,7 +39654,8 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
39510
39654
|
cap: privacyMaskCapability,
|
|
39511
39655
|
ownDeviceId: this.id,
|
|
39512
39656
|
refresh: refreshFromCamera,
|
|
39513
|
-
staleMs:
|
|
39657
|
+
staleMs: OPERATOR_WRITTEN_STALE_MS,
|
|
39658
|
+
staleRead: "serve-and-revalidate",
|
|
39514
39659
|
logger: this.ctx.logger,
|
|
39515
39660
|
empty: () => ({
|
|
39516
39661
|
enabled: false,
|
package/dist/addon.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { networkInterfaces } from "node:os";
|
|
|
5
5
|
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
6
6
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
7
7
|
//#endregion
|
|
8
|
-
//#region ../types/dist/event-category-
|
|
8
|
+
//#region ../types/dist/event-category-CIa_iT6b.mjs
|
|
9
9
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
10
10
|
EventCategory["SystemBoot"] = "system.boot";
|
|
11
11
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -21,6 +21,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
21
21
|
*/
|
|
22
22
|
EventCategory["SystemRestartCompleted"] = "system.restart-completed";
|
|
23
23
|
/**
|
|
24
|
+
* The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
|
|
25
|
+
* Emitted only when the material on disk actually changed, so an
|
|
26
|
+
* operator who trusted the old certificate by hand is told rather than
|
|
27
|
+
* discovering it as a browser error. Payload `TlsCertChangedPayload`.
|
|
28
|
+
*
|
|
29
|
+
* Rule: docs/decisions/adr-0227-*.md
|
|
30
|
+
*/
|
|
31
|
+
EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
|
|
32
|
+
/**
|
|
24
33
|
* A newer addon or server-root package version was found by the
|
|
25
34
|
* authoritative registry check. Emitted once when any observed
|
|
26
35
|
* `latestVersion` changes (or a package/node first appears behind);
|
|
@@ -18975,6 +18984,12 @@ var CameraStatusSchema = object({
|
|
|
18975
18984
|
/** Unix timestamp (ms) when this snapshot was composed server-side. */
|
|
18976
18985
|
fetchedAt: number()
|
|
18977
18986
|
});
|
|
18987
|
+
var InferenceDeviceExclusionReasonSchema = _enum([
|
|
18988
|
+
"disabled",
|
|
18989
|
+
"unavailable",
|
|
18990
|
+
"cannot-host-camera-root",
|
|
18991
|
+
"accelerator-preferred"
|
|
18992
|
+
]);
|
|
18978
18993
|
var NodeInferenceDeviceSchema = object({
|
|
18979
18994
|
/** Stable per-node device key, e.g. `openvino:npu`, `edgetpu:usb`, `cpu`. */
|
|
18980
18995
|
key: string(),
|
|
@@ -19005,7 +19020,17 @@ var NodeInferenceDeviceSchema = object({
|
|
|
19005
19020
|
* available per format; this is the stored selection that becomes the
|
|
19006
19021
|
* default for EVERY camera landing on this accelerator.
|
|
19007
19022
|
*/
|
|
19008
|
-
steps: record(string(), DeviceStepConfigSchema).optional()
|
|
19023
|
+
steps: record(string(), DeviceStepConfigSchema).optional(),
|
|
19024
|
+
/**
|
|
19025
|
+
* `null` when the device IS a camera-root candidate on this node; otherwise
|
|
19026
|
+
* the reason the dispatcher drops it. Computed by the SAME
|
|
19027
|
+
* `resolveInferenceDeviceEligibility` the dispatcher runs, so this view can
|
|
19028
|
+
* never disagree with the election — deriving it in the UI from
|
|
19029
|
+
* `enabled`/`available` would silently miss `cannot-host-camera-root` (needs
|
|
19030
|
+
* the node's model catalog) and `accelerator-preferred` (needs the node-wide
|
|
19031
|
+
* "an accelerator is serving" predicate).
|
|
19032
|
+
*/
|
|
19033
|
+
exclusion: InferenceDeviceExclusionReasonSchema.nullable()
|
|
19009
19034
|
});
|
|
19010
19035
|
var NodeInferenceDevicesSchema = object({
|
|
19011
19036
|
nodeId: string(),
|
|
@@ -23960,7 +23985,12 @@ var ListResultSchema = object({
|
|
|
23960
23985
|
probedAt: number()
|
|
23961
23986
|
});
|
|
23962
23987
|
var PreferredSchema = LocalInterfaceSchema.nullable();
|
|
23963
|
-
|
|
23988
|
+
/**
|
|
23989
|
+
* Candidate base URL for the SDK to race on connect. Order matters —
|
|
23990
|
+
* the SDK should attempt these top-to-bottom with a short per-candidate
|
|
23991
|
+
* timeout (e.g. 1500ms) and cache the winner for the session.
|
|
23992
|
+
*/
|
|
23993
|
+
var ConnectionEndpointSchema = object({
|
|
23964
23994
|
/** Operator-facing label (e.g. "LAN — en0", "Public tunnel"). */
|
|
23965
23995
|
label: string(),
|
|
23966
23996
|
/** Fully-formed base URL with scheme + host + port. */
|
|
@@ -24003,7 +24033,42 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
|
|
|
24003
24033
|
* ordering between polls.
|
|
24004
24034
|
*/
|
|
24005
24035
|
priority: number()
|
|
24006
|
-
})
|
|
24036
|
+
});
|
|
24037
|
+
/**
|
|
24038
|
+
* Where the advertised local port came from. Ordered most → least
|
|
24039
|
+
* authoritative, and the whole point of returning it: a client must be able to
|
|
24040
|
+
* tell a FACT about the hub's socket from an echo of its own guess.
|
|
24041
|
+
*/
|
|
24042
|
+
var LocalPortSourceEnum = _enum([
|
|
24043
|
+
"server-config",
|
|
24044
|
+
"server-env",
|
|
24045
|
+
"caller-hint",
|
|
24046
|
+
"default"
|
|
24047
|
+
]);
|
|
24048
|
+
/** The port every LAN/loopback `baseUrl` in the same result was built with. */
|
|
24049
|
+
var AdvertisedLocalPortSchema = object({
|
|
24050
|
+
port: number().int().min(1).max(65535),
|
|
24051
|
+
source: LocalPortSourceEnum
|
|
24052
|
+
});
|
|
24053
|
+
var GetConnectionEndpointsResultSchema = object({
|
|
24054
|
+
endpoints: array(ConnectionEndpointSchema).readonly(),
|
|
24055
|
+
/**
|
|
24056
|
+
* The port the hub built the LAN/loopback URLs with, and where that number
|
|
24057
|
+
* came from.
|
|
24058
|
+
*
|
|
24059
|
+
* Returned rather than merely applied, because "the URL is right" and "the
|
|
24060
|
+
* client can KNOW the URL is right" are different properties. A client that
|
|
24061
|
+
* only sees a corrected URL cannot distinguish a hub that fixed the port from
|
|
24062
|
+
* a hub that echoed the port the client sent, so it cannot decide whether to
|
|
24063
|
+
* race the candidate or discard it. With `source` it can: anything but
|
|
24064
|
+
* `caller-hint` is the hub's own socket.
|
|
24065
|
+
*
|
|
24066
|
+
* Absent on hubs predating this field — a client that finds it missing is
|
|
24067
|
+
* talking to an echoing hub and must degrade exactly as it does for
|
|
24068
|
+
* `caller-hint`.
|
|
24069
|
+
*/
|
|
24070
|
+
localPort: AdvertisedLocalPortSchema
|
|
24071
|
+
});
|
|
24007
24072
|
/**
|
|
24008
24073
|
* The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
|
|
24009
24074
|
* AUTO (resolved from the candidate ranking at send time); `resolved` reports
|
|
@@ -24024,8 +24089,13 @@ var AllowedAddressesSchema = object({
|
|
|
24024
24089
|
*/
|
|
24025
24090
|
addresses: array(string()).readonly() });
|
|
24026
24091
|
method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
|
|
24027
|
-
/**
|
|
24028
|
-
|
|
24092
|
+
/**
|
|
24093
|
+
* LEGACY HINT — do not send from new code. Kept optional so clients
|
|
24094
|
+
* written against the echoing contract keep working; the hub uses it
|
|
24095
|
+
* only when it cannot read its own port, and says so via
|
|
24096
|
+
* `localPort.source === 'caller-hint'`.
|
|
24097
|
+
*/
|
|
24098
|
+
port: number().int().min(1).max(65535).optional(),
|
|
24029
24099
|
/** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
|
|
24030
24100
|
* candidate. Default `true`. */
|
|
24031
24101
|
includeLoopback: boolean().optional(),
|
|
@@ -30184,11 +30254,51 @@ function startReachabilityPoll(options) {
|
|
|
30184
30254
|
} };
|
|
30185
30255
|
}
|
|
30186
30256
|
var LAST_FETCHED_FIELD = "lastFetchedAt";
|
|
30257
|
+
/**
|
|
30258
|
+
* The freshness window for a cap whose value changes ONLY when WE write it.
|
|
30259
|
+
*
|
|
30260
|
+
* The usual `staleMs: 10_000` is tuned for a reading that moves on its own — a
|
|
30261
|
+
* battery level, a day/night state that flips at dusk. It is the wrong number
|
|
30262
|
+
* for a cap like `privacy-mask`, whose two facts (is the video mask on, is the
|
|
30263
|
+
* microphone on) change when an operator changes them, through a `setMask` /
|
|
30264
|
+
* `setAudioEnabled` that **re-reads the camera and rewrites this very slice**.
|
|
30265
|
+
* A write is therefore its own invalidation: the window never delays an
|
|
30266
|
+
* operator's own change, however long it is.
|
|
30267
|
+
*
|
|
30268
|
+
* What the window really bounds is the ONE case we do not write: somebody
|
|
30269
|
+
* changing the mask in the vendor app or the camera's web UI. Ten minutes is
|
|
30270
|
+
* the trade — that change is visible within ten minutes, and a fleet of 29
|
|
30271
|
+
* cameras behind a page that polls every 5s costs about 3 camera round trips a
|
|
30272
|
+
* minute instead of 696
|
|
30273
|
+
* ([D221](../../../../docs/decisions/adr-0221-a-polled-list-never-dials-a-camera.md)).
|
|
30274
|
+
*
|
|
30275
|
+
* Use it ONLY with {@link RuntimeStateStaleReadPolicy} `'serve-and-revalidate'`.
|
|
30276
|
+
* On the awaiting default a window this long does not remove the stall, it
|
|
30277
|
+
* merely makes it rarer and just as long.
|
|
30278
|
+
*/
|
|
30279
|
+
var OPERATOR_WRITTEN_STALE_MS = 10 * 6e4;
|
|
30187
30280
|
function createRuntimeStateBridge(params) {
|
|
30188
30281
|
const { runtimeState, cap, ownDeviceId, refresh, staleMs, empty, logger } = params;
|
|
30189
30282
|
const missCooldownMs = params.refreshMissCooldownMs ?? 6e4;
|
|
30283
|
+
const staleRead = params.staleRead ?? "await-refresh";
|
|
30190
30284
|
/** Epoch ms until which a refresh is not re-attempted. 0 = no cooldown. */
|
|
30191
30285
|
let missCooldownUntil = 0;
|
|
30286
|
+
/**
|
|
30287
|
+
* The refresh this bridge currently has in the air, if any.
|
|
30288
|
+
*
|
|
30289
|
+
* Providers single-flight their own camera client, so this is not what stops
|
|
30290
|
+
* a second round trip. What it stops is a POLLED reader JOINING one: under
|
|
30291
|
+
* `'serve-and-revalidate'` a read that finds a refresh already outstanding is
|
|
30292
|
+
* answered from the slice at once — even a cold, empty slice, which reports
|
|
30293
|
+
* UNKNOWN, which is the truth about a camera nobody has reached.
|
|
30294
|
+
*
|
|
30295
|
+
* That distinction is the whole cost of an unreachable camera. Measured on
|
|
30296
|
+
* the live hub (device 3629, an offline battery Reolink): its refresh takes
|
|
30297
|
+
* 23.1s to give up, so without this every 5s poll landing inside those 23s
|
|
30298
|
+
* joined the wait and spent the caller's full 1.2s source budget — five
|
|
30299
|
+
* stalled polls per cooldown cycle, for one camera, forever.
|
|
30300
|
+
*/
|
|
30301
|
+
let refreshInFlight = null;
|
|
30192
30302
|
const readFetchedAt = () => {
|
|
30193
30303
|
const value = runtimeState.getCapState(cap.name)?.[LAST_FETCHED_FIELD];
|
|
30194
30304
|
return typeof value === "number" ? value : 0;
|
|
@@ -30209,14 +30319,14 @@ function createRuntimeStateBridge(params) {
|
|
|
30209
30319
|
}
|
|
30210
30320
|
});
|
|
30211
30321
|
};
|
|
30212
|
-
|
|
30213
|
-
|
|
30214
|
-
|
|
30215
|
-
|
|
30216
|
-
|
|
30217
|
-
|
|
30218
|
-
|
|
30219
|
-
|
|
30322
|
+
/**
|
|
30323
|
+
* One refresh attempt, plus the LANDED check that decides the cooldown.
|
|
30324
|
+
*
|
|
30325
|
+
* @param fetchedAt What `lastFetchedAt` was before the attempt — the only
|
|
30326
|
+
* evidence the bridge has that the refresh persisted
|
|
30327
|
+
* anything, since providers swallow their own camera errors.
|
|
30328
|
+
*/
|
|
30329
|
+
const runRefresh = async (fetchedAt) => {
|
|
30220
30330
|
try {
|
|
30221
30331
|
await refresh();
|
|
30222
30332
|
} catch (err) {
|
|
@@ -30232,6 +30342,41 @@ function createRuntimeStateBridge(params) {
|
|
|
30232
30342
|
}
|
|
30233
30343
|
openMissCooldown(void 0);
|
|
30234
30344
|
};
|
|
30345
|
+
/**
|
|
30346
|
+
* Start a refresh and remember it, at most one at a time.
|
|
30347
|
+
*
|
|
30348
|
+
* It never rejects: under `'serve-and-revalidate'` the caller is answered
|
|
30349
|
+
* from the slice either way, so a floating rejection would take the process
|
|
30350
|
+
* down for a fault the miss cooldown has already recorded and logged.
|
|
30351
|
+
*/
|
|
30352
|
+
const startRefresh = (fetchedAt) => {
|
|
30353
|
+
const existing = refreshInFlight;
|
|
30354
|
+
if (existing !== null) return existing;
|
|
30355
|
+
const started = runRefresh(fetchedAt).catch(() => void 0).finally(() => {
|
|
30356
|
+
refreshInFlight = null;
|
|
30357
|
+
});
|
|
30358
|
+
refreshInFlight = started;
|
|
30359
|
+
return started;
|
|
30360
|
+
};
|
|
30361
|
+
const ensureFresh = async () => {
|
|
30362
|
+
const slice = runtimeState.getCapState(cap.name);
|
|
30363
|
+
const fetchedAt = readFetchedAt();
|
|
30364
|
+
if (slice && Date.now() - fetchedAt <= staleMs) {
|
|
30365
|
+
missCooldownUntil = 0;
|
|
30366
|
+
return;
|
|
30367
|
+
}
|
|
30368
|
+
if (Date.now() < missCooldownUntil) return;
|
|
30369
|
+
if (staleRead === "serve-and-revalidate") {
|
|
30370
|
+
if (refreshInFlight !== null) return;
|
|
30371
|
+
if (slice && fetchedAt > 0) {
|
|
30372
|
+
startRefresh(fetchedAt);
|
|
30373
|
+
return;
|
|
30374
|
+
}
|
|
30375
|
+
await startRefresh(fetchedAt);
|
|
30376
|
+
return;
|
|
30377
|
+
}
|
|
30378
|
+
await runRefresh(fetchedAt);
|
|
30379
|
+
};
|
|
30235
30380
|
const projectStatus = () => {
|
|
30236
30381
|
const slice = runtimeState.getCapState(cap.name);
|
|
30237
30382
|
if (!slice) return empty();
|
|
@@ -39456,7 +39601,6 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
39456
39601
|
static PRIVACY_COORD_MAX = 8191;
|
|
39457
39602
|
registerPrivacyMaskCap() {
|
|
39458
39603
|
const CAP_NAME = "privacy-mask";
|
|
39459
|
-
const STALE_MS = 1e4;
|
|
39460
39604
|
const maxRegions = AmcrestCamera.PRIVACY_MASK_MAX_REGIONS;
|
|
39461
39605
|
const coordMax = AmcrestCamera.PRIVACY_COORD_MAX;
|
|
39462
39606
|
const refreshFromCamera = async () => {
|
|
@@ -39511,7 +39655,8 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
39511
39655
|
cap: privacyMaskCapability,
|
|
39512
39656
|
ownDeviceId: this.id,
|
|
39513
39657
|
refresh: refreshFromCamera,
|
|
39514
|
-
staleMs:
|
|
39658
|
+
staleMs: OPERATOR_WRITTEN_STALE_MS,
|
|
39659
|
+
staleRead: "serve-and-revalidate",
|
|
39515
39660
|
logger: this.ctx.logger,
|
|
39516
39661
|
empty: () => ({
|
|
39517
39662
|
enabled: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-provider-amcrest",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.27",
|
|
4
4
|
"description": "Amcrest/Dahua camera device provider addon for CamStack — Dahua CGI over HTTP(S) with digest auth (snapshot, RTSP catalog, PTZ, image/day-night config)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|