@camstack/system 1.2.102 → 1.2.103
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-manager.addon.js +1 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
- 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 +150 -22
- package/dist/builtins/snapshot/index.mjs +150 -22
- package/dist/builtins/snapshot/snapshot-cache.d.ts +63 -0
- package/dist/builtins/snapshot/snapshot-resize.d.ts +33 -1
- package/dist/builtins/snapshot/snapshot.addon.d.ts +6 -0
- 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-3wh5LCad.mjs → dist-DHKXob1m.mjs} +124 -20
- package/dist/{dist-9sC0wZw6.js → dist-qnwy_iWM.js} +124 -20
- package/dist/index.js +264 -62
- package/dist/index.mjs +264 -62
- package/dist/kernel/config-schema.d.ts +14 -10
- package/dist/kernel/heap-watch.d.ts +39 -7
- package/dist/kernel/moleculer/process-service.d.ts +34 -0
- package/dist/logging/log-manager.d.ts +7 -5
- package/dist/logging/partitioned-log-buffer.d.ts +95 -16
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Ct as DeviceFeature, Gt as EventCategory, M as deriveBatteryPresence, Rt as nodePin, T as batteryCapability, Tt as DeviceType, i as BatteryStatusSchema, mt as streamQualityLabel, ut as snapshotCapability, vt as errMsg, w as bareAddonId, yt as BaseAddon } from "../../dist-
|
|
1
|
+
import { Ct as DeviceFeature, Gt as EventCategory, M as deriveBatteryPresence, Rt as nodePin, T as batteryCapability, Tt as DeviceType, i as BatteryStatusSchema, mt as streamQualityLabel, ut as snapshotCapability, vt as errMsg, w as bareAddonId, yt as BaseAddon } from "../../dist-DHKXob1m.mjs";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import { signExpiringUrl, verifyExpiringUrl } from "@camstack/types/node";
|
|
@@ -16,25 +16,94 @@ var SnapshotCache = class {
|
|
|
16
16
|
byKey = /* @__PURE__ */ new Map();
|
|
17
17
|
/** deviceId → its live keys, so invalidation is O(streams) not O(cache). */
|
|
18
18
|
keysByDevice = /* @__PURE__ */ new Map();
|
|
19
|
+
maxBytes;
|
|
20
|
+
idleTtlMs;
|
|
21
|
+
now;
|
|
22
|
+
onEvict;
|
|
23
|
+
totalBytes = 0;
|
|
24
|
+
constructor(options = {}) {
|
|
25
|
+
this.maxBytes = options.maxBytes ?? 67108864;
|
|
26
|
+
this.idleTtlMs = options.idleTtlMs ?? 36e5;
|
|
27
|
+
this.now = options.now ?? (() => Date.now());
|
|
28
|
+
this.onEvict = options.onEvict;
|
|
29
|
+
}
|
|
19
30
|
get(deviceId, streamId) {
|
|
20
|
-
return this.
|
|
31
|
+
return this.read(keyOf(deviceId, streamId));
|
|
21
32
|
}
|
|
22
33
|
set(deviceId, streamId, entry) {
|
|
23
34
|
const key = keyOf(deviceId, streamId);
|
|
24
|
-
this.
|
|
35
|
+
this.forget(key);
|
|
36
|
+
this.byKey.set(key, {
|
|
37
|
+
entry,
|
|
38
|
+
deviceId,
|
|
39
|
+
bytes: entry.data.base64.length,
|
|
40
|
+
lastAccessAt: this.now()
|
|
41
|
+
});
|
|
42
|
+
this.totalBytes += entry.data.base64.length;
|
|
25
43
|
const keys = this.keysByDevice.get(deviceId) ?? /* @__PURE__ */ new Set();
|
|
26
44
|
keys.add(key);
|
|
27
45
|
this.keysByDevice.set(deviceId, keys);
|
|
46
|
+
this.enforceBudget(key);
|
|
28
47
|
}
|
|
29
48
|
/** The newest entry for a device, whichever stream produced it. */
|
|
30
49
|
latest(deviceId) {
|
|
31
50
|
let newest;
|
|
32
|
-
for (const key of this.keysByDevice.get(deviceId) ?? []) {
|
|
33
|
-
const entry = this.
|
|
51
|
+
for (const key of [...this.keysByDevice.get(deviceId) ?? []]) {
|
|
52
|
+
const entry = this.read(key);
|
|
34
53
|
if (entry !== void 0 && (newest === void 0 || entry.ts > newest.ts)) newest = entry;
|
|
35
54
|
}
|
|
36
55
|
return newest;
|
|
37
56
|
}
|
|
57
|
+
/** Base64 bytes currently retained. The number the budget is enforced on. */
|
|
58
|
+
sizeBytes() {
|
|
59
|
+
return this.totalBytes;
|
|
60
|
+
}
|
|
61
|
+
/** Entries currently retained. */
|
|
62
|
+
entryCount() {
|
|
63
|
+
return this.byKey.size;
|
|
64
|
+
}
|
|
65
|
+
/** Read through the idle timeout: an entry nobody has touched inside
|
|
66
|
+
* `idleTtlMs` is dropped here rather than on a sweep, so the cost is paid by
|
|
67
|
+
* the caller that proves it was not wanted. */
|
|
68
|
+
read(key) {
|
|
69
|
+
const slot = this.byKey.get(key);
|
|
70
|
+
if (slot === void 0) return void 0;
|
|
71
|
+
const at = this.now();
|
|
72
|
+
if (at - slot.lastAccessAt > this.idleTtlMs) {
|
|
73
|
+
this.evict(key, "idle");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
slot.lastAccessAt = at;
|
|
77
|
+
return slot.entry;
|
|
78
|
+
}
|
|
79
|
+
/** Evict least-recently-ACCESSED first until the budget holds. `protectedKey`
|
|
80
|
+
* is the entry just written — evicting it would make `set` a no-op. */
|
|
81
|
+
enforceBudget(protectedKey) {
|
|
82
|
+
if (this.totalBytes <= this.maxBytes) return;
|
|
83
|
+
const coldestFirst = [...this.byKey.entries()].filter(([key]) => key !== protectedKey).toSorted((a, b) => a[1].lastAccessAt - b[1].lastAccessAt);
|
|
84
|
+
for (const [key] of coldestFirst) {
|
|
85
|
+
if (this.totalBytes <= this.maxBytes) return;
|
|
86
|
+
this.evict(key, "budget");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
evict(key, reason) {
|
|
90
|
+
const slot = this.byKey.get(key);
|
|
91
|
+
if (slot === void 0) return;
|
|
92
|
+
this.forget(key);
|
|
93
|
+
this.onEvict?.(slot.deviceId, slot.entry.streamId, reason);
|
|
94
|
+
}
|
|
95
|
+
/** Remove a key and everything that indexes or accounts for it. */
|
|
96
|
+
forget(key) {
|
|
97
|
+
const slot = this.byKey.get(key);
|
|
98
|
+
if (slot === void 0) return;
|
|
99
|
+
this.totalBytes -= slot.bytes;
|
|
100
|
+
this.byKey.delete(key);
|
|
101
|
+
const keys = this.keysByDevice.get(slot.deviceId);
|
|
102
|
+
if (keys !== void 0) {
|
|
103
|
+
keys.delete(key);
|
|
104
|
+
if (keys.size === 0) this.keysByDevice.delete(slot.deviceId);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
38
107
|
/**
|
|
39
108
|
* Drop every stream of one device.
|
|
40
109
|
*
|
|
@@ -43,12 +112,13 @@ var SnapshotCache = class {
|
|
|
43
112
|
* serving the pre-change frame from the other key.
|
|
44
113
|
*/
|
|
45
114
|
deleteDevice(deviceId) {
|
|
46
|
-
for (const key of this.keysByDevice.get(deviceId) ?? []) this.
|
|
115
|
+
for (const key of [...this.keysByDevice.get(deviceId) ?? []]) this.forget(key);
|
|
47
116
|
this.keysByDevice.delete(deviceId);
|
|
48
117
|
}
|
|
49
118
|
clear() {
|
|
50
119
|
this.byKey.clear();
|
|
51
120
|
this.keysByDevice.clear();
|
|
121
|
+
this.totalBytes = 0;
|
|
52
122
|
}
|
|
53
123
|
};
|
|
54
124
|
//#endregion
|
|
@@ -753,45 +823,89 @@ function resizeJpeg(bytes, width, timeoutMs = RESIZE_TIMEOUT_MS) {
|
|
|
753
823
|
if (timer !== void 0) clearTimeout(timer);
|
|
754
824
|
});
|
|
755
825
|
}
|
|
756
|
-
/**
|
|
757
|
-
* Resized frames, keyed by (device, stream, width, state treatment) AND
|
|
758
|
-
* validated against the source frame's timestamp.
|
|
759
|
-
*
|
|
760
|
-
* The timestamp is the whole correctness argument: a variant outlives nothing.
|
|
761
|
-
* When the underlying frame is recaptured its `capturedAt` moves, every variant
|
|
762
|
-
* derived from the old one stops matching, and the next request re-derives.
|
|
763
|
-
* Without that check a card would keep showing a thumbnail of a frame the
|
|
764
|
-
* full-size view had already replaced.
|
|
765
|
-
*/
|
|
766
826
|
var SnapshotVariantCache = class SnapshotVariantCache {
|
|
767
827
|
byKey = /* @__PURE__ */ new Map();
|
|
768
828
|
keysByDevice = /* @__PURE__ */ new Map();
|
|
829
|
+
maxBytes;
|
|
830
|
+
now;
|
|
831
|
+
totalBytes = 0;
|
|
832
|
+
constructor(options = {}) {
|
|
833
|
+
this.maxBytes = options.maxBytes ?? 16777216;
|
|
834
|
+
this.now = options.now ?? (() => Date.now());
|
|
835
|
+
}
|
|
769
836
|
static key(deviceId, streamId, width, variantKey) {
|
|
770
837
|
return `${deviceId}:${streamId ?? "auto"}:${width}:${variantKey ?? "raw"}`;
|
|
771
838
|
}
|
|
772
839
|
/** The variant for this exact frame, or undefined when it is missing or was
|
|
773
|
-
* derived from an older capture.
|
|
840
|
+
* derived from an older capture.
|
|
841
|
+
*
|
|
842
|
+
* A superseded variant is DELETED here, not merely reported missing. It used
|
|
843
|
+
* to be left in the map: unreachable by every future read (its `sourceTs` can
|
|
844
|
+
* never match again) and removed by nothing, so every recapture added a dead
|
|
845
|
+
* key. Returning `undefined` while retaining the bytes is the leak. */
|
|
774
846
|
get(deviceId, streamId, width, sourceTs, variantKey) {
|
|
775
|
-
const
|
|
776
|
-
|
|
847
|
+
const key = SnapshotVariantCache.key(deviceId, streamId, width, variantKey);
|
|
848
|
+
const slot = this.byKey.get(key);
|
|
849
|
+
if (slot === void 0) return void 0;
|
|
850
|
+
if (slot.sourceTs !== sourceTs) {
|
|
851
|
+
this.forget(key);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
slot.lastAccessAt = this.now();
|
|
855
|
+
return slot.bytes;
|
|
777
856
|
}
|
|
778
857
|
set(deviceId, streamId, width, sourceTs, bytes, variantKey) {
|
|
779
858
|
const key = SnapshotVariantCache.key(deviceId, streamId, width, variantKey);
|
|
859
|
+
this.forget(key);
|
|
780
860
|
this.byKey.set(key, {
|
|
781
861
|
bytes,
|
|
782
|
-
sourceTs
|
|
862
|
+
sourceTs,
|
|
863
|
+
deviceId,
|
|
864
|
+
lastAccessAt: this.now()
|
|
783
865
|
});
|
|
866
|
+
this.totalBytes += bytes.byteLength;
|
|
784
867
|
const keys = this.keysByDevice.get(deviceId) ?? /* @__PURE__ */ new Set();
|
|
785
868
|
keys.add(key);
|
|
786
869
|
this.keysByDevice.set(deviceId, keys);
|
|
870
|
+
this.enforceBudget(key);
|
|
871
|
+
}
|
|
872
|
+
/** Variant bytes currently retained. */
|
|
873
|
+
sizeBytes() {
|
|
874
|
+
return this.totalBytes;
|
|
875
|
+
}
|
|
876
|
+
/** Variants currently retained — including any that are no longer readable.
|
|
877
|
+
* The number that used to only ever go up. */
|
|
878
|
+
entryCount() {
|
|
879
|
+
return this.byKey.size;
|
|
880
|
+
}
|
|
881
|
+
/** Evict least-recently-accessed first until the budget holds. */
|
|
882
|
+
enforceBudget(protectedKey) {
|
|
883
|
+
if (this.totalBytes <= this.maxBytes) return;
|
|
884
|
+
const coldestFirst = [...this.byKey.entries()].filter(([key]) => key !== protectedKey).toSorted((a, b) => a[1].lastAccessAt - b[1].lastAccessAt);
|
|
885
|
+
for (const [key] of coldestFirst) {
|
|
886
|
+
if (this.totalBytes <= this.maxBytes) return;
|
|
887
|
+
this.forget(key);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
forget(key) {
|
|
891
|
+
const slot = this.byKey.get(key);
|
|
892
|
+
if (slot === void 0) return;
|
|
893
|
+
this.totalBytes -= slot.bytes.byteLength;
|
|
894
|
+
this.byKey.delete(key);
|
|
895
|
+
const keys = this.keysByDevice.get(slot.deviceId);
|
|
896
|
+
if (keys !== void 0) {
|
|
897
|
+
keys.delete(key);
|
|
898
|
+
if (keys.size === 0) this.keysByDevice.delete(slot.deviceId);
|
|
899
|
+
}
|
|
787
900
|
}
|
|
788
901
|
deleteDevice(deviceId) {
|
|
789
|
-
for (const key of this.keysByDevice.get(deviceId) ?? []) this.
|
|
902
|
+
for (const key of [...this.keysByDevice.get(deviceId) ?? []]) this.forget(key);
|
|
790
903
|
this.keysByDevice.delete(deviceId);
|
|
791
904
|
}
|
|
792
905
|
clear() {
|
|
793
906
|
this.byKey.clear();
|
|
794
907
|
this.keysByDevice.clear();
|
|
908
|
+
this.totalBytes = 0;
|
|
795
909
|
}
|
|
796
910
|
};
|
|
797
911
|
//#endregion
|
|
@@ -902,7 +1016,21 @@ function effectiveMaxAgeS(prefs, isBattery) {
|
|
|
902
1016
|
* publishes stream endpoints.
|
|
903
1017
|
*/
|
|
904
1018
|
var SnapshotAddon = class SnapshotAddon extends BaseAddon {
|
|
905
|
-
|
|
1019
|
+
/**
|
|
1020
|
+
* The frame cache. Bounded by a byte budget + an idle timeout — it used to be
|
|
1021
|
+
* unbounded in both. Every eviction is logged with the device tag: a frame
|
|
1022
|
+
* that silently left the cache is the reason a later capture had to happen,
|
|
1023
|
+
* and per-camera is the only way that question is ever asked.
|
|
1024
|
+
*/
|
|
1025
|
+
cache = new SnapshotCache({ onEvict: (deviceId, streamId, reason) => {
|
|
1026
|
+
this.ctx.logger.debug("snapshot cache evicted", {
|
|
1027
|
+
tags: { deviceId },
|
|
1028
|
+
meta: {
|
|
1029
|
+
stream: streamId ?? "auto",
|
|
1030
|
+
reason
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
} });
|
|
906
1034
|
/** Card-sized derivatives of the frames above, keyed by width and validated
|
|
907
1035
|
* against the source frame's timestamp. */
|
|
908
1036
|
variants = new SnapshotVariantCache();
|
|
@@ -25,14 +25,77 @@ export interface SnapshotCacheEntry {
|
|
|
25
25
|
/** Stream id resolved at capture time — 'high' | 'mid' | 'low' | custom | null. */
|
|
26
26
|
readonly streamId: string | null;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Byte budget across every cached frame.
|
|
30
|
+
*
|
|
31
|
+
* The cache shipped with NO bound of any kind: no TTL, no LRU, no size cap. It
|
|
32
|
+
* holds base64 JPEG payloads — a 4K main-stream frame is ~1.6 MB as base64 —
|
|
33
|
+
* keyed by (device, stream), so the ceiling was "every camera × every stream it
|
|
34
|
+
* was ever asked for", which is not a ceiling.
|
|
35
|
+
*
|
|
36
|
+
* 64 MiB is roughly 40 main-stream frames, comfortably above the live 27-camera
|
|
37
|
+
* roster's working set (the whole `snapshot` runner measures 185 MB RSS, of
|
|
38
|
+
* which the node baseline is ~130 MB), so nothing an operator does today starts
|
|
39
|
+
* evicting. It exists so a roster that grows, or a device that gets probed on
|
|
40
|
+
* many stream ids, cannot walk this structure into the OOM.
|
|
41
|
+
*/
|
|
42
|
+
export declare const SNAPSHOT_CACHE_MAX_BYTES: number;
|
|
43
|
+
/**
|
|
44
|
+
* Drop an entry nobody has READ for this long.
|
|
45
|
+
*
|
|
46
|
+
* Deliberately an IDLE timeout and not a capture-age one. Capture age is
|
|
47
|
+
* already governed per device by `snapshotMaxAgeS` (default 15s, but one HOUR
|
|
48
|
+
* for battery cameras), so a TTL on age would either fight those rules or have
|
|
49
|
+
* to sit above the largest of them and bound nothing. "Nobody has looked at
|
|
50
|
+
* this in an hour" is orthogonal to freshness and is the actual condition under
|
|
51
|
+
* which the bytes are dead weight — a removed camera, an unreachable one, a
|
|
52
|
+
* stream id probed once by a debug view.
|
|
53
|
+
*/
|
|
54
|
+
export declare const SNAPSHOT_CACHE_IDLE_TTL_MS = 3600000;
|
|
55
|
+
/** Why an entry was dropped. `budget` = the byte ceiling; `idle` = unread for
|
|
56
|
+
* {@link SNAPSHOT_CACHE_IDLE_TTL_MS}. */
|
|
57
|
+
export type SnapshotCacheEvictReason = 'budget' | 'idle';
|
|
58
|
+
/** Notified for every eviction so the owner can put it on the record with the
|
|
59
|
+
* device tag. A cached frame that vanishes is the explanation for a later
|
|
60
|
+
* capture that did not have to happen; silence reads as "never happened". */
|
|
61
|
+
export type SnapshotCacheEvictListener = (deviceId: number, streamId: string | null, reason: SnapshotCacheEvictReason) => void;
|
|
62
|
+
export interface SnapshotCacheOptions {
|
|
63
|
+
/** Total base64 bytes held across all entries. Default {@link SNAPSHOT_CACHE_MAX_BYTES}. */
|
|
64
|
+
readonly maxBytes?: number;
|
|
65
|
+
/** Idle timeout. Default {@link SNAPSHOT_CACHE_IDLE_TTL_MS}. */
|
|
66
|
+
readonly idleTtlMs?: number;
|
|
67
|
+
/** Injected clock — the bounds must be testable without waiting an hour. */
|
|
68
|
+
readonly now?: () => number;
|
|
69
|
+
readonly onEvict?: SnapshotCacheEvictListener;
|
|
70
|
+
}
|
|
28
71
|
export declare class SnapshotCache {
|
|
29
72
|
private readonly byKey;
|
|
30
73
|
/** deviceId → its live keys, so invalidation is O(streams) not O(cache). */
|
|
31
74
|
private readonly keysByDevice;
|
|
75
|
+
private readonly maxBytes;
|
|
76
|
+
private readonly idleTtlMs;
|
|
77
|
+
private readonly now;
|
|
78
|
+
private readonly onEvict;
|
|
79
|
+
private totalBytes;
|
|
80
|
+
constructor(options?: SnapshotCacheOptions);
|
|
32
81
|
get(deviceId: number, streamId: string | undefined): SnapshotCacheEntry | undefined;
|
|
33
82
|
set(deviceId: number, streamId: string | undefined, entry: SnapshotCacheEntry): void;
|
|
34
83
|
/** The newest entry for a device, whichever stream produced it. */
|
|
35
84
|
latest(deviceId: number): SnapshotCacheEntry | undefined;
|
|
85
|
+
/** Base64 bytes currently retained. The number the budget is enforced on. */
|
|
86
|
+
sizeBytes(): number;
|
|
87
|
+
/** Entries currently retained. */
|
|
88
|
+
entryCount(): number;
|
|
89
|
+
/** Read through the idle timeout: an entry nobody has touched inside
|
|
90
|
+
* `idleTtlMs` is dropped here rather than on a sweep, so the cost is paid by
|
|
91
|
+
* the caller that proves it was not wanted. */
|
|
92
|
+
private read;
|
|
93
|
+
/** Evict least-recently-ACCESSED first until the budget holds. `protectedKey`
|
|
94
|
+
* is the entry just written — evicting it would make `set` a no-op. */
|
|
95
|
+
private enforceBudget;
|
|
96
|
+
private evict;
|
|
97
|
+
/** Remove a key and everything that indexes or accounts for it. */
|
|
98
|
+
private forget;
|
|
36
99
|
/**
|
|
37
100
|
* Drop every stream of one device.
|
|
38
101
|
*
|
|
@@ -69,14 +69,46 @@ export declare function resizeJpeg(bytes: Buffer, width: number, timeoutMs?: num
|
|
|
69
69
|
* Without that check a card would keep showing a thumbnail of a frame the
|
|
70
70
|
* full-size view had already replaced.
|
|
71
71
|
*/
|
|
72
|
+
/**
|
|
73
|
+
* Byte budget across every retained variant.
|
|
74
|
+
*
|
|
75
|
+
* Variants are small individually (a card-sized JPEG is tens of KB) but the key
|
|
76
|
+
* space is a product — device × stream × width × state treatment — and the map
|
|
77
|
+
* had no cap at all. 16 MiB is hundreds of thumbnails, far beyond any real
|
|
78
|
+
* roster's working set, and turns an unbounded product into a ceiling.
|
|
79
|
+
*/
|
|
80
|
+
export declare const SNAPSHOT_VARIANT_CACHE_MAX_BYTES: number;
|
|
81
|
+
export interface SnapshotVariantCacheOptions {
|
|
82
|
+
/** Total variant bytes retained. Default {@link SNAPSHOT_VARIANT_CACHE_MAX_BYTES}. */
|
|
83
|
+
readonly maxBytes?: number;
|
|
84
|
+
/** Injected clock, so LRU order is testable without real time passing. */
|
|
85
|
+
readonly now?: () => number;
|
|
86
|
+
}
|
|
72
87
|
export declare class SnapshotVariantCache {
|
|
73
88
|
private readonly byKey;
|
|
74
89
|
private readonly keysByDevice;
|
|
90
|
+
private readonly maxBytes;
|
|
91
|
+
private readonly now;
|
|
92
|
+
private totalBytes;
|
|
93
|
+
constructor(options?: SnapshotVariantCacheOptions);
|
|
75
94
|
private static key;
|
|
76
95
|
/** The variant for this exact frame, or undefined when it is missing or was
|
|
77
|
-
* derived from an older capture.
|
|
96
|
+
* derived from an older capture.
|
|
97
|
+
*
|
|
98
|
+
* A superseded variant is DELETED here, not merely reported missing. It used
|
|
99
|
+
* to be left in the map: unreachable by every future read (its `sourceTs` can
|
|
100
|
+
* never match again) and removed by nothing, so every recapture added a dead
|
|
101
|
+
* key. Returning `undefined` while retaining the bytes is the leak. */
|
|
78
102
|
get(deviceId: number, streamId: string | undefined, width: number, sourceTs: number, variantKey?: string): Buffer | undefined;
|
|
79
103
|
set(deviceId: number, streamId: string | undefined, width: number, sourceTs: number, bytes: Buffer, variantKey?: string): void;
|
|
104
|
+
/** Variant bytes currently retained. */
|
|
105
|
+
sizeBytes(): number;
|
|
106
|
+
/** Variants currently retained — including any that are no longer readable.
|
|
107
|
+
* The number that used to only ever go up. */
|
|
108
|
+
entryCount(): number;
|
|
109
|
+
/** Evict least-recently-accessed first until the budget holds. */
|
|
110
|
+
private enforceBudget;
|
|
111
|
+
private forget;
|
|
80
112
|
deleteDevice(deviceId: number): void;
|
|
81
113
|
clear(): void;
|
|
82
114
|
}
|
|
@@ -30,6 +30,12 @@ interface SnapshotAddonConfig {
|
|
|
30
30
|
* publishes stream endpoints.
|
|
31
31
|
*/
|
|
32
32
|
export declare class SnapshotAddon extends BaseAddon<SnapshotAddonConfig> {
|
|
33
|
+
/**
|
|
34
|
+
* The frame cache. Bounded by a byte budget + an idle timeout — it used to be
|
|
35
|
+
* unbounded in both. Every eviction is logged with the device tag: a frame
|
|
36
|
+
* that silently left the cache is the reason a later capture had to happen,
|
|
37
|
+
* and per-camera is the only way that question is ever asked.
|
|
38
|
+
*/
|
|
33
39
|
private readonly cache;
|
|
34
40
|
/** Card-sized derivatives of the frames above, keyed by width and validated
|
|
35
41
|
* against the source frame's timestamp. */
|
|
@@ -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-qnwy_iWM.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 { V as filesystemBrowseCapability, pt as storageProviderCapability, yt as BaseAddon } from "../../dist-
|
|
1
|
+
import { V as filesystemBrowseCapability, pt as storageProviderCapability, yt as BaseAddon } from "../../dist-DHKXob1m.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-qnwy_iWM.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 dataStoreProviderCapability, Bt as parseJsonUnknown, _t as vectorStoreCapability, f as RUNTIME_DEFAULTS, gt as vectorDimFromBase64, j as decodeVectorBase64, kt as asJsonObject, vt as errMsg, w as bareAddonId, yt as BaseAddon } from "../../dist-
|
|
1
|
+
import { A as dataStoreProviderCapability, Bt as parseJsonUnknown, _t as vectorStoreCapability, f as RUNTIME_DEFAULTS, gt as vectorDimFromBase64, j as decodeVectorBase64, kt as asJsonObject, vt as errMsg, w as bareAddonId, yt as BaseAddon } from "../../dist-DHKXob1m.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-qnwy_iWM.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 { dt as storageCapability, ft as storageMigrationCapability, g as StorageMigrationJobSchema, h as StorageLocationTypeSchema, lt as settingsStoreCapability, yt as BaseAddon, zt as parseJsonObject } from "../../dist-
|
|
1
|
+
import { dt as storageCapability, ft as storageMigrationCapability, g as StorageMigrationJobSchema, h as StorageLocationTypeSchema, lt as settingsStoreCapability, yt as BaseAddon, zt as parseJsonObject } from "../../dist-DHKXob1m.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-qnwy_iWM.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 { It as hydrateSchema, vt as errMsg, yt as BaseAddon } from "../../dist-
|
|
1
|
+
import { It as hydrateSchema, vt as errMsg, yt as BaseAddon } from "../../dist-DHKXob1m.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-qnwy_iWM.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 { X as logDestinationCapability, yt as BaseAddon } from "../../dist-
|
|
1
|
+
import { X as logDestinationCapability, yt as BaseAddon } from "../../dist-DHKXob1m.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";
|