@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.
|
|
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.
|
|
37
|
-
"@camstack/addon-agent-ui": "1.2.
|
|
38
|
-
"@camstack/addon-auth": "1.2.
|
|
39
|
-
"@camstack/addon-decoder-nodeav": "1.2.
|
|
40
|
-
"@camstack/addon-notifiers": "1.2.
|
|
41
|
-
"@camstack/addon-pipeline": "1.2.
|
|
42
|
-
"@camstack/addon-pipeline-orchestrator": "1.2.
|
|
43
|
-
"@camstack/addon-post-analysis": "1.2.
|
|
44
|
-
"@camstack/sdk": "1.2.
|
|
45
|
-
"@camstack/shm-ring": "1.1.
|
|
46
|
-
"@camstack/system": "1.2.
|
|
47
|
-
"@camstack/types": "1.2.
|
|
48
|
-
"@camstack/ui-library": "1.2.
|
|
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",
|