refactor(device): extract the live-sample async stream into a collaborator (part of #344) - #440
Conversation
…rator (part of #344) DaqifiStreamingDevice hosted the whole pull-based live-sample path itself: the bounded channel, the drop-oldest overflow callback, the per-enumeration subscribe/unsubscribe bookkeeping and the device-wide drop counter. None of it is device state — it is an adapter over the IChannel.SampleReceived events the decoder already raises. Move it to Device/Internal/LiveSampleStream, reached through the existing IDeviceOperationHost.SnapshotChannels(). No interface change, no new host member, no public API change: DefaultLiveSampleBufferCapacity, DroppedLiveSampleCount and StreamSamplesAsync keep their signatures and their semantics. StreamSamplesAsync now hands back the collaborator's async iterator directly rather than wrapping it in one of its own. That is what preserves the two deferred behaviors a caller can observe: WithCancellation still reaches the iterator's own [EnumeratorCancellation] parameter, and an invalid bufferCapacity still throws on the first MoveNextAsync rather than at the call. Both are now pinned by tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
PR Summary by QodoRefactor: extract live-sample async stream into LiveSampleStream collaborator
AI Description
Diagram
High-Level Assessment
Files changed (4)
|
Code Review by Qodo
1.
|
…on fails instead of hanging Two waits in the new tests were unbounded: the post-cancellation await in Enumeration_EndedByCancellation_StillUnsubscribes, and the await foreach inside InvalidBufferCapacity_ThrowsOnFirstMoveNext_NotAtTheCall. A live stream's read is unbounded by design, so if cancellation or the capacity validation ever regressed, those two would park forever and stall the whole run rather than failing it. Both now go through the same .WaitAsync(...) bound the rest of the file already used, hoisted into a named MoveNextTimeout so the intent is stated once and the next test added here inherits it. Verified by simulating both regressions (dropping the Cancel() call, and passing a valid capacity): each test now fails in ~5s with TimeoutException instead of hanging. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit 2cf3bd1 |
|
Ready for review. Not merging — leaving the merge to you. State at head
Since the last review pass, the only change is One thing I deliberately left out of scope: |
Part of #344. Not merging — opened for review.
Problem
DaqifiStreamingDevicehosted the entire pull-based live-sample path itself — the bounded channel, the drop-oldest overflow callback, the per-enumeration subscribe/unsubscribe bookkeeping, and the device-wide drop counter field. None of that is device state. It is an adapter over theIChannel.SampleReceivedevents the decoder already raises, and it was sitting in the god class this issue exists to break up.Fix
Moved it to
Device/Internal/LiveSampleStream, reached through the existingIDeviceOperationHost.SnapshotChannels().InitializeStreamingDevice, alongside the six collaborators already there.DefaultLiveSampleBufferCapacity,DroppedLiveSampleCountandStreamSamplesAsynckeep their signatures and their semantics;docs/DEVICE_INTERFACES.mdneeded no edit.System.Runtime.CompilerServicesandSystem.Threading.Channelswere used only by this block, so both usings go with it.DaqifiStreamingDevice.cs: 1271 → 1239 lines.The one subtle part
StreamSamplesAsyncnow hands back the collaborator's async iterator directly rather than wrapping it in one of its own. That is deliberate, and it is what preserves the two deferred behaviors a caller can actually observe:WithCancellation(token)still reaches the iterator's own[EnumeratorCancellation]parameter. A wrapper without that attribute would drop the token silently and hang.bufferCapacitystill throws on the firstMoveNextAsync, not at the call — async-iterator bodies are deferred.Both are now pinned by tests, because neither was before.
Tests
+8 (
2623→2631). The existingDaqifiStreamingDeviceLiveStreamTestsare deliberately left alone — they are the evidence that the extraction changed nothing.LiveSampleStreamTestscovering what only a direct test can see: every snapshot channel subscribed once at start and unsubscribed on every exit path including cancellation (a leaked handler would keep the decode path writing into a dead buffer for the device's lifetime); the channel set snapshotted exactly once per enumeration and later arrivals ignored; concurrent enumerations each getting their own buffer; the drop counter accumulating across enumerations rather than per-enumeration; and the deferredArgumentOutOfRangeException. The fake host throws on every member outside this block's remit, so a future change that sends a command or takes the channels lock fails loudly.WithCancellationforwarding described above.FULL suite green on net9 + net10 — 2631 passed / 2 skipped each (+23
Daqifi.Mcp.Testson net9), 0 warnings.Bench (real Nq1, fw 3.7.2, USB, non-destructive)
Two sequential enumerations on one connection, consuming
StreamSamplesAsyncend to end against live hardware:IsStreamingafter enumeration endedConflict note
Hunks are disjoint from the open #439 apart from the
usingblock, where the two edits are separated by three unchanged lines; whichever lands second should merge cleanly, and a conflict there would be trivial.🤖 Generated with Claude Code