@camstack/server 1.2.128 → 1.2.129
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.
|
@@ -1195,7 +1195,15 @@ class AddonPackageService {
|
|
|
1195
1195
|
meta: { packageName, error: (0, types_1.errMsg)(err) },
|
|
1196
1196
|
});
|
|
1197
1197
|
}
|
|
1198
|
-
|
|
1198
|
+
// STRICTLY newer, never merely different. The hub runs
|
|
1199
|
+
// `1.2.x-dev.<timestamp>` builds that are AHEAD of the npm tag, and
|
|
1200
|
+
// `latest !== current` lit an "update available" badge whose only
|
|
1201
|
+
// possible outcome was a downgrade. `publishFrameworkAvailability`
|
|
1202
|
+
// already gated on this; the row the UI reads did not, so the same
|
|
1203
|
+
// page disagreed with itself.
|
|
1204
|
+
const hasUpdate = latestVersion !== null &&
|
|
1205
|
+
currentVersion !== 'unknown' &&
|
|
1206
|
+
isVersionNewer(latestVersion, currentVersion);
|
|
1199
1207
|
return {
|
|
1200
1208
|
packageName,
|
|
1201
1209
|
currentVersion,
|
|
@@ -28,11 +28,14 @@ class LoggingService extends system_1.LogManager {
|
|
|
28
28
|
// keep their sparse history. `eventBus.ringBufferSize` still sizes the
|
|
29
29
|
// separate system-event ring; logs get their own per-addon cap.
|
|
30
30
|
const perAddonCapacity = configService.get('eventBus.perAddonLogBufferSize') ?? 5000;
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
31
|
+
// Total ceiling across all buckets. Left UNSET here on purpose so the buffer
|
|
32
|
+
// applies its own default (`DEFAULT_MAX_TOTAL_LOG_ENTRIES`): the per-addon
|
|
33
|
+
// rings are a hard bound only per bucket, and their product grows with the
|
|
34
|
+
// roster — hub-main ingests from every local runner AND every agent, so
|
|
35
|
+
// "unbounded by default" meant ~300k retained entries (~92MB) on the one
|
|
36
|
+
// process that must fit inside the cgroup. An operator who genuinely wants
|
|
37
|
+
// no aggregate bound writes an explicit `null`, which is preserved.
|
|
38
|
+
const maxTotalEntries = configService.get('eventBus.maxTotalLogBufferSize');
|
|
36
39
|
// Only entries at or below this level are ever discarded to meet the total.
|
|
37
40
|
const pruneLevel = configService.get('eventBus.logBufferPruneLevel') ?? 'debug';
|
|
38
41
|
super(perAddonCapacity, { maxTotalEntries, pruneLevel });
|
|
@@ -3,17 +3,38 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.StreamProbeService = void 0;
|
|
4
4
|
const child_process_1 = require("child_process");
|
|
5
5
|
const util_1 = require("util");
|
|
6
|
+
const ttl_cache_1 = require("./ttl-cache");
|
|
6
7
|
const types_1 = require("@camstack/types");
|
|
7
8
|
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
8
9
|
const CACHE_TTL_MS = 3_600_000; // 1 hour
|
|
9
10
|
const PROBE_TIMEOUT_MS = 5_000;
|
|
11
|
+
/**
|
|
12
|
+
* Ceiling on cached probes.
|
|
13
|
+
*
|
|
14
|
+
* The cache is keyed by stream URL, and a URL is not a bounded quantity: every
|
|
15
|
+
* credential rotation, every edited field, every per-camera probe of a value
|
|
16
|
+
* that was later changed mints a new key. 512 is far above any real camera
|
|
17
|
+
* roster (the live cluster runs 27), so a working deployment never evicts;
|
|
18
|
+
* the ceiling exists so the key space cannot be walked into the OOM.
|
|
19
|
+
*/
|
|
20
|
+
const CACHE_MAX_ENTRIES = 512;
|
|
10
21
|
/** Codec aliases normalised to canonical names. */
|
|
11
22
|
const CODEC_ALIASES = {
|
|
12
23
|
hevc: 'h265',
|
|
13
24
|
};
|
|
14
25
|
class StreamProbeService {
|
|
15
26
|
logger;
|
|
16
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Probe results, bounded by BOTH a swept TTL and an entry ceiling.
|
|
29
|
+
*
|
|
30
|
+
* It used to be a plain `Map` whose age was consulted only when the same URL
|
|
31
|
+
* was probed again — so an entry nobody asked about a second time was never
|
|
32
|
+
* found expired and never removed. See {@link TtlCache}.
|
|
33
|
+
*/
|
|
34
|
+
cache = new ttl_cache_1.TtlCache({
|
|
35
|
+
ttlMs: CACHE_TTL_MS,
|
|
36
|
+
maxEntries: CACHE_MAX_ENTRIES,
|
|
37
|
+
});
|
|
17
38
|
constructor(loggingService) {
|
|
18
39
|
this.logger = loggingService.createLogger('StreamProbeService');
|
|
19
40
|
}
|
|
@@ -25,12 +46,11 @@ class StreamProbeService {
|
|
|
25
46
|
const force = options?.force ?? false;
|
|
26
47
|
if (!force) {
|
|
27
48
|
const cached = this.cache.get(url);
|
|
28
|
-
if (cached
|
|
29
|
-
return cached
|
|
30
|
-
}
|
|
49
|
+
if (cached)
|
|
50
|
+
return cached;
|
|
31
51
|
}
|
|
32
52
|
const metadata = await this.runProbe(url);
|
|
33
|
-
this.cache.set(url,
|
|
53
|
+
this.cache.set(url, metadata);
|
|
34
54
|
return metadata;
|
|
35
55
|
}
|
|
36
56
|
/**
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TtlCache = void 0;
|
|
4
|
+
class TtlCache {
|
|
5
|
+
entries = new Map();
|
|
6
|
+
ttlMs;
|
|
7
|
+
maxEntries;
|
|
8
|
+
now;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.ttlMs = options.ttlMs;
|
|
11
|
+
this.maxEntries = options.maxEntries;
|
|
12
|
+
this.now = options.now ?? (() => Date.now());
|
|
13
|
+
}
|
|
14
|
+
/** The live value, or undefined when absent or expired. An expired entry is
|
|
15
|
+
* DELETED here, not merely reported missing. */
|
|
16
|
+
get(key) {
|
|
17
|
+
const slot = this.entries.get(key);
|
|
18
|
+
if (slot === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
const at = this.now();
|
|
21
|
+
if (at - slot.storedAt >= this.ttlMs) {
|
|
22
|
+
this.entries.delete(key);
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
slot.lastAccessAt = at;
|
|
26
|
+
return slot.value;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Store a value, then bring the map back inside both bounds.
|
|
30
|
+
*
|
|
31
|
+
* The sweep runs on WRITE and not on a timer on purpose: a timer would have
|
|
32
|
+
* to be owned, unref'd and stopped by every holder of a cache, and a cache
|
|
33
|
+
* that is never written to is a cache that is not growing.
|
|
34
|
+
*/
|
|
35
|
+
set(key, value) {
|
|
36
|
+
const at = this.now();
|
|
37
|
+
this.entries.set(key, { value, storedAt: at, lastAccessAt: at });
|
|
38
|
+
this.sweepExpired(at);
|
|
39
|
+
this.enforceMaxEntries(key);
|
|
40
|
+
}
|
|
41
|
+
delete(key) {
|
|
42
|
+
this.entries.delete(key);
|
|
43
|
+
}
|
|
44
|
+
clear() {
|
|
45
|
+
this.entries.clear();
|
|
46
|
+
}
|
|
47
|
+
/** Entries currently retained. The number that used to only go up. */
|
|
48
|
+
size() {
|
|
49
|
+
return this.entries.size;
|
|
50
|
+
}
|
|
51
|
+
sweepExpired(at) {
|
|
52
|
+
for (const [key, slot] of this.entries) {
|
|
53
|
+
if (at - slot.storedAt >= this.ttlMs)
|
|
54
|
+
this.entries.delete(key);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Evict least-recently-USED first. `protectedKey` is the entry just written —
|
|
58
|
+
* evicting it would make `set` a no-op. */
|
|
59
|
+
enforceMaxEntries(protectedKey) {
|
|
60
|
+
if (this.entries.size <= this.maxEntries)
|
|
61
|
+
return;
|
|
62
|
+
const coldestFirst = [...this.entries.entries()]
|
|
63
|
+
.filter(([key]) => key !== protectedKey)
|
|
64
|
+
.toSorted((a, b) => a[1].lastAccessAt - b[1].lastAccessAt);
|
|
65
|
+
for (const [key] of coldestFirst) {
|
|
66
|
+
if (this.entries.size <= this.maxEntries)
|
|
67
|
+
return;
|
|
68
|
+
this.entries.delete(key);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.TtlCache = TtlCache;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.129",
|
|
4
4
|
"private": false,
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -38,13 +38,13 @@
|
|
|
38
38
|
"@camstack/addon-auth": "1.2.22",
|
|
39
39
|
"@camstack/addon-decoder-nodeav": "1.2.19",
|
|
40
40
|
"@camstack/addon-notifiers": "1.2.24",
|
|
41
|
-
"@camstack/addon-pipeline": "1.2.
|
|
42
|
-
"@camstack/addon-pipeline-orchestrator": "1.2.
|
|
43
|
-
"@camstack/addon-post-analysis": "1.2.
|
|
41
|
+
"@camstack/addon-pipeline": "1.2.94",
|
|
42
|
+
"@camstack/addon-pipeline-orchestrator": "1.2.77",
|
|
43
|
+
"@camstack/addon-post-analysis": "1.2.92",
|
|
44
44
|
"@camstack/sdk": "1.2.22",
|
|
45
45
|
"@camstack/shm-ring": "1.1.19",
|
|
46
|
-
"@camstack/system": "1.2.
|
|
47
|
-
"@camstack/types": "1.2.
|
|
46
|
+
"@camstack/system": "1.2.103",
|
|
47
|
+
"@camstack/types": "1.2.86",
|
|
48
48
|
"@camstack/ui-library": "1.2.58",
|
|
49
49
|
"@fastify/compress": "^9.0.0",
|
|
50
50
|
"@fastify/cookie": "^11.0.2",
|