signalk-edge-link 2.8.0 → 2.9.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/bonding.js +5 -4
- package/lib/compact-delta.js +167 -0
- package/lib/connection-config.js +98 -0
- package/lib/constants.js +30 -2
- package/lib/delta-sanitizer.js +359 -0
- package/lib/metrics-publisher.js +10 -5
- package/lib/monitoring.js +5 -1
- package/lib/pipeline-utils.js +13 -2
- package/lib/pipeline-v2-client.js +73 -13
- package/lib/pipeline-v2-server.js +32 -6
- package/lib/pipeline.js +48 -13
- package/lib/shared/connection-schema.js +78 -0
- package/lib/source-dispatch.js +23 -1
- package/lib/value-dedup.js +224 -0
- package/package.json +167 -165
- package/public/277.7f5c2d73f7d3387708f5.js +9 -0
- package/public/277.7f5c2d73f7d3387708f5.js.map +1 -0
- package/public/540.70a0bc6aadb412703390.js.map +1 -1
- package/public/982.0f09129baa471abb323c.js +2 -0
- package/public/982.0f09129baa471abb323c.js.map +1 -0
- package/public/main.0b6f5e3267731da945f0.js.map +1 -1
- package/public/remoteEntry.js +1 -1
- package/public/remoteEntry.js.map +1 -1
- package/public/277.d365356803e61762acb0.js +0 -9
- package/public/277.d365356803e61762acb0.js.map +0 -1
- package/public/982.a8caac8a5aef99f5a6bf.js +0 -2
- package/public/982.a8caac8a5aef99f5a6bf.js.map +0 -1
- /package/public/{277.d365356803e61762acb0.js.LICENSE.txt → 277.7f5c2d73f7d3387708f5.js.LICENSE.txt} +0 -0
package/lib/bonding.js
CHANGED
|
@@ -575,14 +575,15 @@ class BondingManager {
|
|
|
575
575
|
return;
|
|
576
576
|
}
|
|
577
577
|
try {
|
|
578
|
+
// Emit a `$source` string rather than a structured `source` object:
|
|
579
|
+
// signalk-server derives `${label}.XX` from a label-only source object,
|
|
580
|
+
// which would split this notification across a spurious
|
|
581
|
+
// `signalk-edge-link.XX` bucket. See src/source-dispatch.ts.
|
|
578
582
|
this.app.handleMessage(this.sourceLabel, {
|
|
579
583
|
context: "vessels.self",
|
|
580
584
|
updates: [
|
|
581
585
|
{
|
|
582
|
-
source:
|
|
583
|
-
label: this.sourceLabel,
|
|
584
|
-
type: "plugin"
|
|
585
|
-
},
|
|
586
|
+
$source: "signalk-edge-link",
|
|
586
587
|
timestamp: new Date().toISOString(),
|
|
587
588
|
values: [
|
|
588
589
|
{
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.encodeCompactDelta = encodeCompactDelta;
|
|
4
|
+
exports.encodeCompactPayload = encodeCompactPayload;
|
|
5
|
+
exports.decodeCompactDelta = decodeCompactDelta;
|
|
6
|
+
exports.isCompactDeltaArray = isCompactDeltaArray;
|
|
7
|
+
exports.decodeCompactDeltaArray = decodeCompactDeltaArray;
|
|
8
|
+
// Update tuple position constants — keeps the encoder and decoder honest.
|
|
9
|
+
const POS_SOURCE = 0;
|
|
10
|
+
const POS_DOLLAR_SOURCE = 1;
|
|
11
|
+
const POS_TIMESTAMP = 2;
|
|
12
|
+
const POS_VALUES = 3;
|
|
13
|
+
const POS_META = 4;
|
|
14
|
+
// Length we always emit so positional access is stable.
|
|
15
|
+
const UPDATE_TUPLE_LEN = 5;
|
|
16
|
+
/**
|
|
17
|
+
* Encode a single update block into a positional 5-element array.
|
|
18
|
+
* Missing fields are encoded as `null` to keep positions stable.
|
|
19
|
+
*/
|
|
20
|
+
function encodeUpdate(update) {
|
|
21
|
+
const valuesArr = [];
|
|
22
|
+
if (Array.isArray(update.values)) {
|
|
23
|
+
for (const v of update.values) {
|
|
24
|
+
if (v && typeof v === "object" && v.path !== undefined) {
|
|
25
|
+
valuesArr.push([v.path, v.value]);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const metaArr = [];
|
|
30
|
+
if (Array.isArray(update.meta)) {
|
|
31
|
+
for (const m of update.meta) {
|
|
32
|
+
if (m && typeof m === "object" && m.path !== undefined) {
|
|
33
|
+
metaArr.push([m.path, m.value]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Use null for absent source/timestamp; null compresses to a single byte in msgpack.
|
|
38
|
+
return [
|
|
39
|
+
update.source ?? null,
|
|
40
|
+
update.$source ?? null,
|
|
41
|
+
update.timestamp ?? null,
|
|
42
|
+
valuesArr,
|
|
43
|
+
metaArr.length > 0 ? metaArr : null
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Encode a single Delta into a positional 2-element array.
|
|
48
|
+
*
|
|
49
|
+
* [context, [updateTuple, updateTuple, ...]]
|
|
50
|
+
*/
|
|
51
|
+
function encodeCompactDelta(delta) {
|
|
52
|
+
const updates = Array.isArray(delta.updates) ? delta.updates : [];
|
|
53
|
+
return [delta.context ?? null, updates.map(encodeUpdate)];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Encode a Delta, Delta[], or Record<string, Delta> as a flat array of
|
|
57
|
+
* compact deltas. The shape (single / array / record) is collapsed to
|
|
58
|
+
* an array because the receiver pipeline already handles both array
|
|
59
|
+
* and object batch forms identically.
|
|
60
|
+
*/
|
|
61
|
+
function encodeCompactPayload(payload) {
|
|
62
|
+
if (Array.isArray(payload)) {
|
|
63
|
+
return payload.map(encodeCompactDelta);
|
|
64
|
+
}
|
|
65
|
+
if (payload && typeof payload === "object" && Array.isArray(payload.updates)) {
|
|
66
|
+
return [encodeCompactDelta(payload)];
|
|
67
|
+
}
|
|
68
|
+
// Record<string, Delta>
|
|
69
|
+
const out = [];
|
|
70
|
+
for (const d of Object.values(payload)) {
|
|
71
|
+
if (d && typeof d === "object") {
|
|
72
|
+
out.push(encodeCompactDelta(d));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
// ── Decoding ─────────────────────────────────────────────────────────────────
|
|
78
|
+
function decodeUpdate(tuple) {
|
|
79
|
+
if (!Array.isArray(tuple) || tuple.length < UPDATE_TUPLE_LEN)
|
|
80
|
+
return null;
|
|
81
|
+
const source = tuple[POS_SOURCE];
|
|
82
|
+
const dollarSource = tuple[POS_DOLLAR_SOURCE];
|
|
83
|
+
const timestamp = tuple[POS_TIMESTAMP];
|
|
84
|
+
const rawValues = tuple[POS_VALUES];
|
|
85
|
+
const rawMeta = tuple[POS_META];
|
|
86
|
+
const values = [];
|
|
87
|
+
if (Array.isArray(rawValues)) {
|
|
88
|
+
for (const vt of rawValues) {
|
|
89
|
+
if (Array.isArray(vt) && vt.length >= 2) {
|
|
90
|
+
values.push({ path: vt[0], value: vt[1] });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const update = { values };
|
|
95
|
+
if (source !== null && source !== undefined) {
|
|
96
|
+
update.source = source;
|
|
97
|
+
}
|
|
98
|
+
if (typeof dollarSource === "string") {
|
|
99
|
+
update.$source = dollarSource;
|
|
100
|
+
}
|
|
101
|
+
if (typeof timestamp === "string") {
|
|
102
|
+
update.timestamp = timestamp;
|
|
103
|
+
}
|
|
104
|
+
if (Array.isArray(rawMeta)) {
|
|
105
|
+
const meta = [];
|
|
106
|
+
for (const mt of rawMeta) {
|
|
107
|
+
if (Array.isArray(mt) && mt.length >= 2) {
|
|
108
|
+
meta.push({ path: mt[0], value: mt[1] });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (meta.length > 0)
|
|
112
|
+
update.meta = meta;
|
|
113
|
+
}
|
|
114
|
+
return update;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Decode a single compact-encoded delta back into a Signal K Delta.
|
|
118
|
+
* Returns null for malformed input so the caller can drop it cleanly.
|
|
119
|
+
*/
|
|
120
|
+
function decodeCompactDelta(payload) {
|
|
121
|
+
if (!Array.isArray(payload) || payload.length < 2)
|
|
122
|
+
return null;
|
|
123
|
+
const context = payload[0];
|
|
124
|
+
const rawUpdates = payload[1];
|
|
125
|
+
if (!Array.isArray(rawUpdates))
|
|
126
|
+
return null;
|
|
127
|
+
const updates = [];
|
|
128
|
+
for (const u of rawUpdates) {
|
|
129
|
+
const decoded = decodeUpdate(u);
|
|
130
|
+
if (decoded)
|
|
131
|
+
updates.push(decoded);
|
|
132
|
+
}
|
|
133
|
+
const out = {
|
|
134
|
+
context: typeof context === "string" ? context : "",
|
|
135
|
+
updates
|
|
136
|
+
};
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Detect whether a parsed message is compact-encoded (flat array of
|
|
141
|
+
* 2-element arrays) versus a standard Signal K Delta array. We use the
|
|
142
|
+
* first element's shape as the discriminator:
|
|
143
|
+
* - compact: [[ctx, [...updates]], ...] → element 0 is itself a 2-tuple
|
|
144
|
+
* - standard: [{context, updates}, ...] → element 0 is an object
|
|
145
|
+
*/
|
|
146
|
+
function isCompactDeltaArray(value) {
|
|
147
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
148
|
+
return false;
|
|
149
|
+
const first = value[0];
|
|
150
|
+
return (Array.isArray(first) && first.length >= 2 && Array.isArray(first[1]) // updates slot is itself an array
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Decode a compact-encoded array of deltas into an array of standard
|
|
155
|
+
* Signal K Deltas. Skips entries that fail to decode.
|
|
156
|
+
*/
|
|
157
|
+
function decodeCompactDeltaArray(payload) {
|
|
158
|
+
if (!Array.isArray(payload))
|
|
159
|
+
return [];
|
|
160
|
+
const out = [];
|
|
161
|
+
for (const item of payload) {
|
|
162
|
+
const d = decodeCompactDelta(item);
|
|
163
|
+
if (d)
|
|
164
|
+
out.push(d);
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
package/lib/connection-config.js
CHANGED
|
@@ -17,7 +17,13 @@ exports.VALID_CONNECTION_KEYS = [
|
|
|
17
17
|
"secretKey",
|
|
18
18
|
"stretchAsciiKey",
|
|
19
19
|
"useMsgpack",
|
|
20
|
+
"useValueDedup",
|
|
21
|
+
"useCompactDeltas",
|
|
22
|
+
"pathFilter",
|
|
20
23
|
"usePathDictionary",
|
|
24
|
+
"brotliQuality",
|
|
25
|
+
"pathPrecision",
|
|
26
|
+
"pathThrottle",
|
|
21
27
|
"enableNotifications",
|
|
22
28
|
"skipOwnData",
|
|
23
29
|
"protocolVersion",
|
|
@@ -100,6 +106,98 @@ function validateConnectionConfig(connection, prefix = "") {
|
|
|
100
106
|
if (conn.useMsgpack !== undefined && typeof conn.useMsgpack !== "boolean") {
|
|
101
107
|
return `${p}useMsgpack must be a boolean`;
|
|
102
108
|
}
|
|
109
|
+
if (conn.useValueDedup !== undefined && typeof conn.useValueDedup !== "boolean") {
|
|
110
|
+
return `${p}useValueDedup must be a boolean`;
|
|
111
|
+
}
|
|
112
|
+
if (conn.useCompactDeltas !== undefined && typeof conn.useCompactDeltas !== "boolean") {
|
|
113
|
+
return `${p}useCompactDeltas must be a boolean`;
|
|
114
|
+
}
|
|
115
|
+
const _pv = conn.protocolVersion ?? 1;
|
|
116
|
+
if (conn.useValueDedup === true && _pv < 2) {
|
|
117
|
+
return `${p}useValueDedup is only supported on protocolVersion 2 or 3`;
|
|
118
|
+
}
|
|
119
|
+
if (conn.useCompactDeltas === true) {
|
|
120
|
+
if (_pv < 2) {
|
|
121
|
+
return `${p}useCompactDeltas is only supported on protocolVersion 2 or 3`;
|
|
122
|
+
}
|
|
123
|
+
if (conn.useMsgpack !== true) {
|
|
124
|
+
return `${p}useCompactDeltas requires useMsgpack to be enabled`;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (conn.pathFilter !== undefined) {
|
|
128
|
+
if (typeof conn.pathFilter !== "object" ||
|
|
129
|
+
conn.pathFilter === null ||
|
|
130
|
+
Array.isArray(conn.pathFilter)) {
|
|
131
|
+
return `${p}pathFilter must be an object`;
|
|
132
|
+
}
|
|
133
|
+
const pf = conn.pathFilter;
|
|
134
|
+
for (const key of Object.keys(pf)) {
|
|
135
|
+
if (key !== "allow" && key !== "deny") {
|
|
136
|
+
return `${p}pathFilter.${key} is not a supported property`;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const key of ["allow", "deny"]) {
|
|
140
|
+
if (pf[key] !== undefined) {
|
|
141
|
+
if (!Array.isArray(pf[key])) {
|
|
142
|
+
return `${p}pathFilter.${key} must be an array of strings`;
|
|
143
|
+
}
|
|
144
|
+
const arr = pf[key];
|
|
145
|
+
for (let i = 0; i < arr.length; i++) {
|
|
146
|
+
if (typeof arr[i] !== "string" || arr[i].length === 0) {
|
|
147
|
+
return `${p}pathFilter.${key}[${i}] must be a non-empty string`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (conn.brotliQuality !== undefined) {
|
|
154
|
+
const q = conn.brotliQuality;
|
|
155
|
+
if (!Number.isInteger(q) || q < 0 || q > 11) {
|
|
156
|
+
return `${p}brotliQuality must be an integer between 0 and 11`;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (conn.pathPrecision !== undefined) {
|
|
160
|
+
if (typeof conn.pathPrecision !== "object" ||
|
|
161
|
+
conn.pathPrecision === null ||
|
|
162
|
+
Array.isArray(conn.pathPrecision)) {
|
|
163
|
+
return `${p}pathPrecision must be an object mapping path to integer 0..15`;
|
|
164
|
+
}
|
|
165
|
+
for (const [path, decimals] of Object.entries(conn.pathPrecision)) {
|
|
166
|
+
if (!Number.isInteger(decimals) || decimals < 0 || decimals > 15) {
|
|
167
|
+
return `${p}pathPrecision["${path}"] must be an integer between 0 and 15`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (conn.pathThrottle !== undefined) {
|
|
172
|
+
if (typeof conn.pathThrottle !== "object" ||
|
|
173
|
+
conn.pathThrottle === null ||
|
|
174
|
+
Array.isArray(conn.pathThrottle)) {
|
|
175
|
+
return `${p}pathThrottle must be an object mapping path to a rule`;
|
|
176
|
+
}
|
|
177
|
+
for (const [path, rule] of Object.entries(conn.pathThrottle)) {
|
|
178
|
+
if (typeof rule !== "object" || rule === null || Array.isArray(rule)) {
|
|
179
|
+
return `${p}pathThrottle["${path}"] must be an object`;
|
|
180
|
+
}
|
|
181
|
+
const r = rule;
|
|
182
|
+
for (const key of Object.keys(r)) {
|
|
183
|
+
if (key !== "minIntervalMs" && key !== "deadband") {
|
|
184
|
+
return `${p}pathThrottle["${path}"].${key} is not a supported property`;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (r.minIntervalMs !== undefined) {
|
|
188
|
+
if (!Number.isInteger(r.minIntervalMs) ||
|
|
189
|
+
r.minIntervalMs < 0 ||
|
|
190
|
+
r.minIntervalMs > 3600000) {
|
|
191
|
+
return `${p}pathThrottle["${path}"].minIntervalMs must be an integer between 0 and 3600000`;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (r.deadband !== undefined) {
|
|
195
|
+
if (!isFiniteNumber(r.deadband) || r.deadband < 0) {
|
|
196
|
+
return `${p}pathThrottle["${path}"].deadband must be a non-negative number`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
103
201
|
if (conn.usePathDictionary !== undefined && typeof conn.usePathDictionary !== "boolean") {
|
|
104
202
|
return `${p}usePathDictionary must be a boolean`;
|
|
105
203
|
}
|
package/lib/constants.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
exports.PACKET_INSPECTOR_MAX_CLIENTS = exports.PACKET_CAPTURE_MAX_PACKETS = exports.MONITORING_ALERT_COOLDOWN = exports.MONITORING_PATH_LATENCY_WINDOW = exports.MONITORING_RETRANSMIT_HISTORY_SIZE = exports.MONITORING_HEATMAP_BUCKET_DURATION = exports.MONITORING_HEATMAP_BUCKETS = exports.HELLO_PAYLOAD_MAX_BYTES = exports.SNAPSHOT_REPLAY_CHUNK_SIZE = exports.DELTA_SEND_RETRY_BACKOFF_MS = exports.DELTA_SEND_MAX_RETRIES = exports.SOURCE_SNAPSHOT_MAX_OBJECT_KEYS = exports.SOURCE_SNAPSHOT_MAX_ARRAY_LENGTH = exports.SOURCE_SNAPSHOT_MAX_DEPTH = exports.SOURCE_SNAPSHOT_MAX_STRING_LENGTH = exports.SOURCE_SNAPSHOT_MAX_KEY_LENGTH = exports.SOURCE_SNAPSHOT_MAX_PROVIDERS = exports.SOURCE_SNAPSHOT_INTERVAL_MS = exports.SOURCE_REGISTRY_TTL_MS = exports.SOURCE_REGISTRY_MAX_RECORDS = exports.OUTBOUND_DEDUPE_MAX_ENTRIES = exports.OUTBOUND_DEDUPE_CLEANUP_INTERVAL_MS = exports.SUPPRESSED_DUPLICATE_STATS_MAX_SIZE = void 0;
|
|
3
|
+
exports.BANDWIDTH_HISTORY_MAX = exports.METRICS_PUBLISH_INTERVAL = exports.MAX_PARSE_PAYLOAD_SIZE = exports.MAX_DECOMPRESSED_SIZE = exports.UDP_RATE_LIMIT_MAX_PACKETS = exports.UDP_RATE_LIMIT_WINDOW = exports.MAX_CLIENT_SESSIONS = exports.BONDING_RTT_EMA_ALPHA = exports.BONDING_HEALTH_WINDOW_SIZE = exports.BONDING_FAILBACK_LOSS_HYSTERESIS = exports.BONDING_FAILBACK_RTT_HYSTERESIS = exports.BONDING_HEARTBEAT_TIMEOUT = exports.BONDING_FAILBACK_DELAY = exports.BONDING_LOSS_THRESHOLD = exports.BONDING_RTT_THRESHOLD = exports.BONDING_HEALTH_CHECK_INTERVAL = exports.CONGESTION_DECREASE_FACTOR = exports.CONGESTION_INCREASE_FACTOR = exports.CONGESTION_RTT_MULTIPLIER_HIGH = exports.CONGESTION_LOSS_THRESHOLD_HIGH = exports.CONGESTION_LOSS_THRESHOLD_LOW = exports.CONGESTION_SMOOTHING_FACTOR = exports.CONGESTION_MAX_ADJUSTMENT = exports.CONGESTION_ADJUST_INTERVAL = exports.CONGESTION_TARGET_RTT = exports.CONGESTION_MAX_DELTA_TIMER = exports.CONGESTION_MIN_DELTA_TIMER = exports.RATE_LIMIT_MAX_REQUESTS = exports.RATE_LIMIT_WINDOW = exports.SMART_BATCH_MAX_DELTAS = exports.SMART_BATCH_MIN_DELTAS = exports.SMART_BATCH_INITIAL_ESTIMATE = exports.SMART_BATCH_SMOOTHING = exports.SMART_BATCH_SAFETY_MARGIN = exports.UDP_SEND_TIMEOUT_MS = exports.UDP_RETRY_DELAY = exports.UDP_RETRY_MAX = exports.BROTLI_QUALITY_MAX = exports.BROTLI_QUALITY_MIN = exports.BROTLI_QUALITY_HIGH = exports.MAX_SAFE_UDP_PAYLOAD = exports.WATCHER_RECOVERY_DELAY = exports.CONTENT_HASH_ALGORITHM = exports.FILE_WATCH_DEBOUNCE_DELAY = exports.MAX_DELTAS_PER_PACKET = exports.DELTA_BUFFER_DROP_RATIO = exports.MAX_DELTAS_BUFFER_SIZE = exports.MILLISECONDS_PER_MINUTE = exports.PING_TIMEOUT_BUFFER = exports.DEFAULT_DELTA_TIMER = void 0;
|
|
4
|
+
exports.PACKET_INSPECTOR_MAX_CLIENTS = exports.PACKET_CAPTURE_MAX_PACKETS = exports.MONITORING_ALERT_COOLDOWN = exports.MONITORING_PATH_LATENCY_WINDOW = exports.MONITORING_RETRANSMIT_HISTORY_SIZE = exports.MONITORING_HEATMAP_BUCKET_DURATION = exports.MONITORING_HEATMAP_BUCKETS = exports.HELLO_PAYLOAD_MAX_BYTES = exports.SNAPSHOT_REPLAY_CHUNK_SIZE = exports.DELTA_SEND_RETRY_BACKOFF_MS = exports.DELTA_SEND_MAX_RETRIES = exports.SOURCE_SNAPSHOT_MAX_OBJECT_KEYS = exports.SOURCE_SNAPSHOT_MAX_ARRAY_LENGTH = exports.SOURCE_SNAPSHOT_MAX_DEPTH = exports.SOURCE_SNAPSHOT_MAX_STRING_LENGTH = exports.SOURCE_SNAPSHOT_MAX_KEY_LENGTH = exports.SOURCE_SNAPSHOT_MAX_PROVIDERS = exports.SOURCE_SNAPSHOT_INTERVAL_MS = exports.SOURCE_REGISTRY_TTL_MS = exports.SOURCE_REGISTRY_MAX_RECORDS = exports.OUTBOUND_DEDUPE_MAX_ENTRIES = exports.OUTBOUND_DEDUPE_CLEANUP_INTERVAL_MS = exports.SUPPRESSED_DUPLICATE_STATS_MAX_SIZE = exports.OUTBOUND_DUPLICATE_SUPPRESS_MS = exports.PATH_THROTTLE_STATE_MAX = exports.VALUE_DEDUP_CACHE_MAX = exports.PATH_STATS_MAX_SIZE = void 0;
|
|
5
5
|
exports.calculateMaxDeltasPerBatch = calculateMaxDeltasPerBatch;
|
|
6
|
+
exports.clampBytesPerDeltaSample = clampBytesPerDeltaSample;
|
|
6
7
|
// Delta and timing
|
|
7
8
|
exports.DEFAULT_DELTA_TIMER = 1000; // milliseconds
|
|
8
9
|
exports.PING_TIMEOUT_BUFFER = 10000; // milliseconds - extra buffer for ping timeout
|
|
@@ -17,6 +18,8 @@ exports.WATCHER_RECOVERY_DELAY = 5000; // milliseconds
|
|
|
17
18
|
// UDP and network
|
|
18
19
|
exports.MAX_SAFE_UDP_PAYLOAD = 1400; // Maximum safe UDP payload size (avoid fragmentation)
|
|
19
20
|
exports.BROTLI_QUALITY_HIGH = 6; // Balanced quality: ~90% of max compression at ~10% of the CPU cost
|
|
21
|
+
exports.BROTLI_QUALITY_MIN = 0; // Fastest, lowest ratio
|
|
22
|
+
exports.BROTLI_QUALITY_MAX = 11; // Highest ratio, ~3-5× more CPU than quality 6
|
|
20
23
|
exports.UDP_RETRY_MAX = 3; // Maximum UDP send retries
|
|
21
24
|
exports.UDP_RETRY_DELAY = 100; // milliseconds - base retry delay
|
|
22
25
|
exports.UDP_SEND_TIMEOUT_MS = 5000; // Maximum ms to wait for socket.send() callback
|
|
@@ -66,6 +69,13 @@ exports.MAX_PARSE_PAYLOAD_SIZE = 512 * 1024; // 512 KB
|
|
|
66
69
|
exports.METRICS_PUBLISH_INTERVAL = 1000; // Interval (ms) for publishing metrics to Signal K
|
|
67
70
|
exports.BANDWIDTH_HISTORY_MAX = 60; // Keep 60 data points (5 minutes at 5s intervals)
|
|
68
71
|
exports.PATH_STATS_MAX_SIZE = 500; // Max tracked paths in pathStats Map (prevent unbounded growth)
|
|
72
|
+
// Outbound bandwidth-optimization per-(context,path) caches. Bounded with
|
|
73
|
+
// LRU eviction so a link that sees many distinct paths (e.g. N2K source-address
|
|
74
|
+
// churn or many vessel contexts) cannot grow these maps without limit. The
|
|
75
|
+
// cap is far above the unique-path count of a normal boat, so eviction is a
|
|
76
|
+
// safety valve rather than a routine event.
|
|
77
|
+
exports.VALUE_DEDUP_CACHE_MAX = 10000; // Max (context,path) entries in the dedup cache
|
|
78
|
+
exports.PATH_THROTTLE_STATE_MAX = 10000; // Max (context,path) entries in the throttle state
|
|
69
79
|
// Outbound delta deduplication. signalk-server's subscriptionmanager can
|
|
70
80
|
// deliver the same cached/live pair about one fixed-policy window apart;
|
|
71
81
|
// 1500 ms catches that without suppressing legitimate periodic resends.
|
|
@@ -113,3 +123,21 @@ function calculateMaxDeltasPerBatch(avgBytes) {
|
|
|
113
123
|
const raw = Math.floor((exports.MAX_SAFE_UDP_PAYLOAD * exports.SMART_BATCH_SAFETY_MARGIN) / avgBytes);
|
|
114
124
|
return Math.max(exports.SMART_BATCH_MIN_DELTAS, Math.min(exports.SMART_BATCH_MAX_DELTAS, raw));
|
|
115
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Clamp a single bytes-per-delta observation before it feeds the smart-batch
|
|
128
|
+
* EMA. Aggressive per-path throttling/dedup can collapse a batch to a single
|
|
129
|
+
* delta whose packet is dominated by fixed overhead (compression header,
|
|
130
|
+
* crypto IV + auth tag) or that is itself oversized. Without a bound, one such
|
|
131
|
+
* outlier sample can drag the rolling average to an extreme and keep the
|
|
132
|
+
* batcher mis-sized long after the burst passes. Clamping each sample to
|
|
133
|
+
* [1, MAX_SAFE_UDP_PAYLOAD] keeps the EMA stable while still letting it track
|
|
134
|
+
* genuine payload-size shifts.
|
|
135
|
+
*
|
|
136
|
+
* @param bytesPerDelta - Raw observation (packet bytes / deltas in packet)
|
|
137
|
+
* @returns Sample clamped to a sane range
|
|
138
|
+
*/
|
|
139
|
+
function clampBytesPerDeltaSample(bytesPerDelta) {
|
|
140
|
+
if (!Number.isFinite(bytesPerDelta) || bytesPerDelta < 1)
|
|
141
|
+
return 1;
|
|
142
|
+
return Math.min(exports.MAX_SAFE_UDP_PAYLOAD, bytesPerDelta);
|
|
143
|
+
}
|