ros-mobile-bridge 0.1.2 → 0.1.4
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/CHANGELOG.md +37 -0
- package/README.md +29 -0
- package/ROADMAP.md +27 -23
- package/SECURITY.md +8 -0
- package/dist/index.cjs +277 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +194 -12
- package/dist/index.d.ts +194 -12
- package/dist/index.mjs +276 -62
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -478,6 +478,16 @@ function jsonSchemaToTemplate(schema) {
|
|
|
478
478
|
return null;
|
|
479
479
|
}
|
|
480
480
|
|
|
481
|
+
// src/schemaName.ts
|
|
482
|
+
var INTERFACE_KIND = /^([^/]+)\/(?:msg|srv|action)\/([^/]+)$/;
|
|
483
|
+
function stripInterfaceKind(name) {
|
|
484
|
+
const m = INTERFACE_KIND.exec(name);
|
|
485
|
+
return m ? `${m[1]}/${m[2]}` : name;
|
|
486
|
+
}
|
|
487
|
+
function matchesSchema(a, b) {
|
|
488
|
+
return stripInterfaceKind(a) === stripInterfaceKind(b);
|
|
489
|
+
}
|
|
490
|
+
|
|
481
491
|
// src/builtinSchemas.ts
|
|
482
492
|
var SEP = "\n================================================================================\n";
|
|
483
493
|
var MSG_PARAMETER_VALUE = `MSG: rcl_interfaces/ParameterValue
|
|
@@ -581,7 +591,13 @@ var BUNDLED = {
|
|
|
581
591
|
}
|
|
582
592
|
};
|
|
583
593
|
function getBundledServiceSchema(serviceType) {
|
|
584
|
-
|
|
594
|
+
const exact = BUNDLED[serviceType];
|
|
595
|
+
if (exact) return exact;
|
|
596
|
+
const target = stripInterfaceKind(serviceType);
|
|
597
|
+
for (const key of Object.keys(BUNDLED)) {
|
|
598
|
+
if (stripInterfaceKind(key) === target) return BUNDLED[key] ?? null;
|
|
599
|
+
}
|
|
600
|
+
return null;
|
|
585
601
|
}
|
|
586
602
|
|
|
587
603
|
// src/FoxgloveClient.ts
|
|
@@ -591,13 +607,30 @@ var NOOP_LOGGER = { log() {
|
|
|
591
607
|
} };
|
|
592
608
|
var TEXT_ENCODER = new TextEncoder();
|
|
593
609
|
var TEXT_DECODER = new TextDecoder();
|
|
610
|
+
var B64_LOOKUP = (() => {
|
|
611
|
+
const table = new Int16Array(256).fill(-1);
|
|
612
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
613
|
+
for (let i = 0; i < chars.length; i++) table[chars.charCodeAt(i)] = i;
|
|
614
|
+
return table;
|
|
615
|
+
})();
|
|
594
616
|
function base64ToUint8(b64) {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
617
|
+
let len = b64.length;
|
|
618
|
+
while (len > 0 && b64.charCodeAt(len - 1) === 61) len--;
|
|
619
|
+
const out = new Uint8Array(len * 3 >> 2);
|
|
620
|
+
let acc = 0;
|
|
621
|
+
let bits = 0;
|
|
622
|
+
let o = 0;
|
|
623
|
+
for (let i = 0; i < len; i++) {
|
|
624
|
+
const v = B64_LOOKUP[b64.charCodeAt(i)] ?? -1;
|
|
625
|
+
if (v < 0) continue;
|
|
626
|
+
acc = acc << 6 | v;
|
|
627
|
+
bits += 6;
|
|
628
|
+
if (bits >= 8) {
|
|
629
|
+
bits -= 8;
|
|
630
|
+
out[o++] = acc >>> bits & 255;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return o === out.length ? out : out.subarray(0, o);
|
|
601
634
|
}
|
|
602
635
|
var CDR_LE_HEADER = new Uint8Array([0, 1, 0, 0]);
|
|
603
636
|
function isEmptyRequest(request) {
|
|
@@ -789,6 +822,7 @@ var _FoxgloveClient = class _FoxgloveClient {
|
|
|
789
822
|
subscribe(topic, onMessage, options) {
|
|
790
823
|
const userMinIntervalMs = options?.maxFrequency && options.maxFrequency > 0 ? 1e3 / options.maxFrequency : void 0;
|
|
791
824
|
const disableAdaptive = options?.disableAdaptive ?? false;
|
|
825
|
+
const dispatchMode = options?.dispatchMode ?? "immediate";
|
|
792
826
|
const existingSubId = this.topicToSubscriptionId.get(topic);
|
|
793
827
|
if (existingSubId !== void 0) {
|
|
794
828
|
const sub = this.subscriptions.get(existingSubId);
|
|
@@ -796,9 +830,14 @@ var _FoxgloveClient = class _FoxgloveClient {
|
|
|
796
830
|
sub.callbacks.set(onMessage, {
|
|
797
831
|
userMinIntervalMs,
|
|
798
832
|
disableAdaptive,
|
|
799
|
-
lastDeliveredAt: 0
|
|
833
|
+
lastDeliveredAt: 0,
|
|
834
|
+
dispatchMode,
|
|
835
|
+
pending: null,
|
|
836
|
+
drainTimer: null
|
|
800
837
|
});
|
|
801
838
|
return () => {
|
|
839
|
+
const entry = sub.callbacks.get(onMessage);
|
|
840
|
+
if (entry) this.cancelDrain(entry);
|
|
802
841
|
sub.callbacks.delete(onMessage);
|
|
803
842
|
if (sub.callbacks.size === 0) {
|
|
804
843
|
this.unsubscribeTopic(topic, existingSubId);
|
|
@@ -819,7 +858,10 @@ var _FoxgloveClient = class _FoxgloveClient {
|
|
|
819
858
|
callbacks.set(onMessage, {
|
|
820
859
|
userMinIntervalMs,
|
|
821
860
|
disableAdaptive,
|
|
822
|
-
lastDeliveredAt: 0
|
|
861
|
+
lastDeliveredAt: 0,
|
|
862
|
+
dispatchMode,
|
|
863
|
+
pending: null,
|
|
864
|
+
drainTimer: null
|
|
823
865
|
});
|
|
824
866
|
const breaker = new CircuitBreaker({
|
|
825
867
|
...DEFAULT_BREAKER_CONFIG,
|
|
@@ -827,6 +869,7 @@ var _FoxgloveClient = class _FoxgloveClient {
|
|
|
827
869
|
const sub = this.subscriptions.get(subscriptionId);
|
|
828
870
|
if (!sub) return;
|
|
829
871
|
sub.isPaused = newState === "tripped_auto" || newState === "tripped_manual";
|
|
872
|
+
if (sub.isPaused) this.cancelAllDrains(sub);
|
|
830
873
|
if (newState === "tripped_auto") {
|
|
831
874
|
if (this.ws && this.status === "connected") {
|
|
832
875
|
this.sendJson({ op: "unsubscribe", subscriptionIds: [subscriptionId] });
|
|
@@ -892,6 +935,8 @@ var _FoxgloveClient = class _FoxgloveClient {
|
|
|
892
935
|
subscriptions: [{ id: subscriptionId, channelId }]
|
|
893
936
|
});
|
|
894
937
|
return () => {
|
|
938
|
+
const entry = callbacks.get(onMessage);
|
|
939
|
+
if (entry) this.cancelDrain(entry);
|
|
895
940
|
callbacks.delete(onMessage);
|
|
896
941
|
if (callbacks.size === 0) {
|
|
897
942
|
this.unsubscribeTopic(topic, subscriptionId);
|
|
@@ -1184,7 +1229,6 @@ var _FoxgloveClient = class _FoxgloveClient {
|
|
|
1184
1229
|
this.log(
|
|
1185
1230
|
`WebSocket handshake successful (protocol: ${negotiated}), waiting for serverInfo...`
|
|
1186
1231
|
);
|
|
1187
|
-
this.clearConnectionTimeout();
|
|
1188
1232
|
};
|
|
1189
1233
|
this.ws.onmessage = (event) => {
|
|
1190
1234
|
this.handleWsMessage(event);
|
|
@@ -1214,10 +1258,14 @@ var _FoxgloveClient = class _FoxgloveClient {
|
|
|
1214
1258
|
if (this.controlOutbox.length > 0) {
|
|
1215
1259
|
this.flushControlOutbox();
|
|
1216
1260
|
}
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1261
|
+
try {
|
|
1262
|
+
if (typeof event.data === "string") {
|
|
1263
|
+
this.handleJsonMessage(event.data);
|
|
1264
|
+
} else if (event.data instanceof ArrayBuffer) {
|
|
1265
|
+
this.handleBinaryMessage(event.data);
|
|
1266
|
+
}
|
|
1267
|
+
} catch (err) {
|
|
1268
|
+
this.logger.error("[FoxgloveClient] Error handling inbound message:", err);
|
|
1221
1269
|
}
|
|
1222
1270
|
}
|
|
1223
1271
|
handleJsonMessage(raw) {
|
|
@@ -1306,52 +1354,110 @@ var _FoxgloveClient = class _FoxgloveClient {
|
|
|
1306
1354
|
}
|
|
1307
1355
|
}
|
|
1308
1356
|
if (deliverTo.length === 0) return;
|
|
1309
|
-
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1312
|
-
|
|
1357
|
+
let parsed = null;
|
|
1358
|
+
for (const [cb, entry] of deliverTo) {
|
|
1359
|
+
entry.lastDeliveredAt = now;
|
|
1360
|
+
if (entry.dispatchMode === "latest-only") {
|
|
1361
|
+
entry.pending = { payload: new Uint8Array(payload), sec, nsec };
|
|
1362
|
+
if (entry.drainTimer === null) {
|
|
1363
|
+
entry.drainTimer = setTimeout(
|
|
1364
|
+
() => this.drainLatestOnly(subscriptionId, cb, entry),
|
|
1365
|
+
0
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
if (parsed === null) {
|
|
1371
|
+
const channelInfo = this.channels.get(sub.channelId);
|
|
1372
|
+
const encoding = channelInfo?.encoding ?? "json";
|
|
1373
|
+
parsed = {
|
|
1374
|
+
topic: sub.topic,
|
|
1375
|
+
schemaName: channelInfo?.schemaName ?? "",
|
|
1376
|
+
encoding: encoding === "json" ? "json" : "cdr",
|
|
1377
|
+
data: this.decodePayload(payload, subscriptionId, encoding),
|
|
1378
|
+
receiveTime: { sec, nsec },
|
|
1379
|
+
byteSize: payload.byteLength
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
try {
|
|
1383
|
+
cb(parsed);
|
|
1384
|
+
} catch (err) {
|
|
1385
|
+
this.logger.error("[FoxgloveClient] Subscriber callback error:", err);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Decode a `messageData` payload to its delivered shape: a parsed object when
|
|
1391
|
+
* a CDR reader or JSON decode succeeds, the raw bytes otherwise. Shared by
|
|
1392
|
+
* the synchronous `immediate` path and the deferred `latest-only` drain.
|
|
1393
|
+
*/
|
|
1394
|
+
decodePayload(payload, subscriptionId, encoding) {
|
|
1313
1395
|
if (encoding === "json") {
|
|
1314
1396
|
try {
|
|
1315
|
-
|
|
1316
|
-
data = JSON.parse(text);
|
|
1397
|
+
return JSON.parse(TEXT_DECODER.decode(payload));
|
|
1317
1398
|
} catch {
|
|
1318
|
-
|
|
1399
|
+
return payload;
|
|
1319
1400
|
}
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
}
|
|
1328
|
-
} else {
|
|
1329
|
-
data = payload;
|
|
1401
|
+
}
|
|
1402
|
+
const reader = this.messageReaders.get(subscriptionId);
|
|
1403
|
+
if (reader) {
|
|
1404
|
+
try {
|
|
1405
|
+
return reader.readMessage(payload);
|
|
1406
|
+
} catch {
|
|
1407
|
+
return payload;
|
|
1330
1408
|
}
|
|
1331
1409
|
}
|
|
1410
|
+
return payload;
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Drain one `latest-only` callback's pending payload: parse the survivor and
|
|
1414
|
+
* deliver it. Cleared state (`pending`, `drainTimer`) is reset *before* the
|
|
1415
|
+
* callback runs, so a throwing callback never wedges future delivery — the
|
|
1416
|
+
* next arrival re-arms normally. Bails if the subscription was torn down or
|
|
1417
|
+
* paused while the drain was armed (no post-teardown delivery).
|
|
1418
|
+
*/
|
|
1419
|
+
drainLatestOnly(subscriptionId, cb, entry) {
|
|
1420
|
+
entry.drainTimer = null;
|
|
1421
|
+
const pending = entry.pending;
|
|
1422
|
+
entry.pending = null;
|
|
1423
|
+
if (!pending) return;
|
|
1424
|
+
const sub = this.subscriptions.get(subscriptionId);
|
|
1425
|
+
if (!sub || sub.isPaused) return;
|
|
1426
|
+
const channelInfo = this.channels.get(sub.channelId);
|
|
1427
|
+
const encoding = channelInfo?.encoding ?? "json";
|
|
1332
1428
|
const rosMsg = {
|
|
1333
1429
|
topic: sub.topic,
|
|
1334
|
-
schemaName,
|
|
1430
|
+
schemaName: channelInfo?.schemaName ?? "",
|
|
1335
1431
|
encoding: encoding === "json" ? "json" : "cdr",
|
|
1336
|
-
data,
|
|
1337
|
-
receiveTime: { sec, nsec },
|
|
1338
|
-
byteSize: payload.byteLength
|
|
1432
|
+
data: this.decodePayload(pending.payload, subscriptionId, encoding),
|
|
1433
|
+
receiveTime: { sec: pending.sec, nsec: pending.nsec },
|
|
1434
|
+
byteSize: pending.payload.byteLength
|
|
1339
1435
|
};
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1436
|
+
try {
|
|
1437
|
+
cb(rosMsg);
|
|
1438
|
+
} catch (err) {
|
|
1439
|
+
this.logger.error("[FoxgloveClient] Subscriber callback error:", err);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
/** Cancel one callback's armed drain and drop its pending payload. */
|
|
1443
|
+
cancelDrain(entry) {
|
|
1444
|
+
if (entry.drainTimer !== null) {
|
|
1445
|
+
clearTimeout(entry.drainTimer);
|
|
1446
|
+
entry.drainTimer = null;
|
|
1347
1447
|
}
|
|
1448
|
+
entry.pending = null;
|
|
1449
|
+
}
|
|
1450
|
+
/** Cancel every armed drain on a subscription (teardown / pause). */
|
|
1451
|
+
cancelAllDrains(sub) {
|
|
1452
|
+
for (const entry of sub.callbacks.values()) this.cancelDrain(entry);
|
|
1348
1453
|
}
|
|
1349
1454
|
handleServerInfo(info) {
|
|
1350
1455
|
this.log(`Received serverInfo: ${info.name} (sessionId: ${info.sessionId ?? "none"})`);
|
|
1351
1456
|
this.serverInfoReceived = true;
|
|
1352
1457
|
}
|
|
1353
1458
|
handleAdvertise(msg) {
|
|
1354
|
-
|
|
1459
|
+
const channels = Array.isArray(msg.channels) ? msg.channels : [];
|
|
1460
|
+
for (const ch of channels) {
|
|
1355
1461
|
this.channels.set(ch.id, ch);
|
|
1356
1462
|
this.topicToChannelId.set(ch.topic, ch.id);
|
|
1357
1463
|
this.log(
|
|
@@ -1359,7 +1465,7 @@ var _FoxgloveClient = class _FoxgloveClient {
|
|
|
1359
1465
|
);
|
|
1360
1466
|
}
|
|
1361
1467
|
if (this.connectResolve && this.serverInfoReceived) {
|
|
1362
|
-
this.log(`Connection established with ${
|
|
1468
|
+
this.log(`Connection established with ${channels.length} initial topics.`);
|
|
1363
1469
|
this.clearConnectionTimeout();
|
|
1364
1470
|
this.reconnectAttempts = 0;
|
|
1365
1471
|
this.setStatus("connected");
|
|
@@ -1371,7 +1477,8 @@ var _FoxgloveClient = class _FoxgloveClient {
|
|
|
1371
1477
|
}
|
|
1372
1478
|
}
|
|
1373
1479
|
handleUnadvertise(msg) {
|
|
1374
|
-
|
|
1480
|
+
const channelIds = Array.isArray(msg.channelIds) ? msg.channelIds : [];
|
|
1481
|
+
for (const id of channelIds) {
|
|
1375
1482
|
const ch = this.channels.get(id);
|
|
1376
1483
|
if (ch) {
|
|
1377
1484
|
this.topicToChannelId.delete(ch.topic);
|
|
@@ -1381,7 +1488,8 @@ var _FoxgloveClient = class _FoxgloveClient {
|
|
|
1381
1488
|
this.notifyTopicsChanged();
|
|
1382
1489
|
}
|
|
1383
1490
|
handleAdvertiseServices(msg) {
|
|
1384
|
-
|
|
1491
|
+
const services = Array.isArray(msg.services) ? msg.services : [];
|
|
1492
|
+
for (const svc of services) {
|
|
1385
1493
|
this.availableServices.set(svc.name, svc);
|
|
1386
1494
|
}
|
|
1387
1495
|
this.notifyServicesChanged();
|
|
@@ -1505,6 +1613,8 @@ var _FoxgloveClient = class _FoxgloveClient {
|
|
|
1505
1613
|
// ── Private: reconnection ────────────────────────────────────────────────
|
|
1506
1614
|
handleConnectionError(error) {
|
|
1507
1615
|
this.clearConnectionTimeout();
|
|
1616
|
+
const wasConnected = this.status === "connected";
|
|
1617
|
+
const reconnecting = this.reconnectAttempts > 0;
|
|
1508
1618
|
if (this.connectReject) {
|
|
1509
1619
|
this.connectReject(error);
|
|
1510
1620
|
this.connectResolve = null;
|
|
@@ -1512,10 +1622,17 @@ var _FoxgloveClient = class _FoxgloveClient {
|
|
|
1512
1622
|
}
|
|
1513
1623
|
this.setStatus("error");
|
|
1514
1624
|
this.cleanup();
|
|
1515
|
-
|
|
1625
|
+
if (wasConnected || reconnecting) {
|
|
1626
|
+
this.scheduleReconnect();
|
|
1627
|
+
}
|
|
1516
1628
|
}
|
|
1517
1629
|
handleClose(_code, _reason) {
|
|
1518
1630
|
const wasConnected = this.status === "connected";
|
|
1631
|
+
if (this.connectReject) {
|
|
1632
|
+
this.connectReject(new Error("Connection closed before the handshake completed"));
|
|
1633
|
+
this.connectResolve = null;
|
|
1634
|
+
this.connectReject = null;
|
|
1635
|
+
}
|
|
1519
1636
|
if (wasConnected && this.hasPublishedTwist && !this.intentionalDisconnect) {
|
|
1520
1637
|
this.safePublishZeroTwist();
|
|
1521
1638
|
}
|
|
@@ -1553,6 +1670,7 @@ var _FoxgloveClient = class _FoxgloveClient {
|
|
|
1553
1670
|
// ── Private: unsubscribe ─────────────────────────────────────────────────
|
|
1554
1671
|
unsubscribeTopic(topic, subscriptionId) {
|
|
1555
1672
|
const sub = this.subscriptions.get(subscriptionId);
|
|
1673
|
+
if (sub) this.cancelAllDrains(sub);
|
|
1556
1674
|
sub?.breaker.destroy();
|
|
1557
1675
|
this.subscriptions.delete(subscriptionId);
|
|
1558
1676
|
this.topicToSubscriptionId.delete(topic);
|
|
@@ -1649,6 +1767,10 @@ var _FoxgloveClient = class _FoxgloveClient {
|
|
|
1649
1767
|
}
|
|
1650
1768
|
this.ws = null;
|
|
1651
1769
|
}
|
|
1770
|
+
for (const sub of this.subscriptions.values()) {
|
|
1771
|
+
this.cancelAllDrains(sub);
|
|
1772
|
+
sub.breaker.destroy();
|
|
1773
|
+
}
|
|
1652
1774
|
this.channels.clear();
|
|
1653
1775
|
this.topicToChannelId.clear();
|
|
1654
1776
|
this.subscriptions.clear();
|
|
@@ -1839,14 +1961,20 @@ var _RosbridgeClient = class _RosbridgeClient {
|
|
|
1839
1961
|
}
|
|
1840
1962
|
const userMinIntervalMs = options?.maxFrequency && options.maxFrequency > 0 ? 1e3 / options.maxFrequency : void 0;
|
|
1841
1963
|
const disableAdaptive = options?.disableAdaptive ?? false;
|
|
1964
|
+
const dispatchMode = options?.dispatchMode ?? "immediate";
|
|
1842
1965
|
const existing = this.activeSubscriptions.get(topic);
|
|
1843
1966
|
if (existing) {
|
|
1844
1967
|
existing.callbacks.set(onMessage, {
|
|
1845
1968
|
userMinIntervalMs,
|
|
1846
1969
|
disableAdaptive,
|
|
1847
|
-
lastDeliveredAt: 0
|
|
1970
|
+
lastDeliveredAt: 0,
|
|
1971
|
+
dispatchMode,
|
|
1972
|
+
pending: null,
|
|
1973
|
+
drainTimer: null
|
|
1848
1974
|
});
|
|
1849
1975
|
return () => {
|
|
1976
|
+
const entry = existing.callbacks.get(onMessage);
|
|
1977
|
+
if (entry) this.cancelDrain(entry);
|
|
1850
1978
|
existing.callbacks.delete(onMessage);
|
|
1851
1979
|
if (existing.callbacks.size === 0) {
|
|
1852
1980
|
this.unsubscribeTopic(topic);
|
|
@@ -1862,7 +1990,10 @@ var _RosbridgeClient = class _RosbridgeClient {
|
|
|
1862
1990
|
callbacks.set(onMessage, {
|
|
1863
1991
|
userMinIntervalMs,
|
|
1864
1992
|
disableAdaptive,
|
|
1865
|
-
lastDeliveredAt: 0
|
|
1993
|
+
lastDeliveredAt: 0,
|
|
1994
|
+
dispatchMode,
|
|
1995
|
+
pending: null,
|
|
1996
|
+
drainTimer: null
|
|
1866
1997
|
});
|
|
1867
1998
|
const breaker = new CircuitBreaker({
|
|
1868
1999
|
...DEFAULT_BREAKER_CONFIG,
|
|
@@ -1870,6 +2001,7 @@ var _RosbridgeClient = class _RosbridgeClient {
|
|
|
1870
2001
|
const sub = this.activeSubscriptions.get(topic);
|
|
1871
2002
|
if (!sub) return;
|
|
1872
2003
|
sub.isPaused = newState === "tripped_auto" || newState === "tripped_manual";
|
|
2004
|
+
if (sub.isPaused) this.cancelAllDrains(sub);
|
|
1873
2005
|
if (newState === "tripped_auto") {
|
|
1874
2006
|
if (this.ws && this.status === "connected") {
|
|
1875
2007
|
this.send({ op: "unsubscribe", topic });
|
|
@@ -1919,6 +2051,8 @@ var _RosbridgeClient = class _RosbridgeClient {
|
|
|
1919
2051
|
queue_length: 1
|
|
1920
2052
|
});
|
|
1921
2053
|
return () => {
|
|
2054
|
+
const entry = callbacks.get(onMessage);
|
|
2055
|
+
if (entry) this.cancelDrain(entry);
|
|
1922
2056
|
callbacks.delete(onMessage);
|
|
1923
2057
|
if (callbacks.size === 0) {
|
|
1924
2058
|
this.unsubscribeTopic(topic);
|
|
@@ -2099,10 +2233,11 @@ var _RosbridgeClient = class _RosbridgeClient {
|
|
|
2099
2233
|
this.ws = new WebSocket(this.url);
|
|
2100
2234
|
this.connectionTimeoutTimer = setTimeout(() => {
|
|
2101
2235
|
this.log(`Connection timeout after ${CONNECTION_TIMEOUT_MS2}ms`);
|
|
2236
|
+
const reconnecting = this.reconnectAttempts > 0;
|
|
2102
2237
|
reject(new Error(`Connection timeout after ${CONNECTION_TIMEOUT_MS2}ms`));
|
|
2103
2238
|
this.cleanup();
|
|
2104
2239
|
this.setStatus("error");
|
|
2105
|
-
this.scheduleReconnect();
|
|
2240
|
+
if (reconnecting) this.scheduleReconnect();
|
|
2106
2241
|
}, CONNECTION_TIMEOUT_MS2);
|
|
2107
2242
|
this.ws.onopen = () => {
|
|
2108
2243
|
this.clearConnectionTimeout();
|
|
@@ -2118,11 +2253,12 @@ var _RosbridgeClient = class _RosbridgeClient {
|
|
|
2118
2253
|
this.log(`Rosbridge error: ${detail}`);
|
|
2119
2254
|
this.logger.error("[RosbridgeClient] Error:", event);
|
|
2120
2255
|
if (this.status === "connecting") {
|
|
2256
|
+
const reconnecting = this.reconnectAttempts > 0;
|
|
2121
2257
|
this.clearConnectionTimeout();
|
|
2122
2258
|
reject(new Error(`Rosbridge error: ${detail}`));
|
|
2123
2259
|
this.cleanup();
|
|
2124
2260
|
this.setStatus("error");
|
|
2125
|
-
this.scheduleReconnect();
|
|
2261
|
+
if (reconnecting) this.scheduleReconnect();
|
|
2126
2262
|
}
|
|
2127
2263
|
};
|
|
2128
2264
|
this.ws.onclose = () => {
|
|
@@ -2218,19 +2354,26 @@ var _RosbridgeClient = class _RosbridgeClient {
|
|
|
2218
2354
|
recordBytes(sub.bandwidth, now, byteSize, mode);
|
|
2219
2355
|
return true;
|
|
2220
2356
|
}
|
|
2221
|
-
let
|
|
2222
|
-
for (const entry of sub.callbacks
|
|
2357
|
+
let anyImmediateWantsThis = false;
|
|
2358
|
+
for (const [cb, entry] of sub.callbacks) {
|
|
2223
2359
|
const interval = effectiveMinInterval(
|
|
2224
2360
|
entry.userMinIntervalMs,
|
|
2225
2361
|
entry.disableAdaptive,
|
|
2226
2362
|
sub.bandwidth
|
|
2227
2363
|
);
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2364
|
+
const eligible = interval <= 0 || now - entry.lastDeliveredAt >= interval;
|
|
2365
|
+
if (!eligible) continue;
|
|
2366
|
+
if (entry.dispatchMode === "latest-only") {
|
|
2367
|
+
entry.lastDeliveredAt = now;
|
|
2368
|
+
entry.pending = { raw: data, receivedAt: now };
|
|
2369
|
+
if (entry.drainTimer === null) {
|
|
2370
|
+
entry.drainTimer = setTimeout(() => this.drainLatestOnly(topic, cb, entry), 0);
|
|
2371
|
+
}
|
|
2372
|
+
} else {
|
|
2373
|
+
anyImmediateWantsThis = true;
|
|
2231
2374
|
}
|
|
2232
2375
|
}
|
|
2233
|
-
if (
|
|
2376
|
+
if (anyImmediateWantsThis) return false;
|
|
2234
2377
|
recordBytes(sub.bandwidth, now, byteSize, mode);
|
|
2235
2378
|
sub.breaker.recordObservation(now, sub.bandwidth.bytesPerSec, getMaxLagMs());
|
|
2236
2379
|
return true;
|
|
@@ -2246,6 +2389,7 @@ var _RosbridgeClient = class _RosbridgeClient {
|
|
|
2246
2389
|
sub.breaker.recordObservation(now, sub.bandwidth.bytesPerSec, getMaxLagMs());
|
|
2247
2390
|
const deliverTo = [];
|
|
2248
2391
|
for (const [cb, entry] of sub.callbacks) {
|
|
2392
|
+
if (entry.dispatchMode === "latest-only") continue;
|
|
2249
2393
|
const interval = effectiveMinInterval(
|
|
2250
2394
|
entry.userMinIntervalMs,
|
|
2251
2395
|
entry.disableAdaptive,
|
|
@@ -2276,6 +2420,55 @@ var _RosbridgeClient = class _RosbridgeClient {
|
|
|
2276
2420
|
}
|
|
2277
2421
|
}
|
|
2278
2422
|
}
|
|
2423
|
+
/**
|
|
2424
|
+
* Drain one `latest-only` callback's pending frame: parse the survivor and
|
|
2425
|
+
* deliver it. State (`pending`, `drainTimer`) is cleared *before* the
|
|
2426
|
+
* callback runs, so a throwing callback never wedges future delivery. Bails
|
|
2427
|
+
* if the subscription was torn down or paused while the drain was armed (no
|
|
2428
|
+
* post-teardown delivery).
|
|
2429
|
+
*/
|
|
2430
|
+
drainLatestOnly(topic, cb, entry) {
|
|
2431
|
+
entry.drainTimer = null;
|
|
2432
|
+
const pending = entry.pending;
|
|
2433
|
+
entry.pending = null;
|
|
2434
|
+
if (!pending) return;
|
|
2435
|
+
const sub = this.activeSubscriptions.get(topic);
|
|
2436
|
+
if (!sub || sub.isPaused) return;
|
|
2437
|
+
let parsed;
|
|
2438
|
+
try {
|
|
2439
|
+
parsed = JSON.parse(pending.raw);
|
|
2440
|
+
} catch {
|
|
2441
|
+
return;
|
|
2442
|
+
}
|
|
2443
|
+
const rosMsg = {
|
|
2444
|
+
topic,
|
|
2445
|
+
schemaName: sub.schemaName,
|
|
2446
|
+
encoding: "json",
|
|
2447
|
+
data: parsed.msg ?? {},
|
|
2448
|
+
receiveTime: {
|
|
2449
|
+
sec: Math.floor(pending.receivedAt / 1e3),
|
|
2450
|
+
nsec: pending.receivedAt % 1e3 * 1e6
|
|
2451
|
+
},
|
|
2452
|
+
byteSize: pending.raw.length
|
|
2453
|
+
};
|
|
2454
|
+
try {
|
|
2455
|
+
cb(rosMsg);
|
|
2456
|
+
} catch (err) {
|
|
2457
|
+
this.logger.error("[RosbridgeClient] Subscriber callback error:", err);
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
/** Cancel one callback's armed drain and drop its pending frame. */
|
|
2461
|
+
cancelDrain(entry) {
|
|
2462
|
+
if (entry.drainTimer !== null) {
|
|
2463
|
+
clearTimeout(entry.drainTimer);
|
|
2464
|
+
entry.drainTimer = null;
|
|
2465
|
+
}
|
|
2466
|
+
entry.pending = null;
|
|
2467
|
+
}
|
|
2468
|
+
/** Cancel every armed drain on a subscription (teardown / pause). */
|
|
2469
|
+
cancelAllDrains(sub) {
|
|
2470
|
+
for (const entry of sub.callbacks.values()) this.cancelDrain(entry);
|
|
2471
|
+
}
|
|
2279
2472
|
handleServiceResponse(msg) {
|
|
2280
2473
|
const id = msg.id;
|
|
2281
2474
|
const pending = this.pendingServiceCalls.get(id);
|
|
@@ -2294,6 +2487,7 @@ var _RosbridgeClient = class _RosbridgeClient {
|
|
|
2294
2487
|
// ── Private: topic management ──────────────────────────────────────────
|
|
2295
2488
|
unsubscribeTopic(topic) {
|
|
2296
2489
|
const sub = this.activeSubscriptions.get(topic);
|
|
2490
|
+
if (sub) this.cancelAllDrains(sub);
|
|
2297
2491
|
sub?.breaker.destroy();
|
|
2298
2492
|
this.activeSubscriptions.delete(topic);
|
|
2299
2493
|
if (this.ws && this.status === "connected" && !sub?.isPaused) {
|
|
@@ -2416,6 +2610,10 @@ var _RosbridgeClient = class _RosbridgeClient {
|
|
|
2416
2610
|
cleanupConnection() {
|
|
2417
2611
|
this.clearConnectionTimeout();
|
|
2418
2612
|
this.stopServicesPoll();
|
|
2613
|
+
for (const sub of this.activeSubscriptions.values()) {
|
|
2614
|
+
this.cancelAllDrains(sub);
|
|
2615
|
+
sub.breaker.destroy();
|
|
2616
|
+
}
|
|
2419
2617
|
this.activeSubscriptions.clear();
|
|
2420
2618
|
for (const [, pending] of this.pendingServiceCalls) {
|
|
2421
2619
|
clearTimeout(pending.timer);
|
|
@@ -2441,6 +2639,10 @@ var _RosbridgeClient = class _RosbridgeClient {
|
|
|
2441
2639
|
this.reconnectTimer = null;
|
|
2442
2640
|
}
|
|
2443
2641
|
if (this.ws) {
|
|
2642
|
+
this.ws.onopen = null;
|
|
2643
|
+
this.ws.onmessage = null;
|
|
2644
|
+
this.ws.onerror = null;
|
|
2645
|
+
this.ws.onclose = null;
|
|
2444
2646
|
try {
|
|
2445
2647
|
this.ws.close();
|
|
2446
2648
|
} catch {
|
|
@@ -2537,7 +2739,7 @@ var ProtocolManager = class {
|
|
|
2537
2739
|
/**
|
|
2538
2740
|
* Create and connect a protocol client for the given options.
|
|
2539
2741
|
*
|
|
2540
|
-
* For `protocol: 'zenoh'`, throws a clear "planned for v0.
|
|
2742
|
+
* For `protocol: 'zenoh'`, throws a clear "planned for v0.3.0" error —
|
|
2541
2743
|
* the v0.1.0 release does not ship a Zenoh implementation.
|
|
2542
2744
|
*/
|
|
2543
2745
|
async connect(options) {
|
|
@@ -2545,7 +2747,8 @@ var ProtocolManager = class {
|
|
|
2545
2747
|
await this.activeClient.disconnect();
|
|
2546
2748
|
}
|
|
2547
2749
|
const client = this.createClient(options);
|
|
2548
|
-
const
|
|
2750
|
+
const pastedTls = /^\s*(wss|https):\/\//i.test(options.host);
|
|
2751
|
+
const scheme = options.secure ?? pastedTls ? "wss" : "ws";
|
|
2549
2752
|
const port = options.port || DEFAULT_PORTS[options.protocol];
|
|
2550
2753
|
const host = sanitizeHost(options.host);
|
|
2551
2754
|
const url = `${scheme}://${host}:${port}`;
|
|
@@ -2556,7 +2759,13 @@ var ProtocolManager = class {
|
|
|
2556
2759
|
`Invalid connection URL "${url}" \u2014 check host and port. Hosts should not include "ws://" or ":port"; use the port field instead.`
|
|
2557
2760
|
);
|
|
2558
2761
|
}
|
|
2559
|
-
|
|
2762
|
+
try {
|
|
2763
|
+
await client.connect(url);
|
|
2764
|
+
} catch (err) {
|
|
2765
|
+
await client.disconnect().catch(() => {
|
|
2766
|
+
});
|
|
2767
|
+
throw err;
|
|
2768
|
+
}
|
|
2560
2769
|
this.activeClient = client;
|
|
2561
2770
|
this.activeOptions = options;
|
|
2562
2771
|
return client;
|
|
@@ -2590,7 +2799,7 @@ var ProtocolManager = class {
|
|
|
2590
2799
|
case "rosbridge":
|
|
2591
2800
|
return new RosbridgeClient(this.clientOptions);
|
|
2592
2801
|
case "zenoh":
|
|
2593
|
-
throw new Error("Zenoh support is planned for v0.
|
|
2802
|
+
throw new Error("Zenoh support is planned for v0.3.0");
|
|
2594
2803
|
default: {
|
|
2595
2804
|
const exhaustive = options.protocol;
|
|
2596
2805
|
throw new Error(`Unknown protocol: ${String(exhaustive)}`);
|
|
@@ -2599,6 +2808,11 @@ var ProtocolManager = class {
|
|
|
2599
2808
|
}
|
|
2600
2809
|
};
|
|
2601
2810
|
|
|
2811
|
+
// src/materializeBytes.ts
|
|
2812
|
+
function materializeBytes(view) {
|
|
2813
|
+
return new Uint8Array(view);
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2602
2816
|
exports.DEFAULT_PORTS = DEFAULT_PORTS;
|
|
2603
2817
|
exports.DEFAULT_PRESETS = DEFAULT_PRESETS;
|
|
2604
2818
|
exports.FoxgloveClient = FoxgloveClient;
|
|
@@ -2610,6 +2824,8 @@ exports.getLagHistoryCsv = getLagHistoryCsv;
|
|
|
2610
2824
|
exports.getLagStats = getLagStats;
|
|
2611
2825
|
exports.getMaxLagMs = getMaxLagMs;
|
|
2612
2826
|
exports.jsonSchemaToTemplate = jsonSchemaToTemplate;
|
|
2827
|
+
exports.matchesSchema = matchesSchema;
|
|
2828
|
+
exports.materializeBytes = materializeBytes;
|
|
2613
2829
|
exports.schemaToTemplate = schemaToTemplate;
|
|
2614
2830
|
//# sourceMappingURL=index.cjs.map
|
|
2615
2831
|
//# sourceMappingURL=index.cjs.map
|