signalk-edge-link 2.6.2 → 2.6.3
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/lib/index.js +17 -0
- package/lib/instance.js +21 -1
- package/lib/pipeline-v2-server.js +16 -1
- package/lib/values-snapshot.js +18 -8
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -209,6 +209,23 @@ module.exports = function createPlugin(app) {
|
|
|
209
209
|
setStatus(`Startup failed: ${startError instanceof Error ? startError.message : String(startError)}`);
|
|
210
210
|
return;
|
|
211
211
|
}
|
|
212
|
+
// Wire up FULL_STATUS_REQUEST cascade: when a client-mode instance receives
|
|
213
|
+
// a FULL_STATUS_REQUEST from its upstream server, it should also forward the
|
|
214
|
+
// request to all downstream clients connected to any co-located server-mode
|
|
215
|
+
// instances. This propagates the request down the chain (Cloud → Proxy → Boat)
|
|
216
|
+
// so one-shot startup values from the furthest-downstream node are re-sent.
|
|
217
|
+
const allStarted = [...instances.values()];
|
|
218
|
+
const serverInsts = allStarted.filter((inst) => inst.isServerMode());
|
|
219
|
+
const clientInsts = allStarted.filter((inst) => !inst.isServerMode());
|
|
220
|
+
if (serverInsts.length > 0 && clientInsts.length > 0) {
|
|
221
|
+
for (const clientInst of clientInsts) {
|
|
222
|
+
clientInst.setFullStatusCascadeHandler(() => {
|
|
223
|
+
for (const serverInst of serverInsts) {
|
|
224
|
+
serverInst.requestFullStatusFromAllClients();
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
212
229
|
// Initial status aggregation after all instances report their status
|
|
213
230
|
updateAggregatedStatus();
|
|
214
231
|
};
|
package/lib/instance.js
CHANGED
|
@@ -379,6 +379,12 @@ function createInstance(app, options, instanceId, pluginId, onStatusChange) {
|
|
|
379
379
|
/** Minimum gap between server-initiated full-status replays. Prevents a
|
|
380
380
|
* restarting or misconfigured server from flooding the link. */
|
|
381
381
|
const FULL_STATUS_REQUEST_RATE_LIMIT_MS = 10000;
|
|
382
|
+
/**
|
|
383
|
+
* Optional callback invoked after this (client-mode) instance handles a
|
|
384
|
+
* FULL_STATUS_REQUEST. Used in multi-hop chains to cascade the request to
|
|
385
|
+
* any downstream clients connected to a co-located server-mode instance.
|
|
386
|
+
*/
|
|
387
|
+
let fullStatusCascadeHandler = null;
|
|
382
388
|
/** Server asked for a full values snapshot (FULL_STATUS_REQUEST control
|
|
383
389
|
* packet). Replays the entire current Signal K tree to the server.
|
|
384
390
|
* Rate-limited to prevent replay floods across rapid server restarts. */
|
|
@@ -391,6 +397,10 @@ function createInstance(app, options, instanceId, pluginId, onStatusChange) {
|
|
|
391
397
|
state.lastFullStatusRequestAt = now;
|
|
392
398
|
app.debug(`[${instanceId}] FULL_STATUS_REQUEST received — replaying values snapshot`);
|
|
393
399
|
replayValuesSnapshot("full-status-request");
|
|
400
|
+
if (fullStatusCascadeHandler) {
|
|
401
|
+
app.debug(`[${instanceId}] FULL_STATUS_REQUEST cascading to downstream clients`);
|
|
402
|
+
fullStatusCascadeHandler();
|
|
403
|
+
}
|
|
394
404
|
}
|
|
395
405
|
async function sendSourceSnapshot() {
|
|
396
406
|
if (state.stopped ||
|
|
@@ -1291,6 +1301,16 @@ function createInstance(app, options, instanceId, pluginId, onStatusChange) {
|
|
|
1291
1301
|
getName: () => state.instanceName,
|
|
1292
1302
|
getStatus: () => ({ text: state.instanceStatus, healthy: state.isHealthy }),
|
|
1293
1303
|
getState: () => state,
|
|
1294
|
-
getMetricsApi: () => metricsApi
|
|
1304
|
+
getMetricsApi: () => metricsApi,
|
|
1305
|
+
/** Register a callback to invoke when this client-mode instance handles
|
|
1306
|
+
* a FULL_STATUS_REQUEST, so the request cascades to downstream clients. */
|
|
1307
|
+
setFullStatusCascadeHandler(handler) {
|
|
1308
|
+
fullStatusCascadeHandler = handler;
|
|
1309
|
+
},
|
|
1310
|
+
/** Forward a FULL_STATUS_REQUEST to all currently-connected clients
|
|
1311
|
+
* (server-mode instances only; no-op on client-mode instances). */
|
|
1312
|
+
requestFullStatusFromAllClients() {
|
|
1313
|
+
state.pipelineServer?.requestFullStatusFromAllClients?.();
|
|
1314
|
+
}
|
|
1295
1315
|
};
|
|
1296
1316
|
}
|
|
@@ -717,6 +717,20 @@ function createPipelineV2Server(app, state, metricsApi) {
|
|
|
717
717
|
throw err;
|
|
718
718
|
}
|
|
719
719
|
}
|
|
720
|
+
/**
|
|
721
|
+
* Send FULL_STATUS_REQUEST to every currently-connected client session.
|
|
722
|
+
* Called when this server instance itself receives a FULL_STATUS_REQUEST
|
|
723
|
+
* from an upstream server, so the request cascades down the chain:
|
|
724
|
+
* Cloud → Proxy (triggers this) → Boat.
|
|
725
|
+
*/
|
|
726
|
+
function requestFullStatusFromAllClients() {
|
|
727
|
+
const secretKey = state.options?.secretKey ?? "";
|
|
728
|
+
for (const session of clientSessions.values()) {
|
|
729
|
+
_sendFullStatusRequest(session, secretKey).catch((err) => {
|
|
730
|
+
app.debug(`[v2-server] cascade FULL_STATUS_REQUEST to ${session.key} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}
|
|
720
734
|
/**
|
|
721
735
|
* Build and send a META_REQUEST (0x07) control packet to a client.
|
|
722
736
|
* Instructs the client to emit a fresh metadata snapshot — used on first
|
|
@@ -1210,6 +1224,7 @@ function createPipelineV2Server(app, state, metricsApi) {
|
|
|
1210
1224
|
startACKTimer,
|
|
1211
1225
|
stopACKTimer,
|
|
1212
1226
|
startMetricsPublishing,
|
|
1213
|
-
stopMetricsPublishing
|
|
1227
|
+
stopMetricsPublishing,
|
|
1228
|
+
requestFullStatusFromAllClients
|
|
1214
1229
|
};
|
|
1215
1230
|
}
|
package/lib/values-snapshot.js
CHANGED
|
@@ -171,18 +171,28 @@ function collectValuesSnapshot(app) {
|
|
|
171
171
|
}
|
|
172
172
|
const context = `${contextGroup}.${contextId}`;
|
|
173
173
|
walkValues(contextNode, [], (leaf) => {
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
174
|
+
// Values stored under "signalk-edge-link.*" $source keys were injected
|
|
175
|
+
// by this plugin (data received via an upstream edge-link server connection
|
|
176
|
+
// or a downstream edge-link client connection). Skip them only when the SK
|
|
177
|
+
// sources table cannot provide a proper original-sensor label — that case
|
|
178
|
+
// would produce wrong attribution on the receiver. When the sources table
|
|
179
|
+
// does resolve to a real label (e.g. "pypilot"), include the value so relay
|
|
180
|
+
// data reaches the upstream server after its restart; the receiver's
|
|
181
|
+
// normalizeDeltaSourceRefs will strip the stale $source and
|
|
182
|
+
// handleMessageBySource will dispatch under the original label.
|
|
181
183
|
const src = leaf.source ?? "";
|
|
182
184
|
if (src === "signalk-edge-link" ||
|
|
183
185
|
src.startsWith("signalk-edge-link.") ||
|
|
184
186
|
src.startsWith("signalk-edge-link:")) {
|
|
185
|
-
|
|
187
|
+
const resolved = sourceLookup.get(src);
|
|
188
|
+
const resolvedLabel = typeof resolved?.label === "string" ? resolved.label.trim() : "";
|
|
189
|
+
if (!resolvedLabel ||
|
|
190
|
+
resolvedLabel === "signalk-edge-link" ||
|
|
191
|
+
resolvedLabel.startsWith("signalk-edge-link.") ||
|
|
192
|
+
resolvedLabel.startsWith("signalk-edge-link:")) {
|
|
193
|
+
return; // No proper label available — skip to avoid wrong attribution
|
|
194
|
+
}
|
|
195
|
+
// Resolved to a real sensor label — fall through and include the value
|
|
186
196
|
}
|
|
187
197
|
const key = `${context}|${leaf.source ?? ""}|${leaf.timestamp}`;
|
|
188
198
|
const existing = grouped.get(key);
|