@@ -3,6 +3,7 @@ import 'dart:convert' as convert;
3
3
import 'dart:io' ;
4
4
5
5
import 'package:http/http.dart' as http;
6
+ import 'package:powersync/src/abort_controller.dart' ;
6
7
import 'package:powersync/src/exceptions.dart' ;
7
8
import 'package:powersync/src/log_internal.dart' ;
8
9
@@ -39,6 +40,8 @@ class StreamingSyncImplementation {
39
40
40
41
SyncStatus lastStatus = const SyncStatus ();
41
42
43
+ AbortController ? _abort;
44
+
42
45
StreamingSyncImplementation (
43
46
{required this .adapter,
44
47
required this .credentialsCallback,
@@ -50,34 +53,66 @@ class StreamingSyncImplementation {
50
53
statusStream = _statusStreamController.stream;
51
54
}
52
55
56
+ /// Close any active streams.
57
+ Future <void > abort () async {
58
+ // If streamingSync() hasn't been called yet, _abort will be null.
59
+ var future = _abort? .abort ();
60
+ // This immediately triggers a new iteration in the merged stream, allowing us
61
+ // to break immediately.
62
+ // However, we still need to close the underlying stream explicitly, otherwise
63
+ // the break will wait for the next line of data received on the stream.
64
+ _localPingController.add (null );
65
+ // According to the documentation, the behavior is undefined when calling
66
+ // close() while requests are pending. However, this is no other
67
+ // known way to cancel open streams, and this appears to end the stream with
68
+ // a consistent ClientException.
69
+ _client.close ();
70
+ // wait for completeAbort() to be called
71
+ await future;
72
+ }
73
+
74
+ bool get aborted {
75
+ return _abort? .aborted ?? false ;
76
+ }
77
+
53
78
Future <void > streamingSync () async {
54
- crudLoop ();
55
- var invalidCredentials = false ;
56
- while (true ) {
57
- _updateStatus (connecting: true );
58
- try {
59
- if (invalidCredentials && invalidCredentialsCallback != null ) {
60
- // This may error. In that case it will be retried again on the next
61
- // iteration.
62
- await invalidCredentialsCallback !();
63
- invalidCredentials = false ;
64
- }
65
- await streamingSyncIteration ();
66
- // Continue immediately
67
- } catch (e, stacktrace) {
68
- final message = _syncErrorMessage (e);
69
- isolateLogger.warning ('Sync error: $message ' , e, stacktrace);
70
- invalidCredentials = true ;
79
+ try {
80
+ _abort = AbortController ();
81
+ crudLoop ();
82
+ var invalidCredentials = false ;
83
+ while (! aborted) {
84
+ _updateStatus (connecting: true );
85
+ try {
86
+ if (invalidCredentials && invalidCredentialsCallback != null ) {
87
+ // This may error. In that case it will be retried again on the next
88
+ // iteration.
89
+ await invalidCredentialsCallback !();
90
+ invalidCredentials = false ;
91
+ }
92
+ await streamingSyncIteration ();
93
+ // Continue immediately
94
+ } catch (e, stacktrace) {
95
+ if (aborted && e is http.ClientException ) {
96
+ // Explicit abort requested - ignore. Example error:
97
+ // ClientException: Connection closed while receiving data, uri=http://localhost:8080/sync/stream
98
+ return ;
99
+ }
100
+ final message = _syncErrorMessage (e);
101
+ isolateLogger.warning ('Sync error: $message ' , e, stacktrace);
102
+ invalidCredentials = true ;
71
103
72
- _updateStatus (
73
- connected: false ,
74
- connecting: true ,
75
- downloading: false ,
76
- downloadError: e);
104
+ _updateStatus (
105
+ connected: false ,
106
+ connecting: true ,
107
+ downloading: false ,
108
+ downloadError: e);
77
109
78
- // On error, wait a little before retrying
79
- await Future .delayed (retryDelay);
110
+ // On error, wait a little before retrying
111
+ await Future .delayed (retryDelay);
112
+ }
80
113
}
114
+ } finally {
115
+ _abort! .completeAbort ();
81
116
}
82
117
}
83
118
@@ -204,6 +239,10 @@ class StreamingSyncImplementation {
204
239
bool haveInvalidated = false ;
205
240
206
241
await for (var line in merged) {
242
+ if (aborted) {
243
+ break ;
244
+ }
245
+
207
246
_updateStatus (connected: true , connecting: false );
208
247
if (line is Checkpoint ) {
209
248
targetCheckpoint = line;
@@ -348,6 +387,9 @@ class StreamingSyncImplementation {
348
387
349
388
// Note: The response stream is automatically closed when this loop errors
350
389
await for (var line in ndjson (res.stream)) {
390
+ if (aborted) {
391
+ break ;
392
+ }
351
393
yield parseStreamingSyncLine (line as Map <String , dynamic >);
352
394
}
353
395
}
0 commit comments