Stage 3: IUniTaskAsyncEnumerable adapter for incremental byte/text streams#297
Open
MaxHeimbrock wants to merge 1 commit into
Open
Conversation
Stage 3 of the UniTask migration. Exposes ByteStreamReader/TextStreamReader incremental reads as IUniTaskAsyncEnumerable<TChunk> so chunks can be consumed with `await foreach`, building on Stage 1's StreamYieldInstruction awaiter and Stage 2's AsUniTask. A single generic extension AsAsyncEnumerable<TChunk>(this ReadIncrementalInstructionBase<TChunk>) covers both byte[] and string readers. The loop mirrors the coroutine consumer's observable behavior: await a chunk, yield it, re-check IsEos AFTER yielding (Reset() is disallowed past EoS), and Reset() for the next chunk. On EoS carrying a StreamError the enumerable throws that error — idiomatic for await foreach, the one place the UniTask surface throws rather than exposing IsError. Cancellation surfaces as OperationCanceledException with abandon-awaiter semantics. To let the separate LiveKit.UniTask assembly drive the loop, two members are widened to public (both already public on the sibling DataTrack.ReadFrameInstruction, behavior-preserving): StreamYieldInstruction.IsCurrentReadDone getter and ReadIncrementalInstructionBase<T>.LatestChunk. The runtime and test UniTask asmdefs gain a UniTask.Linq reference (source of UniTaskAsyncEnumerable.Create / IUniTaskAsyncEnumerable), and InternalsVisibleTo is extended to the PlayModeTests.UniTask assembly so the deterministic tests can construct a synthetic reader (the same FfiHandle-based seam the EditMode tests use). DataTrack frame streaming is intentionally out of scope (its ReadFrameInstruction has no awaiter and no Reset) — a possible follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2 tasks
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.
Summary
AsAsyncEnumerable<TChunk>(this ReadIncrementalInstructionBase<TChunk>, CancellationToken)in the gatedLiveKit.UniTaskassembly, soByteStreamReader/TextStreamReaderincremental reads can be consumed withawait foreach(returnsIUniTaskAsyncEnumerable<byte[]>/<string>).StreamYieldInstructionawaiter and Stage 2'sAsUniTask. The loop mirrors the coroutine consumer's observable behavior: await a chunk → yield it → re-checkIsEosafter yielding (sinceReset()is disallowed past EoS) →Reset()for the next chunk.Design decisions
StreamError, the enumerable throws it (idiomatic forawait foreach— the one place the UniTask surface throws rather than exposingIsError). Note: once an error is set,LatestChunkitself throws, so a chunk and an error never surface from the same step (a chunk delivered before the error is observed normally; the error then ends iteration).OperationCanceledException, abandon-awaiter semantics (consistent with Stage 2).DataTrack.ReadFrameInstructionis out of scope (no awaiter, noReset()) — possible follow-up.Supporting changes (core
LiveKitasm, behavior-preserving)StreamYieldInstruction.IsCurrentReadDonegetter andReadIncrementalInstructionBase<T>.LatestChunktopublicso the separateLiveKit.UniTaskassembly can drive the loop. Both already have public equivalents on the siblingDataTrack.ReadFrameInstruction.UniTask.Linqreference to the runtime + test UniTask asmdefs (source ofUniTaskAsyncEnumerable.Create/IUniTaskAsyncEnumerable).InternalsVisibleTotoPlayModeTests.UniTaskso the deterministic tests can construct a synthetic reader via the publicFfiHandleseam the EditMode tests already use.Testing
Tests/PlayMode/UniTask/StreamUniTaskTests.cs(gated byLIVEKIT_UNITASK), using a syntheticTestIncrementalReader : ReadIncrementalInstructionBase<string>withPushChunk/PushEos— the same FFI-free seam asDataStreamIncrementalReadTests:StreamErroron a subsequent error EoSOperationCanceledException, observes nothingTest plan
Scripts~/run_unity.sh build macos— clean compile.Scripts~/run_unity.sh test -m PlayMode -f "AsAsyncEnumerable|AsUniTask|GetAwaiter"— 8 passed, 0 failed (4 new stream tests + inherited Stage 1/2).Scripts~/run_unity.sh test -m EditMode— 70 passed, 2 skipped (confirms the visibility widening didn't regressDataStreamIncrementalReadTests).Targeting
Base branch is
max/yield-instruction-unitask-extension(Stage 2, PR #290), so this diff shows only the Stage 3 surface.🤖 Generated with Claude Code