@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
- const hasUpdate = latestVersion !== null && currentVersion !== 'unknown' && latestVersion !== currentVersion;
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
- // Soft total ceiling across all buckets, null (unbounded) by default so this
32
- // changes nothing until an operator opts in. `?? null` rather than a numeric
33
- // default on purpose: the per-addon rings are already a hard bound, and picking
34
- // a total for someone would silently start discarding their debug history.
35
- const maxTotalEntries = configService.get('eventBus.maxTotalLogBufferSize') ?? null;
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
- cache = new Map();
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 && Date.now() - cached.timestamp < CACHE_TTL_MS) {
29
- return cached.metadata;
30
- }
49
+ if (cached)
50
+ return cached;
31
51
  }
32
52
  const metadata = await this.runProbe(url);
33
- this.cache.set(url, { metadata, timestamp: Date.now() });
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.128",
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.93",
42
- "@camstack/addon-pipeline-orchestrator": "1.2.76",
43
- "@camstack/addon-post-analysis": "1.2.91",
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.102",
47
- "@camstack/types": "1.2.85",
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",