@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.
Files changed (3) hide show
  1. package/dist/addon.js +151 -15
  2. package/dist/addon.mjs +151 -15
  3. 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
- var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
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
- })).readonly() });
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
- /** Local hub HTTP port to use in base URLs. */
24027
- port: number().int().min(1).max(65535),
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
- const ensureFresh = async () => {
30212
- const slice = runtimeState.getCapState(cap.name);
30213
- const fetchedAt = readFetchedAt();
30214
- if (slice && Date.now() - fetchedAt <= staleMs) {
30215
- missCooldownUntil = 0;
30216
- return;
30217
- }
30218
- if (Date.now() < missCooldownUntil) return;
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: STALE_MS,
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
- var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
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
- })).readonly() });
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
- /** Local hub HTTP port to use in base URLs. */
24028
- port: number().int().min(1).max(65535),
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
- const ensureFresh = async () => {
30213
- const slice = runtimeState.getCapState(cap.name);
30214
- const fetchedAt = readFetchedAt();
30215
- if (slice && Date.now() - fetchedAt <= staleMs) {
30216
- missCooldownUntil = 0;
30217
- return;
30218
- }
30219
- if (Date.now() < missCooldownUntil) return;
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: STALE_MS,
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.25",
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",