@camstack/server 1.2.102 → 1.2.104

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.
@@ -94,6 +94,9 @@ function isSettingsStore(backend) {
94
94
  'getAddonDevice',
95
95
  'setAddonDevice',
96
96
  'clearAddonDevice',
97
+ 'getDeviceRuntimeState',
98
+ 'setDeviceRuntimeState',
99
+ 'clearDeviceRuntimeState',
97
100
  ];
98
101
  for (const key of required) {
99
102
  if (typeof Reflect.get(backend, key) !== 'function')
@@ -27,6 +27,43 @@ const RowDataSchema = zod_1.z.object({
27
27
  descriptor: DescriptorSchema,
28
28
  });
29
29
  const COLLECTION = 'cluster-node-history';
30
+ /**
31
+ * How stale `lastActive` may get on an ONLINE node before an otherwise
32
+ * unchanged snapshot is written anyway. Five minutes turns the 30 s topology
33
+ * poll's 2,880 writes/node/day into 288 while keeping "last seen" honest to
34
+ * within one window. Every real transition — descriptor change, disconnect —
35
+ * is written on its own edge and is not subject to this bound.
36
+ */
37
+ const LAST_ACTIVE_HEARTBEAT_MS = 5 * 60_000;
38
+ /**
39
+ * Structural equality over a node descriptor. Every field is a scalar or an
40
+ * array of scalars/`{name,version}` pairs, so a JSON-shaped deep compare is
41
+ * exact — and reference equality would be useless here, since the descriptor
42
+ * is rebuilt from the live Moleculer registry on every poll.
43
+ */
44
+ function descriptorsEqual(a, b) {
45
+ return (a.id === b.id &&
46
+ a.name === b.name &&
47
+ a.hostname === b.hostname &&
48
+ a.platform === b.platform &&
49
+ a.arch === b.arch &&
50
+ a.cpuModel === b.cpuModel &&
51
+ a.cpuCores === b.cpuCores &&
52
+ a.memoryMB === b.memoryMB &&
53
+ a.isHub === b.isHub &&
54
+ sameStrings(a.engines, b.engines) &&
55
+ sameStrings(a.localIps, b.localIps) &&
56
+ sameStrings(a.addonIds, b.addonIds) &&
57
+ samePackages(a.packages, b.packages));
58
+ }
59
+ function sameStrings(a, b) {
60
+ return a.length === b.length && a.every((v, i) => v === b[i]);
61
+ }
62
+ function samePackages(a, b) {
63
+ if (a === undefined || b === undefined)
64
+ return a === b;
65
+ return (a.length === b.length && a.every((p, i) => p.name === b[i]?.name && p.version === b[i]?.version));
66
+ }
30
67
  class ClusterNodeHistoryStore {
31
68
  resolveSettingsStore;
32
69
  logger;
@@ -69,6 +106,19 @@ class ClusterNodeHistoryStore {
69
106
  * Upsert a node's descriptor + addon roster while it is ONLINE, stamping
70
107
  * `lastActive = Date.now()`. Called on each `listNodes()` refresh for every
71
108
  * live node (R2 capture point).
109
+ *
110
+ * WRITES ONLY ON A CHANGE, OR ONCE PER {@link LAST_ACTIVE_HEARTBEAT_MS}.
111
+ * `listNodes()` runs off the 30 s topology heartbeat, so the unconditional
112
+ * upsert this used to be was the worst write:row ratio in the whole system —
113
+ * measured on the live hub at ~6 upserts/minute = **8,640 a day to maintain
114
+ * three rows**, i.e. 40 % of every ledger write on the hub, on the same shfs
115
+ * SQLite file whose WAL checkpoint can stall hub-main for seconds (D96).
116
+ *
117
+ * Nothing is lost. `lastActive` exists to answer "when was this node last
118
+ * seen", which only matters at OFFLINE resolution — and the disconnect edge
119
+ * is written by {@link touch}, which is unbounded. The bound costs at most
120
+ * five minutes of precision on a node that then disappeared without a
121
+ * `$node.disconnected`, and buys 288 writes/day/node instead of 2,880.
72
122
  */
73
123
  async snapshot(descriptor) {
74
124
  const store = await this.ensureDeclared();
@@ -80,13 +130,13 @@ class ClusterNodeHistoryStore {
80
130
  // the back-fill's intended set on the very next topology poll.
81
131
  const existing = await this.readRow(store, descriptor.id);
82
132
  const packages = descriptor.packages ?? existing?.descriptor.packages;
133
+ const next = packages === undefined ? descriptor : { ...descriptor, packages };
134
+ if (existing !== null && !this.worthWriting(existing, next))
135
+ return;
83
136
  await store.set({
84
137
  collection: COLLECTION,
85
138
  key: descriptor.id,
86
- value: {
87
- lastActive: Date.now(),
88
- descriptor: packages === undefined ? descriptor : { ...descriptor, packages },
89
- },
139
+ value: { lastActive: Date.now(), descriptor: next },
90
140
  });
91
141
  }
92
142
  catch (err) {
@@ -96,6 +146,16 @@ class ClusterNodeHistoryStore {
96
146
  });
97
147
  }
98
148
  }
149
+ /**
150
+ * Is this snapshot worth a commit? Yes when the descriptor CHANGED, or when
151
+ * the stored `lastActive` has aged past the heartbeat bound. A read that
152
+ * fails never reaches here — `snapshot` writes when there is no existing row.
153
+ */
154
+ worthWriting(existing, next) {
155
+ if (Date.now() - existing.lastActive >= LAST_ACTIVE_HEARTBEAT_MS)
156
+ return true;
157
+ return !descriptorsEqual(existing.descriptor, next);
158
+ }
99
159
  /** Read one node's row, validated. `null` when absent or malformed. */
100
160
  async readRow(store, nodeId) {
101
161
  const raw = await store.get({ collection: COLLECTION, key: nodeId });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/server",
3
- "version": "1.2.102",
3
+ "version": "1.2.104",
4
4
  "private": false,
5
5
  "files": [
6
6
  "dist",
@@ -33,19 +33,19 @@
33
33
  ]
34
34
  },
35
35
  "dependencies": {
36
- "@camstack/addon-admin-ui": "1.2.52",
37
- "@camstack/addon-agent-ui": "1.2.15",
38
- "@camstack/addon-auth": "1.2.16",
39
- "@camstack/addon-decoder-nodeav": "1.2.14",
40
- "@camstack/addon-notifiers": "1.2.19",
41
- "@camstack/addon-pipeline": "1.2.71",
42
- "@camstack/addon-pipeline-orchestrator": "1.2.51",
43
- "@camstack/addon-post-analysis": "1.2.68",
44
- "@camstack/sdk": "1.2.16",
45
- "@camstack/shm-ring": "1.1.14",
46
- "@camstack/system": "1.2.86",
47
- "@camstack/types": "1.2.65",
48
- "@camstack/ui-library": "1.2.44",
36
+ "@camstack/addon-admin-ui": "1.2.54",
37
+ "@camstack/addon-agent-ui": "1.2.17",
38
+ "@camstack/addon-auth": "1.2.18",
39
+ "@camstack/addon-decoder-nodeav": "1.2.16",
40
+ "@camstack/addon-notifiers": "1.2.21",
41
+ "@camstack/addon-pipeline": "1.2.73",
42
+ "@camstack/addon-pipeline-orchestrator": "1.2.53",
43
+ "@camstack/addon-post-analysis": "1.2.70",
44
+ "@camstack/sdk": "1.2.18",
45
+ "@camstack/shm-ring": "1.1.16",
46
+ "@camstack/system": "1.2.88",
47
+ "@camstack/types": "1.2.67",
48
+ "@camstack/ui-library": "1.2.46",
49
49
  "@fastify/compress": "^9.0.0",
50
50
  "@fastify/cookie": "^11.0.2",
51
51
  "@fastify/cors": "^11.2.0",