|
| 1 | +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file |
| 2 | +// for details. All rights reserved. Use of this source code is governed by a |
| 3 | +// BSD-style license that can be found in the LICENSE file. |
| 4 | + |
| 5 | +// TODO: This library is a decent proposal for addition to `dart:async` or |
| 6 | +// other similar utility package. It's extremely useful when processing |
| 7 | +// a stream of objects, where I/O is required for each object. |
| 8 | + |
| 9 | +import 'dart:async'; |
| 10 | + |
| 11 | +/// A [Notifier] allows micro-tasks to [wait] for other micro-tasks to |
| 12 | +/// [notify]. |
| 13 | +/// |
| 14 | +/// [Notifier] is a concurrency primitive that allows one micro-task to |
| 15 | +/// wait for notification from another micro-task. The [Future] return from |
| 16 | +/// [wait] will be completed the next time [notify] is called. |
| 17 | +/// |
| 18 | +/// ```dart |
| 19 | +/// var weather = 'rain'; |
| 20 | +/// final notifier = Notifier(); |
| 21 | +/// |
| 22 | +/// // Create a micro task to fetch the weather |
| 23 | +/// scheduleMicrotask(() async { |
| 24 | +/// // Infinitely loop that just keeps the weather up-to-date |
| 25 | +/// while (true) { |
| 26 | +/// weather = await getWeather(); |
| 27 | +/// notifier.notify(); |
| 28 | +/// |
| 29 | +/// // Sleep 5s before updating the weather again |
| 30 | +/// await Future.delayed(Duration(seconds: 5)); |
| 31 | +/// } |
| 32 | +/// }); |
| 33 | +/// |
| 34 | +/// // Wait for sunny weather |
| 35 | +/// while (weather != 'sunny') { |
| 36 | +/// await notifier.wait; |
| 37 | +/// } |
| 38 | +/// ``` |
| 39 | +final class Notifier { |
| 40 | + var _completer = Completer<void>(); |
| 41 | + |
| 42 | + /// Notify everybody waiting for notification. |
| 43 | + /// |
| 44 | + /// This will complete all futures previously returned by [wait]. |
| 45 | + /// Calls to [wait] after this call, will not be resolved, until the |
| 46 | + /// next time [notify] is called. |
| 47 | + void notify() { |
| 48 | + if (!_completer.isCompleted) { |
| 49 | + _completer.complete(); |
| 50 | + } |
| 51 | + } |
| 52 | + |
| 53 | + /// Wait for notification. |
| 54 | + /// |
| 55 | + /// Returns a [Future] that will complete the next time [notify] is called. |
| 56 | + /// |
| 57 | + /// The [Future] returned will always be unresolved, and it will never throw. |
| 58 | + /// Once [notify] is called the future will be completed, and any new calls |
| 59 | + /// to [wait] will return a new future. This new future will also be |
| 60 | + /// unresolved, until [notify] is called. |
| 61 | + Future<void> get wait { |
| 62 | + if (_completer.isCompleted) { |
| 63 | + _completer = Completer(); |
| 64 | + } |
| 65 | + return _completer.future; |
| 66 | + } |
| 67 | +} |
| 68 | + |
| 69 | +/// Utility extensions on [Stream]. |
| 70 | +extension StreamExtensions<T> on Stream<T> { |
| 71 | + /// Call [each] for each item in this stream with [maxParallel] invocations. |
| 72 | + /// |
| 73 | + /// This method will invoke [each] for each item in this stream, and wait for |
| 74 | + /// all futures from [each] to be resolved. [parallelForEach] will call [each] |
| 75 | + /// in parallel, but never more then [maxParallel]. |
| 76 | + /// |
| 77 | + /// If [each] throws and [onError] rethrows (default behavior), then |
| 78 | + /// [parallelForEach] will wait for ongoing [each] invocations to finish, |
| 79 | + /// before throw the first error. |
| 80 | + /// |
| 81 | + /// If [onError] does not throw, then iteration will not be interrupted and |
| 82 | + /// errors from [each] will be ignored. |
| 83 | + /// |
| 84 | + /// ```dart |
| 85 | + /// // Count size of all files in the current folder |
| 86 | + /// var folderSize = 0; |
| 87 | + /// // Use parallelForEach to read at-most 5 files at the same time. |
| 88 | + /// await Directory.current.list().parallelForEach(5, (item) async { |
| 89 | + /// if (item is File) { |
| 90 | + /// final bytes = await item.readAsBytes(); |
| 91 | + /// folderSize += bytes.length; |
| 92 | + /// } |
| 93 | + /// }); |
| 94 | + /// print('Folder size: $folderSize'); |
| 95 | + /// ``` |
| 96 | + Future<void> parallelForEach( |
| 97 | + int maxParallel, |
| 98 | + FutureOr<void> Function(T item) each, { |
| 99 | + FutureOr<void> Function(Object e, StackTrace? st) onError = Future.error, |
| 100 | + }) async { |
| 101 | + // Track the first error, so we rethrow when we're done. |
| 102 | + Object? firstError; |
| 103 | + StackTrace? firstStackTrace; |
| 104 | + |
| 105 | + // Track number of running items. |
| 106 | + var running = 0; |
| 107 | + final itemDone = Notifier(); |
| 108 | + |
| 109 | + try { |
| 110 | + var doBreak = false; |
| 111 | + await for (final item in this) { |
| 112 | + // For each item we increment [running] and call [each] |
| 113 | + running += 1; |
| 114 | + unawaited(() async { |
| 115 | + try { |
| 116 | + await each(item); |
| 117 | + } catch (e, st) { |
| 118 | + try { |
| 119 | + // If [onError] doesn't throw, we'll just continue. |
| 120 | + await onError(e, st); |
| 121 | + } catch (e, st) { |
| 122 | + doBreak = true; |
| 123 | + if (firstError == null) { |
| 124 | + firstError = e; |
| 125 | + firstStackTrace = st; |
| 126 | + } |
| 127 | + } |
| 128 | + } finally { |
| 129 | + // When [each] is done, we decrement [running] and notify |
| 130 | + running -= 1; |
| 131 | + itemDone.notify(); |
| 132 | + } |
| 133 | + }()); |
| 134 | + |
| 135 | + if (running >= maxParallel) { |
| 136 | + await itemDone.wait; |
| 137 | + } |
| 138 | + if (doBreak) { |
| 139 | + break; |
| 140 | + } |
| 141 | + } |
| 142 | + } finally { |
| 143 | + // Wait for all items to be finished |
| 144 | + while (running > 0) { |
| 145 | + await itemDone.wait; |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + // If an error happened, then we rethrow the first one. |
| 150 | + final firstError_ = firstError; |
| 151 | + final firstStackTrace_ = firstStackTrace; |
| 152 | + if (firstError_ != null && firstStackTrace_ != null) { |
| 153 | + Error.throwWithStackTrace(firstError_, firstStackTrace_); |
| 154 | + } |
| 155 | + } |
| 156 | +} |
0 commit comments