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 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
  }
@@ -171,18 +171,28 @@ function collectValuesSnapshot(app) {
171
171
  }
172
172
  const context = `${contextGroup}.${contextId}`;
173
173
  walkValues(contextNode, [], (leaf) => {
174
- // Skip values that this plugin injected from remote instances.
175
- // SK stores them under "signalk-edge-link.*" $source keys. Including
176
- // them in the snapshot would loop remote data back to its origin and
177
- // propagate wrong source labels (the fallback label derived from the
178
- // "signalk-edge-link" prefix is never the original sensor label).
179
- // Live streaming handles relay correctly via subscription callbacks,
180
- // which SK populates with the full source object automatically.
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
- return;
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-edge-link",
3
- "version": "2.6.2",
3
+ "version": "2.6.3",
4
4
  "description": "SignalK Edge Link. Secure UDP link for data exchange.",
5
5
  "main": "lib/index.js",
6
6
  "files": [