Skip to content

JNI: AsyncThrowingStream results, with errors delivered to onError - #15

Open
piercifani wants to merge 2 commits into
jni/10-async-streamfrom
jni/11-async-throwing-stream
Open

piercifani wants to merge 2 commits into
jni/10-async-streamfrom
jni/11-async-throwing-stream

Conversation

@piercifani

@piercifani piercifani commented Nov 3, 2026 •

Copy link
Copy Markdown

Stack 11/21 · base: jni/10-async-stream · previous: #14 · next: #16 (jni/12-optional-nested-arrays)

Problem

jextract skipped functions returning AsyncThrowingStream<T, Failure>, because the type isn't in its model of the standard library. #14 handled AsyncStream only.

Changes

Generator. AsyncThrowingStream is added to the stdlib model next to AsyncStream. A function returning AsyncThrowingStream<T, Failure> is translated exactly like one returning AsyncStream<T>: Java gets a SwiftAsyncStream<T>, and the Swift thunk calls _attachAsyncStream. Both Error and any Error failures are covered.

Runtime. _attachAsyncStream gains an AsyncThrowingStream overload, so the generated code is the same for both stream types. Both overloads share one task body. When the stream throws, the error is converted to a Java Throwable the same way a thrown Swift error is for any call: a jextracted error type becomes its own exception class, and any other error becomes SwiftErrorException.

Java. SwiftAsyncStream.Subscriber gets default void onError(Throwable error), called instead of onComplete when a throwing stream fails. Neither one is called after the subscription is cancelled; the Delivery that already guarantees one finish now covers the error path too.

Kotlin. SwiftKitKotlin's asFlow() closes the flow with that error, so collect throws the Swift error.

Kotlin in this PR

StreamsTest.kt with Streams.swift:

val error = assertFailsWith<ClientError> {
    PriceFeed.init().pricesUntilOffline(2).asFlow().collect { ids += it.id }
}
assertEquals(listOf("price-0", "price-1"), ids)
assertTrue(error.case is ClientError.Case.Offline)

The Swift stream yields two items and then finishes with throwing: ClientError.offline. Kotlin receives both items and then catches the typed Swift error, matching on its case.

Why Skip needs it

Skip bridges AsyncThrowingStream to a Kotlin Flow that fails with the error, the same way it bridges AsyncStream.

Testing

  • Kotlin sample: ./gradlew :SwiftKitKotlin:build :Samples:SwiftJavaExtractJNIKotlinSampleApp:test passes (30 tests).
  • JNIAsyncStreamTests: a new parameterized test for AsyncThrowingStream<Int64, Error> and <Int64, any Error> checks the Java method, the native signature and the Swift attach call.
  • JNI sample: interruptedCountdown delivers its elements and then its error to onError, with the Swift description as the message.
  • Full swift test --disable-experimental-prebuilts --disable-sandbox (654 tests) and ./gradlew :Samples:SwiftJavaExtractJNISampleApp:test (288 tests) pass.
  • swift format lint --strict on changed files; git diff --check.

Not in this PR

  • Other AsyncSequence types, and streams as parameters or properties.
  • Typed Failure isn't reflected in Java: every stream is a SwiftAsyncStream<T>, and the error arrives as a Throwable.

🤖 Generated with Claude Code

piercifani and others added 2 commits November 3, 2026 17:35
Functions returning `AsyncThrowingStream<T, Failure>` were skipped: the
type wasn't in the modeled standard library.

- The generator treats them like `AsyncStream<T>`: Java gets the same
  `SwiftAsyncStream<T>` and the Swift thunk calls `_attachAsyncStream`,
  which gains an `AsyncThrowingStream` overload.
- When the stream throws, the error is converted like any thrown Swift
  error and delivered to the new `Subscriber.onError` instead of
  `onComplete`. Neither is called after the subscription is cancelled.
- SwiftKitKotlin's `asFlow()` closes the flow with that error, so
  `collect` throws it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@piercifani
piercifani force-pushed the jni/11-async-throwing-stream branch from 47472fb to ce59fd8 Compare November 3, 2026 16:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant