signalk-edge-link 2.0.0 → 2.1.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Karl-Erik Gustafsson
3
+ Copyright (c) 2024-2026 Karl-Erik Gustafsson
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -235,6 +235,20 @@ function validateConnectionConfig(connection, prefix = "") {
235
235
  }
236
236
  }
237
237
  }
238
+ // Validate that primary and backup links are different
239
+ const primaryLink = bonding.primary;
240
+ const backupLink = bonding.backup;
241
+ if (primaryLink && backupLink) {
242
+ const sameAddress = primaryLink.address !== undefined &&
243
+ backupLink.address !== undefined &&
244
+ primaryLink.address === backupLink.address;
245
+ const samePort = primaryLink.port !== undefined &&
246
+ backupLink.port !== undefined &&
247
+ primaryLink.port === backupLink.port;
248
+ if (sameAddress && samePort) {
249
+ return `${p}bonding primary and backup links must use different address:port combinations`;
250
+ }
251
+ }
238
252
  if (bonding.failover !== undefined) {
239
253
  if (!bonding.failover ||
240
254
  typeof bonding.failover !== "object" ||
package/lib/instance.js CHANGED
@@ -93,7 +93,6 @@ function createInstance(app, options, instanceId, pluginId, onStatusChange) {
93
93
  pipelineServer: null,
94
94
  heartbeatHandle: null,
95
95
  monitoring: null,
96
- networkSimulator: null,
97
96
  configDebounceTimers: {},
98
97
  configContentHashes: {},
99
98
  configWatcherObjects: [],
@@ -240,7 +239,7 @@ function createInstance(app, options, instanceId, pluginId, onStatusChange) {
240
239
  state.timer = false;
241
240
  return;
242
241
  }
243
- const actualBatchSize = Math.min(batchSize, state.deltas.length);
242
+ const actualBatchSize = Math.min(batchSize, state.deltas.length, state.maxDeltasPerBatch);
244
243
  const batch = state.deltas.slice(0, actualBatchSize);
245
244
  state.batchSendInFlight = true;
246
245
  try {
@@ -269,7 +268,7 @@ function createInstance(app, options, instanceId, pluginId, onStatusChange) {
269
268
  }
270
269
  finally {
271
270
  state.batchSendInFlight = false;
272
- if (state.deltas.length > 0 && !state.pendingRetry) {
271
+ if (state.deltas.length > 0 && !state.pendingRetry && !state.stopped) {
273
272
  setImmediate(() => {
274
273
  flushDeltaBatch();
275
274
  });
@@ -884,7 +883,6 @@ function createInstance(app, options, instanceId, pluginId, onStatusChange) {
884
883
  }
885
884
  state.monitoring = null;
886
885
  }
887
- state.networkSimulator = null;
888
886
  // Stop ping monitor
889
887
  if (state.pingMonitor) {
890
888
  state.pingMonitor.stop();
@@ -140,8 +140,11 @@ function createPipelineV2Client(app, state, metricsApi) {
140
140
  }
141
141
  function _effectiveRetransmitAge() {
142
142
  let maxAge = retransmitMaxAge;
143
- if ((metrics.rtt ?? 0) > 0) {
144
- const rttBasedAge = Math.round((metrics.rtt ?? 0) * retransmitRttMultiplier);
143
+ // Use the congestion controller's smoothed RTT (EMA) instead of the raw
144
+ // latest sample to avoid volatile timeout swings from single RTT spikes.
145
+ const smoothedRtt = congestionControl.getAvgRTT();
146
+ if (smoothedRtt > 0) {
147
+ const rttBasedAge = Math.round(smoothedRtt * retransmitRttMultiplier);
145
148
  maxAge = Math.min(maxAge, Math.max(retransmitMinAge, rttBasedAge));
146
149
  }
147
150
  const ackIdleMs = Date.now() - lastAckAt;
@@ -349,10 +352,12 @@ function createPipelineV2Client(app, state, metricsApi) {
349
352
  const ackedSeq = packetParser.parseACKPayload(parsed.payload);
350
353
  const now = Date.now();
351
354
  let rttSample = null;
352
- // Estimate RTT from original send timestamp (not retransmit timestamp).
355
+ // Only sample RTT from packets that were NOT retransmitted (Karn's algorithm).
356
+ // When a retransmitted packet is ACKed, the measurement is ambiguous — the ACK
357
+ // could be for the original or the retransmit — so we skip it entirely.
353
358
  const entry = retransmitQueue.get(ackedSeq);
354
- if (entry && (entry.originalTimestamp || entry.timestamp)) {
355
- rttSample = Math.max(0, now - (entry.originalTimestamp || entry.timestamp));
359
+ if (entry && entry.attempts === 0) {
360
+ rttSample = Math.max(0, now - entry.originalTimestamp);
356
361
  }
357
362
  if (rttSample !== null) {
358
363
  metrics.rtt = rttSample;
@@ -376,10 +381,13 @@ function createPipelineV2Client(app, state, metricsApi) {
376
381
  lastAckAt = now;
377
382
  lastAckRinfo = rinfo ? { address: rinfo.address, port: rinfo.port } : lastAckRinfo;
378
383
  // Update congestion control with latest network metrics.
384
+ // Only feed RTT when we have a fresh sample; passing -1 causes the
385
+ // congestion controller's >= 0 guard to skip the RTT EMA update,
386
+ // preventing stale values from being repeatedly folded into the average.
379
387
  // Clamp packetLoss to [0, 1] as a defensive measure against any future
380
388
  // changes to _calculatePacketLoss that could produce out-of-range values.
381
389
  congestionControl.updateMetrics({
382
- rtt: metrics.rtt ?? 0,
390
+ rtt: rttSample ?? -1,
383
391
  packetLoss: Math.min(1, Math.max(0, _calculatePacketLoss()))
384
392
  });
385
393
  app.debug(`ACK received: seq=${ackedSeq}, removed=${removed}, queueDepth=${retransmitQueue.getSize()}, rtt=${metrics.rtt}ms`);
@@ -625,7 +625,7 @@ function createPipelineV2Server(app, state, metricsApi) {
625
625
  if (!Array.isArray(deltaMessage.updates) || deltaMessage.updates.length === 0) {
626
626
  continue;
627
627
  }
628
- trackPathStats(deltaMessage, decompressed.length / deltaCount);
628
+ trackPathStats(deltaMessage, decompressed.length / deltas.length);
629
629
  app.handleMessage("", deltaMessage);
630
630
  metrics.deltasReceived++;
631
631
  }
package/lib/pipeline.js CHANGED
@@ -203,7 +203,7 @@ function createPipeline(app, state, metricsApi) {
203
203
  continue;
204
204
  }
205
205
  // Track path stats for server-side analytics
206
- trackPathStats(deltaMessage, decompressed.length / deltaCount);
206
+ trackPathStats(deltaMessage, decompressed.length / deltas.length);
207
207
  app.handleMessage("", deltaMessage);
208
208
  // Log a compact summary only — never log full delta values which may
209
209
  // contain sensitive data (position, fuel, MMSI) in plaintext logs.
@@ -336,15 +336,9 @@ function register(router, ctx) {
336
336
  if (!bundle) {
337
337
  return res.status(503).json({ error: "Plugin not started" });
338
338
  }
339
- const { state } = bundle;
340
- if (!state.networkSimulator) {
341
- return res.json({ enabled: false });
342
- }
343
- res.json({
344
- enabled: true,
345
- conditions: state.networkSimulator.getConditions(),
346
- stats: state.networkSimulator.getStats()
347
- });
339
+ // Network simulator is not currently implemented; endpoint kept for
340
+ // API compatibility and future use.
341
+ res.json({ enabled: false });
348
342
  }
349
343
  catch (err) {
350
344
  res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
package/package.json CHANGED
@@ -1,161 +1,165 @@
1
- {
2
- "name": "signalk-edge-link",
3
- "version": "2.0.0",
4
- "description": "SignalK Edge Link. Secure UDP link for data exchange.",
5
- "main": "lib/index.js",
6
- "files": [
7
- "lib/",
8
- "public/",
9
- "schemas/"
10
- ],
11
- "keywords": [
12
- "signalk-node-server-plugin",
13
- "signalk-category-network",
14
- "signalk-webapp",
15
- "signalk-category-utility",
16
- "signalk-plugin-configurator"
17
- ],
18
- "signalk": {
19
- "appIcon": "./icons/icon-72x72.png",
20
- "displayName": "Edge Link Configuration"
21
- },
22
- "signalk-plugin-enabled-by-default": false,
23
- "scripts": {
24
- "clean:lib": "node -e \"const fs=require('fs');if(fs.existsSync('lib'))fs.rmSync('lib',{recursive:true,force:true});\"",
25
- "clean:public": "node -e \"const fs=require('fs');if(fs.existsSync('public'))fs.rmSync('public',{recursive:true,force:true});\"",
26
- "build": "npm run build:ts && npm run build:web",
27
- "build:web": "npm run clean:public && webpack --mode production",
28
- "build:ts": "npm run clean:lib && tsc",
29
- "check:ts": "tsc --noEmit",
30
- "dev": "webpack --mode development --watch",
31
- "test": "jest --runInBand",
32
- "test:v2": "jest __tests__/v2/",
33
- "test:integration": "jest test/integration/",
34
- "test:watch": "jest --watch",
35
- "test:coverage": "jest --coverage",
36
- "lint": "eslint .",
37
- "lint:fix": "eslint . --fix",
38
- "format": "prettier --write \"**/*.{js,ts,json,md}\"",
39
- "migrate:config": "node lib/scripts/migrate-config.js",
40
- "cli": "node lib/bin/edge-link-cli.js",
41
- "prepare": "husky"
42
- },
43
- "dependencies": {
44
- "@msgpack/msgpack": "^3.0.0",
45
- "@rjsf/core": "^5.18.4",
46
- "@rjsf/utils": "^5.18.4",
47
- "@rjsf/validator-ajv8": "^5.18.4",
48
- "ping-monitor": "^0.8.2"
49
- },
50
- "devDependencies": {
51
- "@babel/core": "^7.22.0",
52
- "@babel/preset-env": "^7.22.0",
53
- "@babel/preset-react": "^7.22.0",
54
- "@testing-library/jest-dom": "^5.17.0",
55
- "@testing-library/react": "^12.1.5",
56
- "@types/node": "^25.3.5",
57
- "@types/react": "^16.14.0",
58
- "@types/react-dom": "^16.9.0",
59
- "babel-loader": "^9.1.2",
60
- "copy-webpack-plugin": "^14.0.0",
61
- "css-loader": "^6.8.1",
62
- "eslint": "^8.57.1",
63
- "eslint-plugin-react": "^7.37.5",
64
- "html-webpack-plugin": "^5.5.3",
65
- "husky": "^9.1.7",
66
- "jest": "^29.7.0",
67
- "jest-environment-jsdom": "^30.3.0",
68
- "lint-staged": "^15.4.3",
69
- "mini-css-extract-plugin": "^2.7.6",
70
- "prettier": "^3.6.2",
71
- "react": "^16.13.1",
72
- "react-dom": "^16.13.1",
73
- "react-test-renderer": "^16.14.0",
74
- "style-loader": "^3.3.3",
75
- "ts-jest": "^29.4.6",
76
- "ts-loader": "^9.5.4",
77
- "typescript": "^5.9.3",
78
- "webpack": "^5.102.1",
79
- "webpack-cli": "^5.1.4"
80
- },
81
- "engines": {
82
- "node": ">=16"
83
- },
84
- "author": "Karl-Erik Gustafsson",
85
- "repository": "https://github.com/KEGustafsson/signalk-edge-link",
86
- "license": "MIT",
87
- "jest": {
88
- "testEnvironment": "node",
89
- "coverageDirectory": "coverage",
90
- "collectCoverageFrom": [
91
- "lib/**/*.js",
92
- "!lib/webapp/**",
93
- "!lib/components/**",
94
- "!lib/utils/**"
95
- ],
96
- "testMatch": [
97
- "**/__tests__/**/*.js",
98
- "**/*.test.js",
99
- "**/*.spec.js"
100
- ],
101
- "transform": {
102
- "^.+\\.js$": [
103
- "babel-jest",
104
- {
105
- "presets": [
106
- [
107
- "@babel/preset-env",
108
- {
109
- "targets": {
110
- "node": "current"
111
- }
112
- }
113
- ]
114
- ]
115
- }
116
- ],
117
- ".+\\.tsx$": [
118
- "ts-jest",
119
- {
120
- "tsconfig": "tsconfig.webapp.json",
121
- "diagnostics": false
122
- }
123
- ],
124
- "^.+\\.ts$": "ts-jest"
125
- },
126
- "moduleFileExtensions": [
127
- "ts",
128
- "tsx",
129
- "js",
130
- "json",
131
- "node"
132
- ],
133
- "testPathIgnorePatterns": [
134
- "/node_modules/",
135
- "/public/"
136
- ],
137
- "coverageThreshold": {
138
- "global": {
139
- "branches": 60,
140
- "functions": 65,
141
- "lines": 65,
142
- "statements": 65
143
- }
144
- }
145
- },
146
- "bin": {
147
- "edge-link-cli": "lib/bin/edge-link-cli.js"
148
- },
149
- "lint-staged": {
150
- "*.js": [
151
- "prettier --write",
152
- "eslint --fix"
153
- ],
154
- "*.ts": [
155
- "prettier --write"
156
- ],
157
- "*.{json,md}": [
158
- "prettier --write"
159
- ]
160
- }
161
- }
1
+ {
2
+ "name": "signalk-edge-link",
3
+ "version": "2.1.1",
4
+ "description": "SignalK Edge Link. Secure UDP link for data exchange.",
5
+ "main": "lib/index.js",
6
+ "files": [
7
+ "lib/",
8
+ "public/",
9
+ "schemas/"
10
+ ],
11
+ "keywords": [
12
+ "signalk-node-server-plugin",
13
+ "signalk-category-network",
14
+ "signalk-webapp",
15
+ "signalk-category-utility",
16
+ "signalk-plugin-configurator"
17
+ ],
18
+ "signalk": {
19
+ "appIcon": "./icons/icon-72x72.png",
20
+ "displayName": "Edge Link Configuration"
21
+ },
22
+ "signalk-plugin-enabled-by-default": false,
23
+ "scripts": {
24
+ "clean:lib": "node -e \"const fs=require('fs');if(fs.existsSync('lib'))fs.rmSync('lib',{recursive:true,force:true});\"",
25
+ "clean:public": "node -e \"const fs=require('fs');if(fs.existsSync('public'))fs.rmSync('public',{recursive:true,force:true});\"",
26
+ "build": "npm run build:ts && npm run build:web",
27
+ "build:web": "npm run clean:public && webpack --mode production",
28
+ "build:ts": "npm run clean:lib && tsc",
29
+ "check:ts": "tsc --noEmit",
30
+ "dev": "webpack --mode development --watch",
31
+ "test": "jest --runInBand",
32
+ "test:v2": "jest __tests__/v2/",
33
+ "test:integration": "jest test/integration/",
34
+ "test:watch": "jest --watch",
35
+ "test:coverage": "jest --coverage",
36
+ "lint": "eslint .",
37
+ "lint:fix": "eslint . --fix",
38
+ "format": "prettier --write \"**/*.{js,ts,json,md}\"",
39
+ "migrate:config": "node lib/scripts/migrate-config.js",
40
+ "cli": "node lib/bin/edge-link-cli.js",
41
+ "prepare": "husky"
42
+ },
43
+ "dependencies": {
44
+ "@msgpack/msgpack": "^3.0.0",
45
+ "@rjsf/core": "^5.18.4",
46
+ "@rjsf/utils": "^5.18.4",
47
+ "@rjsf/validator-ajv8": "^5.18.4",
48
+ "ping-monitor": "^0.8.2"
49
+ },
50
+ "devDependencies": {
51
+ "@babel/core": "^7.22.0",
52
+ "@babel/preset-env": "^7.22.0",
53
+ "@babel/preset-react": "^7.22.0",
54
+ "@testing-library/jest-dom": "^5.17.0",
55
+ "@testing-library/react": "^12.1.5",
56
+ "@types/node": "^25.3.5",
57
+ "@types/react": "^16.14.0",
58
+ "@types/react-dom": "^16.9.0",
59
+ "babel-loader": "^9.1.2",
60
+ "copy-webpack-plugin": "^14.0.0",
61
+ "css-loader": "^6.8.1",
62
+ "eslint": "^8.57.1",
63
+ "eslint-plugin-react": "^7.37.5",
64
+ "html-webpack-plugin": "^5.5.3",
65
+ "husky": "^9.1.7",
66
+ "jest": "^29.7.0",
67
+ "jest-environment-jsdom": "^30.3.0",
68
+ "lint-staged": "^15.4.3",
69
+ "mini-css-extract-plugin": "^2.7.6",
70
+ "prettier": "^3.6.2",
71
+ "react": "^16.13.1",
72
+ "react-dom": "^16.13.1",
73
+ "react-test-renderer": "^16.14.0",
74
+ "style-loader": "^3.3.3",
75
+ "ts-jest": "^29.4.6",
76
+ "ts-loader": "^9.5.4",
77
+ "typescript": "^5.9.3",
78
+ "webpack": "^5.102.1",
79
+ "webpack-cli": "^5.1.4"
80
+ },
81
+ "engines": {
82
+ "node": ">=16"
83
+ },
84
+ "author": "Karl-Erik Gustafsson",
85
+ "repository": "https://github.com/KEGustafsson/signalk-edge-link",
86
+ "homepage": "https://github.com/KEGustafsson/signalk-edge-link#readme",
87
+ "bugs": {
88
+ "url": "https://github.com/KEGustafsson/signalk-edge-link/issues"
89
+ },
90
+ "license": "MIT",
91
+ "jest": {
92
+ "testEnvironment": "node",
93
+ "coverageDirectory": "coverage",
94
+ "collectCoverageFrom": [
95
+ "lib/**/*.js",
96
+ "!lib/webapp/**",
97
+ "!lib/components/**",
98
+ "!lib/utils/**"
99
+ ],
100
+ "testMatch": [
101
+ "**/__tests__/**/*.js",
102
+ "**/*.test.js",
103
+ "**/*.spec.js"
104
+ ],
105
+ "transform": {
106
+ "^.+\\.js$": [
107
+ "babel-jest",
108
+ {
109
+ "presets": [
110
+ [
111
+ "@babel/preset-env",
112
+ {
113
+ "targets": {
114
+ "node": "current"
115
+ }
116
+ }
117
+ ]
118
+ ]
119
+ }
120
+ ],
121
+ ".+\\.tsx$": [
122
+ "ts-jest",
123
+ {
124
+ "tsconfig": "tsconfig.webapp.json",
125
+ "diagnostics": false
126
+ }
127
+ ],
128
+ "^.+\\.ts$": "ts-jest"
129
+ },
130
+ "moduleFileExtensions": [
131
+ "ts",
132
+ "tsx",
133
+ "js",
134
+ "json",
135
+ "node"
136
+ ],
137
+ "testPathIgnorePatterns": [
138
+ "/node_modules/",
139
+ "/public/"
140
+ ],
141
+ "coverageThreshold": {
142
+ "global": {
143
+ "branches": 60,
144
+ "functions": 65,
145
+ "lines": 65,
146
+ "statements": 65
147
+ }
148
+ }
149
+ },
150
+ "bin": {
151
+ "edge-link-cli": "lib/bin/edge-link-cli.js"
152
+ },
153
+ "lint-staged": {
154
+ "*.js": [
155
+ "prettier --write",
156
+ "eslint --fix"
157
+ ],
158
+ "*.ts": [
159
+ "prettier --write"
160
+ ],
161
+ "*.{json,md}": [
162
+ "prettier --write"
163
+ ]
164
+ }
165
+ }