Skip to content

Data streams v2: fix review findings in the native shim - #1278

Draft
1egoman wants to merge 6 commits into
data-streams-v2from
data-streams-v2-review-fixes
Draft

1egoman wants to merge 6 commits into
data-streams-v2from
data-streams-v2-review-fixes

Conversation

@1egoman

@1egoman 1egoman commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

Fixes review findings against #1166 in the Dart layer around the Rust core. One commit per fix, each with a regression test that fails without it.

Warning

Not mergeable as-is. The last commit needs OutgoingPacketBatch from livekit-ffi, which isn't released yet. Until then, pubspec.yaml and example/pubspec.yaml carry a TEMPORARY dependency_overrides pointing at a local rust-sdks worktree, so CI won't build this branch. Once the rust-sdks change below is released, replace the override with a livekit_ffi version bump.

Fixes

  • Bump livekit_ffi to 0.13.3: exports StreamReader.close() and merges trailer attributes into the reader's info.
  • Trailer attributes dropped on native: the handler got the info captured when the stream opened, so header{a:1} + chunk + trailer{b:2} gave {a:1}. The info is now re-read from the core when the stream ends and its attributes merged in, as the web path does.
  • MIME type not inferred for sendFile and sendText attachments: the core does no inference and falls back to application/octet-stream. This restores main's options.mimeType ?? mime(basename(path)) on native and web.
  • Cancelling a subscription didn't end the stream: onCancel only set a flag, so the core kept the stream open and kept buffering until the sender sent again. It now calls the reader's close(), which returns immediately and wakes the pump. The pump still owns dispose(), so there's no use-after-free.
  • No data channel backpressure on native sends: the pump now waits for the buffer status to be low before each packet, as WritableStream did on main. It also sends a whole batch before pulling the next one, which replaces the separate send chain.
  • Send results didn't reflect delivery: sendText/sendBytes/write resolved once packets were buffered in Rust, and transport errors were only logged, so await sendText(); await room.disconnect(); could silently drop the message. The pump now settles each batch: it completes the batch once its packets are through the engine, or fails it with the error, which the core surfaces to the send as SendFailed. Because the core holds the send until then, this also stops sendFile from reading the whole file into Rust memory.

Depends on

A rust-sdks change, branch ffi/polled-outgoing-delivery-ack, not yet pushed. OutgoingPacketQueue::on_packets_available used to acknowledge as soon as packets were buffered, which breaks the delegate's contract (return only once the packets are on the transport). next_packets() now returns an OutgoingPacketBatch that the host completes or fails. Closing the queue, or dropping a batch without settling it, fails the waiting send. At most one batch is outstanding.

Not addressed

  • TextStreamInfo.operationType/version are no longer null on native, because the core reports defaults (create/0) where main and web report null. Left as is pending a decision.
  • Web reset() closes readers without an error. Intentionally deferred.

Testing

  • flutter test: all 518 pass, run against the overridden livekit_ffi.
  • cargo test -p livekit-ffi --no-default-features --features core-modules data_stream: all 27 pass. Clippy is clean.

🤖 Generated with Claude Code

1egoman and others added 6 commits November 4, 2026 13:16
0.13.3 exports StreamReader.close(), nextProgress(), and merges trailer
attributes into the reader's info, which the following fixes rely on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The handler is given the info snapshotted when the stream opened, so
attributes carried on the trailer never reached it on the native path,
while the web path merges them in. Re-read the core's info once the
stream ends, where it has already merged the trailer, and copy the
attributes over before closing the reader.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main fell back to mime(basename(path)) when no MIME type was given, but
both new send paths dropped it. The Rust core does no inference and
defaults to application/octet-stream, so sendFile and sendText
attachments lost their type. Restore the fallback on native and web.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s cancelled

onCancel only set a flag, so a pump parked in next() kept the reader,
and the core kept the stream open and buffering, until the sender sent
again. Call the reader's close() from onCancel: it returns at once, ends
the stream in the core, and fails the pending next(), which wakes the
pump to dispose the reader as before. The resulting AlreadyClosed is not
reported, since nobody is listening.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd path

WritableStream awaited waitForBufferStatusLow before every packet; the
native pump instead chained sends straight into sendDataPacket and
pulled the next batch immediately, so a large send flooded the reliable
data channel. Send each packet only once the buffer is below its
low-water mark, and finish a batch before pulling the next, which also
keeps packet order without the separate send chain.

This paces the wire and bounds what Dart holds. The core's polled queue
still acknowledges packets as soon as they are buffered, so it keeps
accepting them in Rust until it waits for delivery as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The core's polled queue acknowledged packets as soon as they were
buffered, so sendText/sendBytes/write resolved before anything was
sent, transport failures were only logged, and a send followed by
disconnect could silently drop its packets.

Settle each batch the core hands over: complete it once every packet
went through the engine, or fail it with the error, which the core
surfaces to the originating send as SendFailed. The core holds the send
and produces nothing further until then, which also bounds how far
sendFile reads ahead of the data channel.

TEMPORARY: this needs OutgoingPacketBatch from livekit-ffi, which is
not published yet, so pubspec.yaml and example/pubspec.yaml override
livekit_ffi to a local rust-sdks worktree. Replace the override with a
version bump once livekit-ffi releases it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant