Skip to content

Commit f3563a1

Browse files
authored
add reusable completer utility (#904)
1 parent 2c97d47 commit f3563a1

File tree

2 files changed

+537
-0
lines changed

2 files changed

+537
-0
lines changed
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
// Copyright 2025 LiveKit, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import 'dart:async';
16+
17+
/// Manages a [Completer] lifecycle while exposing only its [Future].
18+
///
19+
/// Features:
20+
/// - Safe completion (prevents double completion exceptions)
21+
/// - Optional timeout handling
22+
/// - Deterministic reset and disposal semantics
23+
/// - Only exposes [Future], not the [Completer] itself
24+
class ReusableCompleter<T> {
25+
Completer<T> _completer;
26+
Timer? _timeoutTimer;
27+
bool _isCompleted = false;
28+
bool _isDisposed = false;
29+
bool _hasPendingListener = false;
30+
31+
/// Creates a new [ReusableCompleter] with an active completer.
32+
ReusableCompleter() : _completer = Completer<T>();
33+
34+
/// The current future. Creates a new completer if the previous one finished.
35+
///
36+
/// Throws [StateError] when called after [dispose].
37+
Future<T> get future {
38+
if (_isDisposed) {
39+
throw StateError('ReusableCompleter disposed');
40+
}
41+
if (_isCompleted) {
42+
_createCompleter();
43+
}
44+
_hasPendingListener = true;
45+
return _completer.future;
46+
}
47+
48+
/// Whether the current completer has completed (with value or error).
49+
bool get isCompleted => _isCompleted;
50+
51+
/// Whether the completer is still managing an active future.
52+
bool get isActive => !_isDisposed && !_isCompleted;
53+
54+
/// Whether [dispose] has been called.
55+
bool get isDisposed => _isDisposed;
56+
57+
/// Completes the current completer with the given [value].
58+
/// Returns `true` if successfully completed, otherwise `false`.
59+
bool complete([FutureOr<T>? value]) {
60+
if (_isDisposed || _isCompleted) {
61+
return false;
62+
}
63+
64+
_completeCurrent((completer) => completer.complete(value));
65+
return true;
66+
}
67+
68+
/// Completes the current completer with an [error].
69+
/// Returns `true` if successfully completed, otherwise `false`.
70+
bool completeError(Object error, [StackTrace? stackTrace]) {
71+
if (_isDisposed || _isCompleted) {
72+
return false;
73+
}
74+
75+
_completeCurrent((completer) => completer.completeError(error, stackTrace));
76+
return true;
77+
}
78+
79+
/// Sets up a timeout for the current completer.
80+
/// If not completed within [timeout], completes with a [TimeoutException].
81+
void setTimer(Duration timeout, {String? timeoutReason}) {
82+
if (_isDisposed || _isCompleted) {
83+
return;
84+
}
85+
86+
_clearTimer();
87+
_timeoutTimer = Timer(timeout, () {
88+
if (_isDisposed || _isCompleted) {
89+
return;
90+
}
91+
final reason = timeoutReason ?? 'Operation timed out after $timeout';
92+
completeError(TimeoutException(reason, timeout));
93+
});
94+
}
95+
96+
/// Resets the completer for reuse.
97+
///
98+
/// Any pending future is completed with a [StateError] (or [error] if provided)
99+
/// before a new completer is created.
100+
void reset({Object? error, StackTrace? stackTrace}) {
101+
if (_isDisposed) {
102+
throw StateError('ReusableCompleter disposed');
103+
}
104+
105+
if (!_isCompleted && _hasPendingListener) {
106+
_completeCurrent(
107+
(completer) => completer.completeError(
108+
error ?? StateError('ReusableCompleter reset'),
109+
stackTrace,
110+
),
111+
);
112+
} else {
113+
_markCompletedWithoutNotify();
114+
}
115+
116+
_createCompleter();
117+
}
118+
119+
/// Disposes the completer, completing any pending future with an error.
120+
///
121+
/// After disposal, the completer cannot be reused; calls to [future] throw
122+
/// [StateError] and completion methods return `false`.
123+
void dispose({Object? error, StackTrace? stackTrace}) {
124+
if (_isDisposed) {
125+
return;
126+
}
127+
128+
if (!_isCompleted && _hasPendingListener) {
129+
_completeCurrent(
130+
(completer) => completer.completeError(
131+
error ?? StateError('ReusableCompleter disposed'),
132+
stackTrace,
133+
),
134+
);
135+
} else {
136+
_markCompletedWithoutNotify();
137+
}
138+
139+
_clearTimer();
140+
_isDisposed = true;
141+
}
142+
143+
void _createCompleter() {
144+
_clearTimer();
145+
_completer = Completer<T>();
146+
_isCompleted = false;
147+
_hasPendingListener = false;
148+
}
149+
150+
void _completeCurrent(
151+
void Function(Completer<T> completer) complete,
152+
) {
153+
_isCompleted = true;
154+
_clearTimer();
155+
complete(_completer);
156+
_hasPendingListener = false;
157+
}
158+
159+
void _markCompletedWithoutNotify() {
160+
_isCompleted = true;
161+
_clearTimer();
162+
_hasPendingListener = false;
163+
}
164+
165+
void _clearTimer() {
166+
_timeoutTimer?.cancel();
167+
_timeoutTimer = null;
168+
}
169+
}

0 commit comments

Comments
 (0)