@camstack/system 1.2.79 → 1.2.81
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/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-bindings-store.d.ts +21 -0
- package/dist/builtins/device-manager/device-manager.addon.js +23 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +23 -2
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +557 -533
- package/dist/builtins/snapshot/index.mjs +556 -533
- package/dist/builtins/snapshot/snapshot-coalescing.d.ts +6 -1
- package/dist/builtins/snapshot/snapshot-courtesy.d.ts +36 -0
- package/dist/builtins/snapshot/snapshot-resize.d.ts +43 -11
- package/dist/builtins/snapshot/snapshot.addon.d.ts +52 -45
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-cINpngi1.js → dist-CA2mim74.js} +48 -16
- package/dist/{dist-CIIFhXqm.mjs → dist-DZOZtn-0.mjs} +48 -16
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/dist/builtins/snapshot/snapshot-keep-warm.d.ts +0 -86
|
@@ -42,8 +42,13 @@ export declare const SWR_COLD_WAIT_MS = 10000;
|
|
|
42
42
|
* Max simultaneous ffmpeg keyframe grabs — the WRAPPER path, the common case for
|
|
43
43
|
* cameras with no vendor snapshot URL. A 20-cam grid mount spawns at most this
|
|
44
44
|
* many ffmpeg processes; the rest serve cached frames (SWR) or queue.
|
|
45
|
+
*
|
|
46
|
+
* Raised 3 → 6 on 2026-08-12 (operator): a 17-camera cold start serialised
|
|
47
|
+
* into 6 waves and the client-side "image displayed" clustering matched the
|
|
48
|
+
* pool exactly (p90 10.8 s). Measured client-side after the raise — if the
|
|
49
|
+
* hub shows decode pressure, this is the first knob to lower.
|
|
45
50
|
*/
|
|
46
|
-
export declare const MAX_CONCURRENT_GRABS =
|
|
51
|
+
export declare const MAX_CONCURRENT_GRABS = 6;
|
|
47
52
|
/**
|
|
48
53
|
* Max simultaneous cheap native snapshot fetches (vendor HTTP/ONVIF). Higher than
|
|
49
54
|
* the ffmpeg bound because a native fetch is a lightweight HTTP round-trip, not a
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Why a camera cannot produce a frame right now. */
|
|
2
|
+
export type CourtesyReason = 'disabled' | 'offline' | 'sleeping';
|
|
3
|
+
/** Default tile size when the caller asks for no particular width. */
|
|
4
|
+
export declare const COURTESY_DEFAULT_WIDTH = 640;
|
|
5
|
+
/** The word the frame carries. Deliberately the operator's vocabulary. */
|
|
6
|
+
export declare function courtesyLabel(reason: CourtesyReason): string;
|
|
7
|
+
/**
|
|
8
|
+
* Background per reason. A disabled camera is a DELIBERATE state and must not
|
|
9
|
+
* read as an alarm, so it is neutral grey; offline is a fault and is warmer.
|
|
10
|
+
*/
|
|
11
|
+
export declare function courtesyBackground(reason: CourtesyReason): string;
|
|
12
|
+
/** XML escaping for text placed inside an SVG `<text>` node. */
|
|
13
|
+
export declare function escapeCourtesyText(value: string): string;
|
|
14
|
+
export interface CourtesyFrameSpec {
|
|
15
|
+
readonly deviceName: string;
|
|
16
|
+
readonly reason: CourtesyReason;
|
|
17
|
+
readonly width: number;
|
|
18
|
+
readonly height: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The SVG the frame is rasterized from: the state large and centred, the
|
|
22
|
+
* camera's name under it, smaller and dimmer.
|
|
23
|
+
*
|
|
24
|
+
* Sizes derive from the width so a 240 px grid tile and a 1920 px full-bleed
|
|
25
|
+
* frame read the same, with a floor so a thumbnail stays legible.
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildCourtesySvg(spec: CourtesyFrameSpec): string;
|
|
28
|
+
/** Cache key — a courtesy frame is a pure function of these four. */
|
|
29
|
+
export declare function courtesyCacheKey(spec: CourtesyFrameSpec): string;
|
|
30
|
+
/**
|
|
31
|
+
* Render the frame.
|
|
32
|
+
*
|
|
33
|
+
* Rejects on failure — the caller decides what to do, exactly as `resizeJpeg`
|
|
34
|
+
* does, so a broken courtesy path is never mistaken for a broken camera.
|
|
35
|
+
*/
|
|
36
|
+
export declare function renderCourtesyJpeg(spec: CourtesyFrameSpec): Promise<Buffer>;
|
|
@@ -7,24 +7,56 @@
|
|
|
7
7
|
* ffmpeg filter chain to hook into. Resizing the finished bytes gives both the
|
|
8
8
|
* same behaviour with one implementation.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* ── Why sharp and not ffmpeg (2026-08-11) ─────────────────────────────────
|
|
11
|
+
* This used to `spawn('ffmpeg')` per resize. The snapshot wrapper is a system
|
|
12
|
+
* builtin, so it loads in the hub's ROOT process: sampled on the live hub,
|
|
13
|
+
* ~1.6 of those children were running at any instant, every one of them
|
|
14
|
+
* parented by the root PID.
|
|
13
15
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
+
* Be precise about what that cost the root process, because it is easy to
|
|
17
|
+
* overstate. The transcode itself ran in the CHILD and was charged to ffmpeg
|
|
18
|
+
* (16 % + 13 % of a core in that same sample), not to the parent. What the
|
|
19
|
+
* parent paid was the fork, the exec, the JPEG written into one pipe and read
|
|
20
|
+
* back out of the other, and the base64 — real event-loop work, on the loop
|
|
21
|
+
* that also serves the tRPC API, but NOT the 52 ms below.
|
|
22
|
+
*
|
|
23
|
+
* Benchmarked in that container, 2560×1440 → 640 wide, wall-clock per resize:
|
|
24
|
+
*
|
|
25
|
+
* ffmpeg 52.0 ms (fork + exec + pipe round-trip + teardown)
|
|
26
|
+
* sharp 4.8 ms (in-process libvips, on its own threadpool)
|
|
27
|
+
*
|
|
28
|
+
* So this removes ~29 % of a core of container CPU and the per-resize process
|
|
29
|
+
* churn from the root process. It was NOT verified to be the cause of that
|
|
30
|
+
* process sitting at 90 % — nobody has profiled it.
|
|
31
|
+
*
|
|
32
|
+
* `sharp` is not a new dependency: it is already a host-external
|
|
33
|
+
* (`HOST_EXTERNAL_SPECIFIERS`), already resolved from the framework closure at
|
|
34
|
+
* runtime, and already used by the terminal frame renderer. The build preset
|
|
35
|
+
* keeps it out of the bundle, so this import costs nothing at pack time.
|
|
36
|
+
*
|
|
37
|
+
* The cost is still one resize per (device, stream, width) per cache window —
|
|
38
|
+
* not one per request. What changed is what a resize COSTS.
|
|
16
39
|
*/
|
|
17
40
|
/** Runs the resize. Injected so the caller is testable without a real ffmpeg. */
|
|
18
41
|
export type JpegResizer = (bytes: Buffer, width: number) => Promise<Buffer>;
|
|
19
42
|
export declare const RESIZE_TIMEOUT_MS = 10000;
|
|
20
43
|
/**
|
|
21
|
-
* Scale to `width`, preserving aspect ratio
|
|
22
|
-
*
|
|
44
|
+
* Scale to `width`, preserving aspect ratio.
|
|
45
|
+
*
|
|
46
|
+
* Rejects on failure or timeout. It NEVER falls back to the original silently —
|
|
47
|
+
* a caller that quietly served the 4K frame when the resize failed would
|
|
48
|
+
* reproduce exactly the bug this whole module exists to fix, and nobody would
|
|
49
|
+
* see it. The caller decides, and logs.
|
|
23
50
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
51
|
+
* It DOES upscale a source narrower than `width`, and that is deliberate. A
|
|
52
|
+
* `withoutEnlargement: true` was tried first — it is the obvious saving, since
|
|
53
|
+
* upscaling pays encode cost for a blurrier, larger image. But the caller
|
|
54
|
+
* stamps the response ETag from the width it asked for, and its resize-FAILURE
|
|
55
|
+
* path already returns `width: undefined` specifically so a response can never
|
|
56
|
+
* "claim a width the bytes do not have". Silently returning 320 px bytes for a
|
|
57
|
+
* `w=640` request breaks that invariant on the SUCCESS path, where nobody is
|
|
58
|
+
* looking. Honest output width beats a few saved pixels; revisit only together
|
|
59
|
+
* with the ETag.
|
|
28
60
|
*/
|
|
29
61
|
export declare function resizeJpeg(bytes: Buffer, width: number, timeoutMs?: number): Promise<Buffer>;
|
|
30
62
|
/**
|
|
@@ -34,6 +34,15 @@ export declare class SnapshotAddon extends BaseAddon<SnapshotAddonConfig> {
|
|
|
34
34
|
/** Card-sized derivatives of the frames above, keyed by width and validated
|
|
35
35
|
* against the source frame's timestamp. */
|
|
36
36
|
private readonly variants;
|
|
37
|
+
/**
|
|
38
|
+
* Rendered courtesy frames, by (reason, size, device name).
|
|
39
|
+
*
|
|
40
|
+
* A courtesy frame is a pure function of those three, so it is rendered once
|
|
41
|
+
* and reused for as long as the process lives — a disabled camera must not
|
|
42
|
+
* cost an ffmpeg run per poll. Unbounded on purpose: the key space is
|
|
43
|
+
* (3 reasons × this node's own cameras), not user input.
|
|
44
|
+
*/
|
|
45
|
+
private readonly courtesyFrames;
|
|
37
46
|
/**
|
|
38
47
|
* De-dupes concurrent captures per `${deviceId}:${streamId}` and holds a
|
|
39
48
|
* settled SUCCESS for COALESCE_MS so a grid-mount burst (and a row of refresh
|
|
@@ -72,17 +81,6 @@ export declare class SnapshotAddon extends BaseAddon<SnapshotAddonConfig> {
|
|
|
72
81
|
* secret that rolled on every boot would invalidate every link a client is
|
|
73
82
|
* currently holding, which is a fleet-wide blank grid after each restart. */
|
|
74
83
|
private linkSecret;
|
|
75
|
-
/**
|
|
76
|
-
* Who is being looked at, inferred from data-plane traffic. The keep-warm
|
|
77
|
-
* loop's entire bound — see `snapshot-keep-warm.ts`.
|
|
78
|
-
*/
|
|
79
|
-
private readonly subscriptions;
|
|
80
|
-
/** Chained warm-tick timer (never `setInterval` — see `scheduleWarmTick`). */
|
|
81
|
-
private warmTimer;
|
|
82
|
-
/** Set while a warm tick is running so ticks can never stack. */
|
|
83
|
-
private warmTickRunning;
|
|
84
|
-
/** Flipped on shutdown so an in-flight tick does not reschedule itself. */
|
|
85
|
-
private stopped;
|
|
86
84
|
/**
|
|
87
85
|
* How a full frame becomes a card-sized one. Defaults to the real ffmpeg
|
|
88
86
|
* resize; injectable so the wrapper's own logic is testable without spawning a
|
|
@@ -190,40 +188,6 @@ export declare class SnapshotAddon extends BaseAddon<SnapshotAddonConfig> {
|
|
|
190
188
|
* which is the exact staleness this change exists to remove.
|
|
191
189
|
*/
|
|
192
190
|
private peekFreshFrame;
|
|
193
|
-
/**
|
|
194
|
-
* Schedule the next warm tick. A CHAINED timer, not `setInterval`: the next
|
|
195
|
-
* tick is armed only after the previous one settles, so a fleet that is slower
|
|
196
|
-
* than the interval degrades to "as fast as it can" instead of stacking ticks
|
|
197
|
-
* on top of each other and exhausting the ffmpeg pool.
|
|
198
|
-
*
|
|
199
|
-
* `unref` so the loop never holds the process open.
|
|
200
|
-
*/
|
|
201
|
-
private scheduleWarmTick;
|
|
202
|
-
/**
|
|
203
|
-
* Refresh the cache for every camera somebody is currently looking at.
|
|
204
|
-
*
|
|
205
|
-
* Deliberately NOT a blind poll of the whole fleet: with nobody watching this
|
|
206
|
-
* walks an empty list and costs nothing, which is the entire difference
|
|
207
|
-
* between this and a background poller.
|
|
208
|
-
*
|
|
209
|
-
* Battery cameras need no special case here and must not get one. A warm tick
|
|
210
|
-
* is a plain non-forced `getSnapshot`, so it meets the two gates that already
|
|
211
|
-
* exist — the 3600 s battery max-age (which makes the tick a cache hit) and
|
|
212
|
-
* the sleeping-battery gate (which refuses to capture without `force`). A
|
|
213
|
-
* second battery check here would be a rule that disagrees with the authority
|
|
214
|
-
* that already owns it.
|
|
215
|
-
*/
|
|
216
|
-
private runWarmTick;
|
|
217
|
-
/**
|
|
218
|
-
* Warm one (device, stream): refresh the frame, then pre-derive the widths
|
|
219
|
-
* that were actually requested.
|
|
220
|
-
*
|
|
221
|
-
* Deriving the variants matters as much as the capture. Warming only the full
|
|
222
|
-
* frame would leave every grid tile still paying an ffmpeg resize on the
|
|
223
|
-
* request path — the cache would be fresh and the tile would still be slow.
|
|
224
|
-
*/
|
|
225
|
-
private warmOne;
|
|
226
|
-
private warmOneUnbounded;
|
|
227
191
|
/**
|
|
228
192
|
* The frame at a card-sized width, derived once per (device, stream, width)
|
|
229
193
|
* per capture.
|
|
@@ -253,6 +217,16 @@ export declare class SnapshotAddon extends BaseAddon<SnapshotAddonConfig> {
|
|
|
253
217
|
* and no cache propagates.
|
|
254
218
|
*/
|
|
255
219
|
private resolveOutcome;
|
|
220
|
+
/**
|
|
221
|
+
* Reconcile backstop for a stale hit: is the frame we are about to serve the
|
|
222
|
+
* property of a device that no longer exists?
|
|
223
|
+
*
|
|
224
|
+
* Asked ONLY past `staleTtlMs`, which is already the "something is wrong"
|
|
225
|
+
* threshold — so the common path costs nothing. A `true` answer evicts and
|
|
226
|
+
* the caller returns null; anything else leaves the existing
|
|
227
|
+
* keep-the-UI-from-going-blank contract untouched.
|
|
228
|
+
*/
|
|
229
|
+
private staleHitBelongsToRemovedDevice;
|
|
256
230
|
/**
|
|
257
231
|
* Run the capture ladder ONCE for a device: native provider first, then the
|
|
258
232
|
* stream-broker ffmpeg fallback. Never rejects — resolves a {@link
|
|
@@ -325,6 +299,25 @@ export declare class SnapshotAddon extends BaseAddon<SnapshotAddonConfig> {
|
|
|
325
299
|
*/
|
|
326
300
|
private runGrabWithResumeRetry;
|
|
327
301
|
private invalidateCache;
|
|
302
|
+
/** Forget every cached artefact of one device: frames, derived variants and
|
|
303
|
+
* any settled single-flight result that would answer the next request. */
|
|
304
|
+
private dropDeviceCaches;
|
|
305
|
+
/**
|
|
306
|
+
* The device is GONE — drop its cached frames and say so.
|
|
307
|
+
*
|
|
308
|
+
* Distinct from `invalidateCache` (a refresh, expected and silent): this is
|
|
309
|
+
* the terminal case, and a deleted camera that keeps answering with its last
|
|
310
|
+
* JPEG looks to an operator exactly like a camera that was never deleted.
|
|
311
|
+
*/
|
|
312
|
+
private evictRemovedDevice;
|
|
313
|
+
/**
|
|
314
|
+
* Does device-manager still know this id?
|
|
315
|
+
*
|
|
316
|
+
* `null` means the question could not be answered — a transport hiccup is
|
|
317
|
+
* not a deletion (D49), and the caller keeps serving what it has. Only an
|
|
318
|
+
* explicit "no such device" evicts.
|
|
319
|
+
*/
|
|
320
|
+
private deviceStillExists;
|
|
328
321
|
/**
|
|
329
322
|
* Sleep state from the device-state MIRROR, not from a cap round-trip.
|
|
330
323
|
*
|
|
@@ -378,6 +371,20 @@ export declare class SnapshotAddon extends BaseAddon<SnapshotAddonConfig> {
|
|
|
378
371
|
private getStatus;
|
|
379
372
|
private readDeviceSettings;
|
|
380
373
|
private buildDeviceSettingsContribution;
|
|
374
|
+
/**
|
|
375
|
+
* The frame a camera that CANNOT capture answers with.
|
|
376
|
+
*
|
|
377
|
+
* Returns null only if rendering itself fails — the caller then falls through
|
|
378
|
+
* to its previous behaviour, so a broken courtesy path degrades to the old
|
|
379
|
+
* 404 rather than masking a real capture failure as a placeholder.
|
|
380
|
+
*
|
|
381
|
+
* Rendered at the full tile size; the existing resize ladder derives the
|
|
382
|
+
* card-sized variants from it exactly as it does for a real frame, so no
|
|
383
|
+
* downstream consumer needs to know this frame is synthetic.
|
|
384
|
+
*/
|
|
385
|
+
private courtesyFrame;
|
|
386
|
+
/** A courtesy frame shaped as the cap's response, or null when unrenderable. */
|
|
387
|
+
private courtesyImage;
|
|
381
388
|
/**
|
|
382
389
|
* Single-trip device lookup against device-manager. Returns the
|
|
383
390
|
* fields the wrapper actually consults — name (logging) + battery
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-CA2mim74.js");
|
|
7
7
|
let node_crypto = require("node:crypto");
|
|
8
8
|
let node_fs_promises = require("node:fs/promises");
|
|
9
9
|
let node_path = require("node:path");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { gt as BaseAddon, ut as storageProviderCapability, z as filesystemBrowseCapability } from "../../dist-
|
|
1
|
+
import { gt as BaseAddon, ut as storageProviderCapability, z as filesystemBrowseCapability } from "../../dist-DZOZtn-0.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { mkdir, readdir, realpath, statfs } from "node:fs/promises";
|
|
4
4
|
import * as path$1 from "node:path";
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-CA2mim74.js");
|
|
7
7
|
let node_crypto = require("node:crypto");
|
|
8
8
|
let node_fs = require("node:fs");
|
|
9
9
|
let node_module = require("node:module");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as decodeVectorBase64, Et as asJsonObject, Lt as parseJsonUnknown, f as RUNTIME_DEFAULTS, gt as BaseAddon, ht as errMsg, k as dataStoreProviderCapability, mt as vectorStoreCapability, pt as vectorDimFromBase64, w as bareAddonId } from "../../dist-
|
|
1
|
+
import { A as decodeVectorBase64, Et as asJsonObject, Lt as parseJsonUnknown, f as RUNTIME_DEFAULTS, gt as BaseAddon, ht as errMsg, k as dataStoreProviderCapability, mt as vectorStoreCapability, pt as vectorDimFromBase64, w as bareAddonId } from "../../dist-DZOZtn-0.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { statSync } from "node:fs";
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-CA2mim74.js");
|
|
7
7
|
let node_crypto = require("node:crypto");
|
|
8
8
|
let node_fs_promises = require("node:fs/promises");
|
|
9
9
|
node_fs_promises = require_chunk.__toESM(node_fs_promises);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { It as parseJsonObject, ct as storageCapability, g as StorageMigrationJobSchema, gt as BaseAddon, h as StorageLocationTypeSchema, lt as storageMigrationCapability, ot as settingsStoreCapability } from "../../dist-
|
|
1
|
+
import { It as parseJsonObject, ct as storageCapability, g as StorageMigrationJobSchema, gt as BaseAddon, h as StorageLocationTypeSchema, lt as storageMigrationCapability, ot as settingsStoreCapability } from "../../dist-DZOZtn-0.mjs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import * as path$1 from "node:path";
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-CA2mim74.js");
|
|
7
7
|
//#region src/builtins/system-config/system-config.addon.ts
|
|
8
8
|
/**
|
|
9
9
|
* Built-in `system-config` addon — Phase 4 of the settings redesign.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Nt as hydrateSchema, gt as BaseAddon, ht as errMsg } from "../../dist-
|
|
1
|
+
import { Nt as hydrateSchema, gt as BaseAddon, ht as errMsg } from "../../dist-DZOZtn-0.mjs";
|
|
2
2
|
//#region src/builtins/system-config/system-config.addon.ts
|
|
3
3
|
/**
|
|
4
4
|
* Built-in `system-config` addon — Phase 4 of the settings redesign.
|
|
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
|
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
5
|
const require_chunk = require("../../chunk-Cek0wNdY.js");
|
|
6
|
-
const require_dist = require("../../dist-
|
|
6
|
+
const require_dist = require("../../dist-CA2mim74.js");
|
|
7
7
|
const require_formatter = require("../../formatter-DqAKDlvN.js");
|
|
8
8
|
let node_path = require("node:path");
|
|
9
9
|
node_path = require_chunk.__toESM(node_path);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { J as logDestinationCapability, gt as BaseAddon } from "../../dist-
|
|
1
|
+
import { J as logDestinationCapability, gt as BaseAddon } from "../../dist-DZOZtn-0.mjs";
|
|
2
2
|
import { t as formatLogLine } from "../../formatter-B7qW8bPJ.mjs";
|
|
3
3
|
import * as path$1 from "node:path";
|
|
4
4
|
import path from "node:path";
|
|
@@ -17500,24 +17500,28 @@ var snapshotCapability = {
|
|
|
17500
17500
|
*
|
|
17501
17501
|
* `getSnapshotOverview` is cache-only by contract: it answers from whatever
|
|
17502
17502
|
* the wrapper happens to hold and never captures. Under D93 the client
|
|
17503
|
-
* versions its image URL on that answer, and an image REQUEST
|
|
17504
|
-
*
|
|
17505
|
-
*
|
|
17506
|
-
*
|
|
17507
|
-
*
|
|
17508
|
-
*
|
|
17509
|
-
*
|
|
17510
|
-
*
|
|
17503
|
+
* versions its image URL on that answer, and an image REQUEST was the only
|
|
17504
|
+
* demand signal. Both of those are satisfiable by the client's own image
|
|
17505
|
+
* cache — `expo-image` is URL-keyed and never revalidates — so a URL painted
|
|
17506
|
+
* in a previous session comes off disk with no network, no demand, and no
|
|
17507
|
+
* capture. Measured on the live hub: reopening after two minutes idle
|
|
17508
|
+
* painted 15 of 16 tiles at **168 s old** with zero HTTP requests, and the
|
|
17509
|
+
* fleet only recovered because a later poll happened to observe a different
|
|
17510
|
+
* identity.
|
|
17511
17511
|
*
|
|
17512
17512
|
* ## The two properties that fix it
|
|
17513
17513
|
*
|
|
17514
17514
|
* **It is an RPC, so no client cache can answer it.** The demand signal
|
|
17515
|
-
* always reaches the wrapper. This method therefore
|
|
17516
|
-
*
|
|
17517
|
-
*
|
|
17518
|
-
*
|
|
17519
|
-
*
|
|
17520
|
-
*
|
|
17515
|
+
* always reaches the wrapper. This method therefore CAPTURES, where
|
|
17516
|
+
* `getSnapshotOverview` must never (D93) — the distinction is not "one is
|
|
17517
|
+
* newer" but that the overview poll is app-wide (a capturing overview would
|
|
17518
|
+
* dial every camera on the install) while this is called by a rendered
|
|
17519
|
+
* surface naming the tiles it is actually painting, at the width it is
|
|
17520
|
+
* painting them.
|
|
17521
|
+
*
|
|
17522
|
+
* Since 2026-08-11 this is the ONLY thing that refreshes a snapshot: the
|
|
17523
|
+
* server-side keep-warm loop was removed (operator directive — on-demand,
|
|
17524
|
+
* always), so a camera nobody is looking at costs nothing at all.
|
|
17521
17525
|
*
|
|
17522
17526
|
* **It waits, briefly and boundedly, for the capture it triggered.** The
|
|
17523
17527
|
* returned `capturedAt` is the frame the link will serve, not the frame the
|
|
@@ -27344,9 +27348,10 @@ var DeclaredDevices = class {
|
|
|
27344
27348
|
}
|
|
27345
27349
|
const integrationId = spec.integrationId ?? await this.ensureIntegration(spec.integrationName);
|
|
27346
27350
|
const index = await this.readIndex();
|
|
27351
|
+
const live = await this.readLiveByStableId();
|
|
27347
27352
|
const outcomes = [];
|
|
27348
27353
|
for (const declaration of spec.devices) {
|
|
27349
|
-
const outcome = await this.applyDeclaration(declaration, integrationId, index);
|
|
27354
|
+
const outcome = await this.applyDeclaration(declaration, integrationId, index, live);
|
|
27350
27355
|
if (outcome !== null) outcomes.push(outcome);
|
|
27351
27356
|
}
|
|
27352
27357
|
return {
|
|
@@ -27392,6 +27397,26 @@ var DeclaredDevices = class {
|
|
|
27392
27397
|
return new Map(rows.map((row) => [row.stableId, row]));
|
|
27393
27398
|
}
|
|
27394
27399
|
/**
|
|
27400
|
+
* Devices this kernel already has CONSTRUCTED, by stableId.
|
|
27401
|
+
*
|
|
27402
|
+
* Distinct from {@link readIndex}, and the distinction is the bug: the index
|
|
27403
|
+
* is persisted rows, this is live objects. A row without an object must be
|
|
27404
|
+
* adopted; an object must be left exactly as it is.
|
|
27405
|
+
*
|
|
27406
|
+
* Failure is non-fatal and deliberately so — an empty map degrades to the
|
|
27407
|
+
* previous behaviour (attempt the adopt) rather than skipping a device that
|
|
27408
|
+
* genuinely needs bringing up.
|
|
27409
|
+
*/
|
|
27410
|
+
async readLiveByStableId() {
|
|
27411
|
+
try {
|
|
27412
|
+
const devices = await this.ports.devices.getAll();
|
|
27413
|
+
return new Map(devices.map((device) => [device.stableId, device]));
|
|
27414
|
+
} catch (err) {
|
|
27415
|
+
this.ports.logger.warn("could not read live devices — falling back to adopt-by-row", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
27416
|
+
return /* @__PURE__ */ new Map();
|
|
27417
|
+
}
|
|
27418
|
+
}
|
|
27419
|
+
/**
|
|
27395
27420
|
* One declaration: adopt what exists, create what does not.
|
|
27396
27421
|
*
|
|
27397
27422
|
* The create branch is the destructive one — it seeds `initialMeta`, and
|
|
@@ -27400,8 +27425,15 @@ var DeclaredDevices = class {
|
|
|
27400
27425
|
* the declared name over the operator's rename. D49: that branch needs a
|
|
27401
27426
|
* second read to agree.
|
|
27402
27427
|
*/
|
|
27403
|
-
async applyDeclaration(declaration, integrationId, index) {
|
|
27428
|
+
async applyDeclaration(declaration, integrationId, index, live) {
|
|
27404
27429
|
try {
|
|
27430
|
+
const alreadyLive = live.get(declaration.stableId);
|
|
27431
|
+
if (alreadyLive !== void 0) return {
|
|
27432
|
+
stableId: declaration.stableId,
|
|
27433
|
+
deviceId: alreadyLive.id,
|
|
27434
|
+
device: alreadyLive,
|
|
27435
|
+
created: false
|
|
27436
|
+
};
|
|
27405
27437
|
let existing = index.get(declaration.stableId);
|
|
27406
27438
|
if (existing === void 0) {
|
|
27407
27439
|
existing = (await this.readIndex()).get(declaration.stableId);
|
|
@@ -17500,24 +17500,28 @@ var snapshotCapability = {
|
|
|
17500
17500
|
*
|
|
17501
17501
|
* `getSnapshotOverview` is cache-only by contract: it answers from whatever
|
|
17502
17502
|
* the wrapper happens to hold and never captures. Under D93 the client
|
|
17503
|
-
* versions its image URL on that answer, and an image REQUEST
|
|
17504
|
-
*
|
|
17505
|
-
*
|
|
17506
|
-
*
|
|
17507
|
-
*
|
|
17508
|
-
*
|
|
17509
|
-
*
|
|
17510
|
-
*
|
|
17503
|
+
* versions its image URL on that answer, and an image REQUEST was the only
|
|
17504
|
+
* demand signal. Both of those are satisfiable by the client's own image
|
|
17505
|
+
* cache — `expo-image` is URL-keyed and never revalidates — so a URL painted
|
|
17506
|
+
* in a previous session comes off disk with no network, no demand, and no
|
|
17507
|
+
* capture. Measured on the live hub: reopening after two minutes idle
|
|
17508
|
+
* painted 15 of 16 tiles at **168 s old** with zero HTTP requests, and the
|
|
17509
|
+
* fleet only recovered because a later poll happened to observe a different
|
|
17510
|
+
* identity.
|
|
17511
17511
|
*
|
|
17512
17512
|
* ## The two properties that fix it
|
|
17513
17513
|
*
|
|
17514
17514
|
* **It is an RPC, so no client cache can answer it.** The demand signal
|
|
17515
|
-
* always reaches the wrapper. This method therefore
|
|
17516
|
-
*
|
|
17517
|
-
*
|
|
17518
|
-
*
|
|
17519
|
-
*
|
|
17520
|
-
*
|
|
17515
|
+
* always reaches the wrapper. This method therefore CAPTURES, where
|
|
17516
|
+
* `getSnapshotOverview` must never (D93) — the distinction is not "one is
|
|
17517
|
+
* newer" but that the overview poll is app-wide (a capturing overview would
|
|
17518
|
+
* dial every camera on the install) while this is called by a rendered
|
|
17519
|
+
* surface naming the tiles it is actually painting, at the width it is
|
|
17520
|
+
* painting them.
|
|
17521
|
+
*
|
|
17522
|
+
* Since 2026-08-11 this is the ONLY thing that refreshes a snapshot: the
|
|
17523
|
+
* server-side keep-warm loop was removed (operator directive — on-demand,
|
|
17524
|
+
* always), so a camera nobody is looking at costs nothing at all.
|
|
17521
17525
|
*
|
|
17522
17526
|
* **It waits, briefly and boundedly, for the capture it triggered.** The
|
|
17523
17527
|
* returned `capturedAt` is the frame the link will serve, not the frame the
|
|
@@ -27344,9 +27348,10 @@ var DeclaredDevices = class {
|
|
|
27344
27348
|
}
|
|
27345
27349
|
const integrationId = spec.integrationId ?? await this.ensureIntegration(spec.integrationName);
|
|
27346
27350
|
const index = await this.readIndex();
|
|
27351
|
+
const live = await this.readLiveByStableId();
|
|
27347
27352
|
const outcomes = [];
|
|
27348
27353
|
for (const declaration of spec.devices) {
|
|
27349
|
-
const outcome = await this.applyDeclaration(declaration, integrationId, index);
|
|
27354
|
+
const outcome = await this.applyDeclaration(declaration, integrationId, index, live);
|
|
27350
27355
|
if (outcome !== null) outcomes.push(outcome);
|
|
27351
27356
|
}
|
|
27352
27357
|
return {
|
|
@@ -27392,6 +27397,26 @@ var DeclaredDevices = class {
|
|
|
27392
27397
|
return new Map(rows.map((row) => [row.stableId, row]));
|
|
27393
27398
|
}
|
|
27394
27399
|
/**
|
|
27400
|
+
* Devices this kernel already has CONSTRUCTED, by stableId.
|
|
27401
|
+
*
|
|
27402
|
+
* Distinct from {@link readIndex}, and the distinction is the bug: the index
|
|
27403
|
+
* is persisted rows, this is live objects. A row without an object must be
|
|
27404
|
+
* adopted; an object must be left exactly as it is.
|
|
27405
|
+
*
|
|
27406
|
+
* Failure is non-fatal and deliberately so — an empty map degrades to the
|
|
27407
|
+
* previous behaviour (attempt the adopt) rather than skipping a device that
|
|
27408
|
+
* genuinely needs bringing up.
|
|
27409
|
+
*/
|
|
27410
|
+
async readLiveByStableId() {
|
|
27411
|
+
try {
|
|
27412
|
+
const devices = await this.ports.devices.getAll();
|
|
27413
|
+
return new Map(devices.map((device) => [device.stableId, device]));
|
|
27414
|
+
} catch (err) {
|
|
27415
|
+
this.ports.logger.warn("could not read live devices — falling back to adopt-by-row", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
27416
|
+
return /* @__PURE__ */ new Map();
|
|
27417
|
+
}
|
|
27418
|
+
}
|
|
27419
|
+
/**
|
|
27395
27420
|
* One declaration: adopt what exists, create what does not.
|
|
27396
27421
|
*
|
|
27397
27422
|
* The create branch is the destructive one — it seeds `initialMeta`, and
|
|
@@ -27400,8 +27425,15 @@ var DeclaredDevices = class {
|
|
|
27400
27425
|
* the declared name over the operator's rename. D49: that branch needs a
|
|
27401
27426
|
* second read to agree.
|
|
27402
27427
|
*/
|
|
27403
|
-
async applyDeclaration(declaration, integrationId, index) {
|
|
27428
|
+
async applyDeclaration(declaration, integrationId, index, live) {
|
|
27404
27429
|
try {
|
|
27430
|
+
const alreadyLive = live.get(declaration.stableId);
|
|
27431
|
+
if (alreadyLive !== void 0) return {
|
|
27432
|
+
stableId: declaration.stableId,
|
|
27433
|
+
deviceId: alreadyLive.id,
|
|
27434
|
+
device: alreadyLive,
|
|
27435
|
+
created: false
|
|
27436
|
+
};
|
|
27405
27437
|
let existing = index.get(declaration.stableId);
|
|
27406
27438
|
if (existing === void 0) {
|
|
27407
27439
|
existing = (await this.readIndex()).get(declaration.stableId);
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_chunk = require("./chunk-Cek0wNdY.js");
|
|
3
|
-
const require_dist = require("./dist-
|
|
3
|
+
const require_dist = require("./dist-CA2mim74.js");
|
|
4
4
|
const require_builtins_alerts_alerts_addon = require("./builtins/alerts/alerts.addon.js");
|
|
5
5
|
require("./builtins/alerts/index.js");
|
|
6
6
|
const require_formatter = require("./formatter-DqAKDlvN.js");
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as __toCommonJS, i as __require, n as __esmMin, o as __toESM$1, r as __exportAll, t as __commonJSMin$1 } from "./chunk-CNf5ZN-e.mjs";
|
|
2
|
-
import { $ as objectInputDeclaresAddonId, At as emitDownForOwnedCaps, B as isArrayOutputSchema, Bt as scopeKey, Ct as ReadinessRegistry, Dt as asNumber, Et as asJsonObject$1, G as kebabToCamel, H as isObjectInput, Ht as EventCategory$1, It as parseJsonObject, K as lifecycleJobSchema, Lt as parseJsonUnknown$1, Mt as expandCapMethods, Ot as asString$1, R as extractNestedAddonId, Rt as readinessKey, V as isCollectionArrayMethod, W as isVoidInput, X as looseSchema, Y as logLevelAtMost, _t as DATAPLANE_SECRET_HEADER$1, d as METHOD_ACCESS_MAP, f as RUNTIME_DEFAULTS, ht as errMsg$1, it as scopesAllowDeviceCap, kt as createEvent, nt as procedureAuthKey, rt as scopesAllowAddon, t as ALL_CAPABILITY_DEFINITIONS, vt as DEVICE_SETTINGS_CONTRIBUTION_METHODS, wt as ReadinessTimeoutError, y as addonSettingsCapability, yt as DEVICE_STATUS_METHOD, zt as resolveCapMount } from "./dist-
|
|
2
|
+
import { $ as objectInputDeclaresAddonId, At as emitDownForOwnedCaps, B as isArrayOutputSchema, Bt as scopeKey, Ct as ReadinessRegistry, Dt as asNumber, Et as asJsonObject$1, G as kebabToCamel, H as isObjectInput, Ht as EventCategory$1, It as parseJsonObject, K as lifecycleJobSchema, Lt as parseJsonUnknown$1, Mt as expandCapMethods, Ot as asString$1, R as extractNestedAddonId, Rt as readinessKey, V as isCollectionArrayMethod, W as isVoidInput, X as looseSchema, Y as logLevelAtMost, _t as DATAPLANE_SECRET_HEADER$1, d as METHOD_ACCESS_MAP, f as RUNTIME_DEFAULTS, ht as errMsg$1, it as scopesAllowDeviceCap, kt as createEvent, nt as procedureAuthKey, rt as scopesAllowAddon, t as ALL_CAPABILITY_DEFINITIONS, vt as DEVICE_SETTINGS_CONTRIBUTION_METHODS, wt as ReadinessTimeoutError, y as addonSettingsCapability, yt as DEVICE_STATUS_METHOD, zt as resolveCapMount } from "./dist-DZOZtn-0.mjs";
|
|
3
3
|
import { AlertCenterAddon } from "./builtins/alerts/alerts.addon.mjs";
|
|
4
4
|
import "./builtins/alerts/index.mjs";
|
|
5
5
|
import { t as formatLogLine } from "./formatter-B7qW8bPJ.mjs";
|