ros-mobile-bridge 0.1.1 → 0.1.2

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 CHANGED
@@ -4,6 +4,21 @@ All notable changes to `ros-mobile-bridge` will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.1.2] - 2026-06-01
8
+
9
+ ### Performance
10
+
11
+ - **Zero-copy payload view on Foxglove WS binary topic ingest.** Previously every inbound `messageData` frame allocated a fresh `ArrayBuffer` (`ArrayBuffer.prototype.slice`) for the payload, costing roughly 10 MB/frame on raw 1080p image streams even on frames that the throttle subsequently dropped before parse. The library now uses a zero-copy `Uint8Array` view; downstream consumers (CDR reader, JSON decoder, byte-size accounting) see no behavior change. Verified end-to-end against a sustained 138 MB/s harness: `max` lag dropped from 96 ms to 29 ms (3.4× tail reduction), `mean` and `p50` unchanged.
12
+
13
+ ### Changed
14
+
15
+ - **Control-priority publishes now coalesce by destination.** When multiple `priority: 'control'` publishes for the same topic queue in the outbox during JS-thread saturation, only the latest drains rather than every queued tick in FIFO order. The release-the-joystick zero-Twist that follows N stale-value Twists on `/cmd_vel` now sends in one WebSocket frame, so the robot stops within one round-trip of release regardless of how deep the queue grew during the block. Insertion order across **distinct** topics is preserved (intra-topic conflation only). Reduced the worst-case stop latency by orders of magnitude under sustained-overload scenarios.
16
+ - **Tightened CircuitBreaker defaults for CDR-realistic workloads.** The breaker's `lagThresholdMs` (`250 → 150`), `tripDwellMs` (`5000 → 2000`), and the internal `WARMUP_MS` grace period (`2000 → 500`) were originally tuned against JSON-sim spike patterns (transient 100–333 ms spikes interleaved with healthy stretches). CDR sensor streams on real hardware fail in a different regime — sustained multi-second freezes during the cold-start window — which warrants faster detection. Total subscribe-to-unsubscribe floor on a genuinely saturating topic drops from approximately 7 s to approximately 2.5 s, comfortably below the threshold at which a user perceives the app as frozen.
17
+
18
+ ### Documented
19
+
20
+ - **Zero-copy contract on `RosMessage.data` when it is a `Uint8Array`.** The byte values delivered to a subscriber callback as `Uint8Array` are now views into the inbound WebSocket frame's `ArrayBuffer`, not copies. The view's `byteOffset` is significant. Consumers handing `data` directly to native bindings that ignore `byteOffset` (some Skia binding paths, some FFI calls) must first materialize an owned copy via `new Uint8Array(data)`. A `materializeBytes(view)` helper that performs this copy conditionally is planned for the next release; until then the explicit copy is the recommended idiom. TSDoc on `RosMessage` carries the full contract.
21
+
7
22
  ## [0.1.1] - 2026-05-27
8
23
 
9
24
  ### Fixed
package/dist/index.cjs CHANGED
@@ -10,7 +10,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
10
10
 
11
11
  // src/CircuitBreaker.ts
12
12
  var HEALTHY_DEBOUNCE_MS = 4e3;
13
- var WARMUP_MS = 2e3;
13
+ var WARMUP_MS = 500;
14
14
  var CircuitBreaker = class {
15
15
  constructor(config) {
16
16
  __publicField(this, "config", config);
@@ -151,8 +151,8 @@ var CircuitBreaker = class {
151
151
  }
152
152
  };
153
153
  var DEFAULT_BREAKER_CONFIG = {
154
- lagThresholdMs: 250,
155
- tripDwellMs: 5e3,
154
+ lagThresholdMs: 150,
155
+ tripDwellMs: 2e3,
156
156
  recoveryDwellMs: 1e4,
157
157
  cooldownsMs: [3e4, 6e4, 12e4, 3e5]
158
158
  };
@@ -927,7 +927,12 @@ var _FoxgloveClient = class _FoxgloveClient {
927
927
  return;
928
928
  }
929
929
  if (options?.priority === "control") {
930
- this.controlOutbox.push({ channelId: clientChannelId, data });
930
+ const existing = this.controlOutbox.findIndex((e) => e.channelId === clientChannelId);
931
+ if (existing >= 0) {
932
+ this.controlOutbox[existing] = { channelId: clientChannelId, data };
933
+ } else {
934
+ this.controlOutbox.push({ channelId: clientChannelId, data });
935
+ }
931
936
  this.scheduleControlFlush();
932
937
  return;
933
938
  }
@@ -1281,7 +1286,7 @@ var _FoxgloveClient = class _FoxgloveClient {
1281
1286
  const sec = Math.floor(timestampNs / 1e9);
1282
1287
  const nsec = timestampNs % 1e9;
1283
1288
  const payloadOffset = 13;
1284
- const payload = buffer.slice(payloadOffset);
1289
+ const payload = new Uint8Array(buffer, payloadOffset);
1285
1290
  const sub = this.subscriptions.get(subscriptionId);
1286
1291
  if (!sub) return;
1287
1292
  if (sub.isPaused) return;
@@ -1310,18 +1315,18 @@ var _FoxgloveClient = class _FoxgloveClient {
1310
1315
  const text = TEXT_DECODER.decode(payload);
1311
1316
  data = JSON.parse(text);
1312
1317
  } catch {
1313
- data = new Uint8Array(payload);
1318
+ data = payload;
1314
1319
  }
1315
1320
  } else {
1316
1321
  const reader = this.messageReaders.get(subscriptionId);
1317
1322
  if (reader) {
1318
1323
  try {
1319
- data = reader.readMessage(new Uint8Array(payload));
1324
+ data = reader.readMessage(payload);
1320
1325
  } catch {
1321
- data = new Uint8Array(payload);
1326
+ data = payload;
1322
1327
  }
1323
1328
  } else {
1324
- data = new Uint8Array(payload);
1329
+ data = payload;
1325
1330
  }
1326
1331
  }
1327
1332
  const rosMsg = {
@@ -1937,7 +1942,12 @@ var _RosbridgeClient = class _RosbridgeClient {
1937
1942
  }
1938
1943
  const payload = { op: "publish", topic, msg: data };
1939
1944
  if (options?.priority === "control") {
1940
- this.controlOutbox.push(payload);
1945
+ const existing = this.controlOutbox.findIndex((e) => e.topic === topic);
1946
+ if (existing >= 0) {
1947
+ this.controlOutbox[existing] = payload;
1948
+ } else {
1949
+ this.controlOutbox.push(payload);
1950
+ }
1941
1951
  this.scheduleControlFlush();
1942
1952
  return;
1943
1953
  }