Skip to content

Stream.first hangs for combineLatest + switchMap with open-ended async* streams #2348

@saibotma

Description

@saibotma

Awaiting stream.first hangs for a stream built from combineLatest and
switchMap over open-ended async* streams.

This looks very similar to the RxDart issue below, but for stream_transform:

Minimal reproduction

import 'dart:async';
import 'dart:io';

import 'package:stream_transform/stream_transform.dart';

Future<void> main() async {
  final stream = _emitOnceAndNeverClose('left').combineLatest(
    Stream.value('right').switchMap(_emitOnceAndNeverClose),
    (left, right) => '$left|$right',
  );

  final value = await stream.first.timeout(const Duration(milliseconds: 200));
  stdout.writeln(value);
}

Stream<T> _emitOnceAndNeverClose<T>(T value) async* {
  yield value;
  await Completer<void>().future;
}

How to run

dart run repro.dart

Expected behavior

The program should complete and print:

left|right

Actual behavior

The program times out:

Unhandled exception:
TimeoutException after 0:00:00.200000: Future not completed

Without the timeout, it hangs indefinitely.

What I verified

  • switchMap is required. Replacing the switchMap(...) branch with a direct
    _emitOnceAndNeverClose('right') stream makes the problem disappear.
  • The non-switchMap branch also matters. Replacing
    _emitOnceAndNeverClose('left') with Stream.value('left') makes the
    problem disappear.
  • Stream.value('right').switchMap(_emitOnceAndNeverClose) alone works.
  • combineLatest without switchMap works.

Additional context

We ran into this in a Flutter app where the source streams were long-lived
database watchers from Drift (watchSingleOrNull()), wrapped in async*
syncers. Those streams are intentionally open-ended until canceled.

The failure happened when we composed those watcher streams with
combineLatest and switchMap, then awaited .first to get one initial
bootstrap value. The minimal repro above removes Drift entirely and still
reproduces, so this does not appear to be Drift-specific.

Environment

  • stream_transform: 2.1.1
  • Dart SDK: 3.12.0-113.1.beta
  • Flutter: 3.42.0-0.0.pre
  • Platform: macos_arm64

Related context

This seems especially relevant because stream_transform's changelog already
mentions cancellation behavior in switchLatest / switchMap:

And the current implementation of both switchMap and combineLatest
explicitly waits for cancel() futures during teardown, which seems related to
this hang.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions