@@ -15,7 +15,6 @@ import 'package:powersync_core/src/log_internal.dart';
15
15
import 'package:powersync_core/src/open_factory/abstract_powersync_open_factory.dart' ;
16
16
import 'package:powersync_core/src/open_factory/native/native_open_factory.dart' ;
17
17
import 'package:powersync_core/src/schema.dart' ;
18
- import 'package:powersync_core/src/schema_logic.dart' ;
19
18
import 'package:powersync_core/src/streaming_sync.dart' ;
20
19
import 'package:powersync_core/src/sync_status.dart' ;
21
20
import 'package:sqlite_async/sqlite3_common.dart' ;
@@ -109,42 +108,55 @@ class PowerSyncDatabaseImpl
109
108
/// [logger] defaults to [autoLogger] , which logs to the console in debug builds.s
110
109
PowerSyncDatabaseImpl .withDatabase (
111
110
{required this .schema, required this .database, Logger ? logger}) {
112
- if (logger != null ) {
113
- this .logger = logger;
114
- } else {
115
- this .logger = autoLogger;
116
- }
111
+ this .logger = logger ?? autoLogger;
117
112
isInitialized = baseInit ();
118
113
}
119
114
120
115
@override
121
116
@internal
122
-
123
- /// Connect to the PowerSync service, and keep the databases in sync.
124
- ///
125
- /// The connection is automatically re-opened if it fails for any reason.
126
- ///
127
- /// Status changes are reported on [statusStream] .
128
- baseConnect (
129
- {required PowerSyncBackendConnector connector,
130
-
131
- /// Throttle time between CRUD operations
132
- /// Defaults to 10 milliseconds.
133
- required Duration crudThrottleTime,
134
- required Future <void > Function () reconnect,
135
- Map <String , dynamic >? params}) async {
117
+ Future <void > connectInternal ({
118
+ required PowerSyncBackendConnector connector,
119
+ required Duration crudThrottleTime,
120
+ required AbortController abort,
121
+ Map <String , dynamic >? params,
122
+ }) async {
136
123
await initialize ();
137
-
138
- // Disconnect if connected
139
- await disconnect ();
140
- final disconnector = AbortController ();
141
- disconnecter = disconnector;
142
-
143
- await isInitialized;
144
124
final dbRef = database.isolateConnectionFactory ();
145
- ReceivePort rPort = ReceivePort ();
125
+
126
+ Isolate ? isolate;
146
127
StreamSubscription <UpdateNotification >? crudUpdateSubscription;
147
- rPort.listen ((data) async {
128
+ final receiveMessages = ReceivePort ();
129
+ final receiveUnhandledErrors = ReceivePort ();
130
+ final receiveExit = ReceivePort ();
131
+
132
+ SendPort ? initPort;
133
+ final hasInitPort = Completer <void >();
134
+ final receivedIsolateExit = Completer <void >();
135
+
136
+ Future <void > waitForShutdown () async {
137
+ // Only complete the abortion signal after the isolate shuts down. This
138
+ // ensures absolutely no trace of this sync iteration remains.
139
+ if (isolate != null ) {
140
+ await receivedIsolateExit.future;
141
+ }
142
+
143
+ // Cleanup
144
+ crudUpdateSubscription? .cancel ();
145
+ receiveMessages.close ();
146
+ receiveUnhandledErrors.close ();
147
+ receiveExit.close ();
148
+
149
+ // Clear status apart from lastSyncedAt
150
+ setStatus (SyncStatus (lastSyncedAt: currentStatus.lastSyncedAt));
151
+ abort.completeAbort ();
152
+ }
153
+
154
+ Future <void > close () async {
155
+ initPort? .send (['close' ]);
156
+ await waitForShutdown ();
157
+ }
158
+
159
+ receiveMessages.listen ((data) async {
148
160
if (data is List ) {
149
161
String action = data[0 ] as String ;
150
162
if (action == "getCredentials" ) {
@@ -159,27 +171,20 @@ class PowerSyncDatabaseImpl
159
171
await connector.prefetchCredentials ();
160
172
});
161
173
} else if (action == 'init' ) {
162
- SendPort port = data[1 ] as SendPort ;
174
+ final port = initPort = data[1 ] as SendPort ;
175
+ hasInitPort.complete ();
163
176
var crudStream =
164
177
database.onChange (['ps_crud' ], throttle: crudThrottleTime);
165
178
crudUpdateSubscription = crudStream.listen ((event) {
166
179
port.send (['update' ]);
167
180
});
168
- disconnector.onAbort.then ((_) {
169
- port.send (['close' ]);
170
- }).ignore ();
171
181
} else if (action == 'uploadCrud' ) {
172
182
await (data[1 ] as PortCompleter ).handle (() async {
173
183
await connector.uploadData (this );
174
184
});
175
185
} else if (action == 'status' ) {
176
186
final SyncStatus status = data[1 ] as SyncStatus ;
177
187
setStatus (status);
178
- } else if (action == 'close' ) {
179
- // Clear status apart from lastSyncedAt
180
- setStatus (SyncStatus (lastSyncedAt: currentStatus.lastSyncedAt));
181
- rPort.close ();
182
- crudUpdateSubscription? .cancel ();
183
188
} else if (action == 'log' ) {
184
189
LogRecord record = data[1 ] as LogRecord ;
185
190
logger.log (
@@ -188,8 +193,7 @@ class PowerSyncDatabaseImpl
188
193
}
189
194
});
190
195
191
- var errorPort = ReceivePort ();
192
- errorPort.listen ((message) async {
196
+ receiveUnhandledErrors.listen ((message) async {
193
197
// Sample error:
194
198
// flutter: [PowerSync] WARNING: 2023-06-28 16:34:11.566122: Sync Isolate error
195
199
// flutter: [Connection closed while receiving data, #0 IOClient.send.<anonymous closure> (package:http/src/io_client.dart:76:13)
@@ -200,38 +204,37 @@ class PowerSyncDatabaseImpl
200
204
// ...
201
205
logger.severe ('Sync Isolate error' , message);
202
206
203
- // Reconnect
204
- // Use the param like this instead of directly calling connect(), to avoid recursive
205
- // locks in some edge cases.
206
- reconnect ();
207
+ // Fatal errors are enabled, so the isolate will exit soon, causing us to
208
+ // complete the abort controller which will make the db mixin reconnect if
209
+ // necessary. There's no need to reconnect manually.
207
210
});
208
211
209
- disconnected () {
210
- disconnector.completeAbort ();
211
- disconnecter = null ;
212
- rPort.close ();
213
- // Clear status apart from lastSyncedAt
214
- setStatus (SyncStatus (lastSyncedAt: currentStatus.lastSyncedAt));
212
+ // Don't spawn isolate if this operation was cancelled already.
213
+ if (abort.aborted) {
214
+ return waitForShutdown ();
215
215
}
216
216
217
- var exitPort = ReceivePort ();
218
- exitPort.listen ((message) {
217
+ receiveExit.listen ((message) {
219
218
logger.fine ('Sync Isolate exit' );
220
- disconnected ();
219
+ receivedIsolateExit. complete ();
221
220
});
222
221
223
- if (disconnecter? .aborted == true ) {
224
- disconnected ();
225
- return ;
226
- }
227
-
228
- Isolate .spawn (
229
- _powerSyncDatabaseIsolate,
230
- _PowerSyncDatabaseIsolateArgs (
231
- rPort.sendPort, dbRef, retryDelay, clientParams),
232
- debugName: 'PowerSyncDatabase' ,
233
- onError: errorPort.sendPort,
234
- onExit: exitPort.sendPort);
222
+ // Spawning the isolate can't be interrupted
223
+ isolate = await Isolate .spawn (
224
+ _syncIsolate,
225
+ _PowerSyncDatabaseIsolateArgs (
226
+ receiveMessages.sendPort, dbRef, retryDelay, clientParams),
227
+ debugName: 'Sync ${database .openFactory .path }' ,
228
+ onError: receiveUnhandledErrors.sendPort,
229
+ errorsAreFatal: true ,
230
+ onExit: receiveExit.sendPort,
231
+ );
232
+ await hasInitPort.future;
233
+
234
+ abort.onAbort.whenComplete (close);
235
+
236
+ // Automatically complete the abort controller once the isolate exits.
237
+ unawaited (waitForShutdown ());
235
238
}
236
239
237
240
/// Takes a read lock, without starting a transaction.
@@ -255,16 +258,6 @@ class PowerSyncDatabaseImpl
255
258
return database.writeLock (callback,
256
259
debugContext: debugContext, lockTimeout: lockTimeout);
257
260
}
258
-
259
- @override
260
- Future <void > updateSchema (Schema schema) {
261
- if (disconnecter != null ) {
262
- throw AssertionError ('Cannot update schema while connected' );
263
- }
264
- schema.validate ();
265
- this .schema = schema;
266
- return updateSchemaInIsolate (database, schema);
267
- }
268
261
}
269
262
270
263
class _PowerSyncDatabaseIsolateArgs {
@@ -277,64 +270,70 @@ class _PowerSyncDatabaseIsolateArgs {
277
270
this .sPort, this .dbRef, this .retryDelay, this .parameters);
278
271
}
279
272
280
- Future <void > _powerSyncDatabaseIsolate (
281
- _PowerSyncDatabaseIsolateArgs args) async {
273
+ Future <void > _syncIsolate (_PowerSyncDatabaseIsolateArgs args) async {
282
274
final sPort = args.sPort;
283
- ReceivePort rPort = ReceivePort ();
275
+ final rPort = ReceivePort ();
284
276
StreamController <String > crudUpdateController = StreamController .broadcast ();
285
277
final upstreamDbClient = args.dbRef.upstreamPort.open ();
286
278
287
279
CommonDatabase ? db;
288
280
final Mutex mutex = args.dbRef.mutex.open ();
289
281
StreamingSyncImplementation ? openedStreamingSync;
282
+ StreamSubscription <void >? localUpdatesSubscription;
283
+
284
+ Future <void > shutdown () async {
285
+ localUpdatesSubscription? .cancel ();
286
+ db? .dispose ();
287
+ crudUpdateController.close ();
288
+ upstreamDbClient.close ();
289
+
290
+ // The SyncSqliteConnection uses this mutex
291
+ // It needs to be closed before killing the isolate
292
+ // in order to free the mutex for other operations.
293
+ await mutex.close ();
294
+ await openedStreamingSync? .abort ();
295
+
296
+ rPort.close ();
297
+ }
290
298
291
299
rPort.listen ((message) async {
292
300
if (message is List ) {
293
301
String action = message[0 ] as String ;
294
302
if (action == 'update' ) {
295
- crudUpdateController.add ('update' );
303
+ if (! crudUpdateController.isClosed) {
304
+ crudUpdateController.add ('update' );
305
+ }
296
306
} else if (action == 'close' ) {
297
- // The SyncSqliteConnection uses this mutex
298
- // It needs to be closed before killing the isolate
299
- // in order to free the mutex for other operations.
300
- await mutex.close ();
301
- db? .dispose ();
302
- crudUpdateController.close ();
303
- upstreamDbClient.close ();
304
- // Abort any open http requests, and wait for it to be closed properly
305
- await openedStreamingSync? .abort ();
306
- // No kill the Isolate
307
- Isolate .current.kill ();
307
+ await shutdown ();
308
308
}
309
309
}
310
310
});
311
- Isolate .current.addOnExitListener (sPort, response: const ['close' ]);
312
- sPort.send (["init" , rPort.sendPort]);
311
+ sPort.send (['init' , rPort.sendPort]);
313
312
314
313
// Is there a way to avoid the overhead if logging is not enabled?
315
314
// This only takes effect in this isolate.
316
315
isolateLogger.level = Level .ALL ;
317
316
isolateLogger.onRecord.listen ((record) {
318
317
var copy = LogRecord (record.level, record.message, record.loggerName,
319
318
record.error, record.stackTrace);
320
- sPort.send ([" log" , copy]);
319
+ sPort.send ([' log' , copy]);
321
320
});
322
321
323
322
Future <PowerSyncCredentials ?> loadCredentials () async {
324
323
final r = IsolateResult <PowerSyncCredentials ?>();
325
- sPort.send ([" getCredentials" , r.completer]);
324
+ sPort.send ([' getCredentials' , r.completer]);
326
325
return r.future;
327
326
}
328
327
329
328
Future <void > invalidateCredentials () async {
330
329
final r = IsolateResult <void >();
331
- sPort.send ([" invalidateCredentials" , r.completer]);
330
+ sPort.send ([' invalidateCredentials' , r.completer]);
332
331
return r.future;
333
332
}
334
333
335
334
Future <void > uploadCrud () async {
336
335
final r = IsolateResult <void >();
337
- sPort.send ([" uploadCrud" , r.completer]);
336
+ sPort.send ([' uploadCrud' , r.completer]);
338
337
return r.future;
339
338
}
340
339
@@ -372,18 +371,18 @@ Future<void> _powerSyncDatabaseIsolate(
372
371
}
373
372
}
374
373
375
- db! .updates.listen ((event) {
374
+ localUpdatesSubscription = db! .updates.listen ((event) {
376
375
updatedTables.add (event.tableName);
377
376
378
377
updateDebouncer ?? =
379
378
Timer (const Duration (milliseconds: 1 ), maybeFireUpdates);
380
379
});
381
- }, (error, stack) {
380
+ }, (error, stack) async {
382
381
// Properly dispose the database if an uncaught error occurs.
383
382
// Unfortunately, this does not handle disposing while the database is opening.
384
383
// This should be rare - any uncaught error is a bug. And in most cases,
385
384
// it should occur after the database is already open.
386
- db ? . dispose ();
385
+ await shutdown ();
387
386
throw error;
388
387
});
389
388
}
0 commit comments