Skip to content

Make JS iteration over ReceiveChannel cancel iteration on early exits - #4718

Open
dkhalanskyjb wants to merge 2 commits into
developfrom
dkhalanskyjb/fixup-channel-js-asynciterator
Open

Make JS iteration over ReceiveChannel cancel iteration on early exits#4718
dkhalanskyjb wants to merge 2 commits into
developfrom
dkhalanskyjb/fixup-channel-js-asynciterator

Conversation

@dkhalanskyjb

Copy link
Copy Markdown
Collaborator

Also, mark asyncIterator as ExperimentalCoroutinesApi.

Also, mark `asyncIterator` as `ExperimentalCoroutinesApi`.
@dkhalanskyjb
dkhalanskyjb requested a review from JSMonk August 10, 2026 08:55
@dkhalanskyjb dkhalanskyjb changed the title Make JS iteration over ReceiveChannel cancel iteration on stopping Make JS iteration over ReceiveChannel cancel iteration on early exits Aug 10, 2026
@JSMonk

JSMonk commented Aug 19, 2026

Copy link
Copy Markdown
Member

@dkhalanskyjb as we discussed in #4625 I've added methods for the Channel to configure the cancellation behavior during iteration. I've introduced a few overloads that are hidden in either JS or Kotlin to provide a natural API for both languages.

@dkhalanskyjb dkhalanskyjb left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't request changes for my own pull request, which complicates the review. Please consider making another branch on top of mine and opening a separate PR. No need to bother making it pretty, it's only for code review.

import kotlin.js.Promise

@JsImplicitExport(couldBeConvertedToExplicitExport = true)
public actual interface ReceiveChannel<out E> : JsAsyncIterable<E> {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that ReceiveChannel is still JsAsyncIterable. We have to choose a single entry point: either the channel itself is iterable, or the users go through a separate function. Having both defeats the purpose of the function to educate the user about the channel cancellation behavior.

*/
@ExperimentalCoroutinesApi
// We can't use DeprecationLevel.HIDDEN, because the generated declaration will also be deprecated in .d.ts
@LowPriorityInOverloadResolution

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this does anything. This overload is never in conflict with the other one.

@@ -1,5 +1,5 @@
@file:OptIn(ExperimentalJsExport::class, ExperimentalStdlibApi::class)
@file:Suppress("EXPOSED_FUNCTION_RETURN_TYPE", "INVISIBLE_REFERENCE", "EXPOSED_SUPER_INTERFACE")
@file:Suppress("EXPOSED_FUNCTION_RETURN_TYPE", "INVISIBLE_REFERENCE", "EXPOSED_SUPER_INTERFACE", "EXPOSED_PARAMETER_TYPE")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ChannelIteratorOptions being internal but visible as an parameter of a public overload is not acceptable. I tried using this from another Kotlin/JS project, and on the Kotlin side, the IDE does not hide the overload that accepts ChannelIteratorOptions, doesn't show that it can't be constructed, and in general, behaves as if the overload is entirely valid. This can lead users on a wild goose chase.

* - `preventCancel = false` or omitted: early iterator completion cancels the channel.
*/
@ExperimentalCoroutinesApi
// We can't use DeprecationLevel.HIDDEN, because the generated declaration will also be deprecated in .d.ts

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, does DeprecationLevel.ERROR work?

If not, we'll have to settle for just the boolean overload and remove this one. Getting people relying on autocompletion on the Kotlin side into a trap where they try and fail to construct a ChannelIteratorOptions object would be a major usability issue.

import kotlin.js.Promise

@JsImplicitExport(couldBeConvertedToExplicitExport = true)
public actual interface ReceiveChannel<out E> : JsAsyncIterable<E> {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the new function(s), we need to reevaluate ReceiveChannel implementing JsAsyncIterable directly.

Since both possible AsyncIterable behaviors on exit—cancelling or not cancelling—may surprise a subset of users, and we provide a function controlling this, we'd like to emphasize that there's this variability in behavior and encourage checking the options. If someone just writes for await (const v of channel), nothing here indicates a non-trivial choice being made.

I realize ReadableStream does this as well—it's both iterable and has a values method—but as you pointed out, its default cancellation behavior is more obvious, due to it being exclusive to a single consumer.

@ExperimentalCoroutinesApi
// We can't use DeprecationLevel.HIDDEN, because the generated declaration will also be deprecated in .d.ts
@LowPriorityInOverloadResolution
@JsName("values") // We use "values" here to mimic the ReadableStream API: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is ReadableStream.values an AsyncIterator or an AsyncIterable? I couldn't easily find this in the documentation.

* @param options iteration behavior options:
* - `preventCancel = true`: early iterator completion does not cancel the channel;
* - `preventCancel = false` or omitted: early iterator completion cancels the channel.
*/

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we don't expect Kotlin users to call this function, we shouldn't generate an API reference page for it, so please /** @suppress */ it.

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.

2 participants