fix: emit mq payloads as standalone arrays, not views into the packet buffer - #402
Merged
Conversation
… buffer
A feature toggle published to a device was rejected as a malformed payload even
though its bytes were exactly `{"value":true}`. The bytes were fine; reading them
was not.
mqtt-packet builds a message payload by slicing the stream buffer
(`parser.js:323`), so what MQTT.js delivers is a *view*: `byteOffset` is non-zero
and `message.buffer` is the whole packet rather than the message. Both services-
side handlers - `appstate/toggle-sync.ts` and `activities/active-rides/mq.ts` -
read the payload as `Buffer.from(message.buffer).toString()`, which therefore
returned the entire backing store instead of the 14 bytes that were sent, and
JSON.parse failed.
That code is unchanged and worked before, because every other binding hands over
a standalone array: the native module this replaced did, and desktop's does
because Electron IPC copies on the way through. Mobile was the only one exposing
a view, so it is normalised here rather than making every consumer defend against
it - which also fixes both handlers at once and needs no services release.
`new Uint8Array(message)` copies element-wise into an array that owns its buffer
at offset 0. Note `Buffer.from(message)` would not do: Node's Buffer pools small
allocations, so the copy would itself be a view at a non-zero offset - correct on
device, where the polyfill does not pool, but wrong under test.
Active rides would have hit this too. It had been listed as unverified for
exactly this reason.
|
gdoumen
marked this pull request as ready for review
August 19, 2026 10:01
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



A feature toggle published to a device was rejected as a malformed payload — even though the bytes were exactly
{"value":true}:Those 14 bytes decode to
{"value":true}. The data arrived intact; reading it was the problem.Cause
mqtt-packetbuilds a payload by slicing the stream buffer (parser.js:323), so what MQTT.js delivers is a view:byteOffsetis non-zero andmessage.bufferis the whole packet, not the message.Both services-side handlers —
appstate/toggle-sync.ts:159andactivities/active-rides/mq.ts:149— read the payload as:Buffer.from(arrayBuffer)wraps the entire buffer, ignoringbyteOffset/byteLength. Reproduced locally: for a 14-byte payload at offset 14, that call returned the whole 8KB backing store including unrelated memory.JSON.parsethen failed and the toggle was discarded.Why it is fixed in the binding, not in services
That services code is unchanged and worked before, because every other binding hands over a standalone array: the native module this replaced did, and desktop's does because Electron IPC copies on the way through. Mobile was the only one exposing a view into a larger buffer.
Normalising here also fixes both consumers at once and needs no
servicesrelease.The fix
new Uint8Array(view)copies element-wise into an array that owns its buffer at offset 0. Payloads are small JSON documents, so the copy is cheap.Buffer.from(message)would not work: Node pools small Buffer allocations, so the copy would itself sit at a non-zero offset in a shared pool — correct on device, where thebufferpolyfill does not pool, but wrong under test.new Uint8Arrayis deterministic on both.Scope
Active rides would have hit this too —
active-rides/mq.tshas the identical line. It was listed as unverified inTESTING_BACKLOG#58 precisely because no multi-user ride had been run over the new transport; this is what that would have found.Test plan
npm test— 794 tests / 111 suites pass, including a new regression guard that feeds the binding a payload at a non-zero offset inside a larger packet and asserts the emitted array owns its buffer, then asserts the consumers' exactBuffer.from(payload.buffer).toString()pattern yields the message.payload.toString()returned'hello', which pinned Buffer semantics theUint8Arrayinterface never promised. It now decodes the way consumers actually do.npx tsc --noEmit— clean.npm run lint— 0 errors.Not verified: not yet on a device. The check is to publish a feature toggle and confirm
feature-toggle sync: applied toggleinstead ofignoring malformed payload.