signalk-edge-link 2.3.0 → 2.4.1
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/connection-config.js +8 -0
- package/lib/delta-sanitizer.js +85 -0
- package/lib/instance.js +68 -3
- package/lib/metadata.js +46 -2
- package/lib/pipeline-v2-client.js +174 -29
- package/lib/pipeline-v2-server.js +125 -8
- package/lib/pipeline.js +3 -1
- package/lib/routes/metrics.js +25 -1
- package/lib/routes.js +6 -0
- package/lib/shared/connection-schema.js +10 -1
- package/lib/source-dispatch.js +98 -0
- package/lib/source-replication.js +241 -0
- package/lib/source-snapshot.js +68 -0
- package/package.json +1 -1
- package/public/982.cc4f5aca99be921e0171.js +2 -0
- package/public/982.cc4f5aca99be921e0171.js.map +1 -0
- package/public/index.html +1 -1
- package/public/main.0b6f5e3267731da945f0.js.map +1 -1
- package/public/{main.e2b9c98749816ac2e285.css → main.2ae3dd54effad689f0da.css} +16 -1
- package/public/main.2ae3dd54effad689f0da.css.map +1 -0
- package/public/remoteEntry.js +1 -1
- package/public/remoteEntry.js.map +1 -1
- package/public/982.63949a2b2f6c5854e034.js +0 -2
- package/public/982.63949a2b2f6c5854e034.js.map +0 -1
- package/public/main.e2b9c98749816ac2e285.css.map +0 -1
- package/public/main.js +0 -467
|
@@ -56,6 +56,8 @@ const msgpack = __importStar(require("@msgpack/msgpack"));
|
|
|
56
56
|
const crypto_1 = require("./crypto");
|
|
57
57
|
const pathDictionary_1 = require("./pathDictionary");
|
|
58
58
|
const delta_sanitizer_1 = require("./delta-sanitizer");
|
|
59
|
+
const source_dispatch_1 = require("./source-dispatch");
|
|
60
|
+
const source_snapshot_1 = require("./source-snapshot");
|
|
59
61
|
const packet_1 = require("./packet");
|
|
60
62
|
const sequence_1 = require("./sequence");
|
|
61
63
|
const metrics_publisher_1 = require("./metrics-publisher");
|
|
@@ -154,6 +156,8 @@ function createPipelineV2Server(app, state, metricsApi) {
|
|
|
154
156
|
// packet can still be processed for this request without polluting state.
|
|
155
157
|
return {
|
|
156
158
|
key,
|
|
159
|
+
sourceClientInstanceId: null,
|
|
160
|
+
clientId: null,
|
|
157
161
|
address: rinfo.address,
|
|
158
162
|
port: rinfo.port,
|
|
159
163
|
sequenceTracker: new sequence_1.SequenceTracker({
|
|
@@ -175,11 +179,15 @@ function createPipelineV2Server(app, state, metricsApi) {
|
|
|
175
179
|
rateLimitWindowStart: Date.now(),
|
|
176
180
|
metaRequested: false,
|
|
177
181
|
lastMetaEnvSeq: null,
|
|
178
|
-
seenMetaChunkIdx: new Set()
|
|
182
|
+
seenMetaChunkIdx: new Set(),
|
|
183
|
+
lastSourceEnvSeq: null,
|
|
184
|
+
seenSourceChunkIdx: new Set()
|
|
179
185
|
};
|
|
180
186
|
}
|
|
181
187
|
const session = {
|
|
182
188
|
key,
|
|
189
|
+
sourceClientInstanceId: null,
|
|
190
|
+
clientId: null,
|
|
183
191
|
address: rinfo.address,
|
|
184
192
|
port: rinfo.port,
|
|
185
193
|
sequenceTracker: new sequence_1.SequenceTracker({
|
|
@@ -206,7 +214,9 @@ function createPipelineV2Server(app, state, metricsApi) {
|
|
|
206
214
|
metaRequested: false,
|
|
207
215
|
// Stale-envelope rejection for METADATA packets
|
|
208
216
|
lastMetaEnvSeq: null,
|
|
209
|
-
seenMetaChunkIdx: new Set()
|
|
217
|
+
seenMetaChunkIdx: new Set(),
|
|
218
|
+
lastSourceEnvSeq: null,
|
|
219
|
+
seenSourceChunkIdx: new Set()
|
|
210
220
|
};
|
|
211
221
|
clientSessions.set(key, session);
|
|
212
222
|
app.debug(`[v2-server] new client session: ${key}`);
|
|
@@ -252,6 +262,8 @@ function createPipelineV2Server(app, state, metricsApi) {
|
|
|
252
262
|
let lastMetricsTime = Date.now();
|
|
253
263
|
let lastBytesReceived = 0;
|
|
254
264
|
let lastPacketsReceived = 0;
|
|
265
|
+
let previousSourceMissingIdentity = 0;
|
|
266
|
+
let previousSourceConflicts = 0;
|
|
255
267
|
// Rate-limit operator-visible warnings for protocol-version mismatches so a
|
|
256
268
|
// persistently misconfigured peer is noticeable in logs without flooding them.
|
|
257
269
|
const PROTOCOL_VERSION_MISMATCH_WARN_INTERVAL_MS = 60000;
|
|
@@ -450,6 +462,61 @@ function createPipelineV2Server(app, state, metricsApi) {
|
|
|
450
462
|
ackTimer = null;
|
|
451
463
|
}
|
|
452
464
|
}
|
|
465
|
+
function shouldDropEnvelopeBySeq(session, env, channel) {
|
|
466
|
+
if (!session || typeof env.seq !== "number" || !Number.isFinite(env.seq)) {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
const envSeq = env.seq >>> 0;
|
|
470
|
+
const envIdx = typeof env.idx === "number" && Number.isFinite(env.idx) ? env.idx >>> 0 : 0;
|
|
471
|
+
const isSource = channel === "source snapshot";
|
|
472
|
+
const seenChunkIdx = isSource ? session.seenSourceChunkIdx : session.seenMetaChunkIdx;
|
|
473
|
+
let lastEnvSeq = isSource ? session.lastSourceEnvSeq : session.lastMetaEnvSeq;
|
|
474
|
+
const setLastEnvSeq = (value) => {
|
|
475
|
+
if (isSource) {
|
|
476
|
+
session.lastSourceEnvSeq = value;
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
session.lastMetaEnvSeq = value;
|
|
480
|
+
}
|
|
481
|
+
lastEnvSeq = value;
|
|
482
|
+
};
|
|
483
|
+
// Sender-restart detection: the client's envelope sequence counter is
|
|
484
|
+
// initialised to 0 at process start, so an incoming envSeq of 0 with a
|
|
485
|
+
// sufficiently-advanced previous seq is a strong signal that the peer
|
|
486
|
+
// restarted. The threshold guards against first-packet replays.
|
|
487
|
+
if (lastEnvSeq !== null && envSeq === 0 && lastEnvSeq >= META_RESTART_THRESHOLD) {
|
|
488
|
+
app.debug(`[v2-server] ${channel} sender restart detected for ${session.key} ` +
|
|
489
|
+
`(last seq was ${lastEnvSeq}); resetting ${channel} state`);
|
|
490
|
+
setLastEnvSeq(null);
|
|
491
|
+
seenChunkIdx.clear();
|
|
492
|
+
if (!isSource) {
|
|
493
|
+
session.metaRequested = false;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (lastEnvSeq !== null) {
|
|
497
|
+
const distance = (envSeq - lastEnvSeq) >>> 0;
|
|
498
|
+
if (distance !== 0 && distance >= 0x80000000) {
|
|
499
|
+
metrics.duplicatePackets = (metrics.duplicatePackets || 0) + 1;
|
|
500
|
+
app.debug(`[v2-server] stale ${channel} envelope seq=${envSeq} from ${session.key} (last=${lastEnvSeq}), dropping`);
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
if (distance !== 0) {
|
|
504
|
+
setLastEnvSeq(envSeq);
|
|
505
|
+
seenChunkIdx.clear();
|
|
506
|
+
}
|
|
507
|
+
else if (seenChunkIdx.has(envIdx)) {
|
|
508
|
+
metrics.duplicatePackets = (metrics.duplicatePackets || 0) + 1;
|
|
509
|
+
app.debug(`[v2-server] duplicate ${channel} chunk seq=${envSeq} idx=${envIdx} from ${session.key}, dropping`);
|
|
510
|
+
return true;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
setLastEnvSeq(envSeq);
|
|
515
|
+
seenChunkIdx.clear();
|
|
516
|
+
}
|
|
517
|
+
seenChunkIdx.add(envIdx);
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
453
520
|
/**
|
|
454
521
|
* Decrypt and dispatch a METADATA (0x06) packet.
|
|
455
522
|
*
|
|
@@ -496,12 +563,20 @@ function createPipelineV2Server(app, state, metricsApi) {
|
|
|
496
563
|
return;
|
|
497
564
|
}
|
|
498
565
|
const env = content;
|
|
499
|
-
|
|
566
|
+
const hasSourceSnapshot = env.kind === "sources" &&
|
|
567
|
+
env.sources !== null &&
|
|
568
|
+
typeof env.sources === "object" &&
|
|
569
|
+
!Array.isArray(env.sources);
|
|
570
|
+
const entries = Array.isArray(env.entries) ? env.entries : [];
|
|
571
|
+
if (!hasSourceSnapshot && entries.length === 0) {
|
|
500
572
|
metrics.malformedPackets = (metrics.malformedPackets || 0) + 1;
|
|
501
573
|
app.debug("v2 META envelope has no entries, dropping");
|
|
502
574
|
recordError("general", "v2 META envelope has no entries");
|
|
503
575
|
return;
|
|
504
576
|
}
|
|
577
|
+
if (hasSourceSnapshot && shouldDropEnvelopeBySeq(session, env, "source snapshot")) {
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
505
580
|
// Drop stale/duplicate envelopes that UDP reordered or replayed. The
|
|
506
581
|
// inner envelope `seq` identifies a batch (shared across all chunks of
|
|
507
582
|
// a multi-chunk snapshot/diff); the inner `idx` identifies a specific
|
|
@@ -510,7 +585,10 @@ function createPipelineV2Server(app, state, metricsApi) {
|
|
|
510
585
|
// - within the current batch, reject any (seq, idx) pair already
|
|
511
586
|
// processed ("exact replay"); other idx values for the same seq
|
|
512
587
|
// remain accepted so multi-chunk batches still apply in full.
|
|
513
|
-
if (
|
|
588
|
+
if (!hasSourceSnapshot &&
|
|
589
|
+
session &&
|
|
590
|
+
typeof env.seq === "number" &&
|
|
591
|
+
Number.isFinite(env.seq)) {
|
|
514
592
|
const envSeq = env.seq >>> 0;
|
|
515
593
|
const envIdx = typeof env.idx === "number" && Number.isFinite(env.idx) ? env.idx >>> 0 : 0;
|
|
516
594
|
// Sender-restart detection: the client's meta sequence counter is
|
|
@@ -558,12 +636,17 @@ function createPipelineV2Server(app, state, metricsApi) {
|
|
|
558
636
|
}
|
|
559
637
|
session.seenMetaChunkIdx.add(envIdx);
|
|
560
638
|
}
|
|
639
|
+
if (hasSourceSnapshot) {
|
|
640
|
+
const added = (0, source_snapshot_1.mergeSourceSnapshot)(app, env.sources);
|
|
641
|
+
app.debug(`v2 source snapshot received: sources=${Object.keys(env.sources || {}).length}, added=${added}, envSeq=${env.seq ?? "?"}`);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
561
644
|
// Group entries by context so the local Signal K server sees one delta
|
|
562
645
|
// per context rather than one per path. Reduces app.handleMessage
|
|
563
646
|
// overhead on big snapshots without changing semantics.
|
|
564
647
|
const nowIso = new Date().toISOString();
|
|
565
648
|
const byContext = new Map();
|
|
566
|
-
for (const rawEntry of
|
|
649
|
+
for (const rawEntry of entries) {
|
|
567
650
|
if (!rawEntry || typeof rawEntry.meta !== "object" || !rawEntry.meta) {
|
|
568
651
|
continue;
|
|
569
652
|
}
|
|
@@ -605,7 +688,7 @@ function createPipelineV2Server(app, state, metricsApi) {
|
|
|
605
688
|
};
|
|
606
689
|
app.handleMessage("", deltaMessage);
|
|
607
690
|
}
|
|
608
|
-
app.debug(`v2 meta received: kind=${env.kind ?? "?"}, entries=${
|
|
691
|
+
app.debug(`v2 meta received: kind=${env.kind ?? "?"}, entries=${entries.length}, contexts=${byContext.size}, envSeq=${env.seq ?? "?"}`);
|
|
609
692
|
}
|
|
610
693
|
catch (err) {
|
|
611
694
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -712,6 +795,16 @@ function createPipelineV2Server(app, state, metricsApi) {
|
|
|
712
795
|
try {
|
|
713
796
|
const info = JSON.parse(parsed.payload.toString());
|
|
714
797
|
app.debug(`v2 hello from client: ${JSON.stringify(info)}`);
|
|
798
|
+
if (session && info && typeof info === "object") {
|
|
799
|
+
const helloClientId = typeof info.clientId === "string" && info.clientId.trim()
|
|
800
|
+
? info.clientId.trim()
|
|
801
|
+
: null;
|
|
802
|
+
const helloInstanceId = typeof info.instanceId === "string" && info.instanceId.trim()
|
|
803
|
+
? info.instanceId.trim()
|
|
804
|
+
: null;
|
|
805
|
+
session.clientId = helloClientId;
|
|
806
|
+
session.sourceClientInstanceId = helloInstanceId || helloClientId;
|
|
807
|
+
}
|
|
715
808
|
}
|
|
716
809
|
catch (parseErr) {
|
|
717
810
|
app.error(`v2 failed to parse HELLO payload: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`);
|
|
@@ -879,12 +972,25 @@ function createPipelineV2Server(app, state, metricsApi) {
|
|
|
879
972
|
continue;
|
|
880
973
|
}
|
|
881
974
|
deltaMessage = sanitizedDelta;
|
|
975
|
+
deltaMessage = (0, source_dispatch_1.normalizeDeltaSourceRefs)(deltaMessage);
|
|
882
976
|
_ingestRemoteTelemetry(deltaMessage);
|
|
883
977
|
if (!Array.isArray(deltaMessage.updates) || deltaMessage.updates.length === 0) {
|
|
884
978
|
continue;
|
|
885
979
|
}
|
|
980
|
+
if (state.sourceRegistry && typeof state.sourceRegistry.upsertFromDelta === "function") {
|
|
981
|
+
const deltaRecord = deltaMessage;
|
|
982
|
+
const deltaSourceInstanceId = deltaMessage &&
|
|
983
|
+
typeof deltaMessage === "object" &&
|
|
984
|
+
typeof deltaRecord.sourceClientInstanceId === "string"
|
|
985
|
+
? deltaRecord.sourceClientInstanceId || null
|
|
986
|
+
: null;
|
|
987
|
+
const stableSourceClientId = (session && (session.sourceClientInstanceId || session.clientId)) ||
|
|
988
|
+
deltaSourceInstanceId ||
|
|
989
|
+
"unknown";
|
|
990
|
+
state.sourceRegistry.upsertFromDelta(deltaMessage, stableSourceClientId);
|
|
991
|
+
}
|
|
886
992
|
trackPathStats(deltaMessage, decompressed.length / deltas.length);
|
|
887
|
-
|
|
993
|
+
(0, source_dispatch_1.handleMessageBySource)(app, deltaMessage);
|
|
888
994
|
metrics.deltasReceived++;
|
|
889
995
|
}
|
|
890
996
|
app.debug(`v2 received: seq=${parsed.sequence}, ${deltaCount} deltas, ${packet.length} bytes`);
|
|
@@ -943,7 +1049,8 @@ function createPipelineV2Server(app, state, metricsApi) {
|
|
|
943
1049
|
sessions,
|
|
944
1050
|
totalSessions: clientSessions.size,
|
|
945
1051
|
acksSent: metrics.acksSent,
|
|
946
|
-
naksSent: metrics.naksSent
|
|
1052
|
+
naksSent: metrics.naksSent,
|
|
1053
|
+
sourceReplication: state.sourceRegistry ? state.sourceRegistry.getMetrics() : null
|
|
947
1054
|
};
|
|
948
1055
|
}
|
|
949
1056
|
/**
|
|
@@ -1027,6 +1134,16 @@ function createPipelineV2Server(app, state, metricsApi) {
|
|
|
1027
1134
|
const effectiveQueueDepth = hasRemoteTelemetry ? remote.queueDepth || 0 : 0;
|
|
1028
1135
|
const effectiveRetransmitRate = hasRemoteTelemetry ? remote.retransmitRate || 0 : 0;
|
|
1029
1136
|
const effectiveActiveLink = hasRemoteTelemetry ? remote.activeLink || "primary" : "primary";
|
|
1137
|
+
const sourceReplicationMetrics = state.sourceRegistry
|
|
1138
|
+
? state.sourceRegistry.getMetrics()
|
|
1139
|
+
: { upserts: 0, noops: 0, missingIdentity: 0, conflicts: 0 };
|
|
1140
|
+
const deltaMissing = sourceReplicationMetrics.missingIdentity - previousSourceMissingIdentity;
|
|
1141
|
+
const deltaConflicts = sourceReplicationMetrics.conflicts - previousSourceConflicts;
|
|
1142
|
+
if (deltaMissing > 0 || deltaConflicts > 0) {
|
|
1143
|
+
app.debug(`[source-replication] +missingIdentity=${deltaMissing} +conflicts=${deltaConflicts} totalMissingIdentity=${sourceReplicationMetrics.missingIdentity} totalConflicts=${sourceReplicationMetrics.conflicts} size=${state.sourceRegistry.snapshot().size}`);
|
|
1144
|
+
}
|
|
1145
|
+
previousSourceMissingIdentity = sourceReplicationMetrics.missingIdentity;
|
|
1146
|
+
previousSourceConflicts = sourceReplicationMetrics.conflicts;
|
|
1030
1147
|
// Publish to Signal K
|
|
1031
1148
|
metricsPublisher.publish({
|
|
1032
1149
|
rtt: effectiveRtt,
|
package/lib/pipeline.js
CHANGED
|
@@ -36,6 +36,7 @@ const msgpack = __importStar(require("@msgpack/msgpack"));
|
|
|
36
36
|
const crypto_1 = require("./crypto");
|
|
37
37
|
const pathDictionary_1 = require("./pathDictionary");
|
|
38
38
|
const delta_sanitizer_1 = require("./delta-sanitizer");
|
|
39
|
+
const source_dispatch_1 = require("./source-dispatch");
|
|
39
40
|
const pipeline_utils_1 = require("./pipeline-utils");
|
|
40
41
|
const constants_1 = require("./constants");
|
|
41
42
|
const metadata_1 = require("./metadata");
|
|
@@ -294,9 +295,10 @@ function createPipeline(app, state, metricsApi) {
|
|
|
294
295
|
app.debug(`Skipping delta with no valid Signal K values at index ${i}`);
|
|
295
296
|
continue;
|
|
296
297
|
}
|
|
298
|
+
deltaMessage = (0, source_dispatch_1.normalizeDeltaSourceRefs)(deltaMessage);
|
|
297
299
|
// Track path stats for server-side analytics
|
|
298
300
|
trackPathStats(deltaMessage, decompressed.length / deltas.length);
|
|
299
|
-
|
|
301
|
+
(0, source_dispatch_1.handleMessageBySource)(app, deltaMessage);
|
|
300
302
|
// Log a compact summary only — never log full delta values which may
|
|
301
303
|
// contain sensitive data (position, fuel, MMSI) in plaintext logs.
|
|
302
304
|
app.debug(`delta ctx=${deltaMessage.context ?? "?"} updates=${Array.isArray(deltaMessage.updates) ? deltaMessage.updates.length : 0}`);
|
package/lib/routes/metrics.js
CHANGED
|
@@ -9,7 +9,7 @@ const prometheus_1 = require("../prometheus");
|
|
|
9
9
|
* @param ctx - Shared route context (helpers, middleware, registry)
|
|
10
10
|
*/
|
|
11
11
|
function register(router, ctx) {
|
|
12
|
-
const { rateLimitMiddleware, instanceRegistry, getFirstBundle, getEffectiveNetworkQuality, getActiveMetricsPublisher, buildFullMetricsResponse } = ctx;
|
|
12
|
+
const { rateLimitMiddleware, instanceRegistry, getFirstBundle, getEffectiveNetworkQuality, getActiveMetricsPublisher, buildFullMetricsResponse, managementAuthMiddleware } = ctx;
|
|
13
13
|
router.get("/metrics", rateLimitMiddleware, (req, res) => {
|
|
14
14
|
try {
|
|
15
15
|
const bundle = getFirstBundle();
|
|
@@ -124,4 +124,28 @@ function register(router, ctx) {
|
|
|
124
124
|
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
125
125
|
}
|
|
126
126
|
});
|
|
127
|
+
router.get("/sources", rateLimitMiddleware, managementAuthMiddleware("sources.read"), (req, res) => {
|
|
128
|
+
try {
|
|
129
|
+
const serverBundle = instanceRegistry
|
|
130
|
+
.getAll()
|
|
131
|
+
.find((bundle) => bundle.state && bundle.state.isServerMode && bundle.state.sourceRegistry) || null;
|
|
132
|
+
const bundle = serverBundle || getFirstBundle();
|
|
133
|
+
if (!bundle) {
|
|
134
|
+
return res.status(503).json({ error: "Plugin not started" });
|
|
135
|
+
}
|
|
136
|
+
const { state } = bundle;
|
|
137
|
+
if (!state.sourceRegistry) {
|
|
138
|
+
return res.json({
|
|
139
|
+
schemaVersion: 1,
|
|
140
|
+
size: 0,
|
|
141
|
+
sources: [],
|
|
142
|
+
legacy: { byLabel: {}, bySourceRef: {} }
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return res.json(state.sourceRegistry.snapshot());
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
return res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
127
151
|
}
|
package/lib/routes.js
CHANGED
|
@@ -442,6 +442,12 @@ function createRoutes(app, instanceRegistry, pluginRef) {
|
|
|
442
442
|
timestamp: metrics.lastErrorTime,
|
|
443
443
|
timeAgo: metrics.lastErrorTime ? Date.now() - metrics.lastErrorTime : null
|
|
444
444
|
}
|
|
445
|
+
: null,
|
|
446
|
+
sourceReplication: state.sourceRegistry
|
|
447
|
+
? {
|
|
448
|
+
metrics: state.sourceRegistry.getMetrics(),
|
|
449
|
+
registry: null
|
|
450
|
+
}
|
|
445
451
|
: null
|
|
446
452
|
};
|
|
447
453
|
return metricsData;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* results to `RJSFSchema` at call sites.
|
|
16
16
|
*/
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.alertThresholdsProperty = exports.enableNotificationsProperty = exports.bondingProperty = exports.congestionControlProperty = exports.serverReliabilityProperty = exports.clientReliabilityProperty = exports.clientTransportProperties = exports.commonConnectionProperties = void 0;
|
|
18
|
+
exports.alertThresholdsProperty = exports.skipOwnDataProperty = exports.enableNotificationsProperty = exports.bondingProperty = exports.congestionControlProperty = exports.serverReliabilityProperty = exports.clientReliabilityProperty = exports.clientTransportProperties = exports.commonConnectionProperties = void 0;
|
|
19
19
|
exports.buildConnectionItemSchema = buildConnectionItemSchema;
|
|
20
20
|
exports.buildWebappConnectionSchema = buildWebappConnectionSchema;
|
|
21
21
|
const crypto_constants_1 = require("./crypto-constants");
|
|
@@ -424,6 +424,13 @@ exports.enableNotificationsProperty = {
|
|
|
424
424
|
description: "Emit Signal K notifications for alerts and failover events.",
|
|
425
425
|
default: false
|
|
426
426
|
};
|
|
427
|
+
// ── Client-only: skip forwarding plugin-generated data ────────────────────────
|
|
428
|
+
exports.skipOwnDataProperty = {
|
|
429
|
+
type: "boolean",
|
|
430
|
+
title: "Skip Plugin's Own Data",
|
|
431
|
+
description: "Do not forward data this plugin publishes locally over the link. Strips entries under 'networking.edgeLink.*' and the v1 RTT path 'networking.modem.rtt' / 'networking.modem.<id>.rtt'; other 'networking.modem.*' paths from external providers are left intact. Also suppresses the v2/v3 client telemetry packet that mirrors local link metrics to the receiver.",
|
|
432
|
+
default: false
|
|
433
|
+
};
|
|
427
434
|
// ── v2/v3 monitoring alert thresholds (client) ────────────────────────────────
|
|
428
435
|
exports.alertThresholdsProperty = {
|
|
429
436
|
type: "object",
|
|
@@ -501,6 +508,7 @@ function buildConnectionItemSchema() {
|
|
|
501
508
|
congestionControl: exports.congestionControlProperty,
|
|
502
509
|
bonding: exports.bondingProperty,
|
|
503
510
|
enableNotifications: exports.enableNotificationsProperty,
|
|
511
|
+
skipOwnData: exports.skipOwnDataProperty,
|
|
504
512
|
alertThresholds: exports.alertThresholdsProperty
|
|
505
513
|
},
|
|
506
514
|
required: ["udpAddress", "testAddress", "testPort"]
|
|
@@ -524,6 +532,7 @@ function buildWebappConnectionSchema(isClient, protocolVersion) {
|
|
|
524
532
|
if (isClient) {
|
|
525
533
|
Object.assign(props, exports.clientTransportProperties);
|
|
526
534
|
props.enableNotifications = exports.enableNotificationsProperty;
|
|
535
|
+
props.skipOwnData = exports.skipOwnDataProperty;
|
|
527
536
|
required.push("udpAddress", "testAddress", "testPort");
|
|
528
537
|
if (isReliableProtocol) {
|
|
529
538
|
props.reliability = exports.clientReliabilityProperty;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeDeltaSourceRefs = normalizeDeltaSourceRefs;
|
|
4
|
+
exports.handleMessageBySource = handleMessageBySource;
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function getSourceLabel(update) {
|
|
9
|
+
const source = isRecord(update.source) ? update.source : null;
|
|
10
|
+
const label = source && typeof source.label === "string" ? source.label.trim() : "";
|
|
11
|
+
return label.length > 0 ? label : "";
|
|
12
|
+
}
|
|
13
|
+
function hasStaleEdgeLinkSourceRef(update) {
|
|
14
|
+
const sourceLabel = getSourceLabel(update);
|
|
15
|
+
const sourceRef = typeof update.$source === "string" ? update.$source.trim() : "";
|
|
16
|
+
if (!sourceLabel || !sourceRef || sourceLabel === "signalk-edge-link") {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return (sourceRef === "signalk-edge-link" ||
|
|
20
|
+
sourceRef.startsWith("signalk-edge-link.") ||
|
|
21
|
+
sourceRef.startsWith("signalk-edge-link:"));
|
|
22
|
+
}
|
|
23
|
+
function normalizeUpdateSourceRef(update) {
|
|
24
|
+
if (!hasStaleEdgeLinkSourceRef(update)) {
|
|
25
|
+
return update;
|
|
26
|
+
}
|
|
27
|
+
const cloned = { ...update };
|
|
28
|
+
delete cloned.$source;
|
|
29
|
+
return cloned;
|
|
30
|
+
}
|
|
31
|
+
function cloneUpdate(update) {
|
|
32
|
+
const normalized = normalizeUpdateSourceRef(update);
|
|
33
|
+
const cloned = {
|
|
34
|
+
...normalized,
|
|
35
|
+
source: isRecord(normalized.source)
|
|
36
|
+
? { ...normalized.source }
|
|
37
|
+
: normalized.source,
|
|
38
|
+
values: Array.isArray(normalized.values)
|
|
39
|
+
? normalized.values.map((value) => ({ ...value }))
|
|
40
|
+
: normalized.values,
|
|
41
|
+
meta: Array.isArray(normalized.meta)
|
|
42
|
+
? normalized.meta.map((entry) => ({ ...entry }))
|
|
43
|
+
: normalized.meta
|
|
44
|
+
};
|
|
45
|
+
return cloned;
|
|
46
|
+
}
|
|
47
|
+
function normalizeDeltaSourceRefs(delta) {
|
|
48
|
+
if (!delta || !Array.isArray(delta.updates)) {
|
|
49
|
+
return delta;
|
|
50
|
+
}
|
|
51
|
+
let changed = false;
|
|
52
|
+
const updates = delta.updates.map((update) => {
|
|
53
|
+
const normalized = normalizeUpdateSourceRef(update);
|
|
54
|
+
if (normalized !== update) {
|
|
55
|
+
changed = true;
|
|
56
|
+
}
|
|
57
|
+
return normalized;
|
|
58
|
+
});
|
|
59
|
+
return changed ? { ...delta, updates } : delta;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Signal K's app.handleMessage(providerId, delta) rewrites update.source.label
|
|
63
|
+
* to providerId before applying the delta. Remote updates can contain several
|
|
64
|
+
* original source labels, so dispatch them under their original label. Stale
|
|
65
|
+
* edge-link `$source` values are removed separately before dispatch so Signal K
|
|
66
|
+
* can recompute them from the structured source object.
|
|
67
|
+
*/
|
|
68
|
+
function handleMessageBySource(app, delta) {
|
|
69
|
+
if (!delta || !Array.isArray(delta.updates) || delta.updates.length === 0) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const grouped = new Map();
|
|
73
|
+
let hasOriginalSourceLabel = false;
|
|
74
|
+
for (const update of delta.updates) {
|
|
75
|
+
const sourceLabel = getSourceLabel(update);
|
|
76
|
+
if (sourceLabel) {
|
|
77
|
+
hasOriginalSourceLabel = true;
|
|
78
|
+
}
|
|
79
|
+
const providerId = sourceLabel || "";
|
|
80
|
+
const updates = grouped.get(providerId);
|
|
81
|
+
if (updates) {
|
|
82
|
+
updates.push(update);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
grouped.set(providerId, [update]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (!hasOriginalSourceLabel) {
|
|
89
|
+
app.handleMessage("", delta);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
for (const [providerId, updates] of grouped) {
|
|
93
|
+
app.handleMessage(providerId, {
|
|
94
|
+
...delta,
|
|
95
|
+
updates: updates.map(cloneUpdate)
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|