Skip to content

Adding async/await and UniTask support#319

Open
MaxHeimbrock wants to merge 5 commits into
mainfrom
max/unitask-complete
Open

Adding async/await and UniTask support#319
MaxHeimbrock wants to merge 5 commits into
mainfrom
max/unitask-complete

Conversation

@MaxHeimbrock

Copy link
Copy Markdown
Contributor

Background

We currently only support Coroutines for async execution, with this PR we will support async/await and if clients import the UniTask package, we also support UniTasks natively.

MaxHeimbrock and others added 2 commits June 17, 2026 14:37
Stage 1 of the UniTask migration: enable `await room.Connect(...)` and
similar without taking on a UniTask dependency. The awaiter's continuation
is invoked from the existing IsDone / IsCurrentReadDone / IsEos property
setters, so all nine concrete instructions (Connect, PublishTrack, RPC,
SendText/File, stream open/write/close, etc.) become awaitable with no
change to their completion code paths.

Race between FFI-thread completion and main-thread await registration is
resolved with a sentinel-value Interlocked.CompareExchange on a single
continuation slot. GetResult() is intentionally a no-op so the await
surface keeps strict parity with `yield return` (callers still inspect
IsError); a throwing variant can be layered on later.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Connect_FailsWithInvalidUrl_Awaitable failed intermittently in the full
PlayMode suite: awaiting the ConnectInstruction resumes the instant IsDone
is set, but the FFI emits its "error while connecting" log batch a frame or
two later — after the test had already reset LogAssert.ignoreFailingMessages,
so the late error surfaced as an unhandled message and failed the test. It
only passed in isolation because the timing happened to line up.

Replace it with two deterministic tests driven by a synthetic YieldInstruction
subclass: one for the OnCompleted path (await registered while pending, then
completed) and one for the IsCompleted fast path (already done before await).
These exercise the GetAwaiter logic directly with no FFI, no dev server, and
no LogAssert race. The real connect-fail path stays covered by the existing
Connect_FailsWithInvalidUrl coroutine test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@MaxHeimbrock MaxHeimbrock force-pushed the max/unitask-complete branch from a629011 to 6145795 Compare June 17, 2026 12:39
MaxHeimbrock and others added 3 commits June 17, 2026 14:42
Stage 2 of the UniTask migration. The new LiveKit.UniTask asmdef hosts
an AsUniTask extension on YieldInstruction and StreamYieldInstruction;
the asmdef compiles only when com.cysharp.unitask is installed (the
versionDefine auto-activates LIVEKIT_UNITASK). When UniTask is absent,
the extension simply does not exist — no compile error, no runtime cost,
no impact on Stage 1's awaiter.

AsUniTask wraps the existing one-shot completion path in a UniTaskCompletionSource
and adds CancellationToken support with "abandon awaiter" semantics: a cancel
faults the UniTask with OperationCanceledException, but the underlying FFI
request is not aborted. GetResult stays non-throwing for IsError parity with
yield return / await; throwing variants can be layered on later.

Includes a UniTask migration of Samples~/Meet to demonstrate the new path
end-to-end (Connect / PublishLocalCamera / PublishLocalMicrophone all switch
to async UniTask with cancellation tied to GetCancellationTokenOnDestroy).
Long-running per-frame pumps stay on StartCoroutine since they aren't
request/response.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Remove UniTask package
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>
Stage 4 (capstone) of the UniTask migration. Adds a README section covering
the three interchangeable async styles the SDK now supports, and states the
policy: coroutines remain the default and fully supported; async/await and
UniTask are additive opt-ins; the coroutine API is not deprecated.

- async/await with no dependency (instructions are awaitable; inspect IsError,
  await does not throw — parity with yield return).
- UniTask opt-in (com.cysharp.unitask + LIVEKIT_UNITASK): AsUniTask with
  CancellationToken, UniTask.WhenAll composition, and AsAsyncEnumerable for
  await foreach over incremental streams (throws StreamError on error EoS).

Examples use the verified public signatures (Connect(url, token, RoomOptions),
PublishTrack(track, options), ReadIncremental().AsAsyncEnumerable()) and point
to the Meet sample (UniTask) and Basic sample (coroutines) as references.
Docs-only; no code, no deprecation, no version bump.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@MaxHeimbrock MaxHeimbrock force-pushed the max/unitask-complete branch from 6145795 to 632297b Compare June 17, 2026 12:42
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