@camstack/addon-provider-amcrest 0.2.25 → 0.2.26
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 +151 -15
- package/dist/addon.mjs +151 -15
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -18974,6 +18974,12 @@ var CameraStatusSchema = object({
|
|
|
18974
18974
|
/** Unix timestamp (ms) when this snapshot was composed server-side. */
|
|
18975
18975
|
fetchedAt: number()
|
|
18976
18976
|
});
|
|
18977
|
+
var InferenceDeviceExclusionReasonSchema = _enum([
|
|
18978
|
+
"disabled",
|
|
18979
|
+
"unavailable",
|
|
18980
|
+
"cannot-host-camera-root",
|
|
18981
|
+
"accelerator-preferred"
|
|
18982
|
+
]);
|
|
18977
18983
|
var NodeInferenceDeviceSchema = object({
|
|
18978
18984
|
/** Stable per-node device key, e.g. `openvino:npu`, `edgetpu:usb`, `cpu`. */
|
|
18979
18985
|
key: string(),
|
|
@@ -19004,7 +19010,17 @@ var NodeInferenceDeviceSchema = object({
|
|
|
19004
19010
|
* available per format; this is the stored selection that becomes the
|
|
19005
19011
|
* default for EVERY camera landing on this accelerator.
|
|
19006
19012
|
*/
|
|
19007
|
-
steps: record(string(), DeviceStepConfigSchema).optional()
|
|
19013
|
+
steps: record(string(), DeviceStepConfigSchema).optional(),
|
|
19014
|
+
/**
|
|
19015
|
+
* `null` when the device IS a camera-root candidate on this node; otherwise
|
|
19016
|
+
* the reason the dispatcher drops it. Computed by the SAME
|
|
19017
|
+
* `resolveInferenceDeviceEligibility` the dispatcher runs, so this view can
|
|
19018
|
+
* never disagree with the election — deriving it in the UI from
|
|
19019
|
+
* `enabled`/`available` would silently miss `cannot-host-camera-root` (needs
|
|
19020
|
+
* the node's model catalog) and `accelerator-preferred` (needs the node-wide
|
|
19021
|
+
* "an accelerator is serving" predicate).
|
|
19022
|
+
*/
|
|
19023
|
+
exclusion: InferenceDeviceExclusionReasonSchema.nullable()
|
|
19008
19024
|
});
|
|
19009
19025
|
var NodeInferenceDevicesSchema = object({
|
|
19010
19026
|
nodeId: string(),
|
|
@@ -23959,7 +23975,12 @@ var ListResultSchema = object({
|
|
|
23959
23975
|
probedAt: number()
|
|
23960
23976
|
});
|
|
23961
23977
|
var PreferredSchema = LocalInterfaceSchema.nullable();
|
|
23962
|
-
|
|
23978
|
+
/**
|
|
23979
|
+
* Candidate base URL for the SDK to race on connect. Order matters —
|
|
23980
|
+
* the SDK should attempt these top-to-bottom with a short per-candidate
|
|
23981
|
+
* timeout (e.g. 1500ms) and cache the winner for the session.
|
|
23982
|
+
*/
|
|
23983
|
+
var ConnectionEndpointSchema = object({
|
|
23963
23984
|
/** Operator-facing label (e.g. "LAN — en0", "Public tunnel"). */
|
|
23964
23985
|
label: string(),
|
|
23965
23986
|
/** Fully-formed base URL with scheme + host + port. */
|
|
@@ -24002,7 +24023,42 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
|
|
|
24002
24023
|
* ordering between polls.
|
|
24003
24024
|
*/
|
|
24004
24025
|
priority: number()
|
|
24005
|
-
})
|
|
24026
|
+
});
|
|
24027
|
+
/**
|
|
24028
|
+
* Where the advertised local port came from. Ordered most → least
|
|
24029
|
+
* authoritative, and the whole point of returning it: a client must be able to
|
|
24030
|
+
* tell a FACT about the hub's socket from an echo of its own guess.
|
|
24031
|
+
*/
|
|
24032
|
+
var LocalPortSourceEnum = _enum([
|
|
24033
|
+
"server-config",
|
|
24034
|
+
"server-env",
|
|
24035
|
+
"caller-hint",
|
|
24036
|
+
"default"
|
|
24037
|
+
]);
|
|
24038
|
+
/** The port every LAN/loopback `baseUrl` in the same result was built with. */
|
|
24039
|
+
var AdvertisedLocalPortSchema = object({
|
|
24040
|
+
port: number().int().min(1).max(65535),
|
|
24041
|
+
source: LocalPortSourceEnum
|
|
24042
|
+
});
|
|
24043
|
+
var GetConnectionEndpointsResultSchema = object({
|
|
24044
|
+
endpoints: array(ConnectionEndpointSchema).readonly(),
|
|
24045
|
+
/**
|
|
24046
|
+
* The port the hub built the LAN/loopback URLs with, and where that number
|
|
24047
|
+
* came from.
|
|
24048
|
+
*
|
|
24049
|
+
* Returned rather than merely applied, because "the URL is right" and "the
|
|
24050
|
+
* client can KNOW the URL is right" are different properties. A client that
|
|
24051
|
+
* only sees a corrected URL cannot distinguish a hub that fixed the port from
|
|
24052
|
+
* a hub that echoed the port the client sent, so it cannot decide whether to
|
|
24053
|
+
* race the candidate or discard it. With `source` it can: anything but
|
|
24054
|
+
* `caller-hint` is the hub's own socket.
|
|
24055
|
+
*
|
|
24056
|
+
* Absent on hubs predating this field — a client that finds it missing is
|
|
24057
|
+
* talking to an echoing hub and must degrade exactly as it does for
|
|
24058
|
+
* `caller-hint`.
|
|
24059
|
+
*/
|
|
24060
|
+
localPort: AdvertisedLocalPortSchema
|
|
24061
|
+
});
|
|
24006
24062
|
/**
|
|
24007
24063
|
* The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
|
|
24008
24064
|
* AUTO (resolved from the candidate ranking at send time); `resolved` reports
|
|
@@ -24023,8 +24079,13 @@ var AllowedAddressesSchema = object({
|
|
|
24023
24079
|
*/
|
|
24024
24080
|
addresses: array(string()).readonly() });
|
|
24025
24081
|
method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
|
|
24026
|
-
/**
|
|
24027
|
-
|
|
24082
|
+
/**
|
|
24083
|
+
* LEGACY HINT — do not send from new code. Kept optional so clients
|
|
24084
|
+
* written against the echoing contract keep working; the hub uses it
|
|
24085
|
+
* only when it cannot read its own port, and says so via
|
|
24086
|
+
* `localPort.source === 'caller-hint'`.
|
|
24087
|
+
*/
|
|
24088
|
+
port: number().int().min(1).max(65535).optional(),
|
|
24028
24089
|
/** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
|
|
24029
24090
|
* candidate. Default `true`. */
|
|
24030
24091
|
includeLoopback: boolean().optional(),
|
|
@@ -30183,11 +30244,51 @@ function startReachabilityPoll(options) {
|
|
|
30183
30244
|
} };
|
|
30184
30245
|
}
|
|
30185
30246
|
var LAST_FETCHED_FIELD = "lastFetchedAt";
|
|
30247
|
+
/**
|
|
30248
|
+
* The freshness window for a cap whose value changes ONLY when WE write it.
|
|
30249
|
+
*
|
|
30250
|
+
* The usual `staleMs: 10_000` is tuned for a reading that moves on its own — a
|
|
30251
|
+
* battery level, a day/night state that flips at dusk. It is the wrong number
|
|
30252
|
+
* for a cap like `privacy-mask`, whose two facts (is the video mask on, is the
|
|
30253
|
+
* microphone on) change when an operator changes them, through a `setMask` /
|
|
30254
|
+
* `setAudioEnabled` that **re-reads the camera and rewrites this very slice**.
|
|
30255
|
+
* A write is therefore its own invalidation: the window never delays an
|
|
30256
|
+
* operator's own change, however long it is.
|
|
30257
|
+
*
|
|
30258
|
+
* What the window really bounds is the ONE case we do not write: somebody
|
|
30259
|
+
* changing the mask in the vendor app or the camera's web UI. Ten minutes is
|
|
30260
|
+
* the trade — that change is visible within ten minutes, and a fleet of 29
|
|
30261
|
+
* cameras behind a page that polls every 5s costs about 3 camera round trips a
|
|
30262
|
+
* minute instead of 696
|
|
30263
|
+
* ([D221](../../../../docs/decisions/adr-0221-a-polled-list-never-dials-a-camera.md)).
|
|
30264
|
+
*
|
|
30265
|
+
* Use it ONLY with {@link RuntimeStateStaleReadPolicy} `'serve-and-revalidate'`.
|
|
30266
|
+
* On the awaiting default a window this long does not remove the stall, it
|
|
30267
|
+
* merely makes it rarer and just as long.
|
|
30268
|
+
*/
|
|
30269
|
+
var OPERATOR_WRITTEN_STALE_MS = 10 * 6e4;
|
|
30186
30270
|
function createRuntimeStateBridge(params) {
|
|
30187
30271
|
const { runtimeState, cap, ownDeviceId, refresh, staleMs, empty, logger } = params;
|
|
30188
30272
|
const missCooldownMs = params.refreshMissCooldownMs ?? 6e4;
|
|
30273
|
+
const staleRead = params.staleRead ?? "await-refresh";
|
|
30189
30274
|
/** Epoch ms until which a refresh is not re-attempted. 0 = no cooldown. */
|
|
30190
30275
|
let missCooldownUntil = 0;
|
|
30276
|
+
/**
|
|
30277
|
+
* The refresh this bridge currently has in the air, if any.
|
|
30278
|
+
*
|
|
30279
|
+
* Providers single-flight their own camera client, so this is not what stops
|
|
30280
|
+
* a second round trip. What it stops is a POLLED reader JOINING one: under
|
|
30281
|
+
* `'serve-and-revalidate'` a read that finds a refresh already outstanding is
|
|
30282
|
+
* answered from the slice at once — even a cold, empty slice, which reports
|
|
30283
|
+
* UNKNOWN, which is the truth about a camera nobody has reached.
|
|
30284
|
+
*
|
|
30285
|
+
* That distinction is the whole cost of an unreachable camera. Measured on
|
|
30286
|
+
* the live hub (device 3629, an offline battery Reolink): its refresh takes
|
|
30287
|
+
* 23.1s to give up, so without this every 5s poll landing inside those 23s
|
|
30288
|
+
* joined the wait and spent the caller's full 1.2s source budget — five
|
|
30289
|
+
* stalled polls per cooldown cycle, for one camera, forever.
|
|
30290
|
+
*/
|
|
30291
|
+
let refreshInFlight = null;
|
|
30191
30292
|
const readFetchedAt = () => {
|
|
30192
30293
|
const value = runtimeState.getCapState(cap.name)?.[LAST_FETCHED_FIELD];
|
|
30193
30294
|
return typeof value === "number" ? value : 0;
|
|
@@ -30208,14 +30309,14 @@ function createRuntimeStateBridge(params) {
|
|
|
30208
30309
|
}
|
|
30209
30310
|
});
|
|
30210
30311
|
};
|
|
30211
|
-
|
|
30212
|
-
|
|
30213
|
-
|
|
30214
|
-
|
|
30215
|
-
|
|
30216
|
-
|
|
30217
|
-
|
|
30218
|
-
|
|
30312
|
+
/**
|
|
30313
|
+
* One refresh attempt, plus the LANDED check that decides the cooldown.
|
|
30314
|
+
*
|
|
30315
|
+
* @param fetchedAt What `lastFetchedAt` was before the attempt — the only
|
|
30316
|
+
* evidence the bridge has that the refresh persisted
|
|
30317
|
+
* anything, since providers swallow their own camera errors.
|
|
30318
|
+
*/
|
|
30319
|
+
const runRefresh = async (fetchedAt) => {
|
|
30219
30320
|
try {
|
|
30220
30321
|
await refresh();
|
|
30221
30322
|
} catch (err) {
|
|
@@ -30231,6 +30332,41 @@ function createRuntimeStateBridge(params) {
|
|
|
30231
30332
|
}
|
|
30232
30333
|
openMissCooldown(void 0);
|
|
30233
30334
|
};
|
|
30335
|
+
/**
|
|
30336
|
+
* Start a refresh and remember it, at most one at a time.
|
|
30337
|
+
*
|
|
30338
|
+
* It never rejects: under `'serve-and-revalidate'` the caller is answered
|
|
30339
|
+
* from the slice either way, so a floating rejection would take the process
|
|
30340
|
+
* down for a fault the miss cooldown has already recorded and logged.
|
|
30341
|
+
*/
|
|
30342
|
+
const startRefresh = (fetchedAt) => {
|
|
30343
|
+
const existing = refreshInFlight;
|
|
30344
|
+
if (existing !== null) return existing;
|
|
30345
|
+
const started = runRefresh(fetchedAt).catch(() => void 0).finally(() => {
|
|
30346
|
+
refreshInFlight = null;
|
|
30347
|
+
});
|
|
30348
|
+
refreshInFlight = started;
|
|
30349
|
+
return started;
|
|
30350
|
+
};
|
|
30351
|
+
const ensureFresh = async () => {
|
|
30352
|
+
const slice = runtimeState.getCapState(cap.name);
|
|
30353
|
+
const fetchedAt = readFetchedAt();
|
|
30354
|
+
if (slice && Date.now() - fetchedAt <= staleMs) {
|
|
30355
|
+
missCooldownUntil = 0;
|
|
30356
|
+
return;
|
|
30357
|
+
}
|
|
30358
|
+
if (Date.now() < missCooldownUntil) return;
|
|
30359
|
+
if (staleRead === "serve-and-revalidate") {
|
|
30360
|
+
if (refreshInFlight !== null) return;
|
|
30361
|
+
if (slice && fetchedAt > 0) {
|
|
30362
|
+
startRefresh(fetchedAt);
|
|
30363
|
+
return;
|
|
30364
|
+
}
|
|
30365
|
+
await startRefresh(fetchedAt);
|
|
30366
|
+
return;
|
|
30367
|
+
}
|
|
30368
|
+
await runRefresh(fetchedAt);
|
|
30369
|
+
};
|
|
30234
30370
|
const projectStatus = () => {
|
|
30235
30371
|
const slice = runtimeState.getCapState(cap.name);
|
|
30236
30372
|
if (!slice) return empty();
|
|
@@ -39455,7 +39591,6 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
39455
39591
|
static PRIVACY_COORD_MAX = 8191;
|
|
39456
39592
|
registerPrivacyMaskCap() {
|
|
39457
39593
|
const CAP_NAME = "privacy-mask";
|
|
39458
|
-
const STALE_MS = 1e4;
|
|
39459
39594
|
const maxRegions = AmcrestCamera.PRIVACY_MASK_MAX_REGIONS;
|
|
39460
39595
|
const coordMax = AmcrestCamera.PRIVACY_COORD_MAX;
|
|
39461
39596
|
const refreshFromCamera = async () => {
|
|
@@ -39510,7 +39645,8 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
39510
39645
|
cap: privacyMaskCapability,
|
|
39511
39646
|
ownDeviceId: this.id,
|
|
39512
39647
|
refresh: refreshFromCamera,
|
|
39513
|
-
staleMs:
|
|
39648
|
+
staleMs: OPERATOR_WRITTEN_STALE_MS,
|
|
39649
|
+
staleRead: "serve-and-revalidate",
|
|
39514
39650
|
logger: this.ctx.logger,
|
|
39515
39651
|
empty: () => ({
|
|
39516
39652
|
enabled: false,
|
package/dist/addon.mjs
CHANGED
|
@@ -18975,6 +18975,12 @@ var CameraStatusSchema = object({
|
|
|
18975
18975
|
/** Unix timestamp (ms) when this snapshot was composed server-side. */
|
|
18976
18976
|
fetchedAt: number()
|
|
18977
18977
|
});
|
|
18978
|
+
var InferenceDeviceExclusionReasonSchema = _enum([
|
|
18979
|
+
"disabled",
|
|
18980
|
+
"unavailable",
|
|
18981
|
+
"cannot-host-camera-root",
|
|
18982
|
+
"accelerator-preferred"
|
|
18983
|
+
]);
|
|
18978
18984
|
var NodeInferenceDeviceSchema = object({
|
|
18979
18985
|
/** Stable per-node device key, e.g. `openvino:npu`, `edgetpu:usb`, `cpu`. */
|
|
18980
18986
|
key: string(),
|
|
@@ -19005,7 +19011,17 @@ var NodeInferenceDeviceSchema = object({
|
|
|
19005
19011
|
* available per format; this is the stored selection that becomes the
|
|
19006
19012
|
* default for EVERY camera landing on this accelerator.
|
|
19007
19013
|
*/
|
|
19008
|
-
steps: record(string(), DeviceStepConfigSchema).optional()
|
|
19014
|
+
steps: record(string(), DeviceStepConfigSchema).optional(),
|
|
19015
|
+
/**
|
|
19016
|
+
* `null` when the device IS a camera-root candidate on this node; otherwise
|
|
19017
|
+
* the reason the dispatcher drops it. Computed by the SAME
|
|
19018
|
+
* `resolveInferenceDeviceEligibility` the dispatcher runs, so this view can
|
|
19019
|
+
* never disagree with the election — deriving it in the UI from
|
|
19020
|
+
* `enabled`/`available` would silently miss `cannot-host-camera-root` (needs
|
|
19021
|
+
* the node's model catalog) and `accelerator-preferred` (needs the node-wide
|
|
19022
|
+
* "an accelerator is serving" predicate).
|
|
19023
|
+
*/
|
|
19024
|
+
exclusion: InferenceDeviceExclusionReasonSchema.nullable()
|
|
19009
19025
|
});
|
|
19010
19026
|
var NodeInferenceDevicesSchema = object({
|
|
19011
19027
|
nodeId: string(),
|
|
@@ -23960,7 +23976,12 @@ var ListResultSchema = object({
|
|
|
23960
23976
|
probedAt: number()
|
|
23961
23977
|
});
|
|
23962
23978
|
var PreferredSchema = LocalInterfaceSchema.nullable();
|
|
23963
|
-
|
|
23979
|
+
/**
|
|
23980
|
+
* Candidate base URL for the SDK to race on connect. Order matters —
|
|
23981
|
+
* the SDK should attempt these top-to-bottom with a short per-candidate
|
|
23982
|
+
* timeout (e.g. 1500ms) and cache the winner for the session.
|
|
23983
|
+
*/
|
|
23984
|
+
var ConnectionEndpointSchema = object({
|
|
23964
23985
|
/** Operator-facing label (e.g. "LAN — en0", "Public tunnel"). */
|
|
23965
23986
|
label: string(),
|
|
23966
23987
|
/** Fully-formed base URL with scheme + host + port. */
|
|
@@ -24003,7 +24024,42 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
|
|
|
24003
24024
|
* ordering between polls.
|
|
24004
24025
|
*/
|
|
24005
24026
|
priority: number()
|
|
24006
|
-
})
|
|
24027
|
+
});
|
|
24028
|
+
/**
|
|
24029
|
+
* Where the advertised local port came from. Ordered most → least
|
|
24030
|
+
* authoritative, and the whole point of returning it: a client must be able to
|
|
24031
|
+
* tell a FACT about the hub's socket from an echo of its own guess.
|
|
24032
|
+
*/
|
|
24033
|
+
var LocalPortSourceEnum = _enum([
|
|
24034
|
+
"server-config",
|
|
24035
|
+
"server-env",
|
|
24036
|
+
"caller-hint",
|
|
24037
|
+
"default"
|
|
24038
|
+
]);
|
|
24039
|
+
/** The port every LAN/loopback `baseUrl` in the same result was built with. */
|
|
24040
|
+
var AdvertisedLocalPortSchema = object({
|
|
24041
|
+
port: number().int().min(1).max(65535),
|
|
24042
|
+
source: LocalPortSourceEnum
|
|
24043
|
+
});
|
|
24044
|
+
var GetConnectionEndpointsResultSchema = object({
|
|
24045
|
+
endpoints: array(ConnectionEndpointSchema).readonly(),
|
|
24046
|
+
/**
|
|
24047
|
+
* The port the hub built the LAN/loopback URLs with, and where that number
|
|
24048
|
+
* came from.
|
|
24049
|
+
*
|
|
24050
|
+
* Returned rather than merely applied, because "the URL is right" and "the
|
|
24051
|
+
* client can KNOW the URL is right" are different properties. A client that
|
|
24052
|
+
* only sees a corrected URL cannot distinguish a hub that fixed the port from
|
|
24053
|
+
* a hub that echoed the port the client sent, so it cannot decide whether to
|
|
24054
|
+
* race the candidate or discard it. With `source` it can: anything but
|
|
24055
|
+
* `caller-hint` is the hub's own socket.
|
|
24056
|
+
*
|
|
24057
|
+
* Absent on hubs predating this field — a client that finds it missing is
|
|
24058
|
+
* talking to an echoing hub and must degrade exactly as it does for
|
|
24059
|
+
* `caller-hint`.
|
|
24060
|
+
*/
|
|
24061
|
+
localPort: AdvertisedLocalPortSchema
|
|
24062
|
+
});
|
|
24007
24063
|
/**
|
|
24008
24064
|
* The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
|
|
24009
24065
|
* AUTO (resolved from the candidate ranking at send time); `resolved` reports
|
|
@@ -24024,8 +24080,13 @@ var AllowedAddressesSchema = object({
|
|
|
24024
24080
|
*/
|
|
24025
24081
|
addresses: array(string()).readonly() });
|
|
24026
24082
|
method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
|
|
24027
|
-
/**
|
|
24028
|
-
|
|
24083
|
+
/**
|
|
24084
|
+
* LEGACY HINT — do not send from new code. Kept optional so clients
|
|
24085
|
+
* written against the echoing contract keep working; the hub uses it
|
|
24086
|
+
* only when it cannot read its own port, and says so via
|
|
24087
|
+
* `localPort.source === 'caller-hint'`.
|
|
24088
|
+
*/
|
|
24089
|
+
port: number().int().min(1).max(65535).optional(),
|
|
24029
24090
|
/** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
|
|
24030
24091
|
* candidate. Default `true`. */
|
|
24031
24092
|
includeLoopback: boolean().optional(),
|
|
@@ -30184,11 +30245,51 @@ function startReachabilityPoll(options) {
|
|
|
30184
30245
|
} };
|
|
30185
30246
|
}
|
|
30186
30247
|
var LAST_FETCHED_FIELD = "lastFetchedAt";
|
|
30248
|
+
/**
|
|
30249
|
+
* The freshness window for a cap whose value changes ONLY when WE write it.
|
|
30250
|
+
*
|
|
30251
|
+
* The usual `staleMs: 10_000` is tuned for a reading that moves on its own — a
|
|
30252
|
+
* battery level, a day/night state that flips at dusk. It is the wrong number
|
|
30253
|
+
* for a cap like `privacy-mask`, whose two facts (is the video mask on, is the
|
|
30254
|
+
* microphone on) change when an operator changes them, through a `setMask` /
|
|
30255
|
+
* `setAudioEnabled` that **re-reads the camera and rewrites this very slice**.
|
|
30256
|
+
* A write is therefore its own invalidation: the window never delays an
|
|
30257
|
+
* operator's own change, however long it is.
|
|
30258
|
+
*
|
|
30259
|
+
* What the window really bounds is the ONE case we do not write: somebody
|
|
30260
|
+
* changing the mask in the vendor app or the camera's web UI. Ten minutes is
|
|
30261
|
+
* the trade — that change is visible within ten minutes, and a fleet of 29
|
|
30262
|
+
* cameras behind a page that polls every 5s costs about 3 camera round trips a
|
|
30263
|
+
* minute instead of 696
|
|
30264
|
+
* ([D221](../../../../docs/decisions/adr-0221-a-polled-list-never-dials-a-camera.md)).
|
|
30265
|
+
*
|
|
30266
|
+
* Use it ONLY with {@link RuntimeStateStaleReadPolicy} `'serve-and-revalidate'`.
|
|
30267
|
+
* On the awaiting default a window this long does not remove the stall, it
|
|
30268
|
+
* merely makes it rarer and just as long.
|
|
30269
|
+
*/
|
|
30270
|
+
var OPERATOR_WRITTEN_STALE_MS = 10 * 6e4;
|
|
30187
30271
|
function createRuntimeStateBridge(params) {
|
|
30188
30272
|
const { runtimeState, cap, ownDeviceId, refresh, staleMs, empty, logger } = params;
|
|
30189
30273
|
const missCooldownMs = params.refreshMissCooldownMs ?? 6e4;
|
|
30274
|
+
const staleRead = params.staleRead ?? "await-refresh";
|
|
30190
30275
|
/** Epoch ms until which a refresh is not re-attempted. 0 = no cooldown. */
|
|
30191
30276
|
let missCooldownUntil = 0;
|
|
30277
|
+
/**
|
|
30278
|
+
* The refresh this bridge currently has in the air, if any.
|
|
30279
|
+
*
|
|
30280
|
+
* Providers single-flight their own camera client, so this is not what stops
|
|
30281
|
+
* a second round trip. What it stops is a POLLED reader JOINING one: under
|
|
30282
|
+
* `'serve-and-revalidate'` a read that finds a refresh already outstanding is
|
|
30283
|
+
* answered from the slice at once — even a cold, empty slice, which reports
|
|
30284
|
+
* UNKNOWN, which is the truth about a camera nobody has reached.
|
|
30285
|
+
*
|
|
30286
|
+
* That distinction is the whole cost of an unreachable camera. Measured on
|
|
30287
|
+
* the live hub (device 3629, an offline battery Reolink): its refresh takes
|
|
30288
|
+
* 23.1s to give up, so without this every 5s poll landing inside those 23s
|
|
30289
|
+
* joined the wait and spent the caller's full 1.2s source budget — five
|
|
30290
|
+
* stalled polls per cooldown cycle, for one camera, forever.
|
|
30291
|
+
*/
|
|
30292
|
+
let refreshInFlight = null;
|
|
30192
30293
|
const readFetchedAt = () => {
|
|
30193
30294
|
const value = runtimeState.getCapState(cap.name)?.[LAST_FETCHED_FIELD];
|
|
30194
30295
|
return typeof value === "number" ? value : 0;
|
|
@@ -30209,14 +30310,14 @@ function createRuntimeStateBridge(params) {
|
|
|
30209
30310
|
}
|
|
30210
30311
|
});
|
|
30211
30312
|
};
|
|
30212
|
-
|
|
30213
|
-
|
|
30214
|
-
|
|
30215
|
-
|
|
30216
|
-
|
|
30217
|
-
|
|
30218
|
-
|
|
30219
|
-
|
|
30313
|
+
/**
|
|
30314
|
+
* One refresh attempt, plus the LANDED check that decides the cooldown.
|
|
30315
|
+
*
|
|
30316
|
+
* @param fetchedAt What `lastFetchedAt` was before the attempt — the only
|
|
30317
|
+
* evidence the bridge has that the refresh persisted
|
|
30318
|
+
* anything, since providers swallow their own camera errors.
|
|
30319
|
+
*/
|
|
30320
|
+
const runRefresh = async (fetchedAt) => {
|
|
30220
30321
|
try {
|
|
30221
30322
|
await refresh();
|
|
30222
30323
|
} catch (err) {
|
|
@@ -30232,6 +30333,41 @@ function createRuntimeStateBridge(params) {
|
|
|
30232
30333
|
}
|
|
30233
30334
|
openMissCooldown(void 0);
|
|
30234
30335
|
};
|
|
30336
|
+
/**
|
|
30337
|
+
* Start a refresh and remember it, at most one at a time.
|
|
30338
|
+
*
|
|
30339
|
+
* It never rejects: under `'serve-and-revalidate'` the caller is answered
|
|
30340
|
+
* from the slice either way, so a floating rejection would take the process
|
|
30341
|
+
* down for a fault the miss cooldown has already recorded and logged.
|
|
30342
|
+
*/
|
|
30343
|
+
const startRefresh = (fetchedAt) => {
|
|
30344
|
+
const existing = refreshInFlight;
|
|
30345
|
+
if (existing !== null) return existing;
|
|
30346
|
+
const started = runRefresh(fetchedAt).catch(() => void 0).finally(() => {
|
|
30347
|
+
refreshInFlight = null;
|
|
30348
|
+
});
|
|
30349
|
+
refreshInFlight = started;
|
|
30350
|
+
return started;
|
|
30351
|
+
};
|
|
30352
|
+
const ensureFresh = async () => {
|
|
30353
|
+
const slice = runtimeState.getCapState(cap.name);
|
|
30354
|
+
const fetchedAt = readFetchedAt();
|
|
30355
|
+
if (slice && Date.now() - fetchedAt <= staleMs) {
|
|
30356
|
+
missCooldownUntil = 0;
|
|
30357
|
+
return;
|
|
30358
|
+
}
|
|
30359
|
+
if (Date.now() < missCooldownUntil) return;
|
|
30360
|
+
if (staleRead === "serve-and-revalidate") {
|
|
30361
|
+
if (refreshInFlight !== null) return;
|
|
30362
|
+
if (slice && fetchedAt > 0) {
|
|
30363
|
+
startRefresh(fetchedAt);
|
|
30364
|
+
return;
|
|
30365
|
+
}
|
|
30366
|
+
await startRefresh(fetchedAt);
|
|
30367
|
+
return;
|
|
30368
|
+
}
|
|
30369
|
+
await runRefresh(fetchedAt);
|
|
30370
|
+
};
|
|
30235
30371
|
const projectStatus = () => {
|
|
30236
30372
|
const slice = runtimeState.getCapState(cap.name);
|
|
30237
30373
|
if (!slice) return empty();
|
|
@@ -39456,7 +39592,6 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
39456
39592
|
static PRIVACY_COORD_MAX = 8191;
|
|
39457
39593
|
registerPrivacyMaskCap() {
|
|
39458
39594
|
const CAP_NAME = "privacy-mask";
|
|
39459
|
-
const STALE_MS = 1e4;
|
|
39460
39595
|
const maxRegions = AmcrestCamera.PRIVACY_MASK_MAX_REGIONS;
|
|
39461
39596
|
const coordMax = AmcrestCamera.PRIVACY_COORD_MAX;
|
|
39462
39597
|
const refreshFromCamera = async () => {
|
|
@@ -39511,7 +39646,8 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
39511
39646
|
cap: privacyMaskCapability,
|
|
39512
39647
|
ownDeviceId: this.id,
|
|
39513
39648
|
refresh: refreshFromCamera,
|
|
39514
|
-
staleMs:
|
|
39649
|
+
staleMs: OPERATOR_WRITTEN_STALE_MS,
|
|
39650
|
+
staleRead: "serve-and-revalidate",
|
|
39515
39651
|
logger: this.ctx.logger,
|
|
39516
39652
|
empty: () => ({
|
|
39517
39653
|
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.26",
|
|
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",
|